@karmaniverous/jeeves-watcher 0.14.0 → 0.15.1

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
@@ -1637,8 +1637,10 @@ interface ApiServerOptions {
1637
1637
  embeddingProvider: EmbeddingProvider;
1638
1638
  /** The event queue. */
1639
1639
  queue: EventQueue;
1640
- /** The application configuration. */
1640
+ /** The application configuration (used as initial/fallback value). */
1641
1641
  config: JeevesWatcherConfig;
1642
+ /** Config getter for live config access after hot-reload. */
1643
+ getConfig?: () => JeevesWatcherConfig;
1642
1644
  /** The logger instance. */
1643
1645
  logger: pino.Logger;
1644
1646
  /** The issues manager. */
@@ -1734,9 +1736,7 @@ declare function startFromConfig(configPath?: string): Promise<JeevesWatcher>;
1734
1736
  * Main application orchestrator. Wires components, manages lifecycle (start/stop/reload).
1735
1737
  */
1736
1738
 
1737
- /**
1738
- * Runtime options for {@link JeevesWatcher} that aren't serializable in config.
1739
- */
1739
+ /** Runtime options for {@link JeevesWatcher} that aren't serializable in config. */
1740
1740
  interface JeevesWatcherRuntimeOptions {
1741
1741
  /** Callback invoked on unrecoverable system error. If not set, throws. */
1742
1742
  onFatalError?: (error: unknown) => void;
package/dist/index.js CHANGED
@@ -1561,12 +1561,13 @@ function wrapHandler(fn, logger, label) {
1561
1561
  function createConfigApplyHandler(deps) {
1562
1562
  return wrapHandler(async (request, reply) => {
1563
1563
  const { config: submittedConfig } = request.body;
1564
- const { candidateRaw, errors } = mergeAndValidateConfig(deps.config, submittedConfig);
1564
+ const config = deps.getConfig();
1565
+ const { candidateRaw, errors } = mergeAndValidateConfig(config, submittedConfig);
1565
1566
  if (errors.length > 0) {
1566
1567
  return await reply.status(400).send({ valid: false, errors });
1567
1568
  }
1568
1569
  await writeFile(deps.configPath, JSON.stringify(candidateRaw, null, 2), 'utf-8');
1569
- const reindexScope = deps.config.configWatch?.reindex ?? 'issues';
1570
+ const reindexScope = config.configWatch?.reindex ?? 'issues';
1570
1571
  if (deps.triggerReindex) {
1571
1572
  deps.triggerReindex(reindexScope);
1572
1573
  }
@@ -1706,32 +1707,38 @@ function compileRules(rules) {
1706
1707
  * @returns The handler function.
1707
1708
  */
1708
1709
  function createConfigMatchHandler(options) {
1709
- const { config, logger } = options;
1710
- // Compile rules once at handler creation time
1711
- const compiledRules = compileRules(config.inferenceRules ?? []);
1712
- // Compile watch path matchers
1713
- const watchMatcher = picomatch(config.watch.paths, { dot: true });
1714
- const ignoreMatcher = config.watch.ignored?.length
1715
- ? picomatch(config.watch.ignored, { dot: true })
1716
- : null;
1710
+ const { getConfig, logger } = options;
1711
+ // Cache compiled artifacts per config reference — recompute only on hot-reload.
1712
+ let cachedConfig;
1713
+ let compiledRules = [];
1714
+ let watchMatcher;
1715
+ let ignoreMatcher;
1717
1716
  const handler = async (req, res) => {
1718
1717
  const body = req.body;
1719
1718
  if (!Array.isArray(body.paths)) {
1720
1719
  res.code(400).send({ error: 'Request body must include "paths" array' });
1721
1720
  return;
1722
1721
  }
1722
+ const config = getConfig();
1723
+ if (config !== cachedConfig) {
1724
+ compiledRules = compileRules(config.inferenceRules ?? []);
1725
+ watchMatcher = picomatch(config.watch.paths, { dot: true });
1726
+ ignoreMatcher = config.watch.ignored?.length
1727
+ ? picomatch(config.watch.ignored, { dot: true })
1728
+ : undefined;
1729
+ cachedConfig = config;
1730
+ }
1723
1731
  const matches = body.paths.map((path) => {
1724
1732
  const attrs = buildSyntheticAttributes(path);
1725
- // Find matching rules
1726
1733
  const matchingRules = [];
1727
1734
  for (const compiled of compiledRules) {
1728
1735
  if (compiled.validate(attrs)) {
1729
1736
  matchingRules.push(compiled.rule.name);
1730
1737
  }
1731
1738
  }
1732
- // Check watch scope: matches watch paths and not in ignored
1733
1739
  const normalised = attrs.file.path;
1734
- const watched = watchMatcher(normalised) && !ignoreMatcher?.(normalised);
1740
+ const watched = (watchMatcher?.(normalised) ?? false) &&
1741
+ !(ignoreMatcher?.(normalised) ?? false);
1735
1742
  return { rules: matchingRules, watched };
1736
1743
  });
1737
1744
  const response = { matches };
@@ -1910,7 +1917,7 @@ function resolveReferences(doc, resolveTypes) {
1910
1917
  */
1911
1918
  function createConfigQueryHandler(deps) {
1912
1919
  const coreHandler = createConfigQueryHandler$1(() => buildMergedDocument({
1913
- config: deps.config,
1920
+ config: deps.getConfig(),
1914
1921
  valuesManager: deps.valuesManager,
1915
1922
  issuesManager: deps.issuesManager,
1916
1923
  helperIntrospection: deps.helperIntrospection,
@@ -1971,7 +1978,7 @@ function createConfigReindexHandler(deps) {
1971
1978
  });
1972
1979
  }
1973
1980
  const reindexDeps = {
1974
- config: deps.config,
1981
+ config: deps.getConfig(),
1975
1982
  processor: deps.processor,
1976
1983
  logger: deps.logger,
1977
1984
  reindexTracker: deps.reindexTracker,
@@ -3189,7 +3196,7 @@ function validateInferenceRuleSchemas(parsed) {
3189
3196
  function createConfigValidateHandler(deps) {
3190
3197
  return wrapHandler(async (request) => {
3191
3198
  const { config: submittedConfig, testPaths } = request.body;
3192
- const { candidateRaw, parsed, errors } = mergeAndValidateConfig(deps.config, submittedConfig);
3199
+ const { candidateRaw, parsed, errors } = mergeAndValidateConfig(deps.getConfig(), submittedConfig);
3193
3200
  if (errors.length > 0) {
3194
3201
  return { valid: false, errors };
3195
3202
  }
@@ -3309,13 +3316,14 @@ function buildFacetSchema(rules, mergeOptions) {
3309
3316
  * @returns Fastify route handler (plain return, compatible with `withCache`).
3310
3317
  */
3311
3318
  function createFacetsHandler(deps) {
3312
- const { config, valuesManager, configDir } = deps;
3319
+ const { getConfig, valuesManager, configDir } = deps;
3313
3320
  let cached;
3314
- const mergeOptions = {
3315
- globalSchemas: config.schemas,
3316
- configDir,
3317
- };
3318
3321
  return () => {
3322
+ const config = getConfig();
3323
+ const mergeOptions = {
3324
+ globalSchemas: config.schemas,
3325
+ configDir,
3326
+ };
3319
3327
  // Rebuild schema cache if rules changed
3320
3328
  const currentHash = computeRulesHash(config.inferenceRules);
3321
3329
  if (!cached || cached.rulesHash !== currentHash) {
@@ -3420,7 +3428,7 @@ function getValueType(value) {
3420
3428
  *
3421
3429
  * Unknown properties are allowed.
3422
3430
  */
3423
- function validateMetadataPayload(config, path, metadata) {
3431
+ function validateMetadataPayload(config, path, metadata, configDir) {
3424
3432
  const compiled = compileRules(config.inferenceRules ?? []);
3425
3433
  const attrs = buildSyntheticAttributes(path);
3426
3434
  const matched = compiled.filter((r) => r.validate(attrs));
@@ -3431,6 +3439,7 @@ function validateMetadataPayload(config, path, metadata) {
3431
3439
  }
3432
3440
  const merged = mergeSchemas(schemaRefs, {
3433
3441
  globalSchemas: config.schemas ?? {},
3442
+ configDir,
3434
3443
  });
3435
3444
  const details = [];
3436
3445
  for (const [key, value] of Object.entries(metadata)) {
@@ -3451,6 +3460,7 @@ function validateMetadataPayload(config, path, metadata) {
3451
3460
  return false;
3452
3461
  const ruleSchema = mergeSchemas(m.rule.schema, {
3453
3462
  globalSchemas: config.schemas ?? {},
3463
+ configDir,
3454
3464
  });
3455
3465
  return (Object.hasOwn(ruleSchema.properties, key) &&
3456
3466
  ruleSchema.properties[key].type === expectedType);
@@ -3490,7 +3500,7 @@ function validateMetadataPayload(config, path, metadata) {
3490
3500
  function createMetadataHandler(deps) {
3491
3501
  return wrapHandler(async (request, reply) => {
3492
3502
  const { path, metadata } = request.body;
3493
- const validation = validateMetadataPayload(deps.config, path, metadata);
3503
+ const validation = validateMetadataPayload(deps.getConfig(), path, metadata, deps.configDir);
3494
3504
  if (!validation.ok) {
3495
3505
  return reply
3496
3506
  .code(400)
@@ -3594,13 +3604,14 @@ function createRebuildMetadataHandler(deps) {
3594
3604
  * @returns Fastify route handler.
3595
3605
  */
3596
3606
  function createRenderHandler(deps) {
3597
- const { processor, watch, logger } = deps;
3607
+ const { processor, getWatch, logger } = deps;
3598
3608
  return async (request, reply) => {
3599
3609
  const { path: filePath } = request.body;
3600
3610
  if (!filePath || typeof filePath !== 'string') {
3601
3611
  return reply.status(400).send({ error: 'Missing required field: path' });
3602
3612
  }
3603
- // Validate path is within watched scope
3613
+ // Validate path is within watched scope (live config)
3614
+ const watch = getWatch();
3604
3615
  if (!isPathWatched(filePath, watch.paths, watch.ignored)) {
3605
3616
  return reply.status(403).send({ error: 'Path is outside watched scope' });
3606
3617
  }
@@ -3786,8 +3797,9 @@ function createSearchHandler(deps) {
3786
3797
  return wrapHandler(async (request) => {
3787
3798
  const { query, limit = 10, offset, filter } = request.body;
3788
3799
  const vectors = await deps.embeddingProvider.embed([query]);
3789
- if (deps.hybridConfig?.enabled) {
3790
- return deps.vectorStore.hybridSearch(vectors[0], query, limit, deps.hybridConfig.textWeight, filter);
3800
+ const hybridConfig = deps.getHybridConfig?.();
3801
+ if (hybridConfig?.enabled) {
3802
+ return deps.vectorStore.hybridSearch(vectors[0], query, limit, hybridConfig.textWeight, filter);
3791
3803
  }
3792
3804
  return deps.vectorStore.search(vectors[0], limit, filter, offset);
3793
3805
  }, deps.logger, 'Search');
@@ -3810,7 +3822,7 @@ function createStatusHandler(deps) {
3810
3822
  version: deps.version,
3811
3823
  uptime: process.uptime(),
3812
3824
  collection: {
3813
- name: deps.collectionName,
3825
+ name: deps.getCollectionName(),
3814
3826
  pointCount: collectionInfo.pointCount,
3815
3827
  dimensions: collectionInfo.dimensions,
3816
3828
  },
@@ -3857,7 +3869,7 @@ function createWalkHandler(deps) {
3857
3869
  const normGlobs = globs.map((g) => normalizeSlashes(g));
3858
3870
  const matchGlobs = picomatch(normGlobs, { dot: true, nocase: true });
3859
3871
  const paths = watchedFiles.filter((f) => matchGlobs(normalizeSlashes(f)));
3860
- const scannedRoots = getWatchRootBases(deps.watchPaths);
3872
+ const scannedRoots = getWatchRootBases(deps.getWatchPaths());
3861
3873
  return {
3862
3874
  paths,
3863
3875
  matchedCount: paths.length,
@@ -4118,6 +4130,59 @@ async function introspectHelpers(config, configDir) {
4118
4130
  function getConfigDir(configPath) {
4119
4131
  return configPath ? dirname(configPath) : '.';
4120
4132
  }
4133
+ /**
4134
+ * Tear down and rebuild the filesystem watcher with new config.
4135
+ * Falls back to the old watcher if the new one fails to start.
4136
+ */
4137
+ async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache) {
4138
+ try {
4139
+ await oldState.watcher.stop();
4140
+ const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache);
4141
+ newWatcher.start();
4142
+ logger.info('Filesystem watcher rebuilt successfully');
4143
+ return { watcher: newWatcher, gitignoreFilter: newGitignoreFilter };
4144
+ }
4145
+ catch (error) {
4146
+ logger.error({ err: normalizeError(error) }, 'Failed to rebuild watcher, restoring previous');
4147
+ try {
4148
+ oldState.watcher.start();
4149
+ }
4150
+ catch (restartError) {
4151
+ logger.error({ err: normalizeError(restartError) }, 'Failed to restart previous watcher');
4152
+ }
4153
+ return oldState;
4154
+ }
4155
+ }
4156
+ /**
4157
+ * Check whether watch-relevant config fields changed between old and new config.
4158
+ */
4159
+ function watchConfigChanged(oldConfig, newConfig) {
4160
+ return (JSON.stringify(oldConfig.watch.paths) !==
4161
+ JSON.stringify(newConfig.watch.paths) ||
4162
+ JSON.stringify(oldConfig.watch.ignored) !==
4163
+ JSON.stringify(newConfig.watch.ignored) ||
4164
+ JSON.stringify(oldConfig.watch.moveDetection) !==
4165
+ JSON.stringify(newConfig.watch.moveDetection) ||
4166
+ (oldConfig.watch.respectGitignore ?? true) !==
4167
+ (newConfig.watch.respectGitignore ?? true));
4168
+ }
4169
+ /**
4170
+ * Resolve package version from nearest package.json.
4171
+ */
4172
+ function resolveVersion(referenceUrl) {
4173
+ try {
4174
+ const pkgDir = packageDirectorySync({
4175
+ cwd: dirname(fileURLToPath(referenceUrl)),
4176
+ });
4177
+ const pkg = pkgDir
4178
+ ? JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'))
4179
+ : undefined;
4180
+ return pkg?.version ?? 'unknown';
4181
+ }
4182
+ catch {
4183
+ return 'unknown';
4184
+ }
4185
+ }
4121
4186
 
4122
4187
  /**
4123
4188
  * @module rules/virtualRules
@@ -4223,8 +4288,9 @@ function extractMatchGlobs(rules) {
4223
4288
  * @returns A zero-argument callback suitable for passing to rule registration handlers.
4224
4289
  */
4225
4290
  function createOnRulesChanged(deps) {
4226
- const { config, configPath, processor, logger, virtualRuleStore, reindexTracker, valuesManager, issuesManager, gitignoreFilter, vectorStore, } = deps;
4291
+ const { getConfig, configPath, processor, logger, virtualRuleStore, reindexTracker, valuesManager, issuesManager, gitignoreFilter, vectorStore, } = deps;
4227
4292
  return () => {
4293
+ const config = getConfig();
4228
4294
  const configRules = config.inferenceRules ?? [];
4229
4295
  const virtualRulesBySource = virtualRuleStore.getAll();
4230
4296
  const allVirtualRules = Object.values(virtualRulesBySource).flat();
@@ -4381,6 +4447,7 @@ class InitialScanTracker {
4381
4447
  */
4382
4448
  function createApiServer(options) {
4383
4449
  const { processor, vectorStore, embeddingProvider, logger, config, issuesManager, valuesManager, configPath, helperIntrospection, virtualRuleStore, gitignoreFilter, version, initialScanTracker, } = options;
4450
+ const getConfig = options.getConfig ?? (() => config);
4384
4451
  const reindexTracker = options.reindexTracker ?? new ReindexTracker();
4385
4452
  const app = Fastify({ logger: false });
4386
4453
  const triggerReindex = (scope) => {
@@ -4389,7 +4456,7 @@ function createApiServer(options) {
4389
4456
  return;
4390
4457
  }
4391
4458
  void executeReindex({
4392
- config,
4459
+ config: getConfig(),
4393
4460
  processor,
4394
4461
  logger,
4395
4462
  reindexTracker,
@@ -4402,30 +4469,33 @@ function createApiServer(options) {
4402
4469
  const cacheTtlMs = config.api?.cacheTtlMs ?? 30000;
4403
4470
  app.get('/status', withCache(cacheTtlMs, createStatusHandler({
4404
4471
  vectorStore,
4405
- collectionName: config.vectorStore.collectionName,
4472
+ getCollectionName: () => getConfig().vectorStore.collectionName,
4406
4473
  reindexTracker,
4407
4474
  version: version ?? 'unknown',
4408
4475
  initialScanTracker,
4409
4476
  })));
4410
- app.post('/metadata', createMetadataHandler({ processor, config, logger }));
4411
- app.post('/render', withCache(cacheTtlMs, createRenderHandler({ processor, watch: config.watch, logger })));
4477
+ app.post('/metadata', createMetadataHandler({
4478
+ processor,
4479
+ getConfig,
4480
+ logger,
4481
+ configDir: dirname(configPath),
4482
+ }));
4483
+ app.post('/render', withCache(cacheTtlMs, createRenderHandler({
4484
+ processor,
4485
+ getWatch: () => getConfig().watch,
4486
+ logger,
4487
+ })));
4412
4488
  app.get('/search/facets', createFacetsHandler({
4413
- config,
4489
+ getConfig,
4414
4490
  valuesManager,
4415
4491
  configDir: dirname(configPath),
4416
4492
  }));
4417
- const hybridConfig = config.search?.hybrid
4418
- ? {
4419
- enabled: config.search.hybrid.enabled,
4420
- textWeight: config.search.hybrid.textWeight,
4421
- }
4422
- : undefined;
4423
4493
  app.post('/scan', createScanHandler({
4424
4494
  vectorStore,
4425
4495
  logger,
4426
4496
  }));
4427
4497
  app.post('/walk', createWalkHandler({
4428
- watchPaths: config.watch.paths,
4498
+ getWatchPaths: () => getConfig().watch.paths,
4429
4499
  fileSystemWatcher: options.fileSystemWatcher,
4430
4500
  logger,
4431
4501
  }));
@@ -4433,7 +4503,12 @@ function createApiServer(options) {
4433
4503
  embeddingProvider,
4434
4504
  vectorStore,
4435
4505
  logger,
4436
- hybridConfig,
4506
+ getHybridConfig: () => {
4507
+ const hybrid = getConfig().search?.hybrid;
4508
+ return hybrid
4509
+ ? { enabled: hybrid.enabled, textWeight: hybrid.textWeight }
4510
+ : undefined;
4511
+ },
4437
4512
  }));
4438
4513
  app.post('/rebuild-metadata', createRebuildMetadataHandler({
4439
4514
  enrichmentStore: options.enrichmentStore,
@@ -4441,7 +4516,7 @@ function createApiServer(options) {
4441
4516
  logger,
4442
4517
  }));
4443
4518
  app.post('/reindex', createConfigReindexHandler({
4444
- config,
4519
+ getConfig,
4445
4520
  processor,
4446
4521
  logger,
4447
4522
  reindexTracker,
@@ -4452,9 +4527,9 @@ function createApiServer(options) {
4452
4527
  }));
4453
4528
  app.get('/issues', withCache(cacheTtlMs, createIssuesHandler({ issuesManager })));
4454
4529
  app.get('/config/schema', withCache(cacheTtlMs, createConfigSchemaHandler()));
4455
- app.post('/config/match', createConfigMatchHandler({ config, logger }));
4530
+ app.post('/config/match', createConfigMatchHandler({ getConfig, logger }));
4456
4531
  app.get('/config', withCache(cacheTtlMs, createConfigQueryHandler({
4457
- config,
4532
+ getConfig,
4458
4533
  valuesManager,
4459
4534
  issuesManager,
4460
4535
  logger,
@@ -4464,12 +4539,12 @@ function createApiServer(options) {
4464
4539
  : undefined,
4465
4540
  })));
4466
4541
  app.post('/config/validate', createConfigValidateHandler({
4467
- config,
4542
+ getConfig,
4468
4543
  logger,
4469
4544
  configDir: dirname(configPath),
4470
4545
  }));
4471
4546
  app.post('/config/apply', createConfigApplyHandler({
4472
- config,
4547
+ getConfig,
4473
4548
  configPath,
4474
4549
  reindexTracker,
4475
4550
  logger,
@@ -4478,7 +4553,7 @@ function createApiServer(options) {
4478
4553
  // Virtual rules and points deletion routes
4479
4554
  if (virtualRuleStore) {
4480
4555
  const onRulesChanged = createOnRulesChanged({
4481
- config,
4556
+ getConfig,
4482
4557
  configPath,
4483
4558
  processor,
4484
4559
  logger,
@@ -4914,6 +4989,50 @@ class ValuesManager extends JsonFileStore {
4914
4989
  }
4915
4990
  }
4916
4991
 
4992
+ /**
4993
+ * @module app/configReload
4994
+ * Config-reload orchestration extracted from JeevesWatcher to follow SRP.
4995
+ */
4996
+ /**
4997
+ * Reload config, update rules/templates, optionally rebuild watcher, and trigger reindex.
4998
+ * Mutates `state.config`, `state.watcher`, and `state.gitignoreFilter` in place.
4999
+ */
5000
+ async function reloadConfig(state, deps) {
5001
+ const { logger, processor, configPath } = deps;
5002
+ logger.info({ configPath }, 'Config change detected, reloading...');
5003
+ try {
5004
+ const oldConfig = state.config;
5005
+ const newConfig = await deps.factories.loadConfig(configPath);
5006
+ state.config = newConfig;
5007
+ const compiledRules = deps.factories.compileRules(newConfig.inferenceRules ?? []);
5008
+ const reloadConfigDir = getConfigDir(configPath);
5009
+ const { templateEngine: newTemplateEngine, customMapLib: newCustomMapLib } = await buildTemplateEngineAndCustomMapLib(newConfig, reloadConfigDir);
5010
+ processor.updateRules(compiledRules, newTemplateEngine, newCustomMapLib);
5011
+ logger.info({ configPath, rules: compiledRules.length }, 'Config reloaded');
5012
+ // Rebuild filesystem watcher if watch config changed
5013
+ if (watchConfigChanged(oldConfig, newConfig)) {
5014
+ logger.info('Watch config changed, rebuilding filesystem watcher...');
5015
+ const newState = await rebuildWatcher(newConfig, deps.factories, deps.queue, processor, logger, deps.runtimeOptions, { watcher: state.watcher, gitignoreFilter: state.gitignoreFilter }, deps.initialScanTracker, deps.contentHashCache);
5016
+ state.watcher = newState.watcher;
5017
+ state.gitignoreFilter = newState.gitignoreFilter;
5018
+ }
5019
+ // Trigger reindex based on configWatch.reindex setting
5020
+ const reindexScope = newConfig.configWatch?.reindex ?? 'issues';
5021
+ logger.info({ scope: reindexScope }, `Config watch triggering ${reindexScope} reindex`);
5022
+ await executeReindex({
5023
+ config: newConfig,
5024
+ processor,
5025
+ logger,
5026
+ valuesManager: deps.valuesManager,
5027
+ issuesManager: deps.issuesManager,
5028
+ gitignoreFilter: state.gitignoreFilter,
5029
+ }, reindexScope);
5030
+ }
5031
+ catch (error) {
5032
+ logger.error({ err: normalizeError(error) }, 'Failed to reload config');
5033
+ }
5034
+ }
5035
+
4917
5036
  /**
4918
5037
  * @module app/configWatcher
4919
5038
  * Watches the config file for changes and triggers debounced reload. Isolated I/O wrapper around chokidar.
@@ -7383,18 +7502,7 @@ class JeevesWatcher {
7383
7502
  this.runtimeOptions = runtimeOptions;
7384
7503
  this.virtualRuleStore = new VirtualRuleStore();
7385
7504
  this.initialScanTracker = new InitialScanTracker();
7386
- try {
7387
- const pkgDir = packageDirectorySync({
7388
- cwd: dirname(fileURLToPath(import.meta.url)),
7389
- });
7390
- const pkg = pkgDir
7391
- ? JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'))
7392
- : undefined;
7393
- this.version = pkg?.version ?? 'unknown';
7394
- }
7395
- catch {
7396
- this.version = 'unknown';
7397
- }
7505
+ this.version = resolveVersion(import.meta.url);
7398
7506
  }
7399
7507
  /**
7400
7508
  * Start the watcher, API server, and all components.
@@ -7468,6 +7576,7 @@ class JeevesWatcher {
7468
7576
  this.logger?.warn({ timeoutMs: timeout }, 'Queue drain timeout hit, forcing shutdown');
7469
7577
  }
7470
7578
  }
7579
+ this.enrichmentStore?.close();
7471
7580
  if (this.server) {
7472
7581
  await this.server.close();
7473
7582
  }
@@ -7480,6 +7589,7 @@ class JeevesWatcher {
7480
7589
  embeddingProvider: this.embeddingProvider,
7481
7590
  queue: this.queue,
7482
7591
  config: this.config,
7592
+ getConfig: () => this.config,
7483
7593
  logger: this.logger,
7484
7594
  issuesManager: this.issuesManager,
7485
7595
  valuesManager: this.valuesManager,
@@ -7529,30 +7639,26 @@ class JeevesWatcher {
7529
7639
  const processor = this.processor;
7530
7640
  if (!logger || !processor || !this.configPath)
7531
7641
  return;
7532
- logger.info({ configPath: this.configPath }, 'Config change detected, reloading...');
7533
- try {
7534
- const newConfig = await this.factories.loadConfig(this.configPath);
7535
- this.config = newConfig;
7536
- const compiledRules = this.factories.compileRules(newConfig.inferenceRules ?? []);
7537
- const reloadConfigDir = getConfigDir(this.configPath);
7538
- const { templateEngine: newTemplateEngine, customMapLib: newCustomMapLib, } = await buildTemplateEngineAndCustomMapLib(newConfig, reloadConfigDir);
7539
- processor.updateRules(compiledRules, newTemplateEngine, newCustomMapLib);
7540
- logger.info({ configPath: this.configPath, rules: compiledRules.length }, 'Config reloaded');
7541
- // Trigger reindex based on configWatch.reindex setting
7542
- const reindexScope = newConfig.configWatch?.reindex ?? 'issues';
7543
- logger.info({ scope: reindexScope }, `Config watch triggering ${reindexScope} reindex`);
7544
- await executeReindex({
7545
- config: newConfig,
7546
- processor,
7547
- logger,
7548
- valuesManager: this.valuesManager,
7549
- issuesManager: this.issuesManager,
7550
- gitignoreFilter: this.gitignoreFilter,
7551
- }, reindexScope);
7552
- }
7553
- catch (error) {
7554
- logger.error({ err: normalizeError(error) }, 'Failed to reload config');
7555
- }
7642
+ const state = {
7643
+ config: this.config,
7644
+ watcher: this.watcher,
7645
+ gitignoreFilter: this.gitignoreFilter,
7646
+ };
7647
+ await reloadConfig(state, {
7648
+ configPath: this.configPath,
7649
+ factories: this.factories,
7650
+ queue: this.queue,
7651
+ processor,
7652
+ logger,
7653
+ runtimeOptions: this.runtimeOptions,
7654
+ initialScanTracker: this.initialScanTracker,
7655
+ contentHashCache: this.contentHashCache,
7656
+ valuesManager: this.valuesManager,
7657
+ issuesManager: this.issuesManager,
7658
+ });
7659
+ this.config = state.config;
7660
+ this.watcher = state.watcher;
7661
+ this.gitignoreFilter = state.gitignoreFilter;
7556
7662
  }
7557
7663
  }
7558
7664
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/jeeves-watcher",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "author": "Jason Williscroft",
5
5
  "description": "Filesystem watcher that keeps a Qdrant vector store in sync with document changes",
6
6
  "license": "BSD-3-Clause",