@karmaniverous/jeeves-watcher 0.9.5 → 0.9.7

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.
@@ -807,7 +807,7 @@
807
807
  "items": {
808
808
  "$ref": "#/definitions/__schema66"
809
809
  },
810
- "description": "Keys to extract from context and include as YAML frontmatter."
810
+ "description": "Keys or glob patterns to include as YAML frontmatter. Supports picomatch globs (e.g. \"*\") and \"!\"-prefixed exclusion patterns (e.g. \"!_*\"). Explicit names preserve declaration order; glob-matched keys are sorted alphabetically."
811
811
  },
812
812
  "body": {
813
813
  "type": "array",
@@ -1057,6 +1057,72 @@ function rebaseHeadings(markdown, baseHeading) {
1057
1057
  return rebased.join('\n');
1058
1058
  }
1059
1059
 
1060
+ /**
1061
+ * @module templates/resolveFrontmatterKeys
1062
+ * Resolves frontmatter key patterns (with glob/negation support) against
1063
+ * available context keys. Patterns prefixed with `!` are exclusions.
1064
+ * Supports picomatch glob syntax (e.g. `*`, `_*`, `chunk_*`).
1065
+ */
1066
+ /**
1067
+ * Resolve frontmatter patterns against available keys.
1068
+ *
1069
+ * @param patterns - Array of key names or glob patterns. `!`-prefixed patterns exclude.
1070
+ * @param allKeys - All available keys from the rendering context.
1071
+ * @returns Ordered array of resolved keys: explicit names in declaration order,
1072
+ * then glob-matched names sorted alphabetically, minus exclusions.
1073
+ */
1074
+ function resolveFrontmatterKeys(patterns, allKeys) {
1075
+ const includes = [];
1076
+ const excludePatterns = [];
1077
+ for (const p of patterns) {
1078
+ if (p.startsWith('!')) {
1079
+ excludePatterns.push(p.slice(1));
1080
+ }
1081
+ else {
1082
+ includes.push(p);
1083
+ }
1084
+ }
1085
+ const isExcluded = excludePatterns.length
1086
+ ? picomatch(excludePatterns)
1087
+ : () => false;
1088
+ // Collect keys: explicit names first (in order), then glob-expanded (sorted).
1089
+ const result = [];
1090
+ const seen = new Set();
1091
+ // Pass 1: explicit (non-glob) names — preserved in declaration order.
1092
+ const explicitNames = [];
1093
+ const globPatterns = [];
1094
+ for (const p of includes) {
1095
+ if (isGlob(p)) {
1096
+ globPatterns.push(p);
1097
+ }
1098
+ else {
1099
+ explicitNames.push(p);
1100
+ }
1101
+ }
1102
+ for (const name of explicitNames) {
1103
+ if (!isExcluded(name) && allKeys.includes(name) && !seen.has(name)) {
1104
+ result.push(name);
1105
+ seen.add(name);
1106
+ }
1107
+ }
1108
+ // Pass 2: glob patterns — matched keys sorted alphabetically.
1109
+ if (globPatterns.length) {
1110
+ const isIncluded = picomatch(globPatterns);
1111
+ const matched = allKeys.filter((k) => isIncluded(k)).sort();
1112
+ for (const key of matched) {
1113
+ if (!isExcluded(key) && !seen.has(key)) {
1114
+ result.push(key);
1115
+ seen.add(key);
1116
+ }
1117
+ }
1118
+ }
1119
+ return result;
1120
+ }
1121
+ /** Check whether a pattern contains glob characters. */
1122
+ function isGlob(pattern) {
1123
+ return /[*?[\]{}]/.test(pattern);
1124
+ }
1125
+
1060
1126
  /**
1061
1127
  * @module templates/renderDoc
1062
1128
  * Declarative renderer for YAML frontmatter + structured Markdown body.
@@ -1085,8 +1151,11 @@ function renderValueAsMarkdown(hbs, section, value) {
1085
1151
  const md = callFormatHelper(hbs, section.format, value, section.formatArgs);
1086
1152
  return rebaseHeadings(md, section.heading);
1087
1153
  }
1088
- const strValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
1089
- return hbs.Utils.escapeExpression(strValue);
1154
+ // Return string values as-is renderDoc produces markdown, not HTML.
1155
+ // HTML escaping (if needed) happens downstream when the markdown is rendered.
1156
+ if (typeof value === 'string')
1157
+ return value;
1158
+ return JSON.stringify(value, null, 2);
1090
1159
  }
1091
1160
  function renderEach(hbs, section, value) {
1092
1161
  if (!Array.isArray(value))
@@ -1127,9 +1196,10 @@ function renderEach(hbs, section, value) {
1127
1196
  */
1128
1197
  function renderDoc(context, config, hbs) {
1129
1198
  const parts = [];
1130
- // Frontmatter
1199
+ // Frontmatter — resolve patterns (globs, negations) against context keys.
1200
+ const resolvedKeys = resolveFrontmatterKeys(config.frontmatter, Object.keys(context));
1131
1201
  const fmObj = {};
1132
- for (const key of config.frontmatter) {
1202
+ for (const key of resolvedKeys) {
1133
1203
  const v = get(context, key);
1134
1204
  if (v !== undefined) {
1135
1205
  fmObj[key] = v;
@@ -1923,10 +1993,12 @@ const renderBodySectionSchema = z.object({
1923
1993
  });
1924
1994
  /** Render config: YAML frontmatter + ordered body sections. */
1925
1995
  const renderConfigSchema = z.object({
1926
- /** Keys to extract from context and include as YAML frontmatter. */
1996
+ /** Keys or glob patterns to extract from context and include as YAML frontmatter. */
1927
1997
  frontmatter: z
1928
1998
  .array(z.string().min(1))
1929
- .describe('Keys to extract from context and include as YAML frontmatter.'),
1999
+ .describe('Keys or glob patterns to include as YAML frontmatter. ' +
2000
+ 'Supports picomatch globs (e.g. "*") and "!"-prefixed exclusion patterns (e.g. "!_*"). ' +
2001
+ 'Explicit names preserve declaration order; glob-matched keys are sorted alphabetically.'),
1930
2002
  /** Ordered markdown body sections. */
1931
2003
  body: z
1932
2004
  .array(renderBodySectionSchema)
package/dist/index.js CHANGED
@@ -12,9 +12,9 @@ import { capitalize, title, camel, snake, dash, isEqual, get, parallel, omit } f
12
12
  import rehypeParse from 'rehype-parse';
13
13
  import { unified } from 'unified';
14
14
  import yaml from 'js-yaml';
15
+ import picomatch from 'picomatch';
15
16
  import Ajv from 'ajv';
16
17
  import addFormats from 'ajv-formats';
17
- import picomatch from 'picomatch';
18
18
  import { readdir, stat, writeFile, rm, readFile, mkdir } from 'node:fs/promises';
19
19
  import { z, ZodError } from 'zod';
20
20
  import { JSONPath } from 'jsonpath-plus';
@@ -374,6 +374,72 @@ function rebaseHeadings(markdown, baseHeading) {
374
374
  return rebased.join('\n');
375
375
  }
376
376
 
377
+ /**
378
+ * @module templates/resolveFrontmatterKeys
379
+ * Resolves frontmatter key patterns (with glob/negation support) against
380
+ * available context keys. Patterns prefixed with `!` are exclusions.
381
+ * Supports picomatch glob syntax (e.g. `*`, `_*`, `chunk_*`).
382
+ */
383
+ /**
384
+ * Resolve frontmatter patterns against available keys.
385
+ *
386
+ * @param patterns - Array of key names or glob patterns. `!`-prefixed patterns exclude.
387
+ * @param allKeys - All available keys from the rendering context.
388
+ * @returns Ordered array of resolved keys: explicit names in declaration order,
389
+ * then glob-matched names sorted alphabetically, minus exclusions.
390
+ */
391
+ function resolveFrontmatterKeys(patterns, allKeys) {
392
+ const includes = [];
393
+ const excludePatterns = [];
394
+ for (const p of patterns) {
395
+ if (p.startsWith('!')) {
396
+ excludePatterns.push(p.slice(1));
397
+ }
398
+ else {
399
+ includes.push(p);
400
+ }
401
+ }
402
+ const isExcluded = excludePatterns.length
403
+ ? picomatch(excludePatterns)
404
+ : () => false;
405
+ // Collect keys: explicit names first (in order), then glob-expanded (sorted).
406
+ const result = [];
407
+ const seen = new Set();
408
+ // Pass 1: explicit (non-glob) names — preserved in declaration order.
409
+ const explicitNames = [];
410
+ const globPatterns = [];
411
+ for (const p of includes) {
412
+ if (isGlob(p)) {
413
+ globPatterns.push(p);
414
+ }
415
+ else {
416
+ explicitNames.push(p);
417
+ }
418
+ }
419
+ for (const name of explicitNames) {
420
+ if (!isExcluded(name) && allKeys.includes(name) && !seen.has(name)) {
421
+ result.push(name);
422
+ seen.add(name);
423
+ }
424
+ }
425
+ // Pass 2: glob patterns — matched keys sorted alphabetically.
426
+ if (globPatterns.length) {
427
+ const isIncluded = picomatch(globPatterns);
428
+ const matched = allKeys.filter((k) => isIncluded(k)).sort();
429
+ for (const key of matched) {
430
+ if (!isExcluded(key) && !seen.has(key)) {
431
+ result.push(key);
432
+ seen.add(key);
433
+ }
434
+ }
435
+ }
436
+ return result;
437
+ }
438
+ /** Check whether a pattern contains glob characters. */
439
+ function isGlob(pattern) {
440
+ return /[*?[\]{}]/.test(pattern);
441
+ }
442
+
377
443
  /**
378
444
  * @module templates/renderDoc
379
445
  * Declarative renderer for YAML frontmatter + structured Markdown body.
@@ -402,8 +468,11 @@ function renderValueAsMarkdown(hbs, section, value) {
402
468
  const md = callFormatHelper(hbs, section.format, value, section.formatArgs);
403
469
  return rebaseHeadings(md, section.heading);
404
470
  }
405
- const strValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
406
- return hbs.Utils.escapeExpression(strValue);
471
+ // Return string values as-is renderDoc produces markdown, not HTML.
472
+ // HTML escaping (if needed) happens downstream when the markdown is rendered.
473
+ if (typeof value === 'string')
474
+ return value;
475
+ return JSON.stringify(value, null, 2);
407
476
  }
408
477
  function renderEach(hbs, section, value) {
409
478
  if (!Array.isArray(value))
@@ -444,9 +513,10 @@ function renderEach(hbs, section, value) {
444
513
  */
445
514
  function renderDoc(context, config, hbs) {
446
515
  const parts = [];
447
- // Frontmatter
516
+ // Frontmatter — resolve patterns (globs, negations) against context keys.
517
+ const resolvedKeys = resolveFrontmatterKeys(config.frontmatter, Object.keys(context));
448
518
  const fmObj = {};
449
- for (const key of config.frontmatter) {
519
+ for (const key of resolvedKeys) {
450
520
  const v = get(context, key);
451
521
  if (v !== undefined) {
452
522
  fmObj[key] = v;
@@ -1609,10 +1679,12 @@ const renderBodySectionSchema = z.object({
1609
1679
  });
1610
1680
  /** Render config: YAML frontmatter + ordered body sections. */
1611
1681
  const renderConfigSchema = z.object({
1612
- /** Keys to extract from context and include as YAML frontmatter. */
1682
+ /** Keys or glob patterns to extract from context and include as YAML frontmatter. */
1613
1683
  frontmatter: z
1614
1684
  .array(z.string().min(1))
1615
- .describe('Keys to extract from context and include as YAML frontmatter.'),
1685
+ .describe('Keys or glob patterns to include as YAML frontmatter. ' +
1686
+ 'Supports picomatch globs (e.g. "*") and "!"-prefixed exclusion patterns (e.g. "!_*"). ' +
1687
+ 'Explicit names preserve declaration order; glob-matched keys are sorted alphabetically.'),
1616
1688
  /** Ordered markdown body sections. */
1617
1689
  body: z
1618
1690
  .array(renderBodySectionSchema)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/jeeves-watcher",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
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",