@karmaniverous/jeeves-watcher 0.18.12 → 0.19.0-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/README.md CHANGED
@@ -49,7 +49,7 @@ Add a `vcs` block to your config:
49
49
  ```
50
50
 
51
51
  | Property | Default | Description |
52
- |----------|---------|-------------|
52
+ | --- | --- | --- |
53
53
  | `vcs.enabled` | `false` | Enable git-backed version control globally |
54
54
  | `vcs.commitThrottleMs` | `30000` | Throttle interval (ms) for batching file changes into commits. Min: 1000 |
55
55
  | `vcs.maxBatchSize` | `1000` | Maximum files per commit batch. Flushes immediately when exceeded. Min: 1 |
@@ -137,22 +137,22 @@ To prevent unbounded history growth, the squash retention system periodically co
137
137
 
138
138
  ### API Endpoints
139
139
 
140
- | Endpoint | Method | Description |
141
- |----------|--------|-------------|
142
- | `/vcs/status` | GET | VCS state for all roots |
143
- | `/vcs/history` | GET | Commit history for a path/glob |
144
- | `/vcs/show` | GET | File content at a specific commit |
145
- | `/vcs/diff` | GET | Diff between commits |
146
- | `/vcs/revert` | POST | Restore files from a past commit |
147
- | `/vcs/exclude` | POST | Manage gitignore exclusions |
148
- | `/vcs/check-exclusion` | GET | Check gitignore status of a path |
140
+ | Endpoint | Method | Description |
141
+ | ---------------------- | ------ | --------------------------------- |
142
+ | `/vcs/status` | GET | VCS state for all roots |
143
+ | `/vcs/history` | GET | Commit history for a path/glob |
144
+ | `/vcs/show` | GET | File content at a specific commit |
145
+ | `/vcs/diff` | GET | Diff between commits |
146
+ | `/vcs/revert` | POST | Restore files from a past commit |
147
+ | `/vcs/exclude` | POST | Manage gitignore exclusions |
148
+ | `/vcs/check-exclusion` | GET | Check gitignore status of a path |
149
149
 
150
150
  ## JsonMap Built-in Helpers
151
151
 
152
152
  The following helpers are available in every JsonMap `lib` context:
153
153
 
154
154
  | Helper | Description |
155
- |--------|-------------|
155
+ | --- | --- |
156
156
  | `split(str, sep)` | Split a string into an array |
157
157
  | `slice(arr, start, end?)` | Slice an array |
158
158
  | `join(arr, sep)` | Join an array into a string |
@@ -1088,19 +1088,11 @@
1088
1088
  "__schema94": {
1089
1089
  "anyOf": [
1090
1090
  {
1091
- "anyOf": [
1092
- {
1093
- "type": "string"
1094
- },
1095
- {
1096
- "type": "number"
1097
- },
1098
- {
1099
- "type": "boolean"
1100
- },
1101
- {
1102
- "type": "null"
1103
- }
1091
+ "type": [
1092
+ "string",
1093
+ "number",
1094
+ "boolean",
1095
+ "null"
1104
1096
  ]
1105
1097
  },
1106
1098
  {
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { createConfigQueryHandler as createConfigQueryHandler$1, createStatusHandler, createConfigApplyHandler, DEFAULT_PORTS, getBindAddress } from '@karmaniverous/jeeves';
3
- import { p as normalizeSlashes, n as normalizeError, e as execFileAsync, b as GIT_TIMEOUT_STANDARD, G as GIT_TIMEOUT_CHERRY_PICK, d as buildAuthenticatedPushUrl, a as GIT_TIMEOUT_PUSH, h as getExecErrorFields, r as retry, j as gitAddViaStdin, o as normalizePathCase, g as findRootForPath, c as GitignoreFilter, f as executeReindex, V as VALID_REINDEX_SCOPES, m as mergeInferenceRules, k as isPathWatched, i as getWatchRootBases, C as CONFIG_WATCH_VALID_SCOPES, s as sleep, l as loadConfig } from './index.js';
2
+ import { createConfigApplyHandler, createConfigQueryHandler as createConfigQueryHandler$1, createStatusHandler, DEFAULT_PORTS, getBindAddress } from '@karmaniverous/jeeves';
3
+ import { n as normalizeSlashes, a as normalizeError, e as execFileAsync, G as GIT_TIMEOUT_STANDARD, b as GIT_TIMEOUT_CHERRY_PICK, c as buildAuthenticatedPushUrl, d as GIT_TIMEOUT_PUSH, g as getExecErrorFields, r as retry, f as gitAddViaStdin, h as normalizePathCase, i as findRootForPath, j as GitignoreFilter, k as executeReindex, V as VALID_REINDEX_SCOPES, m as mergeInferenceRules, l as isPathWatched, o as getWatchRootBases, C as CONFIG_WATCH_VALID_SCOPES, s as sleep, p as loadConfig } from './index.js';
4
4
  export { q as startFromConfig } from './index.js';
5
5
  import { mkdirSync, existsSync, readFileSync, writeFileSync, rmSync, renameSync, readdirSync, statSync } from 'node:fs';
6
- import { join, dirname, resolve, extname, basename, relative, sep, isAbsolute } from 'node:path';
7
- import Database from 'better-sqlite3';
6
+ import { join, dirname, resolve, extname, basename, sep, isAbsolute, relative } from 'node:path';
7
+ import { DatabaseSync } from 'node:sqlite';
8
8
  import { z } from 'zod';
9
9
  import Ajv from 'ajv';
10
10
  import addFormats from 'ajv-formats';
@@ -25,7 +25,6 @@ import { capitalize, title, camel, snake, dash, isEqual, get, omit } from 'radas
25
25
  import rehypeParse from 'rehype-parse';
26
26
  import { unified } from 'unified';
27
27
  import { dump, load } from 'js-yaml';
28
- import 'ignore';
29
28
  import chokidar from 'chokidar';
30
29
  import Fastify from 'fastify';
31
30
  import crypto from 'crypto';
@@ -38,6 +37,7 @@ import mammoth from 'mammoth';
38
37
  import { MarkdownTextSplitter, RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
39
38
  import { QdrantClient } from '@qdrant/js-client-rest';
40
39
  import 'cosmiconfig';
40
+ import 'ignore';
41
41
  import 'node:child_process';
42
42
  import 'node:util';
43
43
 
@@ -228,10 +228,12 @@ class EnrichmentStore {
228
228
  this.logger = logger;
229
229
  mkdirSync(stateDir, { recursive: true });
230
230
  const dbPath = join(stateDir, 'enrichments.sqlite');
231
- this.db = new Database(dbPath);
232
- this.db.pragma('journal_mode = WAL');
233
- this.db.pragma('busy_timeout = ' + BUSY_TIMEOUT_MS.toString());
234
- const [checkpointStatus] = this.db.pragma('wal_checkpoint(TRUNCATE)');
231
+ this.db = new DatabaseSync(dbPath);
232
+ this.db.exec('PRAGMA journal_mode = WAL;');
233
+ this.db.exec('PRAGMA busy_timeout = ' + BUSY_TIMEOUT_MS.toString() + ';');
234
+ const checkpointStatus = this.db
235
+ .prepare('PRAGMA wal_checkpoint(TRUNCATE)')
236
+ .get();
235
237
  if (checkpointStatus && checkpointStatus.busy > 0) {
236
238
  // EnrichmentStore is expected to be single-writer. If we see a busy WAL
237
239
  // checkpoint at startup, it's most likely from an unclean shutdown where
@@ -3195,61 +3197,6 @@ async function applyRules(compiledRules, attributes, options = {}) {
3195
3197
  return { metadata: merged, renderedContent, matchedRules, renderAs };
3196
3198
  }
3197
3199
 
3198
- /**
3199
- * @module vcs/resolveWatchRoot
3200
- * Resolves which VCS-enabled watch root(s) own a given absolute path or glob.
3201
- */
3202
- /**
3203
- * Resolve which VCS-enabled watch root owns an absolute path.
3204
- *
3205
- * @param coordinator - The VcsCoordinator with root information.
3206
- * @param absolutePath - An absolute filesystem path.
3207
- * @returns The matching root and relative path, or undefined if no root matches.
3208
- */
3209
- function resolveWatchRoot(coordinator, absolutePath) {
3210
- const normalized = normalizeSlashes(resolve(absolutePath));
3211
- const root = findRootForPath(coordinator.getRoots(), normalized);
3212
- if (!root)
3213
- return undefined;
3214
- return {
3215
- root,
3216
- relativePath: normalizeSlashes(relative(root, normalized)),
3217
- };
3218
- }
3219
- /**
3220
- * Resolve which VCS-enabled watch roots match a glob pattern.
3221
- * For globs that could span multiple roots, returns all matching roots.
3222
- *
3223
- * @param coordinator - The VcsCoordinator with root information.
3224
- * @param glob - An absolute glob pattern.
3225
- * @returns Array of matching roots with relative glob patterns.
3226
- */
3227
- function resolveWatchRootsForGlob(coordinator, glob) {
3228
- const normalizedGlob = normalizeSlashes(glob);
3229
- const roots = coordinator.getRoots();
3230
- const results = [];
3231
- const compareGlob = normalizePathCase(normalizedGlob);
3232
- for (const root of roots) {
3233
- const compareRoot = normalizePathCase(root);
3234
- if (compareGlob.startsWith(compareRoot + '/') ||
3235
- compareGlob === compareRoot) {
3236
- // Glob is under this root
3237
- results.push({
3238
- root,
3239
- relativePath: normalizeSlashes(relative(root, normalizedGlob)),
3240
- });
3241
- }
3242
- else if (compareRoot.startsWith(compareGlob.replace(/[*?[\]{}]/g, ''))) {
3243
- // Root is under the glob's base path — include with root-relative glob
3244
- results.push({
3245
- root,
3246
- relativePath: '.',
3247
- });
3248
- }
3249
- }
3250
- return results;
3251
- }
3252
-
3253
3200
  /**
3254
3201
  * @module vcs/validateStateDirOverlap
3255
3202
  * Validates that stateDir does not overlap with any watch path.
@@ -3284,68 +3231,6 @@ function validateStateDirOverlap(stateDir, watchPaths) {
3284
3231
  }
3285
3232
  }
3286
3233
 
3287
- /**
3288
- * @module app/watcherFactory
3289
- * Factory helpers for creating and rebuilding the filesystem watcher.
3290
- * Extracted from initialization.ts to follow SRP.
3291
- */
3292
- /**
3293
- * Create file system watcher with gitignore filtering.
3294
- */
3295
- function createWatcher(config, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
3296
- const respectGitignore = config.watch.respectGitignore ?? true;
3297
- const gitignoreFilter = respectGitignore
3298
- ? new GitignoreFilter(extractWatchPathStrings(config.watch.paths))
3299
- : undefined;
3300
- const watcher = factories.createFileSystemWatcher(config.watch, queue, processor, logger, {
3301
- maxRetries: config.maxRetries ?? runtimeOptions.maxRetries,
3302
- maxBackoffMs: config.maxBackoffMs ?? runtimeOptions.maxBackoffMs,
3303
- onFatalError: runtimeOptions.onFatalError,
3304
- gitignoreFilter,
3305
- initialScanTracker,
3306
- contentHashCache,
3307
- onVcsFileChange,
3308
- onInitialScanComplete,
3309
- });
3310
- return { watcher, gitignoreFilter };
3311
- }
3312
- /**
3313
- * Tear down and rebuild the filesystem watcher with new config.
3314
- * Falls back to the old watcher if the new one fails to start.
3315
- */
3316
- async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
3317
- try {
3318
- await oldState.watcher.stop();
3319
- const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete);
3320
- newWatcher.start();
3321
- logger.info('Filesystem watcher rebuilt successfully');
3322
- return { watcher: newWatcher, gitignoreFilter: newGitignoreFilter };
3323
- }
3324
- catch (error) {
3325
- logger.error({ err: normalizeError(error) }, 'Failed to rebuild watcher, restoring previous');
3326
- try {
3327
- oldState.watcher.start();
3328
- }
3329
- catch (restartError) {
3330
- logger.error({ err: normalizeError(restartError) }, 'Failed to restart previous watcher');
3331
- }
3332
- return oldState;
3333
- }
3334
- }
3335
- /**
3336
- * Check whether watch-relevant config fields changed between old and new config.
3337
- */
3338
- function watchConfigChanged(oldConfig, newConfig) {
3339
- return (JSON.stringify(oldConfig.watch.paths) !==
3340
- JSON.stringify(newConfig.watch.paths) ||
3341
- JSON.stringify(oldConfig.watch.ignored) !==
3342
- JSON.stringify(newConfig.watch.ignored) ||
3343
- JSON.stringify(oldConfig.watch.moveDetection) !==
3344
- JSON.stringify(newConfig.watch.moveDetection) ||
3345
- (oldConfig.watch.respectGitignore ?? true) !==
3346
- (newConfig.watch.respectGitignore ?? true));
3347
- }
3348
-
3349
3234
  /**
3350
3235
  * @module app/initialization
3351
3236
  * Initialization helpers for JeevesWatcher. Extracted to follow SRP.
@@ -3488,6 +3373,68 @@ async function initVcs(config, logger) {
3488
3373
  return true;
3489
3374
  }
3490
3375
 
3376
+ /**
3377
+ * @module app/watcherFactory
3378
+ * Factory helpers for creating and rebuilding the filesystem watcher.
3379
+ * Extracted from initialization.ts to follow SRP.
3380
+ */
3381
+ /**
3382
+ * Create file system watcher with gitignore filtering.
3383
+ */
3384
+ function createWatcher(config, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
3385
+ const respectGitignore = config.watch.respectGitignore ?? true;
3386
+ const gitignoreFilter = respectGitignore
3387
+ ? new GitignoreFilter(extractWatchPathStrings(config.watch.paths))
3388
+ : undefined;
3389
+ const watcher = factories.createFileSystemWatcher(config.watch, queue, processor, logger, {
3390
+ maxRetries: config.maxRetries ?? runtimeOptions.maxRetries,
3391
+ maxBackoffMs: config.maxBackoffMs ?? runtimeOptions.maxBackoffMs,
3392
+ onFatalError: runtimeOptions.onFatalError,
3393
+ gitignoreFilter,
3394
+ initialScanTracker,
3395
+ contentHashCache,
3396
+ onVcsFileChange,
3397
+ onInitialScanComplete,
3398
+ });
3399
+ return { watcher, gitignoreFilter };
3400
+ }
3401
+ /**
3402
+ * Tear down and rebuild the filesystem watcher with new config.
3403
+ * Falls back to the old watcher if the new one fails to start.
3404
+ */
3405
+ async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
3406
+ try {
3407
+ await oldState.watcher.stop();
3408
+ const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete);
3409
+ newWatcher.start();
3410
+ logger.info('Filesystem watcher rebuilt successfully');
3411
+ return { watcher: newWatcher, gitignoreFilter: newGitignoreFilter };
3412
+ }
3413
+ catch (error) {
3414
+ logger.error({ err: normalizeError(error) }, 'Failed to rebuild watcher, restoring previous');
3415
+ try {
3416
+ oldState.watcher.start();
3417
+ }
3418
+ catch (restartError) {
3419
+ logger.error({ err: normalizeError(restartError) }, 'Failed to restart previous watcher');
3420
+ }
3421
+ return oldState;
3422
+ }
3423
+ }
3424
+ /**
3425
+ * Check whether watch-relevant config fields changed between old and new config.
3426
+ */
3427
+ function watchConfigChanged(oldConfig, newConfig) {
3428
+ return (JSON.stringify(oldConfig.watch.paths) !==
3429
+ JSON.stringify(newConfig.watch.paths) ||
3430
+ JSON.stringify(oldConfig.watch.ignored) !==
3431
+ JSON.stringify(newConfig.watch.ignored) ||
3432
+ JSON.stringify(oldConfig.watch.moveDetection) !==
3433
+ JSON.stringify(newConfig.watch.moveDetection) ||
3434
+ (oldConfig.watch.respectGitignore ?? true) !==
3435
+ (newConfig.watch.respectGitignore ?? true));
3436
+ }
3437
+
3491
3438
  /**
3492
3439
  * @module app/configReload
3493
3440
  * Config-reload orchestration extracted from JeevesWatcher to follow SRP.
@@ -3579,6 +3526,49 @@ class ConfigWatcher {
3579
3526
  }
3580
3527
  }
3581
3528
 
3529
+ /**
3530
+ * @module api/handlers/configApply
3531
+ * Fastify route handler for POST /config/apply. Deep-merges a partial config
3532
+ * patch into the running config file, validates the result, writes it
3533
+ * atomically, and fires the apply callback.
3534
+ */
3535
+ /**
3536
+ * Check whether a value is a plain object (not null, not an array).
3537
+ *
3538
+ * @param value - Value to check.
3539
+ * @returns True for plain objects.
3540
+ */
3541
+ function isPlainObject$1(value) {
3542
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3543
+ }
3544
+ /**
3545
+ * Create handler for POST /config/apply.
3546
+ *
3547
+ * @remarks
3548
+ * Delegates to core `createConfigApplyHandler`, passing the service's own
3549
+ * config path so the patch is merged into the file the service actually
3550
+ * loaded (not a path derived from `configRoot`, which may be unset or
3551
+ * different in the service process). Accepts `{ patch }` (sent by the
3552
+ * plugin's `watcher_config_apply`) or `{ config }`.
3553
+ *
3554
+ * @param deps - Route dependencies.
3555
+ */
3556
+ function createConfigApplyRouteHandler(deps) {
3557
+ const apply = createConfigApplyHandler({ ...deps.descriptor, onConfigApply: deps.onConfigApply }, deps.configPath || undefined);
3558
+ return async (request, reply) => {
3559
+ const body = request.body ?? {};
3560
+ const patch = body.patch ?? body.config ?? {};
3561
+ if (!isPlainObject$1(patch)) {
3562
+ return reply.status(400).send({
3563
+ error: 'Config validation failed',
3564
+ issues: [{ path: ['patch'], message: 'patch must be an object' }],
3565
+ });
3566
+ }
3567
+ const result = await apply({ patch, replace: body.replace === true });
3568
+ return reply.status(result.status).send(result.body);
3569
+ };
3570
+ }
3571
+
3582
3572
  /**
3583
3573
  * @module rules/attributes
3584
3574
  * Builds file attribute objects for rule matching. Pure function: derives attributes from path, stats, and extracted data.
@@ -4784,6 +4774,61 @@ function createSearchHandler(deps) {
4784
4774
  }, deps.logger, 'Search');
4785
4775
  }
4786
4776
 
4777
+ /**
4778
+ * @module vcs/resolveWatchRoot
4779
+ * Resolves which VCS-enabled watch root(s) own a given absolute path or glob.
4780
+ */
4781
+ /**
4782
+ * Resolve which VCS-enabled watch root owns an absolute path.
4783
+ *
4784
+ * @param coordinator - The VcsCoordinator with root information.
4785
+ * @param absolutePath - An absolute filesystem path.
4786
+ * @returns The matching root and relative path, or undefined if no root matches.
4787
+ */
4788
+ function resolveWatchRoot(coordinator, absolutePath) {
4789
+ const normalized = normalizeSlashes(resolve(absolutePath));
4790
+ const root = findRootForPath(coordinator.getRoots(), normalized);
4791
+ if (!root)
4792
+ return undefined;
4793
+ return {
4794
+ root,
4795
+ relativePath: normalizeSlashes(relative(root, normalized)),
4796
+ };
4797
+ }
4798
+ /**
4799
+ * Resolve which VCS-enabled watch roots match a glob pattern.
4800
+ * For globs that could span multiple roots, returns all matching roots.
4801
+ *
4802
+ * @param coordinator - The VcsCoordinator with root information.
4803
+ * @param glob - An absolute glob pattern.
4804
+ * @returns Array of matching roots with relative glob patterns.
4805
+ */
4806
+ function resolveWatchRootsForGlob(coordinator, glob) {
4807
+ const normalizedGlob = normalizeSlashes(glob);
4808
+ const roots = coordinator.getRoots();
4809
+ const results = [];
4810
+ const compareGlob = normalizePathCase(normalizedGlob);
4811
+ for (const root of roots) {
4812
+ const compareRoot = normalizePathCase(root);
4813
+ if (compareGlob.startsWith(compareRoot + '/') ||
4814
+ compareGlob === compareRoot) {
4815
+ // Glob is under this root
4816
+ results.push({
4817
+ root,
4818
+ relativePath: normalizeSlashes(relative(root, normalizedGlob)),
4819
+ });
4820
+ }
4821
+ else if (compareRoot.startsWith(compareGlob.replace(/[*?[\]{}]/g, ''))) {
4822
+ // Root is under the glob's base path — include with root-relative glob
4823
+ results.push({
4824
+ root,
4825
+ relativePath: '.',
4826
+ });
4827
+ }
4828
+ }
4829
+ return results;
4830
+ }
4831
+
4787
4832
  /**
4788
4833
  * @module api/handlers/vcs/vcsCheckExclusion
4789
4834
  * Fastify route handler for GET /vcs/check-exclusion. Checks gitignore status of a path.
@@ -5639,20 +5684,15 @@ function createApiServer(options) {
5639
5684
  logger,
5640
5685
  configDir: dirname(configPath),
5641
5686
  }));
5642
- const coreConfigApplyHandler = createConfigApplyHandler({
5643
- ...descriptor,
5687
+ app.post(getEndpoint('configApply').path, createConfigApplyRouteHandler({
5688
+ descriptor,
5689
+ configPath,
5644
5690
  onConfigApply: () => {
5645
5691
  const cfg = getConfig();
5646
- const reindexScope = cfg.configWatch?.reindex ?? 'issues';
5647
- triggerReindex(reindexScope);
5692
+ triggerReindex(cfg.configWatch?.reindex ?? 'issues');
5648
5693
  return Promise.resolve();
5649
5694
  },
5650
- });
5651
- app.post(getEndpoint('configApply').path, async (request, reply) => {
5652
- const { patch, replace } = request.body;
5653
- const result = await coreConfigApplyHandler({ patch, replace });
5654
- return reply.status(result.status).send(result.body);
5655
- });
5695
+ }));
5656
5696
  // Virtual rules and points deletion routes
5657
5697
  if (virtualRuleStore) {
5658
5698
  const onRulesChanged = createOnRulesChanged({
@@ -7261,14 +7301,14 @@ class VectorStoreClient {
7261
7301
  * @returns An array of search results.
7262
7302
  */
7263
7303
  async search(vector, limit, filter, offset) {
7264
- const results = await this.client.search(this.collectionName, {
7265
- vector,
7304
+ const { points } = await this.client.query(this.collectionName, {
7305
+ query: vector,
7266
7306
  limit,
7267
7307
  with_payload: true,
7268
7308
  ...(filter ? { filter } : {}),
7269
7309
  ...(offset !== undefined ? { offset } : {}),
7270
7310
  });
7271
- return results.map((r) => ({
7311
+ return points.map((r) => ({
7272
7312
  id: String(r.id),
7273
7313
  score: r.score,
7274
7314
  payload: r.payload,
@@ -18,6 +18,45 @@ import { promisify } from 'node:util';
18
18
  * @module api/handlers/configMerge
19
19
  * Shared config merge utilities.
20
20
  */
21
+ /**
22
+ * Check whether a value is a plain object (not null, not an array).
23
+ *
24
+ * @param value - Value to check.
25
+ * @returns True for plain objects.
26
+ */
27
+ function isPlainObject(value) {
28
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
29
+ }
30
+ /**
31
+ * Deep-merge a config patch into a base config.
32
+ *
33
+ * @remarks
34
+ * Plain objects are merged recursively; arrays and scalars in the patch
35
+ * replace the base value. `inferenceRules` is merged by rule name (see
36
+ * {@link mergeInferenceRules}). Neither input is mutated.
37
+ *
38
+ * @param base - The current config.
39
+ * @param patch - The partial config to apply.
40
+ * @returns A new merged config.
41
+ */
42
+ function deepMergeConfig(base, patch) {
43
+ const result = { ...base };
44
+ for (const [key, value] of Object.entries(patch)) {
45
+ const current = base[key];
46
+ if (key === 'inferenceRules' && Array.isArray(value)) {
47
+ result[key] = mergeInferenceRules(Array.isArray(current)
48
+ ? current
49
+ : undefined, value);
50
+ }
51
+ else if (isPlainObject(current) && isPlainObject(value)) {
52
+ result[key] = deepMergeConfig(current, value);
53
+ }
54
+ else {
55
+ result[key] = value;
56
+ }
57
+ }
58
+ return result;
59
+ }
21
60
  /**
22
61
  * Merge inference rules by name: submitted rules replace existing by name, new ones are appended.
23
62
  */
@@ -226,7 +265,7 @@ async function startFromConfig(configPath, descriptor) {
226
265
  const config = await loadConfig(resolvedPath);
227
266
  // Dynamic import breaks the circular dependency between this module
228
267
  // and app/index.ts (which defines JeevesWatcher and re-exports startFromConfig).
229
- const { JeevesWatcher } = await import('./index-C2iJGo8u.js');
268
+ const { JeevesWatcher } = await import('./index-BVfdWEhb.js');
230
269
  const app = new JeevesWatcher(config, resolvedPath, descriptor);
231
270
  installShutdownHandlers(() => app.stop());
232
271
  await app.start();
@@ -1585,14 +1624,8 @@ const watcherDescriptor = {
1585
1624
  // onConfigApply is overridden in createApiServer (api/index.ts) via the
1586
1625
  // descriptor passed as a dependency, where it has access to the live
1587
1626
  // reindex tracker and config getter.
1588
- customMerge: (target, source) => {
1589
- const mergedRules = mergeInferenceRules(target['inferenceRules'], source['inferenceRules']);
1590
- return {
1591
- ...target,
1592
- ...source,
1593
- inferenceRules: mergedRules,
1594
- };
1595
- },
1627
+ // Deep merge (like core's default) with inferenceRules merged by name.
1628
+ customMerge: deepMergeConfig,
1596
1629
  startCommand: (configPath) => [
1597
1630
  'node',
1598
1631
  resolve(packageRoot, 'dist/cli/jeeves-watcher/index.js'),
@@ -1603,18 +1636,11 @@ const watcherDescriptor = {
1603
1636
  run: async (configPath) => {
1604
1637
  await startFromConfig(configPath, watcherDescriptor);
1605
1638
  },
1606
- // Content — generateToolsContent is wired in the plugin package
1607
- // (watcherComponent.ts) where it has access to the API URL for menu generation.
1608
- sectionId: 'Watcher',
1609
- refreshIntervalSeconds: 71,
1610
- generateToolsContent: () => '',
1611
1639
  // Extension points
1612
1640
  customCliCommands: (program) => {
1613
1641
  registerCustomCommands(program);
1614
1642
  },
1615
- customPluginTools: (api) => {
1616
- return [];
1617
- },
1643
+ customPluginTools: () => [],
1618
1644
  };
1619
1645
 
1620
1646
  /**
@@ -1626,4 +1652,4 @@ const watcherDescriptor = {
1626
1652
  const program = createServiceCli(watcherDescriptor);
1627
1653
  program.parse();
1628
1654
 
1629
- export { CONFIG_WATCH_VALID_SCOPES as C, GIT_TIMEOUT_CHERRY_PICK as G, VALID_REINDEX_SCOPES as V, GIT_TIMEOUT_PUSH as a, GIT_TIMEOUT_STANDARD as b, GitignoreFilter as c, buildAuthenticatedPushUrl as d, execFileAsync as e, executeReindex as f, findRootForPath as g, getExecErrorFields as h, getWatchRootBases as i, gitAddViaStdin as j, isPathWatched as k, loadConfig as l, mergeInferenceRules as m, normalizeError as n, normalizePathCase as o, normalizeSlashes as p, startFromConfig as q, retry as r, sleep as s };
1655
+ export { CONFIG_WATCH_VALID_SCOPES as C, GIT_TIMEOUT_STANDARD as G, VALID_REINDEX_SCOPES as V, normalizeError as a, GIT_TIMEOUT_CHERRY_PICK as b, buildAuthenticatedPushUrl as c, GIT_TIMEOUT_PUSH as d, execFileAsync as e, gitAddViaStdin as f, getExecErrorFields as g, normalizePathCase as h, findRootForPath as i, GitignoreFilter as j, executeReindex as k, isPathWatched as l, mergeInferenceRules as m, normalizeSlashes as n, getWatchRootBases as o, loadConfig as p, startFromConfig as q, retry as r, sleep as s };
package/dist/index.d.ts CHANGED
@@ -2029,4 +2029,4 @@ declare class SquashManager {
2029
2029
  }
2030
2030
 
2031
2031
  export { CommitMessageGenerator, ContentHashCache, DocumentProcessor, EnrichmentStore, EventQueue, FileSystemWatcher, GitignoreFilter, InitialScanTracker, IssuesManager, JeevesWatcher, ReindexTracker, SquashManager, SystemHealth, TemplateEngine, ValuesManager, VcsCoordinator, VcsManager, VectorStoreClient, VirtualRuleStore, applyRules, buildAttributes, buildTemplateEngine, compileRules, contentHash, createApiServer, createEmbeddingProvider, createHandlebarsInstance, createLogger, extractText, issueRecordSchema, loadConfig, loadCustomHelpers, mergeEnrichment, pointId, registerBuiltinHelpers, resolveTemplateSource, startFromConfig, watcherDescriptor };
2032
- export type { AllHelpersIntrospection, ApiServerOptions, ApplyRulesOptions, ApplyRulesResult, CollectionInfo, CommitInfo, CompiledRule, CompiledTemplate, DocumentProcessorDeps, DocumentProcessorInterface, EmbeddingProvider, EnrichmentStoreInterface, EventQueueOptions, ExtractedText, Extractor, FileAttributes, FileSystemWatcherOptions, HelperModuleIntrospection, InitialScanStatus, IssueRecord, IssuesFile, JeevesWatcherFactories, JeevesWatcherRuntimeOptions, PayloadFieldSchema, PendingReversion, ProcessFn, ProcessorConfig, ProviderFactory, PushError, ReindexStatus, RenderResult, ResolvedWatchRoot, RuleLogger, ScrollPageResult, ScrolledPoint, SearchResult, SquashResult, SystemHealthOptions, ValuesIndex, VectorPoint, VectorStore, WatchEvent };
2032
+ export type { AllHelpersIntrospection, ApiServerOptions, ApplyRulesOptions, ApplyRulesResult, CollectionInfo, CommitInfo, CompiledRule, CompiledTemplate, DocumentProcessorDeps, DocumentProcessorInterface, EmbeddingProvider, EnrichmentStoreInterface, EventQueueOptions, ExtractedText, Extractor, FileAttributes, FileSystemWatcherOptions, HelperModuleIntrospection, InitialScanStatus, IssueRecord, IssuesFile, JeevesWatcherFactories, JeevesWatcherRuntimeOptions, PayloadFieldSchema, PendingReversion, ProcessFn, ProcessorConfig, ProviderFactory, PushError, ReindexStatus, RenderResult, ResolvedWatchRoot, RuleLogger, ScrollPageResult, ScrolledPoint, SearchResult, SquashManagerOptions, SquashResult, SystemHealthOptions, ValuesIndex, VectorPoint, VectorStore, WatchEvent };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { join, dirname, resolve, relative, extname, basename, isAbsolute, sep } from 'node:path';
2
- import { createConfigQueryHandler as createConfigQueryHandler$1, createStatusHandler, createConfigApplyHandler, substituteEnvVars, DEFAULT_PORTS, getBindAddress, postJson, fetchJson, getPackageVersion } from '@karmaniverous/jeeves';
2
+ import { createConfigApplyHandler, createConfigQueryHandler as createConfigQueryHandler$1, createStatusHandler, substituteEnvVars, DEFAULT_PORTS, getBindAddress, postJson, fetchJson, getPackageVersion } from '@karmaniverous/jeeves';
3
3
  import { extractWatchPathStrings, inferenceRuleSchema, jeevesWatcherConfigSchema, normalizeWatchPaths, vcsRetentionConfigSchema, vcsAuthorConfigSchema, getEndpoint, LOGGING_DEFAULTS, API_DEFAULTS, EMBEDDING_DEFAULTS, CONFIG_WATCH_DEFAULTS, WATCH_DEFAULTS, ROOT_DEFAULTS, INIT_CONFIG_TEMPLATE, DEFAULT_PORT as DEFAULT_PORT$1, PLUGIN_PACKAGE, SERVICE_PACKAGE, COMPONENT_NAME } from '@karmaniverous/jeeves-watcher-core';
4
4
  export { apiConfigSchema, configWatchConfigSchema, embeddingConfigSchema, inferenceRuleSchema, jeevesWatcherConfigSchema, loggingConfigSchema, vectorStoreConfigSchema, watchConfigSchema } from '@karmaniverous/jeeves-watcher-core';
5
5
  import Fastify from 'fastify';
@@ -26,7 +26,7 @@ import { dump, load } from 'js-yaml';
26
26
  import crypto from 'crypto';
27
27
  import { packageDirectorySync } from 'package-directory';
28
28
  import { homedir } from 'node:os';
29
- import Database from 'better-sqlite3';
29
+ import { DatabaseSync } from 'node:sqlite';
30
30
  import { deserialize, serialize } from 'node:v8';
31
31
  import chokidar from 'chokidar';
32
32
  import { cosmiconfig } from 'cosmiconfig';
@@ -1141,6 +1141,49 @@ async function getPathFileList(targetPath, config, gitignoreFilter) {
1141
1141
  throw new Error(`Path is neither a file nor a directory: ${targetPath}`);
1142
1142
  }
1143
1143
 
1144
+ /**
1145
+ * @module api/handlers/configApply
1146
+ * Fastify route handler for POST /config/apply. Deep-merges a partial config
1147
+ * patch into the running config file, validates the result, writes it
1148
+ * atomically, and fires the apply callback.
1149
+ */
1150
+ /**
1151
+ * Check whether a value is a plain object (not null, not an array).
1152
+ *
1153
+ * @param value - Value to check.
1154
+ * @returns True for plain objects.
1155
+ */
1156
+ function isPlainObject$2(value) {
1157
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1158
+ }
1159
+ /**
1160
+ * Create handler for POST /config/apply.
1161
+ *
1162
+ * @remarks
1163
+ * Delegates to core `createConfigApplyHandler`, passing the service's own
1164
+ * config path so the patch is merged into the file the service actually
1165
+ * loaded (not a path derived from `configRoot`, which may be unset or
1166
+ * different in the service process). Accepts `{ patch }` (sent by the
1167
+ * plugin's `watcher_config_apply`) or `{ config }`.
1168
+ *
1169
+ * @param deps - Route dependencies.
1170
+ */
1171
+ function createConfigApplyRouteHandler(deps) {
1172
+ const apply = createConfigApplyHandler({ ...deps.descriptor, onConfigApply: deps.onConfigApply }, deps.configPath || undefined);
1173
+ return async (request, reply) => {
1174
+ const body = request.body ?? {};
1175
+ const patch = body.patch ?? body.config ?? {};
1176
+ if (!isPlainObject$2(patch)) {
1177
+ return reply.status(400).send({
1178
+ error: 'Config validation failed',
1179
+ issues: [{ path: ['patch'], message: 'patch must be an object' }],
1180
+ });
1181
+ }
1182
+ const result = await apply({ patch, replace: body.replace === true });
1183
+ return reply.status(result.status).send(result.body);
1184
+ };
1185
+ }
1186
+
1144
1187
  /**
1145
1188
  * @module rules/attributes
1146
1189
  * Builds file attribute objects for rule matching. Pure function: derives attributes from path, stats, and extracted data.
@@ -2775,6 +2818,45 @@ async function applyRules(compiledRules, attributes, options = {}) {
2775
2818
  * @module api/handlers/configMerge
2776
2819
  * Shared config merge utilities.
2777
2820
  */
2821
+ /**
2822
+ * Check whether a value is a plain object (not null, not an array).
2823
+ *
2824
+ * @param value - Value to check.
2825
+ * @returns True for plain objects.
2826
+ */
2827
+ function isPlainObject$1(value) {
2828
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
2829
+ }
2830
+ /**
2831
+ * Deep-merge a config patch into a base config.
2832
+ *
2833
+ * @remarks
2834
+ * Plain objects are merged recursively; arrays and scalars in the patch
2835
+ * replace the base value. `inferenceRules` is merged by rule name (see
2836
+ * {@link mergeInferenceRules}). Neither input is mutated.
2837
+ *
2838
+ * @param base - The current config.
2839
+ * @param patch - The partial config to apply.
2840
+ * @returns A new merged config.
2841
+ */
2842
+ function deepMergeConfig(base, patch) {
2843
+ const result = { ...base };
2844
+ for (const [key, value] of Object.entries(patch)) {
2845
+ const current = base[key];
2846
+ if (key === 'inferenceRules' && Array.isArray(value)) {
2847
+ result[key] = mergeInferenceRules(Array.isArray(current)
2848
+ ? current
2849
+ : undefined, value);
2850
+ }
2851
+ else if (isPlainObject$1(current) && isPlainObject$1(value)) {
2852
+ result[key] = deepMergeConfig(current, value);
2853
+ }
2854
+ else {
2855
+ result[key] = value;
2856
+ }
2857
+ }
2858
+ return result;
2859
+ }
2778
2860
  /**
2779
2861
  * Merge inference rules by name: submitted rules replace existing by name, new ones are appended.
2780
2862
  */
@@ -4290,91 +4372,6 @@ async function introspectAllHelpers(config, configDir) {
4290
4372
  return result;
4291
4373
  }
4292
4374
 
4293
- /**
4294
- * @module vcs/CommitMessageBuilder
4295
- * Builds commit messages — both template-based and AI-generated.
4296
- */
4297
- /**
4298
- * Builds commit messages for VCS commits.
4299
- * Supports template-based messages and AI-generated messages with fallback.
4300
- */
4301
- class CommitMessageBuilder {
4302
- rootPath;
4303
- logger;
4304
- generator;
4305
- constructor(rootPath, logger, generator) {
4306
- this.rootPath = rootPath;
4307
- this.logger = logger;
4308
- this.generator = generator;
4309
- }
4310
- /**
4311
- * Build the template commit message (no AI).
4312
- */
4313
- buildTemplateMessage(fileCount, isBaseline, pendingReversions) {
4314
- if (pendingReversions.length === 0) {
4315
- if (isBaseline) {
4316
- return `baseline: batch (${String(fileCount)} files)`;
4317
- }
4318
- const timestamp = new Date().toISOString();
4319
- return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
4320
- }
4321
- // Count total reverted files across all pending reversions
4322
- const revertedPaths = new Set(pendingReversions.flatMap((r) => r.paths));
4323
- const revertedCount = revertedPaths.size;
4324
- const otherCount = fileCount - revertedCount;
4325
- // Use the first reversion's glob and commit for the message
4326
- const { glob, commit } = pendingReversions[0];
4327
- const shortCommit = commit.slice(0, 7);
4328
- let message = `revert: ${glob} to ${shortCommit} — restored ${String(revertedCount)} files`;
4329
- if (otherCount > 0) {
4330
- message += ` (+ ${String(otherCount)} other changes)`;
4331
- }
4332
- return message;
4333
- }
4334
- /**
4335
- * Build the commit message, using AI when available with template fallback.
4336
- */
4337
- async buildCommitMessage(files, isBaseline, pendingReversions) {
4338
- const fileCount = files.length;
4339
- const templateMessage = this.buildTemplateMessage(fileCount, isBaseline, pendingReversions);
4340
- if (!this.generator) {
4341
- return templateMessage;
4342
- }
4343
- try {
4344
- const diffSummary = await this.getStagedDiff();
4345
- const aiMessage = await this.generator.generate(files, diffSummary);
4346
- if (!aiMessage) {
4347
- this.logger.warn('AI commit message generation returned null; using template');
4348
- return templateMessage;
4349
- }
4350
- // For reversions: prefix with revert info + AI description
4351
- if (pendingReversions.length > 0) {
4352
- const { glob, commit } = pendingReversions[0];
4353
- const shortCommit = commit.slice(0, 7);
4354
- return `revert: ${glob} to ${shortCommit} — ${aiMessage}`;
4355
- }
4356
- return aiMessage;
4357
- }
4358
- catch (error) {
4359
- this.logger.warn({ err: normalizeError(error) }, 'AI commit message generation failed; using template');
4360
- return templateMessage;
4361
- }
4362
- }
4363
- /**
4364
- * Get the staged diff for AI context.
4365
- */
4366
- async getStagedDiff() {
4367
- try {
4368
- const { stdout: stat } = await execFileAsync('git', ['diff', '--cached', '--stat'], { cwd: this.rootPath, timeout: GIT_TIMEOUT_STANDARD });
4369
- const { stdout: patch } = await execFileAsync('git', ['diff', '--cached'], { cwd: this.rootPath, timeout: GIT_TIMEOUT_STANDARD });
4370
- return `${stat}\n${patch}`;
4371
- }
4372
- catch {
4373
- return '';
4374
- }
4375
- }
4376
- }
4377
-
4378
4375
  /**
4379
4376
  * @module vcs/CommitMessageGenerator
4380
4377
  * Generates AI-powered commit messages via the Anthropic Messages API.
@@ -4458,36 +4455,6 @@ class CommitMessageGenerator {
4458
4455
  }
4459
4456
  }
4460
4457
 
4461
- /**
4462
- * @module vcs/resolveCommitMessageApiKey
4463
- * Resolves the API key for AI commit message generation.
4464
- */
4465
- /**
4466
- * Resolve the API key for commit message generation.
4467
- * Checks config first, then falls back to OpenClaw gateway credentials.
4468
- */
4469
- function resolveCommitMessageApiKey(provider, configApiKey, logger) {
4470
- if (configApiKey) {
4471
- logger.debug('Using commit message API key from config');
4472
- return configApiKey;
4473
- }
4474
- try {
4475
- const openclawPath = join(homedir(), '.openclaw', 'openclaw.json');
4476
- const raw = readFileSync(openclawPath, 'utf8');
4477
- const parsed = JSON.parse(raw);
4478
- const gatewayKey = parsed.models?.providers?.[provider]?.apiKey;
4479
- if (gatewayKey) {
4480
- logger.debug({ provider }, 'Using commit message API key from OpenClaw gateway config');
4481
- return gatewayKey;
4482
- }
4483
- logger.warn({ provider }, 'OpenClaw gateway config found but no API key for provider');
4484
- }
4485
- catch {
4486
- logger.warn('No commit message API key in config or OpenClaw gateway');
4487
- }
4488
- return undefined;
4489
- }
4490
-
4491
4458
  /**
4492
4459
  * @module vcs/SquashManager
4493
4460
  * Handles scheduled squash retention for git-backed VCS roots.
@@ -5081,6 +5048,121 @@ function resolveIgnored(ignored) {
5081
5048
  });
5082
5049
  }
5083
5050
 
5051
+ /**
5052
+ * @module vcs/resolveCommitMessageApiKey
5053
+ * Resolves the API key for AI commit message generation.
5054
+ */
5055
+ /**
5056
+ * Resolve the API key for commit message generation.
5057
+ * Checks config first, then falls back to OpenClaw gateway credentials.
5058
+ */
5059
+ function resolveCommitMessageApiKey(provider, configApiKey, logger) {
5060
+ if (configApiKey) {
5061
+ logger.debug('Using commit message API key from config');
5062
+ return configApiKey;
5063
+ }
5064
+ try {
5065
+ const openclawPath = join(homedir(), '.openclaw', 'openclaw.json');
5066
+ const raw = readFileSync(openclawPath, 'utf8');
5067
+ const parsed = JSON.parse(raw);
5068
+ const gatewayKey = parsed.models?.providers?.[provider]?.apiKey;
5069
+ if (gatewayKey) {
5070
+ logger.debug({ provider }, 'Using commit message API key from OpenClaw gateway config');
5071
+ return gatewayKey;
5072
+ }
5073
+ logger.warn({ provider }, 'OpenClaw gateway config found but no API key for provider');
5074
+ }
5075
+ catch {
5076
+ logger.warn('No commit message API key in config or OpenClaw gateway');
5077
+ }
5078
+ return undefined;
5079
+ }
5080
+
5081
+ /**
5082
+ * @module vcs/CommitMessageBuilder
5083
+ * Builds commit messages — both template-based and AI-generated.
5084
+ */
5085
+ /**
5086
+ * Builds commit messages for VCS commits.
5087
+ * Supports template-based messages and AI-generated messages with fallback.
5088
+ */
5089
+ class CommitMessageBuilder {
5090
+ rootPath;
5091
+ logger;
5092
+ generator;
5093
+ constructor(rootPath, logger, generator) {
5094
+ this.rootPath = rootPath;
5095
+ this.logger = logger;
5096
+ this.generator = generator;
5097
+ }
5098
+ /**
5099
+ * Build the template commit message (no AI).
5100
+ */
5101
+ buildTemplateMessage(fileCount, isBaseline, pendingReversions) {
5102
+ if (pendingReversions.length === 0) {
5103
+ if (isBaseline) {
5104
+ return `baseline: batch (${String(fileCount)} files)`;
5105
+ }
5106
+ const timestamp = new Date().toISOString();
5107
+ return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
5108
+ }
5109
+ // Count total reverted files across all pending reversions
5110
+ const revertedPaths = new Set(pendingReversions.flatMap((r) => r.paths));
5111
+ const revertedCount = revertedPaths.size;
5112
+ const otherCount = fileCount - revertedCount;
5113
+ // Use the first reversion's glob and commit for the message
5114
+ const { glob, commit } = pendingReversions[0];
5115
+ const shortCommit = commit.slice(0, 7);
5116
+ let message = `revert: ${glob} to ${shortCommit} — restored ${String(revertedCount)} files`;
5117
+ if (otherCount > 0) {
5118
+ message += ` (+ ${String(otherCount)} other changes)`;
5119
+ }
5120
+ return message;
5121
+ }
5122
+ /**
5123
+ * Build the commit message, using AI when available with template fallback.
5124
+ */
5125
+ async buildCommitMessage(files, isBaseline, pendingReversions) {
5126
+ const fileCount = files.length;
5127
+ const templateMessage = this.buildTemplateMessage(fileCount, isBaseline, pendingReversions);
5128
+ if (!this.generator) {
5129
+ return templateMessage;
5130
+ }
5131
+ try {
5132
+ const diffSummary = await this.getStagedDiff();
5133
+ const aiMessage = await this.generator.generate(files, diffSummary);
5134
+ if (!aiMessage) {
5135
+ this.logger.warn('AI commit message generation returned null; using template');
5136
+ return templateMessage;
5137
+ }
5138
+ // For reversions: prefix with revert info + AI description
5139
+ if (pendingReversions.length > 0) {
5140
+ const { glob, commit } = pendingReversions[0];
5141
+ const shortCommit = commit.slice(0, 7);
5142
+ return `revert: ${glob} to ${shortCommit} — ${aiMessage}`;
5143
+ }
5144
+ return aiMessage;
5145
+ }
5146
+ catch (error) {
5147
+ this.logger.warn({ err: normalizeError(error) }, 'AI commit message generation failed; using template');
5148
+ return templateMessage;
5149
+ }
5150
+ }
5151
+ /**
5152
+ * Get the staged diff for AI context.
5153
+ */
5154
+ async getStagedDiff() {
5155
+ try {
5156
+ const { stdout: stat } = await execFileAsync('git', ['diff', '--cached', '--stat'], { cwd: this.rootPath, timeout: GIT_TIMEOUT_STANDARD });
5157
+ const { stdout: patch } = await execFileAsync('git', ['diff', '--cached'], { cwd: this.rootPath, timeout: GIT_TIMEOUT_STANDARD });
5158
+ return `${stat}\n${patch}`;
5159
+ }
5160
+ catch {
5161
+ return '';
5162
+ }
5163
+ }
5164
+ }
5165
+
5084
5166
  /**
5085
5167
  * @module vcs/vcsPush
5086
5168
  * Push logic for VCS commits.
@@ -5588,68 +5670,6 @@ class VcsCoordinator {
5588
5670
  }
5589
5671
  }
5590
5672
 
5591
- /**
5592
- * @module app/watcherFactory
5593
- * Factory helpers for creating and rebuilding the filesystem watcher.
5594
- * Extracted from initialization.ts to follow SRP.
5595
- */
5596
- /**
5597
- * Create file system watcher with gitignore filtering.
5598
- */
5599
- function createWatcher(config, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
5600
- const respectGitignore = config.watch.respectGitignore ?? true;
5601
- const gitignoreFilter = respectGitignore
5602
- ? new GitignoreFilter(extractWatchPathStrings(config.watch.paths))
5603
- : undefined;
5604
- const watcher = factories.createFileSystemWatcher(config.watch, queue, processor, logger, {
5605
- maxRetries: config.maxRetries ?? runtimeOptions.maxRetries,
5606
- maxBackoffMs: config.maxBackoffMs ?? runtimeOptions.maxBackoffMs,
5607
- onFatalError: runtimeOptions.onFatalError,
5608
- gitignoreFilter,
5609
- initialScanTracker,
5610
- contentHashCache,
5611
- onVcsFileChange,
5612
- onInitialScanComplete,
5613
- });
5614
- return { watcher, gitignoreFilter };
5615
- }
5616
- /**
5617
- * Tear down and rebuild the filesystem watcher with new config.
5618
- * Falls back to the old watcher if the new one fails to start.
5619
- */
5620
- async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
5621
- try {
5622
- await oldState.watcher.stop();
5623
- const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete);
5624
- newWatcher.start();
5625
- logger.info('Filesystem watcher rebuilt successfully');
5626
- return { watcher: newWatcher, gitignoreFilter: newGitignoreFilter };
5627
- }
5628
- catch (error) {
5629
- logger.error({ err: normalizeError(error) }, 'Failed to rebuild watcher, restoring previous');
5630
- try {
5631
- oldState.watcher.start();
5632
- }
5633
- catch (restartError) {
5634
- logger.error({ err: normalizeError(restartError) }, 'Failed to restart previous watcher');
5635
- }
5636
- return oldState;
5637
- }
5638
- }
5639
- /**
5640
- * Check whether watch-relevant config fields changed between old and new config.
5641
- */
5642
- function watchConfigChanged(oldConfig, newConfig) {
5643
- return (JSON.stringify(oldConfig.watch.paths) !==
5644
- JSON.stringify(newConfig.watch.paths) ||
5645
- JSON.stringify(oldConfig.watch.ignored) !==
5646
- JSON.stringify(newConfig.watch.ignored) ||
5647
- JSON.stringify(oldConfig.watch.moveDetection) !==
5648
- JSON.stringify(newConfig.watch.moveDetection) ||
5649
- (oldConfig.watch.respectGitignore ?? true) !==
5650
- (newConfig.watch.respectGitignore ?? true));
5651
- }
5652
-
5653
5673
  /**
5654
5674
  * @module app/initialization
5655
5675
  * Initialization helpers for JeevesWatcher. Extracted to follow SRP.
@@ -6173,20 +6193,15 @@ function createApiServer(options) {
6173
6193
  logger,
6174
6194
  configDir: dirname(configPath),
6175
6195
  }));
6176
- const coreConfigApplyHandler = createConfigApplyHandler({
6177
- ...descriptor,
6196
+ app.post(getEndpoint('configApply').path, createConfigApplyRouteHandler({
6197
+ descriptor,
6198
+ configPath,
6178
6199
  onConfigApply: () => {
6179
6200
  const cfg = getConfig();
6180
- const reindexScope = cfg.configWatch?.reindex ?? 'issues';
6181
- triggerReindex(reindexScope);
6201
+ triggerReindex(cfg.configWatch?.reindex ?? 'issues');
6182
6202
  return Promise.resolve();
6183
6203
  },
6184
- });
6185
- app.post(getEndpoint('configApply').path, async (request, reply) => {
6186
- const { patch, replace } = request.body;
6187
- const result = await coreConfigApplyHandler({ patch, replace });
6188
- return reply.status(result.status).send(result.body);
6189
- });
6204
+ }));
6190
6205
  // Virtual rules and points deletion routes
6191
6206
  if (virtualRuleStore) {
6192
6207
  const onRulesChanged = createOnRulesChanged({
@@ -6356,10 +6371,12 @@ class EnrichmentStore {
6356
6371
  this.logger = logger;
6357
6372
  mkdirSync(stateDir, { recursive: true });
6358
6373
  const dbPath = join(stateDir, 'enrichments.sqlite');
6359
- this.db = new Database(dbPath);
6360
- this.db.pragma('journal_mode = WAL');
6361
- this.db.pragma('busy_timeout = ' + BUSY_TIMEOUT_MS.toString());
6362
- const [checkpointStatus] = this.db.pragma('wal_checkpoint(TRUNCATE)');
6374
+ this.db = new DatabaseSync(dbPath);
6375
+ this.db.exec('PRAGMA journal_mode = WAL;');
6376
+ this.db.exec('PRAGMA busy_timeout = ' + BUSY_TIMEOUT_MS.toString() + ';');
6377
+ const checkpointStatus = this.db
6378
+ .prepare('PRAGMA wal_checkpoint(TRUNCATE)')
6379
+ .get();
6363
6380
  if (checkpointStatus && checkpointStatus.busy > 0) {
6364
6381
  // EnrichmentStore is expected to be single-writer. If we see a busy WAL
6365
6382
  // checkpoint at startup, it's most likely from an unclean shutdown where
@@ -6738,6 +6755,68 @@ class ValuesManager extends BinaryFileStore {
6738
6755
  }
6739
6756
  }
6740
6757
 
6758
+ /**
6759
+ * @module app/watcherFactory
6760
+ * Factory helpers for creating and rebuilding the filesystem watcher.
6761
+ * Extracted from initialization.ts to follow SRP.
6762
+ */
6763
+ /**
6764
+ * Create file system watcher with gitignore filtering.
6765
+ */
6766
+ function createWatcher(config, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
6767
+ const respectGitignore = config.watch.respectGitignore ?? true;
6768
+ const gitignoreFilter = respectGitignore
6769
+ ? new GitignoreFilter(extractWatchPathStrings(config.watch.paths))
6770
+ : undefined;
6771
+ const watcher = factories.createFileSystemWatcher(config.watch, queue, processor, logger, {
6772
+ maxRetries: config.maxRetries ?? runtimeOptions.maxRetries,
6773
+ maxBackoffMs: config.maxBackoffMs ?? runtimeOptions.maxBackoffMs,
6774
+ onFatalError: runtimeOptions.onFatalError,
6775
+ gitignoreFilter,
6776
+ initialScanTracker,
6777
+ contentHashCache,
6778
+ onVcsFileChange,
6779
+ onInitialScanComplete,
6780
+ });
6781
+ return { watcher, gitignoreFilter };
6782
+ }
6783
+ /**
6784
+ * Tear down and rebuild the filesystem watcher with new config.
6785
+ * Falls back to the old watcher if the new one fails to start.
6786
+ */
6787
+ async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
6788
+ try {
6789
+ await oldState.watcher.stop();
6790
+ const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete);
6791
+ newWatcher.start();
6792
+ logger.info('Filesystem watcher rebuilt successfully');
6793
+ return { watcher: newWatcher, gitignoreFilter: newGitignoreFilter };
6794
+ }
6795
+ catch (error) {
6796
+ logger.error({ err: normalizeError(error) }, 'Failed to rebuild watcher, restoring previous');
6797
+ try {
6798
+ oldState.watcher.start();
6799
+ }
6800
+ catch (restartError) {
6801
+ logger.error({ err: normalizeError(restartError) }, 'Failed to restart previous watcher');
6802
+ }
6803
+ return oldState;
6804
+ }
6805
+ }
6806
+ /**
6807
+ * Check whether watch-relevant config fields changed between old and new config.
6808
+ */
6809
+ function watchConfigChanged(oldConfig, newConfig) {
6810
+ return (JSON.stringify(oldConfig.watch.paths) !==
6811
+ JSON.stringify(newConfig.watch.paths) ||
6812
+ JSON.stringify(oldConfig.watch.ignored) !==
6813
+ JSON.stringify(newConfig.watch.ignored) ||
6814
+ JSON.stringify(oldConfig.watch.moveDetection) !==
6815
+ JSON.stringify(newConfig.watch.moveDetection) ||
6816
+ (oldConfig.watch.respectGitignore ?? true) !==
6817
+ (newConfig.watch.respectGitignore ?? true));
6818
+ }
6819
+
6741
6820
  /**
6742
6821
  * @module app/configReload
6743
6822
  * Config-reload orchestration extracted from JeevesWatcher to follow SRP.
@@ -8468,14 +8547,14 @@ class VectorStoreClient {
8468
8547
  * @returns An array of search results.
8469
8548
  */
8470
8549
  async search(vector, limit, filter, offset) {
8471
- const results = await this.client.search(this.collectionName, {
8472
- vector,
8550
+ const { points } = await this.client.query(this.collectionName, {
8551
+ query: vector,
8473
8552
  limit,
8474
8553
  with_payload: true,
8475
8554
  ...(filter ? { filter } : {}),
8476
8555
  ...(offset !== undefined ? { offset } : {}),
8477
8556
  });
8478
- return results.map((r) => ({
8557
+ return points.map((r) => ({
8479
8558
  id: String(r.id),
8480
8559
  score: r.score,
8481
8560
  payload: r.payload,
@@ -9707,14 +9786,8 @@ const watcherDescriptor = {
9707
9786
  // onConfigApply is overridden in createApiServer (api/index.ts) via the
9708
9787
  // descriptor passed as a dependency, where it has access to the live
9709
9788
  // reindex tracker and config getter.
9710
- customMerge: (target, source) => {
9711
- const mergedRules = mergeInferenceRules(target['inferenceRules'], source['inferenceRules']);
9712
- return {
9713
- ...target,
9714
- ...source,
9715
- inferenceRules: mergedRules,
9716
- };
9717
- },
9789
+ // Deep merge (like core's default) with inferenceRules merged by name.
9790
+ customMerge: deepMergeConfig,
9718
9791
  startCommand: (configPath) => [
9719
9792
  'node',
9720
9793
  resolve(packageRoot, 'dist/cli/jeeves-watcher/index.js'),
@@ -9725,18 +9798,11 @@ const watcherDescriptor = {
9725
9798
  run: async (configPath) => {
9726
9799
  await startFromConfig(configPath, watcherDescriptor);
9727
9800
  },
9728
- // Content — generateToolsContent is wired in the plugin package
9729
- // (watcherComponent.ts) where it has access to the API URL for menu generation.
9730
- sectionId: 'Watcher',
9731
- refreshIntervalSeconds: 71,
9732
- generateToolsContent: () => '',
9733
9801
  // Extension points
9734
9802
  customCliCommands: (program) => {
9735
9803
  registerCustomCommands(program);
9736
9804
  },
9737
- customPluginTools: (api) => {
9738
- return [];
9739
- },
9805
+ customPluginTools: () => [],
9740
9806
  };
9741
9807
 
9742
9808
  export { CommitMessageGenerator, ContentHashCache, DocumentProcessor, EnrichmentStore, EventQueue, FileSystemWatcher, GitignoreFilter, InitialScanTracker, IssuesManager, JeevesWatcher, ReindexTracker, SquashManager, SystemHealth, TemplateEngine, ValuesManager, VcsCoordinator, VcsManager, VectorStoreClient, VirtualRuleStore, applyRules, buildAttributes, buildTemplateEngine, compileRules, contentHash, createApiServer, createEmbeddingProvider, createHandlebarsInstance, createLogger, extractText, issueRecordSchema, loadConfig, loadCustomHelpers, mergeEnrichment, pointId, registerBuiltinHelpers, resolveTemplateSource, startFromConfig, watcherDescriptor };
package/package.json CHANGED
@@ -7,66 +7,63 @@
7
7
  "url": "https://github.com/karmaniverous/jeeves-watcher/issues"
8
8
  },
9
9
  "dependencies": {
10
- "@commander-js/extra-typings": "^14.0.0",
11
- "@karmaniverous/jeeves": "^0.5.12",
12
- "@karmaniverous/jeeves-watcher-core": "^0.2.5",
10
+ "@commander-js/extra-typings": "^15.0.0",
11
+ "@karmaniverous/jeeves": "0.6.0-7",
12
+ "@karmaniverous/jeeves-watcher-core": "^0.2.6-0",
13
13
  "@karmaniverous/jsonmap": "^2.1.1",
14
14
  "@langchain/textsplitters": "*",
15
- "@qdrant/js-client-rest": "*",
16
- "ajv": "~8.18.0",
15
+ "@qdrant/js-client-rest": "^1.19.0",
16
+ "ajv": "~8.20.0",
17
17
  "ajv-formats": "*",
18
- "better-sqlite3": "^12.10.0",
19
18
  "cheerio": "^1.2.0",
20
19
  "chokidar": "^5.0.0",
21
20
  "cosmiconfig": "*",
22
- "dayjs": "^1.11.21",
21
+ "dayjs": "^1.11.23",
23
22
  "fastify": "*",
24
23
  "handlebars": "^4.7.9",
25
24
  "hast-util-to-mdast": "^10.1.2",
26
- "ignore": "^7.0.5",
27
- "js-yaml": "^4.1.0",
28
- "mammoth": "^1.12.0",
25
+ "ignore": "^7.0.10",
26
+ "js-yaml": "^5.4.2",
27
+ "mammoth": "^1.12.3",
29
28
  "mdast-util-from-adf": "^2.2.0",
30
29
  "mdast-util-to-markdown": "^2.1.2",
31
30
  "package-directory": "^8.2.0",
32
- "picomatch": "^4.0.4",
31
+ "picomatch": "^4.0.7",
33
32
  "pino": "*",
34
33
  "radash": "^12.1.1",
35
34
  "rehype-parse": "^9.0.1",
36
35
  "unified": "^11.0.5",
37
- "unpdf": "^1.6.2",
36
+ "unpdf": "^1.8.1",
38
37
  "uuid": "*",
39
- "zod": "^4.4.3"
38
+ "zod": "^4.6.5"
40
39
  },
41
40
  "description": "Filesystem watcher that keeps a Qdrant vector store in sync with document changes",
42
41
  "devDependencies": {
43
- "@dotenvx/dotenvx": "^1.69.1",
42
+ "@dotenvx/dotenvx": "^2.30.0",
44
43
  "@rollup/plugin-alias": "^6.0.0",
45
- "@rollup/plugin-commonjs": "^29.0.2",
44
+ "@rollup/plugin-commonjs": "^29.0.3",
46
45
  "@rollup/plugin-json": "^6.1.0",
47
46
  "@rollup/plugin-node-resolve": "^16.0.3",
48
47
  "@rollup/plugin-typescript": "^12.3.0",
49
- "@types/better-sqlite3": "^7.6.13",
50
48
  "@types/fs-extra": "^11.0.4",
51
- "@types/js-yaml": "^4.0.0",
52
- "@types/node": "^25.9.1",
49
+ "@types/node": "^26.6.3",
53
50
  "@types/picomatch": "^4.0.3",
54
- "@vitest/coverage-v8": "^4.1.7",
51
+ "@vitest/coverage-v8": "^5.0.2",
55
52
  "cross-env": "^10.1.0",
56
- "fs-extra": "^11.3.5",
57
- "happy-dom": "^20.9.0",
58
- "knip": "^6.14.2",
59
- "release-it": "^20.0.1",
60
- "rollup": "^4.60.4",
61
- "rollup-plugin-dts": "^6.4.1",
53
+ "fs-extra": "^11.4.1",
54
+ "happy-dom": "^20.14.5",
55
+ "knip": "^6.38.0",
56
+ "release-it": "^21.1.0",
57
+ "rollup": "^4.63.5",
58
+ "rollup-plugin-dts": "^6.5.1",
62
59
  "tslib": "^2.8.1",
63
- "typedoc": "^0.28.19",
60
+ "typedoc": "^0.28.20",
64
61
  "typedoc-plugin-mdn-links": "^5.1.1",
65
62
  "typedoc-plugin-replace-text": "^4.2.0",
66
- "vitest": "^4.1.7"
63
+ "vitest": "^5.0.2"
67
64
  },
68
65
  "engines": {
69
- "node": ">=22"
66
+ "node": ">=22.13"
70
67
  },
71
68
  "exports": {
72
69
  ".": {
@@ -110,6 +107,10 @@
110
107
  "release": true
111
108
  },
112
109
  "hooks": {
110
+ "after:bump": [
111
+ "npm install --package-lock-only --ignore-scripts",
112
+ "git add ../../package-lock.json"
113
+ ],
113
114
  "after:init": [
114
115
  "npm run lint",
115
116
  "npm run typecheck",
@@ -136,18 +137,18 @@
136
137
  "url": "git+https://github.com/karmaniverous/jeeves-watcher.git"
137
138
  },
138
139
  "scripts": {
139
- "build": "npm run generate:schema && rimraf dist && cross-env NO_COLOR=1 rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
140
+ "build": "npm run generate:schema && rimraf dist && cross-env NO_COLOR=1 rollup --config rollup.config.ts --configPlugin \"@rollup/plugin-typescript={paths:{}}\"",
140
141
  "changelog": "git-cliff -o CHANGELOG.md",
141
142
  "diagrams": "cd diagrams && plantuml -tpng -o ../assets -r .",
142
143
  "generate:schema": "tsx src/config/generate-schema.ts",
143
144
  "lint": "eslint .",
144
145
  "lint:fix": "eslint --fix .",
145
146
  "release": "dotenvx run -f .env.local -- release-it",
146
- "release:pre": "dotenvx run -f .env.local -- release-it --no-git.requireBranch --github.prerelease --preRelease",
147
+ "release:pre": "dotenvx run -f .env.local -- release-it --no-git.requireBranch --github.preRelease --preRelease",
147
148
  "test": "vitest run",
148
149
  "typecheck": "tsc"
149
150
  },
150
151
  "type": "module",
151
152
  "types": "dist/index.d.ts",
152
- "version": "0.18.12"
153
+ "version": "0.19.0-1"
153
154
  }