@karmaniverous/jeeves-watcher 0.2.4 → 0.2.5

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/index.d.ts CHANGED
@@ -15,6 +15,7 @@ declare const watchConfigSchema: z.ZodObject<{
15
15
  usePolling: z.ZodOptional<z.ZodBoolean>;
16
16
  debounceMs: z.ZodOptional<z.ZodNumber>;
17
17
  stabilityThresholdMs: z.ZodOptional<z.ZodNumber>;
18
+ respectGitignore: z.ZodOptional<z.ZodBoolean>;
18
19
  }, z.core.$strip>;
19
20
  /** Watch configuration for file system monitoring paths, ignore patterns, and debounce/stability settings. */
20
21
  type WatchConfig = z.infer<typeof watchConfigSchema>;
@@ -91,6 +92,7 @@ declare const jeevesWatcherConfigSchema: z.ZodObject<{
91
92
  usePolling: z.ZodOptional<z.ZodBoolean>;
92
93
  debounceMs: z.ZodOptional<z.ZodNumber>;
93
94
  stabilityThresholdMs: z.ZodOptional<z.ZodNumber>;
95
+ respectGitignore: z.ZodOptional<z.ZodBoolean>;
94
96
  }, z.core.$strip>;
95
97
  configWatch: z.ZodOptional<z.ZodObject<{
96
98
  enabled: z.ZodOptional<z.ZodBoolean>;
@@ -543,6 +545,42 @@ declare function createApiServer(options: ApiServerOptions): FastifyInstance;
543
545
  */
544
546
  declare function createLogger(config?: LoggingConfig): pino.Logger;
545
547
 
548
+ /**
549
+ * @module gitignore
550
+ * Processor-level gitignore filtering. Scans watched paths for `.gitignore` files in git repos, caches parsed patterns, and exposes `isIgnored()` for path checking.
551
+ */
552
+ /**
553
+ * Processor-level gitignore filter. Checks file paths against the nearest
554
+ * `.gitignore` chain in git repositories.
555
+ */
556
+ declare class GitignoreFilter {
557
+ private repos;
558
+ /**
559
+ * Create a GitignoreFilter by scanning watched paths for `.gitignore` files.
560
+ *
561
+ * @param watchPaths - Absolute paths being watched (directories or globs resolved to roots).
562
+ */
563
+ constructor(watchPaths: string[]);
564
+ /**
565
+ * Scan paths for git repos and their `.gitignore` files.
566
+ */
567
+ private scan;
568
+ /**
569
+ * Check whether a file path is ignored by any applicable `.gitignore`.
570
+ *
571
+ * @param filePath - Absolute file path to check.
572
+ * @returns `true` if the file should be ignored.
573
+ */
574
+ isIgnored(filePath: string): boolean;
575
+ /**
576
+ * Invalidate and re-parse a specific `.gitignore` file.
577
+ * Call when a `.gitignore` file is added, changed, or removed.
578
+ *
579
+ * @param gitignorePath - Absolute path to the `.gitignore` file that changed.
580
+ */
581
+ invalidate(gitignorePath: string): void;
582
+ }
583
+
546
584
  /**
547
585
  * @module health
548
586
  * Tracks consecutive system-level failures and applies exponential backoff.
@@ -612,6 +650,8 @@ interface FileSystemWatcherOptions {
612
650
  maxBackoffMs?: number;
613
651
  /** Callback invoked on unrecoverable system error. If not set, throws. */
614
652
  onFatalError?: (error: unknown) => void;
653
+ /** Optional gitignore filter for processor-level filtering. */
654
+ gitignoreFilter?: GitignoreFilter;
615
655
  }
616
656
  /**
617
657
  * Filesystem watcher that maps chokidar events to the processing queue.
@@ -622,6 +662,7 @@ declare class FileSystemWatcher {
622
662
  private readonly processor;
623
663
  private readonly logger;
624
664
  private readonly health;
665
+ private readonly gitignoreFilter?;
625
666
  private watcher;
626
667
  /**
627
668
  * Create a new FileSystemWatcher.
@@ -645,6 +686,14 @@ declare class FileSystemWatcher {
645
686
  * Get the system health tracker.
646
687
  */
647
688
  get systemHealth(): SystemHealth;
689
+ /**
690
+ * Check if a path is gitignored and should be skipped.
691
+ */
692
+ private isGitignored;
693
+ /**
694
+ * If the changed file is a `.gitignore`, invalidate the filter cache.
695
+ */
696
+ private handleGitignoreChange;
648
697
  /**
649
698
  * Wrap a processing operation with health tracking.
650
699
  * On success, resets the failure counter.
@@ -812,5 +861,5 @@ declare function deleteMetadata(filePath: string, metadataDir: string): Promise<
812
861
  */
813
862
  declare function pointId(filePath: string, chunkIndex?: number): string;
814
863
 
815
- export { DocumentProcessor, EventQueue, FileSystemWatcher, JeevesWatcher, SystemHealth, VectorStoreClient, apiConfigSchema, applyRules, buildAttributes, compileRules, configWatchConfigSchema, contentHash, createApiServer, createEmbeddingProvider, createLogger, deleteMetadata, embeddingConfigSchema, extractText, inferenceRuleSchema, jeevesWatcherConfigSchema, loadConfig, loggingConfigSchema, metadataPath, pointId, readMetadata, startFromConfig, vectorStoreConfigSchema, watchConfigSchema, writeMetadata };
864
+ export { DocumentProcessor, EventQueue, FileSystemWatcher, GitignoreFilter, JeevesWatcher, SystemHealth, VectorStoreClient, apiConfigSchema, applyRules, buildAttributes, compileRules, configWatchConfigSchema, contentHash, createApiServer, createEmbeddingProvider, createLogger, deleteMetadata, embeddingConfigSchema, extractText, inferenceRuleSchema, jeevesWatcherConfigSchema, loadConfig, loggingConfigSchema, metadataPath, pointId, readMetadata, startFromConfig, vectorStoreConfigSchema, watchConfigSchema, writeMetadata };
816
865
  export type { ApiConfig, ApiServerOptions, CompiledRule, ConfigWatchConfig, EmbeddingConfig, EmbeddingProvider, EventQueueOptions, ExtractedText, FileAttributes, FileSystemWatcherOptions, InferenceRule, JeevesWatcherConfig, JeevesWatcherFactories, JeevesWatcherRuntimeOptions, LoggingConfig, ProcessFn, ProcessorConfig, RuleLogger, ScrolledPoint, SearchResult, SystemHealthOptions, VectorPoint, VectorStoreConfig, WatchConfig, WatchEvent };
@@ -1,4 +1,4 @@
1
- (function (exports, Fastify, promises, node_path, picomatch, radash, node_crypto, cosmiconfig, zod, jsonmap, googleGenai, pino, uuid, cheerio, yaml, mammoth, Ajv, addFormats, textsplitters, jsClientRest, chokidar) {
1
+ (function (exports, Fastify, promises, node_path, picomatch, radash, node_crypto, cosmiconfig, zod, jsonmap, googleGenai, node_fs, ignore, pino, uuid, cheerio, yaml, mammoth, Ajv, addFormats, textsplitters, jsClientRest, chokidar) {
2
2
  'use strict';
3
3
 
4
4
  function _interopNamespaceDefault(e) {
@@ -434,6 +434,7 @@
434
434
  stabilityThresholdMs: 500,
435
435
  usePolling: false,
436
436
  pollIntervalMs: 1000,
437
+ respectGitignore: true,
437
438
  };
438
439
  /** Default embedding configuration. */
439
440
  const EMBEDDING_DEFAULTS = {
@@ -478,6 +479,11 @@
478
479
  .number()
479
480
  .optional()
480
481
  .describe('Time in milliseconds a file must remain unchanged before processing.'),
482
+ /** Whether to respect .gitignore files when processing. */
483
+ respectGitignore: zod.z
484
+ .boolean()
485
+ .optional()
486
+ .describe('Skip files ignored by .gitignore in git repositories. Only applies to repos with a .git directory. Default: true.'),
481
487
  });
482
488
  /**
483
489
  * Configuration watch settings.
@@ -943,6 +949,212 @@
943
949
  return factory(config, logger);
944
950
  }
945
951
 
952
+ /**
953
+ * @module gitignore
954
+ * Processor-level gitignore filtering. Scans watched paths for `.gitignore` files in git repos, caches parsed patterns, and exposes `isIgnored()` for path checking.
955
+ */
956
+ /**
957
+ * Find the git repo root by walking up from `startDir` looking for `.git/`.
958
+ * Returns `undefined` if no repo is found.
959
+ */
960
+ function findRepoRoot(startDir) {
961
+ let dir = node_path.resolve(startDir);
962
+ const root = node_path.resolve('/');
963
+ while (dir !== root) {
964
+ if (node_fs.existsSync(node_path.join(dir, '.git')) &&
965
+ node_fs.statSync(node_path.join(dir, '.git')).isDirectory()) {
966
+ return dir;
967
+ }
968
+ const parent = node_path.dirname(dir);
969
+ if (parent === dir)
970
+ break;
971
+ dir = parent;
972
+ }
973
+ return undefined;
974
+ }
975
+ /**
976
+ * Convert a watch path (directory, file path, or glob) to a concrete directory
977
+ * that can be scanned for a repo root.
978
+ */
979
+ function watchPathToScanDir(watchPath) {
980
+ const absPath = node_path.resolve(watchPath);
981
+ try {
982
+ return node_fs.statSync(absPath).isDirectory() ? absPath : node_path.dirname(absPath);
983
+ }
984
+ catch {
985
+ // ignore
986
+ }
987
+ // If this is a glob, fall back to the non-glob prefix.
988
+ const globMatch = /[*?[{]/.exec(watchPath);
989
+ if (!globMatch)
990
+ return undefined;
991
+ const prefix = watchPath.slice(0, globMatch.index);
992
+ const trimmed = prefix.trim();
993
+ const baseDir = trimmed.length === 0
994
+ ? '.'
995
+ : trimmed.endsWith('/') || trimmed.endsWith('\\')
996
+ ? trimmed
997
+ : node_path.dirname(trimmed);
998
+ const resolved = node_path.resolve(baseDir);
999
+ if (!node_fs.existsSync(resolved))
1000
+ return undefined;
1001
+ return resolved;
1002
+ }
1003
+ /**
1004
+ * Recursively find all `.gitignore` files under `dir`.
1005
+ * Skips `.git` and `node_modules` directories for performance.
1006
+ */
1007
+ function findGitignoreFiles(dir) {
1008
+ const results = [];
1009
+ const gitignorePath = node_path.join(dir, '.gitignore');
1010
+ if (node_fs.existsSync(gitignorePath)) {
1011
+ results.push(gitignorePath);
1012
+ }
1013
+ let entries;
1014
+ try {
1015
+ entries = node_fs.readdirSync(dir);
1016
+ }
1017
+ catch {
1018
+ return results;
1019
+ }
1020
+ for (const entry of entries) {
1021
+ if (entry === '.git' || entry === 'node_modules')
1022
+ continue;
1023
+ const fullPath = node_path.join(dir, entry);
1024
+ try {
1025
+ if (node_fs.statSync(fullPath).isDirectory()) {
1026
+ results.push(...findGitignoreFiles(fullPath));
1027
+ }
1028
+ }
1029
+ catch {
1030
+ // Skip inaccessible entries
1031
+ }
1032
+ }
1033
+ return results;
1034
+ }
1035
+ /**
1036
+ * Parse a `.gitignore` file into an `ignore` instance.
1037
+ */
1038
+ function parseGitignore(gitignorePath) {
1039
+ const content = node_fs.readFileSync(gitignorePath, 'utf8');
1040
+ return ignore().add(content);
1041
+ }
1042
+ /**
1043
+ * Normalize a path to use forward slashes (required by `ignore` package).
1044
+ */
1045
+ function toForwardSlash(p) {
1046
+ return p.replace(/\\/g, '/');
1047
+ }
1048
+ /**
1049
+ * Processor-level gitignore filter. Checks file paths against the nearest
1050
+ * `.gitignore` chain in git repositories.
1051
+ */
1052
+ class GitignoreFilter {
1053
+ repos = new Map();
1054
+ /**
1055
+ * Create a GitignoreFilter by scanning watched paths for `.gitignore` files.
1056
+ *
1057
+ * @param watchPaths - Absolute paths being watched (directories or globs resolved to roots).
1058
+ */
1059
+ constructor(watchPaths) {
1060
+ this.scan(watchPaths);
1061
+ }
1062
+ /**
1063
+ * Scan paths for git repos and their `.gitignore` files.
1064
+ */
1065
+ scan(watchPaths) {
1066
+ this.repos.clear();
1067
+ const scannedDirs = new Set();
1068
+ for (const watchPath of watchPaths) {
1069
+ const scanDir = watchPathToScanDir(watchPath);
1070
+ if (!scanDir)
1071
+ continue;
1072
+ if (scannedDirs.has(scanDir))
1073
+ continue;
1074
+ scannedDirs.add(scanDir);
1075
+ const repoRoot = findRepoRoot(scanDir);
1076
+ if (!repoRoot)
1077
+ continue;
1078
+ if (this.repos.has(repoRoot))
1079
+ continue;
1080
+ const gitignoreFiles = findGitignoreFiles(repoRoot);
1081
+ const entries = gitignoreFiles.map((gf) => ({
1082
+ dir: node_path.dirname(gf),
1083
+ ig: parseGitignore(gf),
1084
+ }));
1085
+ // Sort deepest-first so nested `.gitignore` files are checked first
1086
+ entries.sort((a, b) => b.dir.length - a.dir.length);
1087
+ this.repos.set(repoRoot, { root: repoRoot, entries });
1088
+ }
1089
+ }
1090
+ /**
1091
+ * Check whether a file path is ignored by any applicable `.gitignore`.
1092
+ *
1093
+ * @param filePath - Absolute file path to check.
1094
+ * @returns `true` if the file should be ignored.
1095
+ */
1096
+ isIgnored(filePath) {
1097
+ const absPath = node_path.resolve(filePath);
1098
+ for (const [, repo] of this.repos) {
1099
+ // Check if file is within this repo
1100
+ const relToRepo = node_path.relative(repo.root, absPath);
1101
+ if (relToRepo.startsWith('..') || relToRepo.startsWith(node_path.resolve('/'))) {
1102
+ continue;
1103
+ }
1104
+ // Check each `.gitignore` entry (deepest-first)
1105
+ for (const entry of repo.entries) {
1106
+ const relToEntry = node_path.relative(entry.dir, absPath);
1107
+ if (relToEntry.startsWith('..'))
1108
+ continue;
1109
+ const normalized = toForwardSlash(relToEntry);
1110
+ if (entry.ig.ignores(normalized)) {
1111
+ return true;
1112
+ }
1113
+ }
1114
+ }
1115
+ return false;
1116
+ }
1117
+ /**
1118
+ * Invalidate and re-parse a specific `.gitignore` file.
1119
+ * Call when a `.gitignore` file is added, changed, or removed.
1120
+ *
1121
+ * @param gitignorePath - Absolute path to the `.gitignore` file that changed.
1122
+ */
1123
+ invalidate(gitignorePath) {
1124
+ const absPath = node_path.resolve(gitignorePath);
1125
+ const gitignoreDir = node_path.dirname(absPath);
1126
+ for (const [, repo] of this.repos) {
1127
+ const relToRepo = node_path.relative(repo.root, gitignoreDir);
1128
+ if (relToRepo.startsWith('..'))
1129
+ continue;
1130
+ // Remove old entry for this directory
1131
+ repo.entries = repo.entries.filter((e) => e.dir !== gitignoreDir);
1132
+ // Re-parse if file still exists
1133
+ if (node_fs.existsSync(absPath)) {
1134
+ repo.entries.push({ dir: gitignoreDir, ig: parseGitignore(absPath) });
1135
+ // Re-sort deepest-first
1136
+ repo.entries.sort((a, b) => b.dir.length - a.dir.length);
1137
+ }
1138
+ return;
1139
+ }
1140
+ // If not in any known repo, check if it's in a repo we haven't scanned
1141
+ const repoRoot = findRepoRoot(gitignoreDir);
1142
+ if (repoRoot && node_fs.existsSync(absPath)) {
1143
+ const entries = [
1144
+ { dir: gitignoreDir, ig: parseGitignore(absPath) },
1145
+ ];
1146
+ if (this.repos.has(repoRoot)) {
1147
+ const repo = this.repos.get(repoRoot);
1148
+ repo.entries.push(entries[0]);
1149
+ repo.entries.sort((a, b) => b.dir.length - a.dir.length);
1150
+ }
1151
+ else {
1152
+ this.repos.set(repoRoot, { root: repoRoot, entries });
1153
+ }
1154
+ }
1155
+ }
1156
+ }
1157
+
946
1158
  /**
947
1159
  * @module logger
948
1160
  * Creates pino logger instances. I/O: optionally writes logs to file via pino/file transport. Defaults to stdout at info level.
@@ -2034,6 +2246,7 @@
2034
2246
  processor;
2035
2247
  logger;
2036
2248
  health;
2249
+ gitignoreFilter;
2037
2250
  watcher;
2038
2251
  /**
2039
2252
  * Create a new FileSystemWatcher.
@@ -2049,6 +2262,7 @@
2049
2262
  this.queue = queue;
2050
2263
  this.processor = processor;
2051
2264
  this.logger = logger;
2265
+ this.gitignoreFilter = options.gitignoreFilter;
2052
2266
  const healthOptions = {
2053
2267
  maxRetries: options.maxRetries,
2054
2268
  maxBackoffMs: options.maxBackoffMs,
@@ -2071,14 +2285,23 @@
2071
2285
  ignoreInitial: false,
2072
2286
  });
2073
2287
  this.watcher.on('add', (path) => {
2288
+ this.handleGitignoreChange(path);
2289
+ if (this.isGitignored(path))
2290
+ return;
2074
2291
  this.logger.debug({ path }, 'File added');
2075
2292
  this.queue.enqueue({ type: 'create', path, priority: 'normal' }, () => this.wrapProcessing(() => this.processor.processFile(path)));
2076
2293
  });
2077
2294
  this.watcher.on('change', (path) => {
2295
+ this.handleGitignoreChange(path);
2296
+ if (this.isGitignored(path))
2297
+ return;
2078
2298
  this.logger.debug({ path }, 'File changed');
2079
2299
  this.queue.enqueue({ type: 'modify', path, priority: 'normal' }, () => this.wrapProcessing(() => this.processor.processFile(path)));
2080
2300
  });
2081
2301
  this.watcher.on('unlink', (path) => {
2302
+ this.handleGitignoreChange(path);
2303
+ if (this.isGitignored(path))
2304
+ return;
2082
2305
  this.logger.debug({ path }, 'File removed');
2083
2306
  this.queue.enqueue({ type: 'delete', path, priority: 'normal' }, () => this.wrapProcessing(() => this.processor.deleteFile(path)));
2084
2307
  });
@@ -2105,6 +2328,29 @@
2105
2328
  get systemHealth() {
2106
2329
  return this.health;
2107
2330
  }
2331
+ /**
2332
+ * Check if a path is gitignored and should be skipped.
2333
+ */
2334
+ isGitignored(path) {
2335
+ if (!this.gitignoreFilter)
2336
+ return false;
2337
+ const ignored = this.gitignoreFilter.isIgnored(path);
2338
+ if (ignored) {
2339
+ this.logger.debug({ path }, 'Skipping gitignored file');
2340
+ }
2341
+ return ignored;
2342
+ }
2343
+ /**
2344
+ * If the changed file is a `.gitignore`, invalidate the filter cache.
2345
+ */
2346
+ handleGitignoreChange(path) {
2347
+ if (!this.gitignoreFilter)
2348
+ return;
2349
+ if (path.endsWith('.gitignore')) {
2350
+ this.logger.info({ path }, 'Gitignore file changed, refreshing filter');
2351
+ this.gitignoreFilter.invalidate(path);
2352
+ }
2353
+ }
2108
2354
  /**
2109
2355
  * Wrap a processing operation with health tracking.
2110
2356
  * On success, resets the failure counter.
@@ -2260,10 +2506,15 @@
2260
2506
  rateLimitPerMinute: this.config.embedding.rateLimitPerMinute,
2261
2507
  });
2262
2508
  this.queue = queue;
2509
+ const respectGitignore = this.config.watch.respectGitignore ?? true;
2510
+ const gitignoreFilter = respectGitignore
2511
+ ? new GitignoreFilter(this.config.watch.paths)
2512
+ : undefined;
2263
2513
  const watcher = this.factories.createFileSystemWatcher(this.config.watch, queue, processor, logger, {
2264
2514
  maxRetries: this.config.maxRetries,
2265
2515
  maxBackoffMs: this.config.maxBackoffMs,
2266
2516
  onFatalError: this.runtimeOptions.onFatalError,
2517
+ gitignoreFilter,
2267
2518
  });
2268
2519
  this.watcher = watcher;
2269
2520
  const server = this.factories.createApiServer({
@@ -2372,6 +2623,7 @@
2372
2623
  exports.DocumentProcessor = DocumentProcessor;
2373
2624
  exports.EventQueue = EventQueue;
2374
2625
  exports.FileSystemWatcher = FileSystemWatcher;
2626
+ exports.GitignoreFilter = GitignoreFilter;
2375
2627
  exports.JeevesWatcher = JeevesWatcher;
2376
2628
  exports.SystemHealth = SystemHealth;
2377
2629
  exports.VectorStoreClient = VectorStoreClient;
@@ -2399,4 +2651,4 @@
2399
2651
  exports.watchConfigSchema = watchConfigSchema;
2400
2652
  exports.writeMetadata = writeMetadata;
2401
2653
 
2402
- })(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);
2654
+ })(this["jeeves-watcher"] = this["jeeves-watcher"] || {}, Fastify, promises, node_path, picomatch, radash, node_crypto, cosmiconfig, zod, jsonmap, googleGenai, node_fs, ignore, pino, uuid, cheerio, yaml, mammoth, Ajv, addFormats, textsplitters, jsClientRest, chokidar);
@@ -1 +1 @@
1
- !function(e,t,r,i,o,n,s,a,c,l,h,u,d,f,g,m,p,y,w,b,v){"use strict";function M(e){var t=Object.create(null);return e&&Object.keys(e).forEach((function(r){if("default"!==r){var i=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(t,r,i.get?i:{enumerable:!0,get:function(){return e[r]}})}})),t.default=e,Object.freeze(t)}var x=M(f);function P(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),r=new Error(t);return r.cause=e,r}function k(e){const t=e.replace(/\\/g,"/"),r=t.search(/[*?\[]/);if(-1===r)return i.resolve(e);const o=t.slice(0,r),n=o.endsWith("/")?o.slice(0,-1):i.dirname(o);return i.resolve(n)}async function*S(e){let t;try{t=(await r.readdir(e,{withFileTypes:!0})).map((e=>({name:e.name,isDirectory:e.isDirectory()})))}catch{return}for(const o of t){const t=i.resolve(e,o.name);if(o.isDirectory)yield*S(t);else try{(await r.stat(t)).isFile()&&(yield t)}catch{}}}async function F(e,t,r,i){const n=await async function(e,t=[]){const r=e.map((e=>e.replace(/\\/g,"/"))),i=t.map((e=>e.replace(/\\/g,"/"))),n=o(r,{dot:!0}),s=i.length?o(i,{dot:!0}):()=>!1,a=Array.from(new Set(e.map(k))),c=new Set;for(const e of a)for await(const t of S(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 r[i](e);return n.length}function z(e,t=!1){let r=e.replace(/\\/g,"/").toLowerCase();return t&&(r=r.replace(/^([a-z]):/,((e,t)=>t))),r}function j(e,t){const r=z(e,!0),o=s.createHash("sha256").update(r,"utf8").digest("hex");return i.join(t,`${o}.meta.json`)}async function R(e,t){try{const i=await r.readFile(j(e,t),"utf8");return JSON.parse(i)}catch{return null}}async function E(e,t,o){const n=j(e,t);await r.mkdir(i.dirname(n),{recursive:!0}),await r.writeFile(n,JSON.stringify(o,null,2),"utf8")}async function C(e,t){try{await r.rm(j(e,t))}catch{}}const T=["file_path","chunk_index","total_chunks","content_hash","chunk_text"];function I(e){const{processor:r,vectorStore:i,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:r,logger:s},async(e,t)=>{try{const{path:t,metadata:r}=e.body;return await l.processor.processMetadataUpdate(t,r),{ok:!0}}catch(e){return l.logger.error({err:P(e)},"Metadata update failed"),t.status(500).send({error:"Internal server error"})}})),c.post("/search",function(e){return async(t,r)=>{try{const{query:r,limit:i=10}=t.body,o=await e.embeddingProvider.embed([r]);return await e.vectorStore.search(o[0],i)}catch(t){return e.logger.error({err:P(t)},"Search failed"),r.status(500).send({error:"Internal server error"})}}}({embeddingProvider:o,vectorStore:i,logger:s})),c.post("/reindex",function(e){return async(t,r)=>{try{const t=await F(e.config.watch.paths,e.config.watch.ignored,e.processor,"processFile");return await r.status(200).send({ok:!0,filesIndexed:t})}catch(t){return e.logger.error({err:P(t)},"Reindex failed"),await r.status(500).send({error:"Internal server error"})}}}({config:a,processor:r,logger:s})),c.post("/rebuild-metadata",function(e){return async(t,r)=>{try{const t=e.config.metadataDir??".jeeves-metadata",i=[...T];for await(const r of e.vectorStore.scroll()){const e=r.payload,o=e.file_path;if("string"!=typeof o||0===o.length)continue;const s=n.omit(e,i);await E(o,t,s)}return await r.status(200).send({ok:!0})}catch(t){return e.logger.error({err:P(t)},"Rebuild metadata failed"),await r.status(500).send({error:"Internal server error"})}}}({config:a,vectorStore:i,logger:s})),c.post("/config-reindex",function(e){return async(t,r)=>{try{const i=t.body.scope??"rules";return(async()=>{try{if("rules"===i){const t=await F(e.config.watch.paths,e.config.watch.ignored,e.processor,"processRulesUpdate");e.logger.info({scope:i,filesProcessed:t},"Config reindex (rules) completed")}else{const t=await F(e.config.watch.paths,e.config.watch.ignored,e.processor,"processFile");e.logger.info({scope:i,filesProcessed:t},"Config reindex (full) completed")}}catch(t){e.logger.error({err:P(t),scope:i},"Config reindex failed")}})(),await r.status(200).send({status:"started",scope:i})}catch(t){return e.logger.error({err:P(t)},"Config reindex request failed"),await r.status(500).send({error:"Internal server error"})}}}({config:a,processor:r,logger:s})),c}const D={metadataDir:".jeeves-watcher",shutdownTimeoutMs:1e4},N={enabled:!0,debounceMs:1e3},A={host:"127.0.0.1",port:3456},O={level:"info"},W={debounceMs:300,stabilityThresholdMs:500,usePolling:!1,pollIntervalMs:1e3},_={chunkSize:1e3,chunkOverlap:200,dimensions:3072,rateLimitPerMinute:300,concurrency:5},q=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.")}),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).")}),B=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:q.describe("File system watch configuration."),configWatch:L.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:B.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."),maxRetries:c.z.number().optional().describe("Maximum consecutive system-level failures before triggering fatal error. Default: Infinity."),maxBackoffMs:c.z.number().optional().describe("Maximum backoff delay in milliseconds for system errors. Default: 60000.")}),V=/\$\{([^}]+)\}/g;function U(e){if("string"==typeof e)return function(e){return e.replace(V,((e,t)=>{const r=process.env[t];return void 0===r?e:r}))}(e);if(Array.isArray(e))return e.map((e=>U(e)));if(null!==e&&"object"==typeof e){const t={};for(const[r,i]of Object.entries(e))t[r]=U(i);return t}return e}const H="jeeves-watcher";async function Y(e){const t=a.cosmiconfig(H),r=e?await t.load(e):await t.search();if(!r||r.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(r.config);return U((i=e,{...D,...i,watch:{...W,...i.watch},configWatch:{...N,...i.configWatch},embedding:{..._,...i.embedding},api:{...A,...i.api},logging:{...O,...i.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 i}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(((r,i)=>{const o=setTimeout((()=>{s(),r()}),e),n=()=>{s(),i(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,r,i=0){const o=Math.max(0,e-1),n=Math.min(r,t*2**o),s=i>0?1+Math.random()*i:1;return Math.round(n*s)}async function te(e,t){const r=Math.max(1,t.attempts);let i;for(let o=1;o<=r;o++)try{return await e(o)}catch(e){i=e;if(o>=r)break;const n=ee(o,t.baseDelayMs,t.maxDelayMs,t.jitter);t.onRetry?.({attempt:o,attempts:r,delayMs:n,error:e}),await X(n,t.signal)}throw i}const re=new Map([["mock",function(e){return function(e){return{dimensions:e,embed:t=>Promise.resolve(t.map((t=>{const r=s.createHash("sha256").update(t,"utf8").digest(),i=[];for(let t=0;t<e;t++){const e=r[t%r.length];i.push(e/127.5-1)}return i})))}}(e.dimensions??768)}],["gemini",function(e,t){if(!e.apiKey)throw new Error("Gemini embedding provider requires config.embedding.apiKey");const r=e.dimensions??3072,i=Z(t),o=new h.GoogleGenerativeAIEmbeddings({apiKey:e.apiKey,model:e.model});return{dimensions:r,async embed(t){const n=await te((async r=>(r>1&&i.warn({attempt:r,provider:"gemini",model:e.model},"Retrying embedding request"),o.embedDocuments(t))),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:t,delayMs:r,error:o})=>{i.warn({attempt:t,delayMs:r,provider:"gemini",model:e.model,err:P(o)},"Embedding call failed; will retry")}});for(const e of n)if(e.length!==r)throw new Error(`Gemini embedding returned invalid dimensions: expected ${String(r)}, got ${String(e.length)}`);return n}}}]]);function ie(e,t){const r=re.get(e.provider);if(!r)throw new Error(`Unsupported embedding provider: ${e.provider}`);return r(e,t)}function oe(e){const t=e?.level??"info";if(e?.file){const r=u.transport({target:"pino/file",options:{destination:e.file,mkdir:!0}});return u({level:t},r)}return u({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 r=void 0!==t?`${z(e)}#${String(t)}`:z(e);return d.v5(r,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 r=t[e];if("string"==typeof r&&r.trim())return r}return JSON.stringify(e)}async function he(e){const t=await r.readFile(e,"utf8"),{frontmatter:i,body:o}=function(e){const t=e.replace(/^\uFEFF/,"");if(!/^\s*---/.test(t))return{body:e};const r=/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/m.exec(t);if(!r)return{body:e};const[,i,o]=r,n=g.load(i);return{frontmatter:n&&"object"==typeof n&&!Array.isArray(n)?n:void 0,body:o}}(t);return{text:o,frontmatter:i}}async function ue(e){return{text:(await r.readFile(e,"utf8")).replace(/^\uFEFF/,"")}}async function de(e){const t=await r.readFile(e,"utf8"),i=x.load(t.replace(/^\uFEFF/,""));i("script, style").remove();return{text:i("body").text().trim()||i.text().trim()}}const fe=new Map([[".md",he],[".markdown",he],[".txt",ue],[".text",ue],[".json",async function(e){const t=await r.readFile(e,"utf8"),i=JSON.parse(t.replace(/^\uFEFF/,"")),o=i&&"object"==typeof i&&!Array.isArray(i)?i:void 0;return{text:le(i),json:o}}],[".pdf",async function(e){const t=await r.readFile(e),i=new Uint8Array(t),{extractText:o}=await import("unpdf"),{text:n}=await o(i);return{text:Array.isArray(n)?n.join("\n\n"):n}}],[".docx",async function(e){const t=await r.readFile(e);return{text:(await m.extractRawText({buffer:t})).value}}],[".html",de],[".htm",de]]);async function ge(e,t){const r=fe.get(t.toLowerCase());return r?r(e):ue(e)}function me(e,t){return"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,((e,r)=>{const i=n.get(t,r);return null==i?"":"string"==typeof i?i:JSON.stringify(i)}))}function pe(e,t){const r={};for(const[i,o]of Object.entries(e))r[i]=me(o,t);return r}async function ye(e,t,r,i){const o={split:(e,t)=>e.split(t),slice:(e,t,r)=>e.slice(t,r),join:(e,t)=>e.join(t),toLowerCase:e=>e.toLowerCase(),replace:(e,t,r)=>e.replace(t,r),get:(e,t)=>n.get(e,t)};let s={};const a=i??console;for(const{rule:i,validate:n}of e)if(n(t)){const e=pe(i.set,t);if(s={...s,...e},i.map){let e;if("string"==typeof i.map){if(e=r?.[i.map],!e){a.warn(`Map reference "${i.map}" not found in named maps. Skipping map transformation.`);continue}}else e=i.map;try{const r=new l.JsonMap(e,o),i=await r.transform(t);i&&"object"==typeof i&&!Array.isArray(i)?s={...s,...i}: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,r,o){const n=e.replace(/\\/g,"/"),s={file:{path:n,directory:i.dirname(n).replace(/\\/g,"/"),filename:i.basename(n),extension:i.extname(n),sizeBytes:t.size,modified:t.mtime.toISOString()}};return r&&(s.frontmatter=r),o&&(s.json=o),s}function be(e){const t=function(){const e=new p({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,r)=>({rule:e,validate:t.compile({$id:`rule-${String(r)}`,...e.match})})))}async function ve(e,t,o,n,s){const a=i.extname(e),c=await r.stat(e),l=await ge(e,a),h=we(e,c,l.frontmatter,l.json),u=await ye(t,h,n,s),d=await R(e,o);return{inferred:u,enrichment:d,metadata:{...u,...d??{}},attributes:h,extracted:l}}function Me(e,t){const r=[];for(let i=0;i<t;i++)r.push(ae(e,i));return r}function xe(e,t=1){if(!e)return t;const r=e.total_chunks;return"number"==typeof r?r:t}class Pe{config;embeddingProvider;vectorStore;compiledRules;logger;constructor(e,t,r,i,o){this.config=e,this.embeddingProvider=t,this.vectorStore=r,this.compiledRules=i,this.logger=o}async processFile(e){try{const t=i.extname(e),{metadata:r,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=xe(a),l=this.config.chunkSize??1e3,h=function(e,t,r){const i=e.toLowerCase();return".md"===i||".markdown"===i?new w.MarkdownTextSplitter({chunkSize:t,chunkOverlap:r}):new w.RecursiveCharacterTextSplitter({chunkSize:t,chunkOverlap:r})}(t,l,this.config.chunkOverlap??200),u=await h.splitText(o.text),d=await this.embeddingProvider.embed(u),f=u.map(((t,i)=>({id:ae(e,i),vector:d[i],payload:{...r,file_path:e.replace(/\\/g,"/"),chunk_index:i,total_chunks:u.length,content_hash:n,chunk_text:t}})));if(await this.vectorStore.upsert(f),c>u.length){const t=Me(e,c).slice(u.length);await this.vectorStore.delete(t)}this.logger.info({filePath:e,chunks:u.length},"File processed successfully")}catch(t){this.logger.error({filePath:e,err:P(t)},"Failed to process file")}}async deleteFile(e){try{const t=ae(e,0),r=await this.vectorStore.getPayload(t),i=Me(e,xe(r));await this.vectorStore.delete(i),await C(e,this.config.metadataDir),this.logger.info({filePath:e},"File deleted from index")}catch(t){this.logger.error({filePath:e,err:P(t)},"Failed to delete file")}}async processMetadataUpdate(e,t){try{const r={...await R(e,this.config.metadataDir)??{},...t};await E(e,this.config.metadataDir,r);const i=ae(e,0),o=await this.vectorStore.getPayload(i);if(!o)return null;const n=xe(o),s=Me(e,n);return await this.vectorStore.setPayload(s,r),this.logger.info({filePath:e,chunks:n},"Metadata updated"),r}catch(t){return this.logger.error({filePath:e,err:P(t)},"Failed to update metadata"),null}}async processRulesUpdate(e){try{const t=ae(e,0),r=await this.vectorStore.getPayload(t);if(!r)return this.logger.debug({filePath:e},"File not indexed, skipping"),null;const{metadata:i}=await ve(e,this.compiledRules,this.config.metadataDir,this.config.maps,this.logger),o=xe(r),n=Me(e,o);return await this.vectorStore.setPayload(n,i),this.logger.info({filePath:e,chunks:o},"Rules re-applied"),i}catch(t){return this.logger.error({filePath:e,err:P(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 r=`${e.priority}:${e.path}`;this.latestByKey.set(r,{event:e,fn:t});const i=this.debounceTimers.get(r);i&&clearTimeout(i);const o=setTimeout((()=>{this.debounceTimers.delete(r);const e=this.latestByKey.get(r);e&&(this.latestByKey.delete(r),this.push(e),this.pump())}),this.debounceMs);this.debounceTimers.set(r,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 Se{client;collectionName;dims;log;constructor(e,t,r){this.client=new b.QdrantClient({url:e.url,apiKey:e.apiKey,checkCompatibility:!1}),this.collectionName=e.collectionName,this.dims=t,this.log=Z(r)}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:r})=>{this.log.warn({attempt:e,delayMs:t,operation:"qdrant.upsert",err:P(r)},"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:r})=>{this.log.warn({attempt:e,delayMs:t,operation:"qdrant.delete",err:P(r)},"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,r){return(await this.client.search(this.collectionName,{vector:e,limit:t,with_payload:!0,...r?{filter:r}:{}})).map((e=>({id:String(e.id),score:e.score,payload:e.payload})))}async*scroll(e,t=100){let r;for(;;){const i=await this.client.scroll(this.collectionName,{limit:t,with_payload:!0,with_vector:!1,...e?{filter:e}:{},...void 0!==r?{offset:r}:{}});for(const e of i.points)yield{id:String(e.id),payload:e.payload};const o=i.next_page_offset;if(null==o)break;if("string"!=typeof o&&"number"!=typeof o)break;r=o}}}class Fe{consecutiveFailures=0;maxRetries;maxBackoffMs;baseDelayMs;onFatalError;logger;constructor(e){this.maxRetries=e.maxRetries??Number.POSITIVE_INFINITY,this.maxBackoffMs=e.maxBackoffMs??6e4,this.baseDelayMs=e.baseDelayMs??1e3,this.onFatalError=e.onFatalError,this.logger=e.logger}recordSuccess(){this.consecutiveFailures>0&&this.logger.info({previousFailures:this.consecutiveFailures},"System health recovered"),this.consecutiveFailures=0}recordFailure(e){if(this.consecutiveFailures+=1,this.logger.error({consecutiveFailures:this.consecutiveFailures,maxRetries:this.maxRetries,err:P(e)},"System-level failure recorded"),this.consecutiveFailures>=this.maxRetries){if(this.logger.fatal({consecutiveFailures:this.consecutiveFailures},"Maximum retries exceeded, triggering fatal error"),this.onFatalError)return this.onFatalError(e),!1;throw e instanceof Error?e:new Error(`Fatal system error: ${String(e)}`)}return!0}get currentBackoffMs(){if(0===this.consecutiveFailures)return 0;const e=Math.max(0,this.consecutiveFailures-1);return Math.min(this.maxBackoffMs,this.baseDelayMs*2**e)}async backoff(e){const t=this.currentBackoffMs;t<=0||(this.logger.warn({delayMs:t,consecutiveFailures:this.consecutiveFailures},"Backing off before next attempt"),await new Promise(((r,i)=>{const o=setTimeout((()=>{s(),r()}),t),n=()=>{s(),i(new Error("Backoff aborted"))},s=()=>{clearTimeout(o),e&&e.removeEventListener("abort",n)};if(e){if(e.aborted)return void n();e.addEventListener("abort",n,{once:!0})}})))}get failures(){return this.consecutiveFailures}}class ze{config;queue;processor;logger;health;watcher;constructor(e,t,r,i,o={}){this.config=e,this.queue=t,this.processor=r,this.logger=i;const n={maxRetries:o.maxRetries,maxBackoffMs:o.maxBackoffMs,onFatalError:o.onFatalError,logger:i};this.health=new Fe(n)}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.wrapProcessing((()=>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.wrapProcessing((()=>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.wrapProcessing((()=>this.processor.deleteFile(e)))))})),this.watcher.on("error",(e=>{this.logger.error({err:P(e)},"Watcher error"),this.health.recordFailure(e)})),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"))}get systemHealth(){return this.health}async wrapProcessing(e){try{await this.health.backoff(),await e(),this.health.recordSuccess()}catch(e){this.health.recordFailure(e)||await this.stop()}}}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:P(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 Re={loadConfig:Y,createLogger:oe,createEmbeddingProvider:ie,createVectorStoreClient:(e,t,r)=>new Se(e,t,r),compileRules:be,createDocumentProcessor:(e,t,r,i,o)=>new Pe(e,t,r,i,o),createEventQueue:e=>new ke(e),createFileSystemWatcher:(e,t,r,i,o)=>new ze(e,t,r,i,o),createApiServer:I};class Ee{config;configPath;factories;runtimeOptions;logger;watcher;queue;server;processor;configWatcher;constructor(e,t,r={},i={}){this.config=e,this.configPath=t,this.factories={...Re,...r},this.runtimeOptions=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:P(t)},"Failed to create embedding provider"),t}const r=this.factories.createVectorStoreClient(this.config.vectorStore,t.dimensions,e);await r.ensureCollection();const i=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,r,i,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,{maxRetries:this.config.maxRetries,maxBackoffMs:this.config.maxBackoffMs,onFatalError:this.runtimeOptions.onFatalError});this.watcher=a;const c=this.factories.createApiServer({processor:n,vectorStore:r,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 r=this.config.configWatch?.debounceMs??1e4;this.configWatcher=new je({configPath:this.configPath,enabled:t,debounceMs:r,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 r=await this.factories.loadConfig(this.configPath);this.config=r;const i=this.factories.compileRules(r.inferenceRules??[]);t.updateRules(i),e.info({configPath:this.configPath,rules:i.length},"Config reloaded")}catch(t){e.error({err:P(t)},"Failed to reload config")}}}}e.DocumentProcessor=Pe,e.EventQueue=ke,e.FileSystemWatcher=ze,e.JeevesWatcher=Ee,e.SystemHealth=Fe,e.VectorStoreClient=Se,e.apiConfigSchema=B,e.applyRules=ye,e.buildAttributes=we,e.compileRules=be,e.configWatchConfigSchema=L,e.contentHash=ne,e.createApiServer=I,e.createEmbeddingProvider=ie,e.createLogger=oe,e.deleteMetadata=C,e.embeddingConfigSchema=Q,e.extractText=ge,e.inferenceRuleSchema=J,e.jeevesWatcherConfigSchema=G,e.loadConfig=Y,e.loggingConfigSchema=K,e.metadataPath=j,e.pointId=ae,e.readMetadata=R,e.startFromConfig=async function(e){const t=await Y(e),r=new Ee(t,e);return function(e){const t=async()=>{await e(),process.exit(0)};process.on("SIGTERM",(()=>{t()})),process.on("SIGINT",(()=>{t()}))}((()=>r.stop())),await r.start(),r},e.vectorStoreConfigSchema=$,e.watchConfigSchema=q,e.writeMetadata=E}(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,h,d,u,g,f,p,m,y,w,b,v,M,x){"use strict";function S(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 F=S(p);function P(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*z(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*z(t);else try{(await i.stat(t)).isFile()&&(yield t)}catch{}}}async function j(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 z(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 R(e,t=!1){let i=e.replace(/\\/g,"/").toLowerCase();return t&&(i=i.replace(/^([a-z]):/,((e,t)=>t))),i}function E(e,t){const i=R(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(E(e,t),"utf8");return JSON.parse(r)}catch{return null}}async function D(e,t,o){const n=E(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(E(e,t))}catch{}}const I=["file_path","chunk_index","total_chunks","content_hash","chunk_text"];function W(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:P(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:P(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 j(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:P(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=[...I];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 D(o,t,s)}return await i.status(200).send({ok:!0})}catch(t){return e.logger.error({err:P(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 j(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 j(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:P(t),scope:r},"Config reindex failed")}})(),await i.status(200).send({status:"started",scope:r})}catch(t){return e.logger.error({err:P(t)},"Config reindex request failed"),await i.status(500).send({error:"Internal server error"})}}}({config:a,processor:i,logger:s})),c}const N={metadataDir:".jeeves-watcher",shutdownTimeoutMs:1e4},O={enabled:!0,debounceMs:1e3},_={host:"127.0.0.1",port:3456},A={level:"info"},q={debounceMs:300,stabilityThresholdMs:500,usePolling:!1,pollIntervalMs:1e3,respectGitignore:!0},L={chunkSize:1e3,chunkOverlap:200,dimensions:3072,rateLimitPerMinute:300,concurrency:5},Q=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."),respectGitignore:c.z.boolean().optional().describe("Skip files ignored by .gitignore in git repositories. Only applies to repos with a .git directory. Default: true.")}),$=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.")}),G=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.")}),B=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).")}),V=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:Q.describe("File system watch configuration."),configWatch:$.optional().describe("Configuration file watch settings."),embedding:G.describe("Embedding model configuration."),vectorStore:B.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(V).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."),maxRetries:c.z.number().optional().describe("Maximum consecutive system-level failures before triggering fatal error. Default: Infinity."),maxBackoffMs:c.z.number().optional().describe("Maximum backoff delay in milliseconds for system errors. Default: 60000.")}),H=/\$\{([^}]+)\}/g;function Y(e){if("string"==typeof e)return function(e){return e.replace(H,((e,t)=>{const i=process.env[t];return void 0===i?e:i}))}(e);if(Array.isArray(e))return e.map((e=>Y(e)));if(null!==e&&"object"==typeof e){const t={};for(const[i,r]of Object.entries(e))t[i]=Y(r);return t}return e}const Z="jeeves-watcher";async function X(e){const t=a.cosmiconfig(Z),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 Y((r=e,{...N,...r,watch:{...q,...r.watch},configWatch:{...O,...r.configWatch},embedding:{...L,...r.embedding},api:{..._,...r.api},logging:{...A,...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 ee(e){return e||{warn(e,t){t?console.warn(e,t):console.warn(e)}}}function te(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 ie(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 re(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=ie(o,t.baseDelayMs,t.maxDelayMs,t.jitter);t.onRetry?.({attempt:o,attempts:i,delayMs:n,error:e}),await te(n,t.signal)}throw r}const oe=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=ee(t),o=new h.GoogleGenerativeAIEmbeddings({apiKey:e.apiKey,model:e.model});return{dimensions:i,async embed(t){const n=await re((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:P(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 ne(e,t){const i=oe.get(e.provider);if(!i)throw new Error(`Unsupported embedding provider: ${e.provider}`);return i(e,t)}function se(e){let t=r.resolve(e);const i=r.resolve("/");for(;t!==i;){if(d.existsSync(r.join(t,".git"))&&d.statSync(r.join(t,".git")).isDirectory())return t;const e=r.dirname(t);if(e===t)break;t=e}}function ae(e){const t=r.resolve(e);try{return d.statSync(t).isDirectory()?t:r.dirname(t)}catch{}const i=/[*?[{]/.exec(e);if(!i)return;const o=e.slice(0,i.index).trim(),n=0===o.length?".":o.endsWith("/")||o.endsWith("\\")?o:r.dirname(o),s=r.resolve(n);return d.existsSync(s)?s:void 0}function ce(e){const t=[],i=r.join(e,".gitignore");let o;d.existsSync(i)&&t.push(i);try{o=d.readdirSync(e)}catch{return t}for(const i of o){if(".git"===i||"node_modules"===i)continue;const o=r.join(e,i);try{d.statSync(o).isDirectory()&&t.push(...ce(o))}catch{}}return t}function le(e){const t=d.readFileSync(e,"utf8");return u().add(t)}class he{repos=new Map;constructor(e){this.scan(e)}scan(e){this.repos.clear();const t=new Set;for(const i of e){const e=ae(i);if(!e)continue;if(t.has(e))continue;t.add(e);const o=se(e);if(!o)continue;if(this.repos.has(o))continue;const n=ce(o).map((e=>({dir:r.dirname(e),ig:le(e)})));n.sort(((e,t)=>t.dir.length-e.dir.length)),this.repos.set(o,{root:o,entries:n})}}isIgnored(e){const t=r.resolve(e);for(const[,e]of this.repos){const i=r.relative(e.root,t);if(!i.startsWith("..")&&!i.startsWith(r.resolve("/")))for(const i of e.entries){const e=r.relative(i.dir,t);if(e.startsWith(".."))continue;const o=e.replace(/\\/g,"/");if(i.ig.ignores(o))return!0}}return!1}invalidate(e){const t=r.resolve(e),i=r.dirname(t);for(const[,e]of this.repos){if(!r.relative(e.root,i).startsWith(".."))return e.entries=e.entries.filter((e=>e.dir!==i)),void(d.existsSync(t)&&(e.entries.push({dir:i,ig:le(t)}),e.entries.sort(((e,t)=>t.dir.length-e.dir.length))))}const o=se(i);if(o&&d.existsSync(t)){const e=[{dir:i,ig:le(t)}];if(this.repos.has(o)){const t=this.repos.get(o);t.entries.push(e[0]),t.entries.sort(((e,t)=>t.dir.length-e.dir.length))}else this.repos.set(o,{root:o,entries:e})}}}function de(e){const t=e?.level??"info";if(e?.file){const i=g.transport({target:"pino/file",options:{destination:e.file,mkdir:!0}});return g({level:t},i)}return g({level:t})}function ue(e){return s.createHash("sha256").update(e,"utf8").digest("hex")}const ge="6a6f686e-6761-4c74-ad6a-656576657321";function fe(e,t){const i=void 0!==t?`${R(e)}#${String(t)}`:R(e);return f.v5(i,ge)}const pe=["content","body","text","snippet","subject","description","summary","transcript"];function me(e){if(!e||"object"!=typeof e)return JSON.stringify(e);const t=e;for(const e of pe){const i=t[e];if("string"==typeof i&&i.trim())return i}return JSON.stringify(e)}async function ye(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=m.load(r);return{frontmatter:n&&"object"==typeof n&&!Array.isArray(n)?n:void 0,body:o}}(t);return{text:o,frontmatter:r}}async function we(e){return{text:(await i.readFile(e,"utf8")).replace(/^\uFEFF/,"")}}async function be(e){const t=await i.readFile(e,"utf8"),r=F.load(t.replace(/^\uFEFF/,""));r("script, style").remove();return{text:r("body").text().trim()||r.text().trim()}}const ve=new Map([[".md",ye],[".markdown",ye],[".txt",we],[".text",we],[".json",async function(e){const t=await i.readFile(e,"utf8"),r=JSON.parse(t.replace(/^\uFEFF/,"")),o=r&&"object"==typeof r&&!Array.isArray(r)?r:void 0;return{text:me(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 y.extractRawText({buffer:t})).value}}],[".html",be],[".htm",be]]);async function Me(e,t){const i=ve.get(t.toLowerCase());return i?i(e):we(e)}function xe(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 Se(e,t){const i={};for(const[r,o]of Object.entries(e))i[r]=xe(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=Se(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 ke(e){const t=function(){const e=new w({allErrors:!0});return b(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 ze(e,t,o,n,s){const a=r.extname(e),c=await i.stat(e),l=await Me(e,a),h=Pe(e,c,l.frontmatter,l.json),d=await Fe(t,h,n,s),u=await C(e,o);return{inferred:d,enrichment:u,metadata:{...d,...u??{}},attributes:h,extracted:l}}function je(e,t){const i=[];for(let r=0;r<t;r++)i.push(fe(e,r));return i}function Re(e,t=1){if(!e)return t;const i=e.total_chunks;return"number"==typeof i?i:t}class Ee{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 ze(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=ue(o.text),s=fe(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=Re(a),l=this.config.chunkSize??1e3,h=function(e,t,i){const r=e.toLowerCase();return".md"===r||".markdown"===r?new v.MarkdownTextSplitter({chunkSize:t,chunkOverlap:i}):new v.RecursiveCharacterTextSplitter({chunkSize:t,chunkOverlap:i})}(t,l,this.config.chunkOverlap??200),d=await h.splitText(o.text),u=await this.embeddingProvider.embed(d),g=d.map(((t,r)=>({id:fe(e,r),vector:u[r],payload:{...i,file_path:e.replace(/\\/g,"/"),chunk_index:r,total_chunks:d.length,content_hash:n,chunk_text:t}})));if(await this.vectorStore.upsert(g),c>d.length){const t=je(e,c).slice(d.length);await this.vectorStore.delete(t)}this.logger.info({filePath:e,chunks:d.length},"File processed successfully")}catch(t){this.logger.error({filePath:e,err:P(t)},"Failed to process file")}}async deleteFile(e){try{const t=fe(e,0),i=await this.vectorStore.getPayload(t),r=je(e,Re(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:P(t)},"Failed to delete file")}}async processMetadataUpdate(e,t){try{const i={...await C(e,this.config.metadataDir)??{},...t};await D(e,this.config.metadataDir,i);const r=fe(e,0),o=await this.vectorStore.getPayload(r);if(!o)return null;const n=Re(o),s=je(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:P(t)},"Failed to update metadata"),null}}async processRulesUpdate(e){try{const t=fe(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 ze(e,this.compiledRules,this.config.metadataDir,this.config.maps,this.logger),o=Re(i),n=je(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:P(t)},"Failed to re-apply rules"),null}}updateRules(e){this.compiledRules=e,this.logger.info({rules:e.length},"Inference rules updated")}}class Ce{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 De{client;collectionName;dims;log;constructor(e,t,i){this.client=new M.QdrantClient({url:e.url,apiKey:e.apiKey,checkCompatibility:!1}),this.collectionName=e.collectionName,this.dims=t,this.log=ee(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 re((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:P(i)},"Qdrant upsert failed; will retry")}})}async delete(e){0!==e.length&&await re((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:P(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 Te{consecutiveFailures=0;maxRetries;maxBackoffMs;baseDelayMs;onFatalError;logger;constructor(e){this.maxRetries=e.maxRetries??Number.POSITIVE_INFINITY,this.maxBackoffMs=e.maxBackoffMs??6e4,this.baseDelayMs=e.baseDelayMs??1e3,this.onFatalError=e.onFatalError,this.logger=e.logger}recordSuccess(){this.consecutiveFailures>0&&this.logger.info({previousFailures:this.consecutiveFailures},"System health recovered"),this.consecutiveFailures=0}recordFailure(e){if(this.consecutiveFailures+=1,this.logger.error({consecutiveFailures:this.consecutiveFailures,maxRetries:this.maxRetries,err:P(e)},"System-level failure recorded"),this.consecutiveFailures>=this.maxRetries){if(this.logger.fatal({consecutiveFailures:this.consecutiveFailures},"Maximum retries exceeded, triggering fatal error"),this.onFatalError)return this.onFatalError(e),!1;throw e instanceof Error?e:new Error(`Fatal system error: ${String(e)}`)}return!0}get currentBackoffMs(){if(0===this.consecutiveFailures)return 0;const e=Math.max(0,this.consecutiveFailures-1);return Math.min(this.maxBackoffMs,this.baseDelayMs*2**e)}async backoff(e){const t=this.currentBackoffMs;t<=0||(this.logger.warn({delayMs:t,consecutiveFailures:this.consecutiveFailures},"Backing off before next attempt"),await new Promise(((i,r)=>{const o=setTimeout((()=>{s(),i()}),t),n=()=>{s(),r(new Error("Backoff aborted"))},s=()=>{clearTimeout(o),e&&e.removeEventListener("abort",n)};if(e){if(e.aborted)return void n();e.addEventListener("abort",n,{once:!0})}})))}get failures(){return this.consecutiveFailures}}class Ie{config;queue;processor;logger;health;gitignoreFilter;watcher;constructor(e,t,i,r,o={}){this.config=e,this.queue=t,this.processor=i,this.logger=r,this.gitignoreFilter=o.gitignoreFilter;const n={maxRetries:o.maxRetries,maxBackoffMs:o.maxBackoffMs,onFatalError:o.onFatalError,logger:r};this.health=new Te(n)}start(){this.watcher=x.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.handleGitignoreChange(e),this.isGitignored(e)||(this.logger.debug({path:e},"File added"),this.queue.enqueue({type:"create",path:e,priority:"normal"},(()=>this.wrapProcessing((()=>this.processor.processFile(e))))))})),this.watcher.on("change",(e=>{this.handleGitignoreChange(e),this.isGitignored(e)||(this.logger.debug({path:e},"File changed"),this.queue.enqueue({type:"modify",path:e,priority:"normal"},(()=>this.wrapProcessing((()=>this.processor.processFile(e))))))})),this.watcher.on("unlink",(e=>{this.handleGitignoreChange(e),this.isGitignored(e)||(this.logger.debug({path:e},"File removed"),this.queue.enqueue({type:"delete",path:e,priority:"normal"},(()=>this.wrapProcessing((()=>this.processor.deleteFile(e))))))})),this.watcher.on("error",(e=>{this.logger.error({err:P(e)},"Watcher error"),this.health.recordFailure(e)})),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"))}get systemHealth(){return this.health}isGitignored(e){if(!this.gitignoreFilter)return!1;const t=this.gitignoreFilter.isIgnored(e);return t&&this.logger.debug({path:e},"Skipping gitignored file"),t}handleGitignoreChange(e){this.gitignoreFilter&&e.endsWith(".gitignore")&&(this.logger.info({path:e},"Gitignore file changed, refreshing filter"),this.gitignoreFilter.invalidate(e))}async wrapProcessing(e){try{await this.health.backoff(),await e(),this.health.recordSuccess()}catch(e){this.health.recordFailure(e)||await this.stop()}}}class We{options;watcher;debounce;constructor(e){this.options=e}start(){this.options.enabled&&(this.watcher=x.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:P(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 Ne={loadConfig:X,createLogger:de,createEmbeddingProvider:ne,createVectorStoreClient:(e,t,i)=>new De(e,t,i),compileRules:ke,createDocumentProcessor:(e,t,i,r,o)=>new Ee(e,t,i,r,o),createEventQueue:e=>new Ce(e),createFileSystemWatcher:(e,t,i,r,o)=>new Ie(e,t,i,r,o),createApiServer:W};class Oe{config;configPath;factories;runtimeOptions;logger;watcher;queue;server;processor;configWatcher;constructor(e,t,i={},r={}){this.config=e,this.configPath=t,this.factories={...Ne,...i},this.runtimeOptions=r}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:P(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.config.watch.respectGitignore??!0?new he(this.config.watch.paths):void 0,c=this.factories.createFileSystemWatcher(this.config.watch,s,n,e,{maxRetries:this.config.maxRetries,maxBackoffMs:this.config.maxBackoffMs,onFatalError:this.runtimeOptions.onFatalError,gitignoreFilter:a});this.watcher=c;const l=this.factories.createApiServer({processor:n,vectorStore:i,embeddingProvider:t,queue:s,config:this.config,logger:e});this.server=l,await l.listen({host:this.config.api?.host??"127.0.0.1",port:this.config.api?.port??3456}),c.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 We({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:P(t)},"Failed to reload config")}}}}e.DocumentProcessor=Ee,e.EventQueue=Ce,e.FileSystemWatcher=Ie,e.GitignoreFilter=he,e.JeevesWatcher=Oe,e.SystemHealth=Te,e.VectorStoreClient=De,e.apiConfigSchema=K,e.applyRules=Fe,e.buildAttributes=Pe,e.compileRules=ke,e.configWatchConfigSchema=$,e.contentHash=ue,e.createApiServer=W,e.createEmbeddingProvider=ne,e.createLogger=de,e.deleteMetadata=T,e.embeddingConfigSchema=G,e.extractText=Me,e.inferenceRuleSchema=V,e.jeevesWatcherConfigSchema=U,e.loadConfig=X,e.loggingConfigSchema=J,e.metadataPath=E,e.pointId=fe,e.readMetadata=C,e.startFromConfig=async function(e){const t=await X(e),i=new Oe(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=B,e.watchConfigSchema=Q,e.writeMetadata=D}(this["jeeves-watcher"]=this["jeeves-watcher"]||{},Fastify,promises,node_path,picomatch,radash,node_crypto,cosmiconfig,zod,jsonmap,googleGenai,node_fs,ignore,pino,uuid,cheerio,yaml,mammoth,Ajv,addFormats,textsplitters,jsClientRest,chokidar);