@karmaniverous/jeeves-watcher 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,12 +3,13 @@ import { Command } from '@commander-js/extra-typings';
3
3
  import { dirname, resolve, relative, join, extname, basename } from 'node:path';
4
4
  import { existsSync, statSync, readdirSync, readFileSync } from 'node:fs';
5
5
  import ignore from 'ignore';
6
+ import { JsonMap, jsonMapMapSchema } from '@karmaniverous/jsonmap';
7
+ import { get, capitalize, title, camel, snake, dash, isEqual, omit } from 'radash';
6
8
  import Handlebars from 'handlebars';
7
9
  import dayjs from 'dayjs';
8
10
  import { toMdast } from 'hast-util-to-mdast';
9
11
  import { fromADF } from 'mdast-util-from-adf';
10
12
  import { toMarkdown } from 'mdast-util-to-markdown';
11
- import { capitalize, title, camel, snake, dash, isEqual, omit, get } from 'radash';
12
13
  import rehypeParse from 'rehype-parse';
13
14
  import { unified } from 'unified';
14
15
  import chokidar from 'chokidar';
@@ -18,7 +19,6 @@ import picomatch from 'picomatch';
18
19
  import { createHash } from 'node:crypto';
19
20
  import { cosmiconfig } from 'cosmiconfig';
20
21
  import { z, ZodError } from 'zod';
21
- import { jsonMapMapSchema, JsonMap } from '@karmaniverous/jsonmap';
22
22
  import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';
23
23
  import pino from 'pino';
24
24
  import { v5 } from 'uuid';
@@ -242,6 +242,251 @@ class GitignoreFilter {
242
242
  }
243
243
  }
244
244
 
245
+ /**
246
+ * @module rules/templates
247
+ * Resolves template variables (`${path.to.value}`) in rule `set` objects against file attributes.
248
+ */
249
+ /**
250
+ * Resolve `${template.vars}` in a value against the given attributes.
251
+ *
252
+ * @param value - The value to resolve.
253
+ * @param attributes - The file attributes for variable lookup.
254
+ * @returns The resolved value.
255
+ */
256
+ function resolveTemplateVars(value, attributes) {
257
+ if (typeof value !== 'string')
258
+ return value;
259
+ return value.replace(/\$\{([^}]+)\}/g, (_match, varPath) => {
260
+ const current = get(attributes, varPath);
261
+ if (current === null || current === undefined)
262
+ return '';
263
+ return typeof current === 'string' ? current : JSON.stringify(current);
264
+ });
265
+ }
266
+ /**
267
+ * Resolve all template variables in a `set` object.
268
+ *
269
+ * @param setObj - The key-value pairs to resolve.
270
+ * @param attributes - The file attributes for variable lookup.
271
+ * @returns The resolved key-value pairs.
272
+ */
273
+ function resolveSet(setObj, attributes) {
274
+ const result = {};
275
+ for (const [key, value] of Object.entries(setObj)) {
276
+ result[key] = resolveTemplateVars(value, attributes);
277
+ }
278
+ return result;
279
+ }
280
+
281
+ /**
282
+ * @module rules/apply
283
+ * Applies compiled inference rules to file attributes, producing merged metadata via template resolution and JsonMap transforms.
284
+ */
285
+ /**
286
+ * Create the lib object for JsonMap transformations.
287
+ *
288
+ * @param configDir - Optional config directory for resolving relative file paths in lookups.
289
+ * @returns The lib object.
290
+ */
291
+ function createJsonMapLib(configDir, customLib) {
292
+ // Cache loaded JSON files within a single applyRules invocation.
293
+ const jsonCache = new Map();
294
+ const loadJson = (filePath) => {
295
+ const resolvedPath = configDir ? resolve(configDir, filePath) : filePath;
296
+ if (!jsonCache.has(resolvedPath)) {
297
+ const raw = readFileSync(resolvedPath, 'utf-8');
298
+ jsonCache.set(resolvedPath, JSON.parse(raw));
299
+ }
300
+ return jsonCache.get(resolvedPath);
301
+ };
302
+ return {
303
+ split: (str, separator) => str.split(separator),
304
+ slice: (arr, start, end) => arr.slice(start, end),
305
+ join: (arr, separator) => arr.join(separator),
306
+ toLowerCase: (str) => str.toLowerCase(),
307
+ replace: (str, search, replacement) => str.replace(search, replacement),
308
+ get: (obj, path) => get(obj, path),
309
+ /**
310
+ * Load a JSON file (relative to configDir) and look up a value by key,
311
+ * optionally drilling into a sub-path.
312
+ *
313
+ * @param filePath - Path to a JSON file (resolved relative to configDir).
314
+ * @param key - Top-level key to look up.
315
+ * @param field - Optional dot-path into the looked-up entry.
316
+ * @returns The resolved value, or null if not found.
317
+ */
318
+ lookupJson: (filePath, key, field) => {
319
+ const data = loadJson(filePath);
320
+ const entry = data[key];
321
+ if (entry === undefined || entry === null)
322
+ return null;
323
+ if (field)
324
+ return get(entry, field) ?? null;
325
+ return entry;
326
+ },
327
+ /**
328
+ * Map an array of keys through a JSON lookup file, collecting a specific
329
+ * field from each matching entry. Non-matching keys are silently skipped.
330
+ * Array-valued fields are flattened into the result.
331
+ *
332
+ * @param filePath - Path to a JSON file (resolved relative to configDir).
333
+ * @param keys - Array of top-level keys to look up.
334
+ * @param field - Dot-path into each looked-up entry.
335
+ * @returns Flat array of resolved values.
336
+ */
337
+ mapLookup: (filePath, keys, field) => {
338
+ if (!Array.isArray(keys))
339
+ return [];
340
+ const data = loadJson(filePath);
341
+ const results = [];
342
+ for (const k of keys) {
343
+ if (typeof k !== 'string')
344
+ continue;
345
+ const entry = data[k];
346
+ if (entry === undefined || entry === null)
347
+ continue;
348
+ const val = get(entry, field);
349
+ if (val === undefined || val === null)
350
+ continue;
351
+ if (Array.isArray(val)) {
352
+ for (const item of val) {
353
+ results.push(item);
354
+ }
355
+ }
356
+ else {
357
+ results.push(val);
358
+ }
359
+ }
360
+ return results;
361
+ },
362
+ ...customLib,
363
+ };
364
+ }
365
+ /**
366
+ * Load custom JsonMap lib functions from file paths.
367
+ *
368
+ * Each module should default-export an object of functions,
369
+ * or use named exports. Only function-valued exports are merged.
370
+ *
371
+ * @param paths - File paths to custom lib modules.
372
+ * @param configDir - Directory to resolve relative paths against.
373
+ * @returns The merged custom lib functions.
374
+ */
375
+ async function loadCustomMapHelpers(paths, configDir) {
376
+ const merged = {};
377
+ for (const p of paths) {
378
+ const resolved = resolve(configDir, p);
379
+ const mod = (await import(resolved));
380
+ const fns = typeof mod.default === 'object' && mod.default !== null
381
+ ? mod.default
382
+ : mod;
383
+ for (const [key, val] of Object.entries(fns)) {
384
+ if (typeof val === 'function') {
385
+ merged[key] = val;
386
+ }
387
+ }
388
+ }
389
+ return merged;
390
+ }
391
+ /**
392
+ * Apply compiled inference rules to file attributes, returning merged metadata and optional rendered content.
393
+ *
394
+ * Rules are evaluated in order; later rules override earlier ones.
395
+ * If a rule has a `map`, the JsonMap transformation is applied after `set` resolution,
396
+ * and map output overrides set output on conflict.
397
+ *
398
+ * @param compiledRules - The compiled rules to evaluate.
399
+ * @param attributes - The file attributes to match against.
400
+ * @param namedMaps - Optional record of named JsonMap definitions.
401
+ * @param logger - Optional logger for warnings (falls back to console.warn).
402
+ * @param templateEngine - Optional template engine for rendering content templates.
403
+ * @param configDir - Optional config directory for resolving .json map file paths.
404
+ * @returns The merged metadata and optional rendered content.
405
+ */
406
+ async function applyRules(compiledRules, attributes, namedMaps, logger, templateEngine, configDir, customMapLib) {
407
+ // JsonMap's type definitions expect a generic JsonMapLib shape with unary functions.
408
+ // Our helper functions accept multiple args, which JsonMap supports at runtime.
409
+ const lib = createJsonMapLib(configDir, customMapLib);
410
+ let merged = {};
411
+ let renderedContent = null;
412
+ const log = logger ?? console;
413
+ for (const [ruleIndex, { rule, validate }] of compiledRules.entries()) {
414
+ if (validate(attributes)) {
415
+ // Apply set resolution
416
+ const setOutput = resolveSet(rule.set, attributes);
417
+ merged = { ...merged, ...setOutput };
418
+ // Apply map transformation if present
419
+ if (rule.map) {
420
+ let mapDef;
421
+ // Resolve map reference
422
+ if (typeof rule.map === 'string') {
423
+ if (rule.map.endsWith('.json') && configDir) {
424
+ // File path: load from .json file
425
+ try {
426
+ const mapPath = resolve(configDir, rule.map);
427
+ const raw = readFileSync(mapPath, 'utf-8');
428
+ mapDef = JSON.parse(raw);
429
+ }
430
+ catch (error) {
431
+ log.warn(`Failed to load map file "${rule.map}": ${error instanceof Error ? error.message : String(error)}`);
432
+ continue;
433
+ }
434
+ }
435
+ else {
436
+ mapDef = namedMaps?.[rule.map];
437
+ if (!mapDef) {
438
+ log.warn(`Map reference "${rule.map}" not found in named maps. Skipping map transformation.`);
439
+ continue;
440
+ }
441
+ }
442
+ }
443
+ else {
444
+ mapDef = rule.map;
445
+ }
446
+ // Execute JsonMap transformation
447
+ try {
448
+ const jsonMap = new JsonMap(mapDef, lib);
449
+ const mapOutput = await jsonMap.transform(attributes);
450
+ if (mapOutput &&
451
+ typeof mapOutput === 'object' &&
452
+ !Array.isArray(mapOutput)) {
453
+ merged = { ...merged, ...mapOutput };
454
+ }
455
+ else {
456
+ log.warn(`JsonMap transformation did not return an object; skipping merge.`);
457
+ }
458
+ }
459
+ catch (error) {
460
+ log.warn(`JsonMap transformation failed: ${error instanceof Error ? error.message : String(error)}`);
461
+ }
462
+ }
463
+ // Render template if present
464
+ if (rule.template && templateEngine) {
465
+ const templateKey = `rule-${String(ruleIndex)}`;
466
+ // Build template context: attributes (with json spread at top) + map output
467
+ const context = {
468
+ ...(attributes.json ?? {}),
469
+ ...attributes,
470
+ ...merged,
471
+ };
472
+ try {
473
+ const result = templateEngine.render(templateKey, context);
474
+ if (result && result.trim()) {
475
+ renderedContent = result;
476
+ }
477
+ else {
478
+ log.warn(`Template for rule ${String(ruleIndex)} rendered empty output. Falling back to raw content.`);
479
+ }
480
+ }
481
+ catch (error) {
482
+ log.warn(`Template render failed for rule ${String(ruleIndex)}: ${error instanceof Error ? error.message : String(error)}. Falling back to raw content.`);
483
+ }
484
+ }
485
+ }
486
+ }
487
+ return { metadata: merged, renderedContent };
488
+ }
489
+
245
490
  /**
246
491
  * @module templates/helpers
247
492
  * Registers built-in Handlebars helpers for content templates.
@@ -1166,11 +1411,11 @@ const jeevesWatcherConfigSchema = z.object({
1166
1411
  .array(inferenceRuleSchema)
1167
1412
  .optional()
1168
1413
  .describe('Rules for inferring metadata from file attributes.'),
1169
- /** Reusable named JsonMap transformations. */
1414
+ /** Reusable named JsonMap transformations (inline objects or .json file paths). */
1170
1415
  maps: z
1171
- .record(z.string(), jsonMapMapSchema)
1416
+ .record(z.string(), jsonMapMapSchema.or(z.string()))
1172
1417
  .optional()
1173
- .describe('Reusable named JsonMap transformations.'),
1418
+ .describe('Reusable named JsonMap transformations (inline definition or .json file path resolved relative to config directory).'),
1174
1419
  /** Reusable named Handlebars templates (inline strings or .hbs/.handlebars file paths). */
1175
1420
  templates: z
1176
1421
  .record(z.string(), z.string())
@@ -1187,6 +1432,17 @@ const jeevesWatcherConfigSchema = z.object({
1187
1432
  })
1188
1433
  .optional()
1189
1434
  .describe('Custom Handlebars helper registration.'),
1435
+ /** Custom JsonMap lib function registration. */
1436
+ mapHelpers: z
1437
+ .object({
1438
+ /** File paths to custom lib modules (each exports functions to merge into the JsonMap lib). */
1439
+ paths: z
1440
+ .array(z.string())
1441
+ .optional()
1442
+ .describe('File paths to JS modules exporting functions to merge into the JsonMap lib.'),
1443
+ })
1444
+ .optional()
1445
+ .describe('Custom JsonMap lib function registration.'),
1190
1446
  /** Logging configuration. */
1191
1447
  logging: loggingConfigSchema.optional().describe('Logging configuration.'),
1192
1448
  /** Timeout in milliseconds for graceful shutdown. */
@@ -1284,6 +1540,18 @@ async function loadConfig(configPath) {
1284
1540
  }
1285
1541
  try {
1286
1542
  const validated = jeevesWatcherConfigSchema.parse(result.config);
1543
+ // Resolve file-path map references relative to config directory.
1544
+ // After this block, all map values are inline JsonMapMap objects.
1545
+ if (validated.maps) {
1546
+ const configDir = dirname(result.filepath);
1547
+ for (const [name, value] of Object.entries(validated.maps)) {
1548
+ if (typeof value === 'string') {
1549
+ const mapPath = resolve(configDir, value);
1550
+ const raw = readFileSync(mapPath, 'utf-8');
1551
+ validated.maps[name] = JSON.parse(raw);
1552
+ }
1553
+ }
1554
+ }
1287
1555
  const withDefaults = applyDefaults(validated);
1288
1556
  return substituteEnvVars(withDefaults);
1289
1557
  }
@@ -1665,160 +1933,6 @@ async function extractText(filePath, extension) {
1665
1933
  return extractPlaintext(filePath);
1666
1934
  }
1667
1935
 
1668
- /**
1669
- * @module rules/templates
1670
- * Resolves template variables (`${path.to.value}`) in rule `set` objects against file attributes.
1671
- */
1672
- /**
1673
- * Resolve `${template.vars}` in a value against the given attributes.
1674
- *
1675
- * @param value - The value to resolve.
1676
- * @param attributes - The file attributes for variable lookup.
1677
- * @returns The resolved value.
1678
- */
1679
- function resolveTemplateVars(value, attributes) {
1680
- if (typeof value !== 'string')
1681
- return value;
1682
- return value.replace(/\$\{([^}]+)\}/g, (_match, varPath) => {
1683
- const current = get(attributes, varPath);
1684
- if (current === null || current === undefined)
1685
- return '';
1686
- return typeof current === 'string' ? current : JSON.stringify(current);
1687
- });
1688
- }
1689
- /**
1690
- * Resolve all template variables in a `set` object.
1691
- *
1692
- * @param setObj - The key-value pairs to resolve.
1693
- * @param attributes - The file attributes for variable lookup.
1694
- * @returns The resolved key-value pairs.
1695
- */
1696
- function resolveSet(setObj, attributes) {
1697
- const result = {};
1698
- for (const [key, value] of Object.entries(setObj)) {
1699
- result[key] = resolveTemplateVars(value, attributes);
1700
- }
1701
- return result;
1702
- }
1703
-
1704
- /**
1705
- * @module rules/apply
1706
- * Applies compiled inference rules to file attributes, producing merged metadata via template resolution and JsonMap transforms.
1707
- */
1708
- /**
1709
- * Create the lib object for JsonMap transformations.
1710
- *
1711
- * @returns The lib object.
1712
- */
1713
- function createJsonMapLib() {
1714
- return {
1715
- split: (str, separator) => str.split(separator),
1716
- slice: (arr, start, end) => arr.slice(start, end),
1717
- join: (arr, separator) => arr.join(separator),
1718
- toLowerCase: (str) => str.toLowerCase(),
1719
- replace: (str, search, replacement) => str.replace(search, replacement),
1720
- get: (obj, path) => get(obj, path),
1721
- };
1722
- }
1723
- /**
1724
- * Apply compiled inference rules to file attributes, returning merged metadata and optional rendered content.
1725
- *
1726
- * Rules are evaluated in order; later rules override earlier ones.
1727
- * If a rule has a `map`, the JsonMap transformation is applied after `set` resolution,
1728
- * and map output overrides set output on conflict.
1729
- *
1730
- * @param compiledRules - The compiled rules to evaluate.
1731
- * @param attributes - The file attributes to match against.
1732
- * @param namedMaps - Optional record of named JsonMap definitions.
1733
- * @param logger - Optional logger for warnings (falls back to console.warn).
1734
- * @param templateEngine - Optional template engine for rendering content templates.
1735
- * @param configDir - Optional config directory for resolving .json map file paths.
1736
- * @returns The merged metadata and optional rendered content.
1737
- */
1738
- async function applyRules(compiledRules, attributes, namedMaps, logger, templateEngine, configDir) {
1739
- // JsonMap's type definitions expect a generic JsonMapLib shape with unary functions.
1740
- // Our helper functions accept multiple args, which JsonMap supports at runtime.
1741
- const lib = createJsonMapLib();
1742
- let merged = {};
1743
- let renderedContent = null;
1744
- const log = logger ?? console;
1745
- for (const [ruleIndex, { rule, validate }] of compiledRules.entries()) {
1746
- if (validate(attributes)) {
1747
- // Apply set resolution
1748
- const setOutput = resolveSet(rule.set, attributes);
1749
- merged = { ...merged, ...setOutput };
1750
- // Apply map transformation if present
1751
- if (rule.map) {
1752
- let mapDef;
1753
- // Resolve map reference
1754
- if (typeof rule.map === 'string') {
1755
- if (rule.map.endsWith('.json') && configDir) {
1756
- // File path: load from .json file
1757
- try {
1758
- const mapPath = resolve(configDir, rule.map);
1759
- const raw = readFileSync(mapPath, 'utf-8');
1760
- mapDef = JSON.parse(raw);
1761
- }
1762
- catch (error) {
1763
- log.warn(`Failed to load map file "${rule.map}": ${error instanceof Error ? error.message : String(error)}`);
1764
- continue;
1765
- }
1766
- }
1767
- else {
1768
- mapDef = namedMaps?.[rule.map];
1769
- if (!mapDef) {
1770
- log.warn(`Map reference "${rule.map}" not found in named maps. Skipping map transformation.`);
1771
- continue;
1772
- }
1773
- }
1774
- }
1775
- else {
1776
- mapDef = rule.map;
1777
- }
1778
- // Execute JsonMap transformation
1779
- try {
1780
- const jsonMap = new JsonMap(mapDef, lib);
1781
- const mapOutput = await jsonMap.transform(attributes);
1782
- if (mapOutput &&
1783
- typeof mapOutput === 'object' &&
1784
- !Array.isArray(mapOutput)) {
1785
- merged = { ...merged, ...mapOutput };
1786
- }
1787
- else {
1788
- log.warn(`JsonMap transformation did not return an object; skipping merge.`);
1789
- }
1790
- }
1791
- catch (error) {
1792
- log.warn(`JsonMap transformation failed: ${error instanceof Error ? error.message : String(error)}`);
1793
- }
1794
- }
1795
- // Render template if present
1796
- if (rule.template && templateEngine) {
1797
- const templateKey = `rule-${String(ruleIndex)}`;
1798
- // Build template context: attributes (with json spread at top) + map output
1799
- const context = {
1800
- ...(attributes.json ?? {}),
1801
- ...attributes,
1802
- ...merged,
1803
- };
1804
- try {
1805
- const result = templateEngine.render(templateKey, context);
1806
- if (result && result.trim()) {
1807
- renderedContent = result;
1808
- }
1809
- else {
1810
- log.warn(`Template for rule ${String(ruleIndex)} rendered empty output. Falling back to raw content.`);
1811
- }
1812
- }
1813
- catch (error) {
1814
- log.warn(`Template render failed for rule ${String(ruleIndex)}: ${error instanceof Error ? error.message : String(error)}. Falling back to raw content.`);
1815
- }
1816
- }
1817
- }
1818
- }
1819
- return { metadata: merged, renderedContent };
1820
- }
1821
-
1822
1936
  /**
1823
1937
  * @module rules/attributes
1824
1938
  * Builds file attribute objects for rule matching. Pure function: derives attributes from path, stats, and extracted data.
@@ -1909,14 +2023,14 @@ function compileRules(rules) {
1909
2023
  * @param configDir - Optional config directory for resolving file paths.
1910
2024
  * @returns The merged metadata and intermediate data.
1911
2025
  */
1912
- async function buildMergedMetadata(filePath, compiledRules, metadataDir, maps, logger, templateEngine, configDir) {
2026
+ async function buildMergedMetadata(filePath, compiledRules, metadataDir, maps, logger, templateEngine, configDir, customMapLib) {
1913
2027
  const ext = extname(filePath);
1914
2028
  const stats = await stat(filePath);
1915
2029
  // 1. Extract text and structured data
1916
2030
  const extracted = await extractText(filePath, ext);
1917
2031
  // 2. Build attributes + apply rules
1918
2032
  const attributes = buildAttributes(filePath, stats, extracted.frontmatter, extracted.json);
1919
- const { metadata: inferred, renderedContent } = await applyRules(compiledRules, attributes, maps, logger, templateEngine, configDir);
2033
+ const { metadata: inferred, renderedContent } = await applyRules(compiledRules, attributes, maps, logger, templateEngine, configDir, customMapLib);
1920
2034
  // 3. Read enrichment metadata (merge, enrichment wins)
1921
2035
  const enrichment = await readMetadata(filePath, metadataDir);
1922
2036
  const metadata = {
@@ -2029,7 +2143,7 @@ class DocumentProcessor {
2029
2143
  try {
2030
2144
  const ext = extname(filePath);
2031
2145
  // 1. Build merged metadata + extract text
2032
- const { metadata, extracted, renderedContent } = await buildMergedMetadata(filePath, this.compiledRules, this.config.metadataDir, this.config.maps, this.logger, this.templateEngine, this.config.configDir);
2146
+ const { metadata, extracted, renderedContent } = await buildMergedMetadata(filePath, this.compiledRules, this.config.metadataDir, this.config.maps, this.logger, this.templateEngine, this.config.configDir, this.config.customMapLib);
2033
2147
  // Use rendered template content if available, otherwise raw extracted text
2034
2148
  const textToEmbed = renderedContent ?? extracted.text;
2035
2149
  if (!textToEmbed.trim()) {
@@ -2143,7 +2257,7 @@ class DocumentProcessor {
2143
2257
  return null;
2144
2258
  }
2145
2259
  // Build merged metadata (lightweight — no embedding)
2146
- const { metadata } = await buildMergedMetadata(filePath, this.compiledRules, this.config.metadataDir, this.config.maps, this.logger, this.templateEngine, this.config.configDir);
2260
+ const { metadata } = await buildMergedMetadata(filePath, this.compiledRules, this.config.metadataDir, this.config.maps, this.logger, this.templateEngine, this.config.configDir, this.config.customMapLib);
2147
2261
  // Update all chunk payloads
2148
2262
  const totalChunks = getChunkCount(existingPayload);
2149
2263
  const ids = chunkIds(filePath, totalChunks);
@@ -2157,21 +2271,20 @@ class DocumentProcessor {
2157
2271
  }
2158
2272
  }
2159
2273
  /**
2160
- * Update compiled inference rules for subsequent file processing.
2161
- *
2162
- * @param compiledRules - The newly compiled rules.
2163
- */
2164
- /**
2165
- * Update compiled inference rules and optionally the template engine.
2274
+ * Update compiled inference rules, template engine, and custom map lib.
2166
2275
  *
2167
2276
  * @param compiledRules - The newly compiled rules.
2168
2277
  * @param templateEngine - Optional updated template engine.
2278
+ * @param customMapLib - Optional updated custom JsonMap lib functions.
2169
2279
  */
2170
- updateRules(compiledRules, templateEngine) {
2280
+ updateRules(compiledRules, templateEngine, customMapLib) {
2171
2281
  this.compiledRules = compiledRules;
2172
2282
  if (templateEngine) {
2173
2283
  this.templateEngine = templateEngine;
2174
2284
  }
2285
+ if (customMapLib !== undefined) {
2286
+ this.config = { ...this.config, customMapLib };
2287
+ }
2175
2288
  this.logger.info({ rules: compiledRules.length }, 'Inference rules updated');
2176
2289
  }
2177
2290
  }
@@ -3051,12 +3164,17 @@ class JeevesWatcher {
3051
3164
  const compiledRules = this.factories.compileRules(this.config.inferenceRules ?? []);
3052
3165
  const configDir = this.configPath ? dirname(this.configPath) : '.';
3053
3166
  const templateEngine = await buildTemplateEngine(this.config.inferenceRules ?? [], this.config.templates, this.config.templateHelpers?.paths, configDir);
3167
+ // Load custom JsonMap lib functions
3168
+ const customMapLib = this.config.mapHelpers?.paths?.length && configDir
3169
+ ? await loadCustomMapHelpers(this.config.mapHelpers.paths, configDir)
3170
+ : undefined;
3054
3171
  const processor = this.factories.createDocumentProcessor({
3055
3172
  metadataDir: this.config.metadataDir ?? '.jeeves-metadata',
3056
3173
  chunkSize: this.config.embedding.chunkSize,
3057
3174
  chunkOverlap: this.config.embedding.chunkOverlap,
3058
3175
  maps: this.config.maps,
3059
3176
  configDir,
3177
+ customMapLib,
3060
3178
  }, embeddingProvider, vectorStore, compiledRules, logger, templateEngine);
3061
3179
  this.processor = processor;
3062
3180
  this.queue = this.factories.createEventQueue({
@@ -3175,7 +3293,10 @@ class JeevesWatcher {
3175
3293
  const compiledRules = this.factories.compileRules(newConfig.inferenceRules ?? []);
3176
3294
  const reloadConfigDir = dirname(this.configPath);
3177
3295
  const newTemplateEngine = await buildTemplateEngine(newConfig.inferenceRules ?? [], newConfig.templates, newConfig.templateHelpers?.paths, reloadConfigDir);
3178
- processor.updateRules(compiledRules, newTemplateEngine);
3296
+ const newCustomMapLib = newConfig.mapHelpers?.paths?.length && reloadConfigDir
3297
+ ? await loadCustomMapHelpers(newConfig.mapHelpers.paths, reloadConfigDir)
3298
+ : undefined;
3299
+ processor.updateRules(compiledRules, newTemplateEngine, newCustomMapLib);
3179
3300
  logger.info({ configPath: this.configPath, rules: compiledRules.length }, 'Config reloaded');
3180
3301
  }
3181
3302
  catch (error) {
package/dist/index.d.ts CHANGED
@@ -127,11 +127,14 @@ declare const jeevesWatcherConfigSchema: z.ZodObject<{
127
127
  map: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<_karmaniverous_jsonmap.JsonMapMap, unknown, z.core.$ZodTypeInternals<_karmaniverous_jsonmap.JsonMapMap, unknown>>, z.ZodString]>>;
128
128
  template: z.ZodOptional<z.ZodString>;
129
129
  }, z.core.$strip>>>;
130
- maps: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<_karmaniverous_jsonmap.JsonMapMap, unknown, z.core.$ZodTypeInternals<_karmaniverous_jsonmap.JsonMapMap, unknown>>>>;
130
+ maps: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodType<_karmaniverous_jsonmap.JsonMapMap, unknown, z.core.$ZodTypeInternals<_karmaniverous_jsonmap.JsonMapMap, unknown>>, z.ZodString]>>>;
131
131
  templates: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
132
132
  templateHelpers: z.ZodOptional<z.ZodObject<{
133
133
  paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
134
134
  }, z.core.$strip>>;
135
+ mapHelpers: z.ZodOptional<z.ZodObject<{
136
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
137
+ }, z.core.$strip>>;
135
138
  logging: z.ZodOptional<z.ZodObject<{
136
139
  level: z.ZodOptional<z.ZodString>;
137
140
  file: z.ZodOptional<z.ZodString>;
@@ -366,7 +369,7 @@ interface ApplyRulesResult {
366
369
  * @param configDir - Optional config directory for resolving .json map file paths.
367
370
  * @returns The merged metadata and optional rendered content.
368
371
  */
369
- declare function applyRules(compiledRules: CompiledRule[], attributes: FileAttributes, namedMaps?: Record<string, JsonMapMap>, logger?: RuleLogger, templateEngine?: TemplateEngine, configDir?: string): Promise<ApplyRulesResult>;
372
+ declare function applyRules(compiledRules: CompiledRule[], attributes: FileAttributes, namedMaps?: Record<string, JsonMapMap>, logger?: RuleLogger, templateEngine?: TemplateEngine, configDir?: string, customMapLib?: Record<string, (...args: unknown[]) => unknown>): Promise<ApplyRulesResult>;
370
373
 
371
374
  /**
372
375
  * A point to upsert into the vector store.
@@ -516,6 +519,8 @@ interface ProcessorConfig {
516
519
  maps?: Record<string, JsonMapMap>;
517
520
  /** Config directory for resolving relative file paths. */
518
521
  configDir?: string;
522
+ /** Custom JsonMap lib functions loaded from mapHelpers config. */
523
+ customMapLib?: Record<string, (...args: unknown[]) => unknown>;
519
524
  }
520
525
  /**
521
526
  * Core document processing pipeline.
@@ -523,7 +528,7 @@ interface ProcessorConfig {
523
528
  * Handles extracting text, computing embeddings, and syncing with the vector store.
524
529
  */
525
530
  declare class DocumentProcessor {
526
- private readonly config;
531
+ private config;
527
532
  private readonly embeddingProvider;
528
533
  private readonly vectorStore;
529
534
  private compiledRules;
@@ -570,17 +575,13 @@ declare class DocumentProcessor {
570
575
  */
571
576
  processRulesUpdate(filePath: string): Promise<Record<string, unknown> | null>;
572
577
  /**
573
- * Update compiled inference rules for subsequent file processing.
574
- *
575
- * @param compiledRules - The newly compiled rules.
576
- */
577
- /**
578
- * Update compiled inference rules and optionally the template engine.
578
+ * Update compiled inference rules, template engine, and custom map lib.
579
579
  *
580
580
  * @param compiledRules - The newly compiled rules.
581
581
  * @param templateEngine - Optional updated template engine.
582
+ * @param customMapLib - Optional updated custom JsonMap lib functions.
582
583
  */
583
- updateRules(compiledRules: CompiledRule[], templateEngine?: TemplateEngine): void;
584
+ updateRules(compiledRules: CompiledRule[], templateEngine?: TemplateEngine, customMapLib?: Record<string, (...args: unknown[]) => unknown>): void;
584
585
  }
585
586
 
586
587
  /**