@karmaniverous/jeeves-watcher 0.16.2 → 0.17.0
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 +29 -1
- package/dist/cli/jeeves-watcher/index-BAJ-z4d0.js +6249 -0
- package/dist/cli/jeeves-watcher/index.js +67 -6240
- package/dist/index.d.ts +8 -3
- package/dist/index.js +1075 -1015
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { join, dirname, resolve, relative, extname, basename, isAbsolute } from 'node:path';
|
|
2
|
-
import { createConfigQueryHandler as createConfigQueryHandler$1, createStatusHandler, getBindAddress, postJson, fetchJson } from '@karmaniverous/jeeves';
|
|
2
|
+
import { createConfigQueryHandler as createConfigQueryHandler$1, createStatusHandler, createConfigApplyHandler, DEFAULT_PORTS, getBindAddress, postJson, fetchJson } from '@karmaniverous/jeeves';
|
|
3
3
|
import Fastify from 'fastify';
|
|
4
|
-
import { readdir, stat,
|
|
4
|
+
import { readdir, stat, readFile } from 'node:fs/promises';
|
|
5
5
|
import { parallel, capitalize, title, camel, snake, dash, isEqual, get, omit } from 'radash';
|
|
6
6
|
import { existsSync, statSync, readFileSync, readdirSync, mkdirSync, writeFileSync, rmSync, renameSync } from 'node:fs';
|
|
7
7
|
import ignore from 'ignore';
|
|
8
8
|
import picomatch from 'picomatch';
|
|
9
|
-
import { z, ZodError } from 'zod';
|
|
10
|
-
import { jsonMapMapSchema, JsonMap } from '@karmaniverous/jsonmap';
|
|
11
9
|
import Ajv from 'ajv';
|
|
12
10
|
import addFormats from 'ajv-formats';
|
|
11
|
+
import { z, ZodError } from 'zod';
|
|
12
|
+
import { jsonMapMapSchema, JsonMap } from '@karmaniverous/jsonmap';
|
|
13
13
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
14
14
|
import Handlebars from 'handlebars';
|
|
15
15
|
import dayjs from 'dayjs';
|
|
@@ -614,10 +614,10 @@ async function fireCallback(url, payload, logger) {
|
|
|
614
614
|
function groupByRoot(filePaths, watchPaths) {
|
|
615
615
|
const byRoot = {};
|
|
616
616
|
for (const fp of filePaths) {
|
|
617
|
-
const normalised = fp
|
|
617
|
+
const normalised = normalizeSlashes(fp);
|
|
618
618
|
let matched = false;
|
|
619
619
|
for (const root of watchPaths) {
|
|
620
|
-
const rootNorm = root
|
|
620
|
+
const rootNorm = normalizeSlashes(root).replace(/\/?\*\*.*$/, '');
|
|
621
621
|
if (normalised.startsWith(rootNorm)) {
|
|
622
622
|
byRoot[rootNorm] = (byRoot[rootNorm] ?? 0) + 1;
|
|
623
623
|
matched = true;
|
|
@@ -969,579 +969,135 @@ async function getPathFileList(targetPath, config, gitignoreFilter) {
|
|
|
969
969
|
}
|
|
970
970
|
|
|
971
971
|
/**
|
|
972
|
-
* @module
|
|
973
|
-
*
|
|
972
|
+
* @module rules/attributes
|
|
973
|
+
* Builds file attribute objects for rule matching. Pure function: derives attributes from path, stats, and extracted data.
|
|
974
974
|
*/
|
|
975
|
+
/** Derive the path-based file properties shared by all attribute builders. */
|
|
976
|
+
function buildFileProps(filePath) {
|
|
977
|
+
const normalised = normalizeSlashes(filePath);
|
|
978
|
+
return {
|
|
979
|
+
path: normalised,
|
|
980
|
+
directory: normalizeSlashes(dirname(normalised)),
|
|
981
|
+
filename: basename(normalised),
|
|
982
|
+
extension: extname(normalised),
|
|
983
|
+
};
|
|
984
|
+
}
|
|
975
985
|
/**
|
|
976
|
-
*
|
|
986
|
+
* Build {@link FileAttributes} from a file path and stat info.
|
|
987
|
+
*
|
|
988
|
+
* @param filePath - The file path.
|
|
989
|
+
* @param stats - The file stats.
|
|
990
|
+
* @param extractedFrontmatter - Optional extracted frontmatter.
|
|
991
|
+
* @param extractedJson - Optional parsed JSON content.
|
|
992
|
+
* @returns The constructed file attributes.
|
|
977
993
|
*/
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
.
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
.optional()
|
|
993
|
-
.describe('Polling interval in milliseconds when usePolling is enabled.'),
|
|
994
|
-
/** Whether to use polling instead of native watchers. */
|
|
995
|
-
usePolling: z
|
|
996
|
-
.boolean()
|
|
997
|
-
.optional()
|
|
998
|
-
.describe('Use polling instead of native file system events (for network drives).'),
|
|
999
|
-
/** Debounce delay in milliseconds for file change events. */
|
|
1000
|
-
debounceMs: z
|
|
1001
|
-
.number()
|
|
1002
|
-
.optional()
|
|
1003
|
-
.describe('Debounce delay in milliseconds for file change events.'),
|
|
1004
|
-
/** Time in milliseconds a file must be stable before processing. */
|
|
1005
|
-
stabilityThresholdMs: z
|
|
1006
|
-
.number()
|
|
1007
|
-
.optional()
|
|
1008
|
-
.describe('Time in milliseconds a file must remain unchanged before processing.'),
|
|
1009
|
-
/** Whether to respect .gitignore files when processing. */
|
|
1010
|
-
respectGitignore: z
|
|
1011
|
-
.boolean()
|
|
1012
|
-
.optional()
|
|
1013
|
-
.describe('Skip files ignored by .gitignore in git repositories. Only applies to repos with a .git directory. Default: true.'),
|
|
1014
|
-
/** Move detection configuration for correlating unlink+add as file moves. */
|
|
1015
|
-
moveDetection: z
|
|
1016
|
-
.object({
|
|
1017
|
-
/** Enable move correlation. Default: true. */
|
|
1018
|
-
enabled: z
|
|
1019
|
-
.boolean()
|
|
1020
|
-
.default(true)
|
|
1021
|
-
.describe('Enable move detection via content hash correlation.'),
|
|
1022
|
-
/** Buffer time in ms for holding unlink events before treating as deletes. Default: 2000. */
|
|
1023
|
-
bufferMs: z
|
|
1024
|
-
.number()
|
|
1025
|
-
.int()
|
|
1026
|
-
.min(100)
|
|
1027
|
-
.default(2000)
|
|
1028
|
-
.describe('How long (ms) to buffer unlink events before treating as deletes.'),
|
|
1029
|
-
})
|
|
1030
|
-
.optional()
|
|
1031
|
-
.describe('Move detection: correlate unlink+add events as file moves to avoid re-embedding.'),
|
|
1032
|
-
});
|
|
994
|
+
function buildAttributes(filePath, stats, extractedFrontmatter, extractedJson) {
|
|
995
|
+
const attrs = {
|
|
996
|
+
file: {
|
|
997
|
+
...buildFileProps(filePath),
|
|
998
|
+
sizeBytes: stats.size,
|
|
999
|
+
modified: stats.mtime.toISOString(),
|
|
1000
|
+
},
|
|
1001
|
+
};
|
|
1002
|
+
if (extractedFrontmatter)
|
|
1003
|
+
attrs.frontmatter = extractedFrontmatter;
|
|
1004
|
+
if (extractedJson)
|
|
1005
|
+
attrs.json = extractedJson;
|
|
1006
|
+
return attrs;
|
|
1007
|
+
}
|
|
1033
1008
|
/**
|
|
1034
|
-
*
|
|
1009
|
+
* Build synthetic file attributes from a path string (no actual file I/O).
|
|
1010
|
+
* Used by API handlers that need to match rules against paths without reading files.
|
|
1011
|
+
*
|
|
1012
|
+
* @param filePath - The file path.
|
|
1013
|
+
* @returns Synthetic file attributes with zeroed stats.
|
|
1035
1014
|
*/
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
.describe('Debounce delay in milliseconds for config file change detection.'),
|
|
1047
|
-
/** Reindex scope triggered on config change. */
|
|
1048
|
-
reindex: z
|
|
1049
|
-
.union([
|
|
1050
|
-
z
|
|
1051
|
-
.literal('issues')
|
|
1052
|
-
.describe('Re-process only files with recorded issues.'),
|
|
1053
|
-
z.literal('full').describe('Full reindex of all watched files.'),
|
|
1054
|
-
z
|
|
1055
|
-
.literal('rules')
|
|
1056
|
-
.describe('Re-apply inference rules to existing points without re-embedding.'),
|
|
1057
|
-
])
|
|
1058
|
-
.optional()
|
|
1059
|
-
.describe('Reindex scope triggered on config change. Default: issues.'),
|
|
1060
|
-
});
|
|
1015
|
+
function buildSyntheticAttributes(filePath) {
|
|
1016
|
+
return {
|
|
1017
|
+
file: {
|
|
1018
|
+
...buildFileProps(filePath),
|
|
1019
|
+
sizeBytes: 0,
|
|
1020
|
+
modified: new Date(0).toISOString(),
|
|
1021
|
+
},
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1061
1025
|
/**
|
|
1062
|
-
*
|
|
1026
|
+
* @module rules/ajvSetup
|
|
1027
|
+
* AJV instance factory with custom glob keyword for picomatch-based pattern matching in rule schemas.
|
|
1063
1028
|
*/
|
|
1064
|
-
const apiConfigSchema = z.object({
|
|
1065
|
-
/** Host to bind to. */
|
|
1066
|
-
host: z
|
|
1067
|
-
.string()
|
|
1068
|
-
.optional()
|
|
1069
|
-
.describe('Host address for API server (e.g., "127.0.0.1", "0.0.0.0").'),
|
|
1070
|
-
/** Port to listen on. */
|
|
1071
|
-
port: z.number().optional().describe('Port for API server (e.g., 1936).'),
|
|
1072
|
-
/** Read endpoint cache TTL in milliseconds. */
|
|
1073
|
-
cacheTtlMs: z
|
|
1074
|
-
.number()
|
|
1075
|
-
.optional()
|
|
1076
|
-
.describe('TTL in milliseconds for caching read-heavy endpoints (e.g., /status, /config). Default: 30000.'),
|
|
1077
|
-
});
|
|
1078
1029
|
/**
|
|
1079
|
-
*
|
|
1030
|
+
* Create an AJV instance with a custom `glob` format for picomatch glob matching.
|
|
1031
|
+
*
|
|
1032
|
+
* @returns The configured AJV instance.
|
|
1080
1033
|
*/
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
});
|
|
1034
|
+
function createRuleAjv() {
|
|
1035
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
1036
|
+
addFormats(ajv);
|
|
1037
|
+
ajv.addKeyword({
|
|
1038
|
+
keyword: 'glob',
|
|
1039
|
+
type: 'string',
|
|
1040
|
+
schemaType: 'string',
|
|
1041
|
+
validate: (pattern, data) => picomatch.isMatch(data, pattern, { dot: true, nocase: true }),
|
|
1042
|
+
});
|
|
1043
|
+
return ajv;
|
|
1044
|
+
}
|
|
1093
1045
|
|
|
1094
1046
|
/**
|
|
1095
|
-
* @module
|
|
1096
|
-
*
|
|
1047
|
+
* @module rules/compile
|
|
1048
|
+
* Compiles inference rule definitions into executable AJV validators for efficient rule evaluation.
|
|
1097
1049
|
*/
|
|
1098
1050
|
/**
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1051
|
+
* Validate that all rule names are unique.
|
|
1052
|
+
* Throws if duplicate names are found.
|
|
1053
|
+
*
|
|
1054
|
+
* @param rules - The inference rule definitions.
|
|
1101
1055
|
*/
|
|
1102
|
-
|
|
1056
|
+
function validateRuleNameUniqueness(rules) {
|
|
1057
|
+
const names = new Set();
|
|
1058
|
+
const duplicates = [];
|
|
1059
|
+
for (const rule of rules) {
|
|
1060
|
+
if (names.has(rule.name)) {
|
|
1061
|
+
duplicates.push(rule.name);
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
names.add(rule.name);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (duplicates.length > 0) {
|
|
1068
|
+
throw new Error(`Duplicate inference rule names found: ${duplicates.join(', ')}. Rule names must be unique.`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1103
1071
|
/**
|
|
1104
|
-
*
|
|
1072
|
+
* Compile an array of inference rules into executable validators.
|
|
1073
|
+
* Validates rule name uniqueness before compilation.
|
|
1074
|
+
*
|
|
1075
|
+
* @param rules - The inference rule definitions.
|
|
1076
|
+
* @returns An array of compiled rules.
|
|
1105
1077
|
*/
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
});
|
|
1078
|
+
function compileRules(rules) {
|
|
1079
|
+
validateRuleNameUniqueness(rules);
|
|
1080
|
+
const ajv = createRuleAjv();
|
|
1081
|
+
return rules.map((rule) => ({
|
|
1082
|
+
rule,
|
|
1083
|
+
validate: ajv.compile({
|
|
1084
|
+
$id: rule.name,
|
|
1085
|
+
...rule.match,
|
|
1086
|
+
}),
|
|
1087
|
+
}));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1116
1090
|
/**
|
|
1117
|
-
*
|
|
1091
|
+
* @module api/handlers/wrapHandler
|
|
1092
|
+
* Generic error-handling wrapper for Fastify route handlers.
|
|
1118
1093
|
*/
|
|
1119
|
-
const schemaEntrySchema = z.union([
|
|
1120
|
-
schemaObjectSchema,
|
|
1121
|
-
z.string().describe('File path to a JSON schema file.'),
|
|
1122
|
-
]);
|
|
1123
1094
|
/**
|
|
1124
|
-
*
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
/** Render body section. */
|
|
1131
|
-
const renderBodySectionSchema = z.object({
|
|
1132
|
-
/** Key path in the template context to render. */
|
|
1133
|
-
path: z.string().min(1).describe('Key path in template context to render.'),
|
|
1134
|
-
/** Markdown heading level for this section (1-6). */
|
|
1135
|
-
heading: z.number().min(1).max(6).describe('Markdown heading level (1-6).'),
|
|
1136
|
-
/** Override heading text (default: titlecased path). */
|
|
1137
|
-
label: z.string().optional().describe('Override heading text.'),
|
|
1138
|
-
/** Name of a registered Handlebars helper used as a format handler. */
|
|
1139
|
-
format: z
|
|
1140
|
-
.string()
|
|
1141
|
-
.optional()
|
|
1142
|
-
.describe('Name of a registered Handlebars helper used as a format handler.'),
|
|
1143
|
-
/** Additional args passed to the format helper. */
|
|
1144
|
-
formatArgs: z
|
|
1145
|
-
.array(z.unknown())
|
|
1146
|
-
.optional()
|
|
1147
|
-
.describe('Additional args passed to the format helper.'),
|
|
1148
|
-
/** If true, the value at path is treated as an array and iterated. */
|
|
1149
|
-
each: z
|
|
1150
|
-
.boolean()
|
|
1151
|
-
.optional()
|
|
1152
|
-
.describe('If true, the value at path is treated as an array and iterated.'),
|
|
1153
|
-
/** Handlebars template string for per-item heading text (used when each=true). */
|
|
1154
|
-
headingTemplate: z
|
|
1155
|
-
.string()
|
|
1156
|
-
.optional()
|
|
1157
|
-
.describe('Handlebars template string for per-item heading text (used when each=true).'),
|
|
1158
|
-
/** Key path within each item to use as renderable content (used when each=true). */
|
|
1159
|
-
contentPath: z
|
|
1160
|
-
.string()
|
|
1161
|
-
.optional()
|
|
1162
|
-
.describe('Key path within each item to use as renderable content (used when each=true).'),
|
|
1163
|
-
/** Key path within each item to sort by (used when each=true). */
|
|
1164
|
-
sort: z
|
|
1165
|
-
.string()
|
|
1166
|
-
.optional()
|
|
1167
|
-
.describe('Key path within each item to sort by (used when each=true).'),
|
|
1168
|
-
});
|
|
1169
|
-
/** Render config: YAML frontmatter + ordered body sections. */
|
|
1170
|
-
const renderConfigSchema = z.object({
|
|
1171
|
-
/** Keys or glob patterns to extract from context and include as YAML frontmatter. */
|
|
1172
|
-
frontmatter: z
|
|
1173
|
-
.array(z.string().min(1))
|
|
1174
|
-
.describe('Keys or glob patterns to include as YAML frontmatter. ' +
|
|
1175
|
-
'Supports picomatch globs (e.g. "*") and "!"-prefixed exclusion patterns (e.g. "!_*"). ' +
|
|
1176
|
-
'Explicit names preserve declaration order; glob-matched keys are sorted alphabetically.'),
|
|
1177
|
-
/** Ordered markdown body sections. */
|
|
1178
|
-
body: z
|
|
1179
|
-
.array(renderBodySectionSchema)
|
|
1180
|
-
.describe('Ordered markdown body sections.'),
|
|
1181
|
-
});
|
|
1182
|
-
/**
|
|
1183
|
-
* An inference rule that enriches document metadata.
|
|
1184
|
-
*/
|
|
1185
|
-
const inferenceRuleSchema = z
|
|
1186
|
-
.object({
|
|
1187
|
-
/** Unique name for this inference rule. */
|
|
1188
|
-
name: z
|
|
1189
|
-
.string()
|
|
1190
|
-
.min(1)
|
|
1191
|
-
.describe('Unique name identifying this inference rule.'),
|
|
1192
|
-
/** Human-readable description of what this rule does. */
|
|
1193
|
-
description: z
|
|
1194
|
-
.string()
|
|
1195
|
-
.min(1)
|
|
1196
|
-
.describe('Human-readable description of what this rule does.'),
|
|
1197
|
-
/** JSON Schema object to match against document metadata. */
|
|
1198
|
-
match: z
|
|
1199
|
-
.record(z.string(), z.unknown())
|
|
1200
|
-
.describe('JSON Schema object to match against file attributes.'),
|
|
1201
|
-
/** Array of schema references to merge (named refs and/or inline objects). */
|
|
1202
|
-
schema: z
|
|
1203
|
-
.array(schemaReferenceSchema)
|
|
1204
|
-
.optional()
|
|
1205
|
-
.describe('Array of schema references (named schema refs or inline objects) merged left-to-right.'),
|
|
1206
|
-
/** JsonMap transformation (inline or reference to named map). */
|
|
1207
|
-
map: z
|
|
1208
|
-
.union([jsonMapMapSchema, z.string()])
|
|
1209
|
-
.optional()
|
|
1210
|
-
.describe('JsonMap transformation (inline definition, named map reference, or .json file path).'),
|
|
1211
|
-
/** Handlebars template (inline string, named ref, or .hbs/.handlebars file path). */
|
|
1212
|
-
template: z
|
|
1213
|
-
.string()
|
|
1214
|
-
.optional()
|
|
1215
|
-
.describe('Handlebars content template (inline string, named ref, or .hbs/.handlebars file path).'),
|
|
1216
|
-
/** Declarative structured renderer configuration (mutually exclusive with template). */
|
|
1217
|
-
render: renderConfigSchema
|
|
1218
|
-
.optional()
|
|
1219
|
-
.describe('Declarative render configuration for frontmatter + structured Markdown output (mutually exclusive with template).'),
|
|
1220
|
-
/** Output file extension override (e.g. "md", "html", "txt"). Requires template or render. */
|
|
1221
|
-
renderAs: z
|
|
1222
|
-
.string()
|
|
1223
|
-
.regex(/^[a-z0-9]{1,10}$/, 'renderAs must be 1-10 lowercase alphanumeric characters')
|
|
1224
|
-
.optional()
|
|
1225
|
-
.describe('Output file extension override (without dot). Requires template or render.'),
|
|
1226
|
-
})
|
|
1227
|
-
.superRefine((val, ctx) => {
|
|
1228
|
-
if (val.render && val.template) {
|
|
1229
|
-
ctx.addIssue({
|
|
1230
|
-
code: 'custom',
|
|
1231
|
-
path: ['render'],
|
|
1232
|
-
message: 'render is mutually exclusive with template',
|
|
1233
|
-
});
|
|
1234
|
-
}
|
|
1235
|
-
if (val.renderAs && !val.template && !val.render) {
|
|
1236
|
-
ctx.addIssue({
|
|
1237
|
-
code: 'custom',
|
|
1238
|
-
path: ['renderAs'],
|
|
1239
|
-
message: 'renderAs requires template or render',
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1242
|
-
});
|
|
1243
|
-
|
|
1244
|
-
/**
|
|
1245
|
-
* @module config/schemas/services
|
|
1246
|
-
* Service configuration schemas: embedding and vector store.
|
|
1247
|
-
*/
|
|
1248
|
-
/**
|
|
1249
|
-
* Embedding model configuration.
|
|
1250
|
-
*/
|
|
1251
|
-
const embeddingConfigSchema = z.object({
|
|
1252
|
-
/** The embedding model provider. */
|
|
1253
|
-
provider: z
|
|
1254
|
-
.string()
|
|
1255
|
-
.default('gemini')
|
|
1256
|
-
.describe('Embedding provider name (e.g., "gemini", "openai").'),
|
|
1257
|
-
/** The embedding model name. */
|
|
1258
|
-
model: z
|
|
1259
|
-
.string()
|
|
1260
|
-
.default('gemini-embedding-001')
|
|
1261
|
-
.describe('Embedding model identifier (e.g., "gemini-embedding-001", "text-embedding-3-small").'),
|
|
1262
|
-
/** Maximum tokens per chunk for splitting. */
|
|
1263
|
-
chunkSize: z
|
|
1264
|
-
.number()
|
|
1265
|
-
.optional()
|
|
1266
|
-
.describe('Maximum chunk size in characters for text splitting.'),
|
|
1267
|
-
/** Overlap between chunks in tokens. */
|
|
1268
|
-
chunkOverlap: z
|
|
1269
|
-
.number()
|
|
1270
|
-
.optional()
|
|
1271
|
-
.describe('Character overlap between consecutive chunks.'),
|
|
1272
|
-
/** Embedding vector dimensions. */
|
|
1273
|
-
dimensions: z
|
|
1274
|
-
.number()
|
|
1275
|
-
.optional()
|
|
1276
|
-
.describe('Embedding vector dimensions (must match model output).'),
|
|
1277
|
-
/** API key for the embedding provider. */
|
|
1278
|
-
apiKey: z
|
|
1279
|
-
.string()
|
|
1280
|
-
.optional()
|
|
1281
|
-
.describe('API key for embedding provider (supports ${ENV_VAR} substitution).'),
|
|
1282
|
-
/** Maximum embedding requests per minute. */
|
|
1283
|
-
rateLimitPerMinute: z
|
|
1284
|
-
.number()
|
|
1285
|
-
.optional()
|
|
1286
|
-
.describe('Maximum embedding API requests per minute (rate limiting).'),
|
|
1287
|
-
/** Maximum concurrent embedding requests. */
|
|
1288
|
-
concurrency: z
|
|
1289
|
-
.number()
|
|
1290
|
-
.optional()
|
|
1291
|
-
.describe('Maximum concurrent embedding requests.'),
|
|
1292
|
-
});
|
|
1293
|
-
/**
|
|
1294
|
-
* Vector store configuration for Qdrant.
|
|
1295
|
-
*/
|
|
1296
|
-
const vectorStoreConfigSchema = z.object({
|
|
1297
|
-
/** Qdrant server URL. */
|
|
1298
|
-
url: z
|
|
1299
|
-
.string()
|
|
1300
|
-
.describe('Qdrant server URL (e.g., "http://localhost:6333").'),
|
|
1301
|
-
/** Qdrant collection name. */
|
|
1302
|
-
collectionName: z
|
|
1303
|
-
.string()
|
|
1304
|
-
.describe('Qdrant collection name for vector storage.'),
|
|
1305
|
-
/** Qdrant API key. */
|
|
1306
|
-
apiKey: z
|
|
1307
|
-
.string()
|
|
1308
|
-
.optional()
|
|
1309
|
-
.describe('Qdrant API key for authentication (supports ${ENV_VAR} substitution).'),
|
|
1310
|
-
});
|
|
1311
|
-
|
|
1312
|
-
/**
|
|
1313
|
-
* @module config/schemas/root
|
|
1314
|
-
* Root configuration schema combining all sub-schemas.
|
|
1315
|
-
*/
|
|
1316
|
-
/**
|
|
1317
|
-
* Top-level configuration for jeeves-watcher.
|
|
1318
|
-
*/
|
|
1319
|
-
const jeevesWatcherConfigSchema = z.object({
|
|
1320
|
-
/** Optional description of this watcher deployment's organizational strategy. */
|
|
1321
|
-
description: z
|
|
1322
|
-
.string()
|
|
1323
|
-
.optional()
|
|
1324
|
-
.describe("Human-readable description of this deployment's organizational strategy and content domains."),
|
|
1325
|
-
/** Global named schema collection. */
|
|
1326
|
-
schemas: z
|
|
1327
|
-
.record(z.string(), schemaEntrySchema)
|
|
1328
|
-
.optional()
|
|
1329
|
-
.describe('Global named schema definitions (inline objects or file paths) referenced by inference rules.'),
|
|
1330
|
-
/** File system watch configuration. */
|
|
1331
|
-
watch: watchConfigSchema.describe('File system watch configuration.'),
|
|
1332
|
-
/** Configuration file watch settings. */
|
|
1333
|
-
configWatch: configWatchConfigSchema
|
|
1334
|
-
.optional()
|
|
1335
|
-
.describe('Configuration file watch settings.'),
|
|
1336
|
-
/** Embedding model configuration. */
|
|
1337
|
-
embedding: embeddingConfigSchema.describe('Embedding model configuration.'),
|
|
1338
|
-
/** Vector store configuration. */
|
|
1339
|
-
vectorStore: vectorStoreConfigSchema.describe('Qdrant vector store configuration.'),
|
|
1340
|
-
/** API server configuration. */
|
|
1341
|
-
api: apiConfigSchema.optional().describe('API server configuration.'),
|
|
1342
|
-
/** Directory for persistent state files (issues.json, values.json, enrichments.sqlite). */
|
|
1343
|
-
stateDir: z
|
|
1344
|
-
.string()
|
|
1345
|
-
.optional()
|
|
1346
|
-
.describe('Directory for persistent state files (issues.json, values.json, enrichments.sqlite). Defaults to .jeeves-metadata.'),
|
|
1347
|
-
/** Rules for inferring metadata from document properties (inline objects or file paths). */
|
|
1348
|
-
inferenceRules: z
|
|
1349
|
-
.array(z.union([inferenceRuleSchema, z.string()]))
|
|
1350
|
-
.optional()
|
|
1351
|
-
.describe('Rules for inferring metadata from file attributes. Each entry may be an inline rule object or a file path to a JSON rule file (resolved relative to config directory).'),
|
|
1352
|
-
/** Reusable named JsonMap transformations (inline objects or .json file paths). */
|
|
1353
|
-
maps: z
|
|
1354
|
-
.record(z.string(), z.union([
|
|
1355
|
-
jsonMapMapSchema,
|
|
1356
|
-
z.string(),
|
|
1357
|
-
z.object({
|
|
1358
|
-
/** The JsonMap definition (inline object or file path). */
|
|
1359
|
-
map: jsonMapMapSchema.or(z.string()),
|
|
1360
|
-
/** Optional human-readable description of this map. */
|
|
1361
|
-
description: z.string().optional(),
|
|
1362
|
-
}),
|
|
1363
|
-
]))
|
|
1364
|
-
.optional()
|
|
1365
|
-
.describe('Reusable named JsonMap transformations (inline definition or .json file path resolved relative to config directory).'),
|
|
1366
|
-
/** Reusable named Handlebars templates (inline strings or .hbs/.handlebars file paths). */
|
|
1367
|
-
templates: z
|
|
1368
|
-
.record(z.string(), z.union([
|
|
1369
|
-
z.string(),
|
|
1370
|
-
z.object({
|
|
1371
|
-
/** The Handlebars template source (inline string or file path). */
|
|
1372
|
-
template: z.string(),
|
|
1373
|
-
/** Optional human-readable description of this template. */
|
|
1374
|
-
description: z.string().optional(),
|
|
1375
|
-
}),
|
|
1376
|
-
]))
|
|
1377
|
-
.optional()
|
|
1378
|
-
.describe('Named reusable Handlebars templates (inline strings or .hbs/.handlebars file paths).'),
|
|
1379
|
-
/** Custom Handlebars helper registration. */
|
|
1380
|
-
templateHelpers: z
|
|
1381
|
-
.record(z.string(), z.object({
|
|
1382
|
-
/** File path to the helper module (resolved relative to config directory). */
|
|
1383
|
-
path: z.string(),
|
|
1384
|
-
/** Optional human-readable description of this helper. */
|
|
1385
|
-
description: z.string().optional(),
|
|
1386
|
-
}))
|
|
1387
|
-
.optional()
|
|
1388
|
-
.describe('Custom Handlebars helper registration.'),
|
|
1389
|
-
/** Custom JsonMap lib function registration. */
|
|
1390
|
-
mapHelpers: z
|
|
1391
|
-
.record(z.string(), z.object({
|
|
1392
|
-
/** File path to the helper module (resolved relative to config directory). */
|
|
1393
|
-
path: z.string(),
|
|
1394
|
-
/** Optional human-readable description of this helper. */
|
|
1395
|
-
description: z.string().optional(),
|
|
1396
|
-
}))
|
|
1397
|
-
.optional()
|
|
1398
|
-
.describe('Custom JsonMap lib function registration.'),
|
|
1399
|
-
/** Reindex configuration. */
|
|
1400
|
-
reindex: z
|
|
1401
|
-
.object({
|
|
1402
|
-
/** URL to call when reindex completes. */
|
|
1403
|
-
callbackUrl: z.url().optional(),
|
|
1404
|
-
/** Maximum concurrent file operations during reindex. */
|
|
1405
|
-
concurrency: z
|
|
1406
|
-
.number()
|
|
1407
|
-
.int()
|
|
1408
|
-
.min(1)
|
|
1409
|
-
.default(50)
|
|
1410
|
-
.describe('Maximum concurrent file operations during reindex (default 50).'),
|
|
1411
|
-
})
|
|
1412
|
-
.optional()
|
|
1413
|
-
.describe('Reindex configuration.'),
|
|
1414
|
-
/** Named Qdrant filter patterns for skill-activated behaviors. */
|
|
1415
|
-
/** Search configuration including score thresholds and hybrid search. */
|
|
1416
|
-
search: z
|
|
1417
|
-
.object({
|
|
1418
|
-
/** Score thresholds for categorizing search result quality. */
|
|
1419
|
-
scoreThresholds: z
|
|
1420
|
-
.object({
|
|
1421
|
-
/** Minimum score for a result to be considered a strong match. */
|
|
1422
|
-
strong: z.number().min(-1).max(1),
|
|
1423
|
-
/** Minimum score for a result to be considered relevant. */
|
|
1424
|
-
relevant: z.number().min(-1).max(1),
|
|
1425
|
-
/** Maximum score below which results are considered noise. */
|
|
1426
|
-
noise: z.number().min(-1).max(1),
|
|
1427
|
-
})
|
|
1428
|
-
.optional(),
|
|
1429
|
-
/** Hybrid search configuration combining vector and full-text search. */
|
|
1430
|
-
hybrid: z
|
|
1431
|
-
.object({
|
|
1432
|
-
/** Enable hybrid search with RRF fusion. Default: false. */
|
|
1433
|
-
enabled: z.boolean().default(false),
|
|
1434
|
-
/** Weight for text (BM25) results in RRF fusion. Default: 0.3. */
|
|
1435
|
-
textWeight: z.number().min(0).max(1).default(0.3),
|
|
1436
|
-
})
|
|
1437
|
-
.optional(),
|
|
1438
|
-
})
|
|
1439
|
-
.optional()
|
|
1440
|
-
.describe('Search configuration including score thresholds and hybrid search.'),
|
|
1441
|
-
/** Logging configuration. */
|
|
1442
|
-
logging: loggingConfigSchema.optional().describe('Logging configuration.'),
|
|
1443
|
-
/** Timeout in milliseconds for graceful shutdown. */
|
|
1444
|
-
shutdownTimeoutMs: z
|
|
1445
|
-
.number()
|
|
1446
|
-
.optional()
|
|
1447
|
-
.describe('Timeout in milliseconds for graceful shutdown.'),
|
|
1448
|
-
/** Maximum consecutive system-level failures before triggering fatal error. Default: Infinity. */
|
|
1449
|
-
maxRetries: z
|
|
1450
|
-
.number()
|
|
1451
|
-
.optional()
|
|
1452
|
-
.describe('Maximum consecutive system-level failures before triggering fatal error. Default: Infinity.'),
|
|
1453
|
-
/** Maximum backoff delay in milliseconds for system errors. Default: 60000. */
|
|
1454
|
-
maxBackoffMs: z
|
|
1455
|
-
.number()
|
|
1456
|
-
.optional()
|
|
1457
|
-
.describe('Maximum backoff delay in milliseconds for system errors. Default: 60000.'),
|
|
1458
|
-
});
|
|
1459
|
-
|
|
1460
|
-
/**
|
|
1461
|
-
* @module api/handlers/configMerge
|
|
1462
|
-
* Shared config merge utilities.
|
|
1463
|
-
*/
|
|
1464
|
-
/**
|
|
1465
|
-
* Merge inference rules by name: submitted rules replace existing by name, new ones are appended.
|
|
1466
|
-
*/
|
|
1467
|
-
function mergeInferenceRules(existing, incoming) {
|
|
1468
|
-
if (!incoming)
|
|
1469
|
-
return existing ?? [];
|
|
1470
|
-
if (!existing)
|
|
1471
|
-
return incoming;
|
|
1472
|
-
const merged = [...existing];
|
|
1473
|
-
for (const rule of incoming) {
|
|
1474
|
-
const name = rule['name'];
|
|
1475
|
-
if (!name) {
|
|
1476
|
-
merged.push(rule);
|
|
1477
|
-
continue;
|
|
1478
|
-
}
|
|
1479
|
-
const idx = merged.findIndex((r) => r['name'] === name);
|
|
1480
|
-
if (idx >= 0) {
|
|
1481
|
-
merged[idx] = rule;
|
|
1482
|
-
}
|
|
1483
|
-
else {
|
|
1484
|
-
merged.push(rule);
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
return merged;
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
/**
|
|
1491
|
-
* @module api/handlers/mergeAndValidate
|
|
1492
|
-
* Shared config merge and validation logic used by both validate and apply handlers.
|
|
1493
|
-
*/
|
|
1494
|
-
/**
|
|
1495
|
-
* Merge a submitted partial config into the current config and validate against schema.
|
|
1496
|
-
*
|
|
1497
|
-
* @param currentConfig - The current running config.
|
|
1498
|
-
* @param submittedPartial - The partial config to merge in.
|
|
1499
|
-
* @returns The merge/validation result.
|
|
1500
|
-
*/
|
|
1501
|
-
function mergeAndValidateConfig(currentConfig, submittedPartial) {
|
|
1502
|
-
let candidateRaw = {
|
|
1503
|
-
...currentConfig,
|
|
1504
|
-
};
|
|
1505
|
-
if (submittedPartial) {
|
|
1506
|
-
const mergedRules = mergeInferenceRules(candidateRaw['inferenceRules'], submittedPartial['inferenceRules']);
|
|
1507
|
-
candidateRaw = {
|
|
1508
|
-
...candidateRaw,
|
|
1509
|
-
...submittedPartial,
|
|
1510
|
-
inferenceRules: mergedRules,
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
const parseResult = jeevesWatcherConfigSchema.safeParse(candidateRaw);
|
|
1514
|
-
const errors = [];
|
|
1515
|
-
if (!parseResult.success) {
|
|
1516
|
-
for (const issue of parseResult.error.issues) {
|
|
1517
|
-
errors.push({
|
|
1518
|
-
path: issue.path.join('.'),
|
|
1519
|
-
message: issue.message,
|
|
1520
|
-
});
|
|
1521
|
-
}
|
|
1522
|
-
return { candidateRaw, errors };
|
|
1523
|
-
}
|
|
1524
|
-
// Note: When accepting external config via API, string rule references are NOT resolved
|
|
1525
|
-
// (no configDir context). API-submitted rules must always be inline objects.
|
|
1526
|
-
// The cast is safe because API config never contains string rule refs.
|
|
1527
|
-
return {
|
|
1528
|
-
candidateRaw,
|
|
1529
|
-
parsed: parseResult.data,
|
|
1530
|
-
errors,
|
|
1531
|
-
};
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
/**
|
|
1535
|
-
* @module api/handlers/wrapHandler
|
|
1536
|
-
* Generic error-handling wrapper for Fastify route handlers.
|
|
1537
|
-
*/
|
|
1538
|
-
/**
|
|
1539
|
-
* Wrap a Fastify route handler with standardised error handling.
|
|
1540
|
-
*
|
|
1541
|
-
* @param fn - The handler function.
|
|
1542
|
-
* @param logger - Logger instance.
|
|
1543
|
-
* @param label - Label for error log messages.
|
|
1544
|
-
* @returns A wrapped handler that catches errors and returns 500.
|
|
1095
|
+
* Wrap a Fastify route handler with standardised error handling.
|
|
1096
|
+
*
|
|
1097
|
+
* @param fn - The handler function.
|
|
1098
|
+
* @param logger - Logger instance.
|
|
1099
|
+
* @param label - Label for error log messages.
|
|
1100
|
+
* @returns A wrapped handler that catches errors and returns 500.
|
|
1545
1101
|
*/
|
|
1546
1102
|
function wrapHandler(fn, logger, label) {
|
|
1547
1103
|
return async (request, reply) => {
|
|
@@ -1565,467 +1121,809 @@ function wrapHandler(fn, logger, label) {
|
|
|
1565
1121
|
}
|
|
1566
1122
|
|
|
1567
1123
|
/**
|
|
1568
|
-
* @module api/handlers/
|
|
1569
|
-
*
|
|
1124
|
+
* @module api/handlers/configMatch
|
|
1125
|
+
* Tests file paths against inference rules and watch scope.
|
|
1570
1126
|
*/
|
|
1571
1127
|
/**
|
|
1572
|
-
*
|
|
1128
|
+
* Factory for POST /config/match handler.
|
|
1573
1129
|
*
|
|
1574
|
-
* @param
|
|
1130
|
+
* @param options - Handler options.
|
|
1131
|
+
* @returns The handler function.
|
|
1575
1132
|
*/
|
|
1576
|
-
function
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1133
|
+
function createConfigMatchHandler(options) {
|
|
1134
|
+
const { getConfig, logger } = options;
|
|
1135
|
+
// Cache compiled artifacts per config reference — recompute only on hot-reload.
|
|
1136
|
+
let cachedConfig;
|
|
1137
|
+
let compiledRules = [];
|
|
1138
|
+
let watchMatcher;
|
|
1139
|
+
let ignoreMatcher;
|
|
1140
|
+
const handler = async (req, res) => {
|
|
1141
|
+
const body = req.body;
|
|
1142
|
+
if (!Array.isArray(body.paths)) {
|
|
1143
|
+
res.code(400).send({ error: 'Request body must include "paths" array' });
|
|
1144
|
+
return;
|
|
1583
1145
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1146
|
+
const config = getConfig();
|
|
1147
|
+
if (config !== cachedConfig) {
|
|
1148
|
+
compiledRules = compileRules(config.inferenceRules ?? []);
|
|
1149
|
+
watchMatcher = picomatch(config.watch.paths, { dot: true });
|
|
1150
|
+
ignoreMatcher = config.watch.ignored?.length
|
|
1151
|
+
? picomatch(config.watch.ignored, { dot: true })
|
|
1152
|
+
: undefined;
|
|
1153
|
+
cachedConfig = config;
|
|
1588
1154
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1155
|
+
const matches = body.paths.map((path) => {
|
|
1156
|
+
const attrs = buildSyntheticAttributes(path);
|
|
1157
|
+
const matchingRules = [];
|
|
1158
|
+
for (const compiled of compiledRules) {
|
|
1159
|
+
if (compiled.validate(attrs)) {
|
|
1160
|
+
matchingRules.push(compiled.rule.name);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
const normalised = attrs.file.path;
|
|
1164
|
+
const watched = (watchMatcher?.(normalised) ?? false) &&
|
|
1165
|
+
!(ignoreMatcher?.(normalised) ?? false);
|
|
1166
|
+
return { rules: matchingRules, watched };
|
|
1167
|
+
});
|
|
1168
|
+
const response = { matches };
|
|
1169
|
+
res.send(response);
|
|
1170
|
+
};
|
|
1171
|
+
return wrapHandler(handler, logger, 'Config match');
|
|
1595
1172
|
}
|
|
1596
1173
|
|
|
1597
1174
|
/**
|
|
1598
|
-
* @module
|
|
1599
|
-
* Builds
|
|
1175
|
+
* @module api/mergedDocument
|
|
1176
|
+
* Builds a merged virtual document from config, values, and issues for JSONPath querying.
|
|
1600
1177
|
*/
|
|
1601
1178
|
/**
|
|
1602
|
-
* Build
|
|
1179
|
+
* Build a helper section for the merged document, injecting introspection exports per namespace.
|
|
1180
|
+
*/
|
|
1181
|
+
function buildHelperSection(configHelpers, legacyExports, introspection) {
|
|
1182
|
+
if (!configHelpers)
|
|
1183
|
+
return {};
|
|
1184
|
+
const result = {};
|
|
1185
|
+
for (const [name, entry] of Object.entries(configHelpers)) {
|
|
1186
|
+
result[name] = {
|
|
1187
|
+
...entry,
|
|
1188
|
+
...(introspection?.[name]?.exports
|
|
1189
|
+
? { exports: introspection[name].exports }
|
|
1190
|
+
: {}),
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
if (legacyExports) {
|
|
1194
|
+
result['_exports'] = legacyExports;
|
|
1195
|
+
}
|
|
1196
|
+
return result;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Safely read and parse a file reference. Returns the original string on failure.
|
|
1200
|
+
*/
|
|
1201
|
+
function readFileReference(filePath) {
|
|
1202
|
+
try {
|
|
1203
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
1204
|
+
if (filePath.endsWith('.json')) {
|
|
1205
|
+
return JSON.parse(content);
|
|
1206
|
+
}
|
|
1207
|
+
return content;
|
|
1208
|
+
}
|
|
1209
|
+
catch {
|
|
1210
|
+
return filePath;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Build a merged virtual document combining config, values, and issues.
|
|
1603
1215
|
*
|
|
1604
|
-
* @param
|
|
1605
|
-
* @
|
|
1606
|
-
* @param extractedFrontmatter - Optional extracted frontmatter.
|
|
1607
|
-
* @param extractedJson - Optional parsed JSON content.
|
|
1608
|
-
* @returns The constructed file attributes.
|
|
1216
|
+
* @param options - The build options.
|
|
1217
|
+
* @returns The merged document object.
|
|
1609
1218
|
*/
|
|
1610
|
-
function
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1219
|
+
function buildMergedDocument(options) {
|
|
1220
|
+
const { config, valuesManager, issuesManager, helperExports, helperIntrospection, virtualRules, } = options;
|
|
1221
|
+
// Merge config rules (source: 'config') and virtual rules (source: registration key)
|
|
1222
|
+
const configRules = (config.inferenceRules ?? []).map((rule) => ({
|
|
1223
|
+
...rule,
|
|
1224
|
+
source: 'config',
|
|
1225
|
+
values: valuesManager.getForRule(rule.name),
|
|
1226
|
+
}));
|
|
1227
|
+
const virtualRuleEntries = [];
|
|
1228
|
+
if (virtualRules) {
|
|
1229
|
+
for (const [source, rules] of Object.entries(virtualRules)) {
|
|
1230
|
+
for (const rule of rules) {
|
|
1231
|
+
const name = typeof rule.name === 'string' ? rule.name : undefined;
|
|
1232
|
+
virtualRuleEntries.push({
|
|
1233
|
+
...rule,
|
|
1234
|
+
source,
|
|
1235
|
+
values: name ? valuesManager.getForRule(name) : {},
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const inferenceRules = [...configRules, ...virtualRuleEntries];
|
|
1241
|
+
const doc = {
|
|
1242
|
+
description: config['description'] ?? '',
|
|
1243
|
+
watch: config.watch,
|
|
1244
|
+
configWatch: config.configWatch ?? {},
|
|
1245
|
+
search: config.search ?? {},
|
|
1246
|
+
schemas: config.schemas ?? {},
|
|
1247
|
+
inferenceRules,
|
|
1248
|
+
mapHelpers: buildHelperSection(config.mapHelpers, helperExports?.mapHelpers, helperIntrospection?.mapHelpers),
|
|
1249
|
+
templateHelpers: buildHelperSection(config.templateHelpers, helperExports?.templateHelpers, helperIntrospection?.templateHelpers),
|
|
1250
|
+
maps: config.maps ?? {},
|
|
1251
|
+
templates: config.templates ?? {},
|
|
1252
|
+
issues: issuesManager.getAll(),
|
|
1621
1253
|
};
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
if (extractedJson)
|
|
1625
|
-
attrs.json = extractedJson;
|
|
1626
|
-
return attrs;
|
|
1254
|
+
// Always resolve file references
|
|
1255
|
+
return resolveReferences(doc, ['files', 'globals']);
|
|
1627
1256
|
}
|
|
1628
1257
|
/**
|
|
1629
|
-
*
|
|
1630
|
-
* Used by API handlers that need to match rules against paths without reading files.
|
|
1258
|
+
* Resolve file references in known config reference positions only.
|
|
1631
1259
|
*
|
|
1632
|
-
*
|
|
1633
|
-
*
|
|
1260
|
+
* Resolves strings in:
|
|
1261
|
+
* - `inferenceRules[*].map` when ending in `.json` (if 'files' in resolveTypes)
|
|
1262
|
+
* - `maps[*]` when a string (file path) (if 'files' in resolveTypes)
|
|
1263
|
+
* - `templates[*]` when a string (file path) (if 'files' in resolveTypes)
|
|
1264
|
+
* - `inferenceRules[*].schema` named references (if 'globals' in resolveTypes)
|
|
1265
|
+
*
|
|
1266
|
+
* @param doc - The document to resolve.
|
|
1267
|
+
* @param resolveTypes - Which resolution types to apply.
|
|
1268
|
+
* @returns The resolved document.
|
|
1269
|
+
*/
|
|
1270
|
+
function resolveReferences(doc, resolveTypes) {
|
|
1271
|
+
const resolved = { ...doc };
|
|
1272
|
+
// Resolve inferenceRules[*] references
|
|
1273
|
+
if (Array.isArray(resolved['inferenceRules'])) {
|
|
1274
|
+
resolved['inferenceRules'] = resolved['inferenceRules'].map((rule) => {
|
|
1275
|
+
let updatedRule = { ...rule };
|
|
1276
|
+
// Resolve map file references (if 'files' requested)
|
|
1277
|
+
if (resolveTypes.includes('files') &&
|
|
1278
|
+
typeof rule['map'] === 'string' &&
|
|
1279
|
+
rule['map'].endsWith('.json')) {
|
|
1280
|
+
updatedRule = { ...updatedRule, map: readFileReference(rule['map']) };
|
|
1281
|
+
}
|
|
1282
|
+
// Resolve schema named references (if 'globals' requested)
|
|
1283
|
+
if (resolveTypes.includes('globals') &&
|
|
1284
|
+
Array.isArray(rule['schema']) &&
|
|
1285
|
+
typeof resolved['schemas'] === 'object') {
|
|
1286
|
+
const globalSchemas = resolved['schemas'];
|
|
1287
|
+
const expandedSchemas = rule['schema'].map((ref) => {
|
|
1288
|
+
if (typeof ref === 'string' && globalSchemas[ref]) {
|
|
1289
|
+
return globalSchemas[ref];
|
|
1290
|
+
}
|
|
1291
|
+
return ref;
|
|
1292
|
+
});
|
|
1293
|
+
updatedRule = { ...updatedRule, schema: expandedSchemas };
|
|
1294
|
+
}
|
|
1295
|
+
return updatedRule;
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
// Resolve maps[*] file references (if 'files' requested)
|
|
1299
|
+
if (resolveTypes.includes('files') &&
|
|
1300
|
+
resolved['maps'] &&
|
|
1301
|
+
typeof resolved['maps'] === 'object') {
|
|
1302
|
+
const maps = { ...resolved['maps'] };
|
|
1303
|
+
for (const [key, value] of Object.entries(maps)) {
|
|
1304
|
+
if (typeof value === 'string') {
|
|
1305
|
+
maps[key] = readFileReference(value);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
resolved['maps'] = maps;
|
|
1309
|
+
}
|
|
1310
|
+
// Resolve templates[*] file references (if 'files' requested)
|
|
1311
|
+
if (resolveTypes.includes('files') &&
|
|
1312
|
+
resolved['templates'] &&
|
|
1313
|
+
typeof resolved['templates'] === 'object') {
|
|
1314
|
+
const templates = {
|
|
1315
|
+
...resolved['templates'],
|
|
1316
|
+
};
|
|
1317
|
+
for (const [key, value] of Object.entries(templates)) {
|
|
1318
|
+
if (typeof value === 'string') {
|
|
1319
|
+
templates[key] = readFileReference(value);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
resolved['templates'] = templates;
|
|
1323
|
+
}
|
|
1324
|
+
return resolved;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* @module api/handlers/configQuery
|
|
1329
|
+
* Fastify route handler for GET /config. Returns the full resolved merged document,
|
|
1330
|
+
* optionally filtered by JSONPath via the `path` query parameter.
|
|
1331
|
+
*
|
|
1332
|
+
* Delegates JSON querying to `createConfigQueryHandler` from `@karmaniverous/jeeves` core.
|
|
1333
|
+
*/
|
|
1334
|
+
/**
|
|
1335
|
+
* Create handler for GET /config.
|
|
1336
|
+
*
|
|
1337
|
+
* Uses `createConfigQueryHandler` from core to handle JSONPath querying.
|
|
1338
|
+
* Invalid JSONPath expressions are client errors and return 400.
|
|
1339
|
+
*
|
|
1340
|
+
* @param deps - Route dependencies.
|
|
1634
1341
|
*/
|
|
1635
|
-
function
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1342
|
+
function createConfigQueryHandler(deps) {
|
|
1343
|
+
const coreHandler = createConfigQueryHandler$1(() => buildMergedDocument({
|
|
1344
|
+
config: deps.getConfig(),
|
|
1345
|
+
valuesManager: deps.valuesManager,
|
|
1346
|
+
issuesManager: deps.issuesManager,
|
|
1347
|
+
helperIntrospection: deps.helperIntrospection,
|
|
1348
|
+
virtualRules: deps.getVirtualRules?.(),
|
|
1349
|
+
}));
|
|
1350
|
+
return async (request, reply) => {
|
|
1351
|
+
try {
|
|
1352
|
+
const { path } = request.query;
|
|
1353
|
+
const result = await coreHandler({ path });
|
|
1354
|
+
if (result.status >= 400) {
|
|
1355
|
+
return await reply.status(result.status).send(result.body);
|
|
1356
|
+
}
|
|
1357
|
+
return result.body;
|
|
1358
|
+
}
|
|
1359
|
+
catch (error) {
|
|
1360
|
+
const err = normalizeError(error);
|
|
1361
|
+
deps.logger.error({ err }, 'Config query failed');
|
|
1362
|
+
return await reply.status(400).send({
|
|
1363
|
+
error: err.message || 'Query failed',
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1646
1366
|
};
|
|
1647
1367
|
}
|
|
1648
1368
|
|
|
1649
1369
|
/**
|
|
1650
|
-
* @module
|
|
1651
|
-
*
|
|
1370
|
+
* @module api/handlers/configReindex
|
|
1371
|
+
* Fastify route handler for POST /reindex. Triggers an async reindex job scoped to issues, full, rules, path, or prune processing.
|
|
1652
1372
|
*/
|
|
1653
1373
|
/**
|
|
1654
|
-
* Create
|
|
1374
|
+
* Create handler for POST /reindex.
|
|
1655
1375
|
*
|
|
1656
|
-
* @
|
|
1376
|
+
* @param deps - Route dependencies.
|
|
1657
1377
|
*/
|
|
1658
|
-
function
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1378
|
+
function createConfigReindexHandler(deps) {
|
|
1379
|
+
return wrapHandler(async (request, reply) => {
|
|
1380
|
+
const scope = request.body.scope ?? 'issues';
|
|
1381
|
+
const dryRun = request.body.dryRun ?? false;
|
|
1382
|
+
if (!VALID_REINDEX_SCOPES.includes(scope)) {
|
|
1383
|
+
return await reply.status(400).send({
|
|
1384
|
+
error: 'Invalid scope',
|
|
1385
|
+
message: `Scope must be one of: ${VALID_REINDEX_SCOPES.join(', ')}. Got: "${scope}"`,
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
const validScope = scope;
|
|
1389
|
+
if (validScope === 'path') {
|
|
1390
|
+
const { path } = request.body;
|
|
1391
|
+
if (!path || (Array.isArray(path) && path.length === 0)) {
|
|
1392
|
+
return await reply.status(400).send({
|
|
1393
|
+
error: 'Missing path',
|
|
1394
|
+
message: 'The "path" field is required when scope is "path".',
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
if (validScope === 'prune' && !deps.vectorStore) {
|
|
1399
|
+
return await reply.status(400).send({
|
|
1400
|
+
error: 'Not available',
|
|
1401
|
+
message: 'Prune scope requires vectorStore to be configured.',
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
const reindexDeps = {
|
|
1405
|
+
config: deps.getConfig(),
|
|
1406
|
+
processor: deps.processor,
|
|
1407
|
+
logger: deps.logger,
|
|
1408
|
+
reindexTracker: deps.reindexTracker,
|
|
1409
|
+
valuesManager: deps.valuesManager,
|
|
1410
|
+
issuesManager: deps.issuesManager,
|
|
1411
|
+
gitignoreFilter: deps.gitignoreFilter,
|
|
1412
|
+
vectorStore: deps.vectorStore,
|
|
1413
|
+
queue: deps.queue,
|
|
1414
|
+
};
|
|
1415
|
+
// Pass path for 'path' and 'rules' scopes
|
|
1416
|
+
const pathParam = validScope === 'path' || validScope === 'rules'
|
|
1417
|
+
? request.body.path
|
|
1418
|
+
: undefined;
|
|
1419
|
+
if (dryRun) {
|
|
1420
|
+
// Dry run: compute plan synchronously and return
|
|
1421
|
+
const result = await executeReindex(reindexDeps, validScope, pathParam, true);
|
|
1422
|
+
return await reply.status(200).send({
|
|
1423
|
+
status: 'dry_run',
|
|
1424
|
+
scope,
|
|
1425
|
+
plan: result.plan,
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
// Fire and forget — plan is computed inside but we need it for the response.
|
|
1429
|
+
// For non-prune scopes, compute plan first then execute async.
|
|
1430
|
+
const planResult = await executeReindex(reindexDeps, validScope, pathParam, true);
|
|
1431
|
+
// Now fire actual reindex async
|
|
1432
|
+
void executeReindex(reindexDeps, validScope, pathParam, false);
|
|
1433
|
+
return await reply
|
|
1434
|
+
.status(200)
|
|
1435
|
+
.send({ status: 'started', scope, plan: planResult.plan });
|
|
1436
|
+
}, deps.logger, 'Reindex request');
|
|
1668
1437
|
}
|
|
1669
1438
|
|
|
1670
1439
|
/**
|
|
1671
|
-
* @module
|
|
1672
|
-
*
|
|
1440
|
+
* @module config/schemas/base
|
|
1441
|
+
* Base configuration schemas: watch, logging, API.
|
|
1673
1442
|
*/
|
|
1674
1443
|
/**
|
|
1675
|
-
*
|
|
1676
|
-
* Throws if duplicate names are found.
|
|
1677
|
-
*
|
|
1678
|
-
* @param rules - The inference rule definitions.
|
|
1444
|
+
* Watch configuration for file system monitoring.
|
|
1679
1445
|
*/
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1446
|
+
const watchConfigSchema = z.object({
|
|
1447
|
+
/** Glob patterns to watch. */
|
|
1448
|
+
paths: z
|
|
1449
|
+
.array(z.string())
|
|
1450
|
+
.min(1)
|
|
1451
|
+
.describe('Glob patterns for files to watch (e.g., "**/*.md"). At least one required.'),
|
|
1452
|
+
/** Glob patterns to ignore. */
|
|
1453
|
+
ignored: z
|
|
1454
|
+
.array(z.string())
|
|
1455
|
+
.optional()
|
|
1456
|
+
.describe('Glob patterns to exclude from watching (e.g., "**/node_modules/**").'),
|
|
1457
|
+
/** Polling interval in milliseconds. */
|
|
1458
|
+
pollIntervalMs: z
|
|
1459
|
+
.number()
|
|
1460
|
+
.optional()
|
|
1461
|
+
.describe('Polling interval in milliseconds when usePolling is enabled.'),
|
|
1462
|
+
/** Whether to use polling instead of native watchers. */
|
|
1463
|
+
usePolling: z
|
|
1464
|
+
.boolean()
|
|
1465
|
+
.optional()
|
|
1466
|
+
.describe('Use polling instead of native file system events (for network drives).'),
|
|
1467
|
+
/** Debounce delay in milliseconds for file change events. */
|
|
1468
|
+
debounceMs: z
|
|
1469
|
+
.number()
|
|
1470
|
+
.optional()
|
|
1471
|
+
.describe('Debounce delay in milliseconds for file change events.'),
|
|
1472
|
+
/** Time in milliseconds a file must be stable before processing. */
|
|
1473
|
+
stabilityThresholdMs: z
|
|
1474
|
+
.number()
|
|
1475
|
+
.optional()
|
|
1476
|
+
.describe('Time in milliseconds a file must remain unchanged before processing.'),
|
|
1477
|
+
/** Whether to respect .gitignore files when processing. */
|
|
1478
|
+
respectGitignore: z
|
|
1479
|
+
.boolean()
|
|
1480
|
+
.optional()
|
|
1481
|
+
.describe('Skip files ignored by .gitignore in git repositories. Only applies to repos with a .git directory. Default: true.'),
|
|
1482
|
+
/** Move detection configuration for correlating unlink+add as file moves. */
|
|
1483
|
+
moveDetection: z
|
|
1484
|
+
.object({
|
|
1485
|
+
/** Enable move correlation. Default: true. */
|
|
1486
|
+
enabled: z
|
|
1487
|
+
.boolean()
|
|
1488
|
+
.default(true)
|
|
1489
|
+
.describe('Enable move detection via content hash correlation.'),
|
|
1490
|
+
/** Buffer time in ms for holding unlink events before treating as deletes. Default: 2000. */
|
|
1491
|
+
bufferMs: z
|
|
1492
|
+
.number()
|
|
1493
|
+
.int()
|
|
1494
|
+
.min(100)
|
|
1495
|
+
.default(2000)
|
|
1496
|
+
.describe('How long (ms) to buffer unlink events before treating as deletes.'),
|
|
1497
|
+
})
|
|
1498
|
+
.optional()
|
|
1499
|
+
.describe('Move detection: correlate unlink+add events as file moves to avoid re-embedding.'),
|
|
1500
|
+
});
|
|
1695
1501
|
/**
|
|
1696
|
-
*
|
|
1697
|
-
* Validates rule name uniqueness before compilation.
|
|
1698
|
-
*
|
|
1699
|
-
* @param rules - The inference rule definitions.
|
|
1700
|
-
* @returns An array of compiled rules.
|
|
1502
|
+
* Configuration watch settings.
|
|
1701
1503
|
*/
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1504
|
+
const configWatchConfigSchema = z.object({
|
|
1505
|
+
/** Whether config file watching is enabled. */
|
|
1506
|
+
enabled: z
|
|
1507
|
+
.boolean()
|
|
1508
|
+
.optional()
|
|
1509
|
+
.describe('Enable automatic reloading when config file changes.'),
|
|
1510
|
+
/** Debounce delay in milliseconds for config change events. */
|
|
1511
|
+
debounceMs: z
|
|
1512
|
+
.number()
|
|
1513
|
+
.optional()
|
|
1514
|
+
.describe('Debounce delay in milliseconds for config file change detection.'),
|
|
1515
|
+
/** Reindex scope triggered on config change. */
|
|
1516
|
+
reindex: z
|
|
1517
|
+
.union([
|
|
1518
|
+
z
|
|
1519
|
+
.literal('issues')
|
|
1520
|
+
.describe('Re-process only files with recorded issues.'),
|
|
1521
|
+
z.literal('full').describe('Full reindex of all watched files.'),
|
|
1522
|
+
z
|
|
1523
|
+
.literal('rules')
|
|
1524
|
+
.describe('Re-apply inference rules to existing points without re-embedding.'),
|
|
1525
|
+
])
|
|
1526
|
+
.optional()
|
|
1527
|
+
.describe('Reindex scope triggered on config change. Default: issues.'),
|
|
1528
|
+
});
|
|
1529
|
+
/**
|
|
1530
|
+
* API server configuration.
|
|
1531
|
+
*/
|
|
1532
|
+
const apiConfigSchema = z.object({
|
|
1533
|
+
/** Host to bind to. */
|
|
1534
|
+
host: z
|
|
1535
|
+
.string()
|
|
1536
|
+
.optional()
|
|
1537
|
+
.describe('Host address for API server (e.g., "127.0.0.1", "0.0.0.0").'),
|
|
1538
|
+
/** Port to listen on. */
|
|
1539
|
+
port: z.number().optional().describe('Port for API server (e.g., 1936).'),
|
|
1540
|
+
/** Read endpoint cache TTL in milliseconds. */
|
|
1541
|
+
cacheTtlMs: z
|
|
1542
|
+
.number()
|
|
1543
|
+
.optional()
|
|
1544
|
+
.describe('TTL in milliseconds for caching read-heavy endpoints (e.g., /status, /config). Default: 30000.'),
|
|
1545
|
+
});
|
|
1546
|
+
/**
|
|
1547
|
+
* Logging configuration.
|
|
1548
|
+
*/
|
|
1549
|
+
const loggingConfigSchema = z.object({
|
|
1550
|
+
/** Log level. */
|
|
1551
|
+
level: z
|
|
1552
|
+
.string()
|
|
1553
|
+
.optional()
|
|
1554
|
+
.describe('Logging level (trace, debug, info, warn, error, fatal).'),
|
|
1555
|
+
/** Log file path. */
|
|
1556
|
+
file: z
|
|
1557
|
+
.string()
|
|
1558
|
+
.optional()
|
|
1559
|
+
.describe('Path to log file (logs to stdout if omitted).'),
|
|
1560
|
+
});
|
|
1713
1561
|
|
|
1714
1562
|
/**
|
|
1715
|
-
* @module
|
|
1716
|
-
*
|
|
1717
|
-
*/
|
|
1718
|
-
/**
|
|
1719
|
-
* Factory for POST /config/match handler.
|
|
1720
|
-
*
|
|
1721
|
-
* @param options - Handler options.
|
|
1722
|
-
* @returns The handler function.
|
|
1563
|
+
* @module config/schemas/inference
|
|
1564
|
+
* Inference rule and schema configuration schemas.
|
|
1723
1565
|
*/
|
|
1724
|
-
function createConfigMatchHandler(options) {
|
|
1725
|
-
const { getConfig, logger } = options;
|
|
1726
|
-
// Cache compiled artifacts per config reference — recompute only on hot-reload.
|
|
1727
|
-
let cachedConfig;
|
|
1728
|
-
let compiledRules = [];
|
|
1729
|
-
let watchMatcher;
|
|
1730
|
-
let ignoreMatcher;
|
|
1731
|
-
const handler = async (req, res) => {
|
|
1732
|
-
const body = req.body;
|
|
1733
|
-
if (!Array.isArray(body.paths)) {
|
|
1734
|
-
res.code(400).send({ error: 'Request body must include "paths" array' });
|
|
1735
|
-
return;
|
|
1736
|
-
}
|
|
1737
|
-
const config = getConfig();
|
|
1738
|
-
if (config !== cachedConfig) {
|
|
1739
|
-
compiledRules = compileRules(config.inferenceRules ?? []);
|
|
1740
|
-
watchMatcher = picomatch(config.watch.paths, { dot: true });
|
|
1741
|
-
ignoreMatcher = config.watch.ignored?.length
|
|
1742
|
-
? picomatch(config.watch.ignored, { dot: true })
|
|
1743
|
-
: undefined;
|
|
1744
|
-
cachedConfig = config;
|
|
1745
|
-
}
|
|
1746
|
-
const matches = body.paths.map((path) => {
|
|
1747
|
-
const attrs = buildSyntheticAttributes(path);
|
|
1748
|
-
const matchingRules = [];
|
|
1749
|
-
for (const compiled of compiledRules) {
|
|
1750
|
-
if (compiled.validate(attrs)) {
|
|
1751
|
-
matchingRules.push(compiled.rule.name);
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
const normalised = attrs.file.path;
|
|
1755
|
-
const watched = (watchMatcher?.(normalised) ?? false) &&
|
|
1756
|
-
!(ignoreMatcher?.(normalised) ?? false);
|
|
1757
|
-
return { rules: matchingRules, watched };
|
|
1758
|
-
});
|
|
1759
|
-
const response = { matches };
|
|
1760
|
-
res.send(response);
|
|
1761
|
-
};
|
|
1762
|
-
return wrapHandler(handler, logger, 'Config match');
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
1566
|
/**
|
|
1766
|
-
*
|
|
1767
|
-
*
|
|
1567
|
+
* A JSON Schema property definition with optional custom keywords.
|
|
1568
|
+
* Supports standard JSON Schema keywords plus custom `set` and `uiHint`.
|
|
1768
1569
|
*/
|
|
1570
|
+
const propertySchemaSchema = z.record(z.string(), z.unknown());
|
|
1769
1571
|
/**
|
|
1770
|
-
*
|
|
1572
|
+
* A schema object: properties with JSON Schema definitions.
|
|
1771
1573
|
*/
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
};
|
|
1783
|
-
}
|
|
1784
|
-
if (legacyExports) {
|
|
1785
|
-
result['_exports'] = legacyExports;
|
|
1786
|
-
}
|
|
1787
|
-
return result;
|
|
1788
|
-
}
|
|
1574
|
+
const schemaObjectSchema = z.object({
|
|
1575
|
+
type: z
|
|
1576
|
+
.literal('object')
|
|
1577
|
+
.optional()
|
|
1578
|
+
.describe('JSON Schema type (always "object" for schema definitions).'),
|
|
1579
|
+
properties: z
|
|
1580
|
+
.record(z.string(), propertySchemaSchema)
|
|
1581
|
+
.optional()
|
|
1582
|
+
.describe('Map of property names to JSON Schema property definitions.'),
|
|
1583
|
+
});
|
|
1789
1584
|
/**
|
|
1790
|
-
*
|
|
1585
|
+
* Global schema entry: inline object or file path.
|
|
1791
1586
|
*/
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
return JSON.parse(content);
|
|
1797
|
-
}
|
|
1798
|
-
return content;
|
|
1799
|
-
}
|
|
1800
|
-
catch {
|
|
1801
|
-
return filePath;
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1587
|
+
const schemaEntrySchema = z.union([
|
|
1588
|
+
schemaObjectSchema,
|
|
1589
|
+
z.string().describe('File path to a JSON schema file.'),
|
|
1590
|
+
]);
|
|
1804
1591
|
/**
|
|
1805
|
-
*
|
|
1806
|
-
*
|
|
1807
|
-
* @param options - The build options.
|
|
1808
|
-
* @returns The merged document object.
|
|
1592
|
+
* Schema reference: either a named schema reference (string) or an inline schema object.
|
|
1809
1593
|
*/
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1594
|
+
const schemaReferenceSchema = z.union([
|
|
1595
|
+
z.string().describe('Named reference to a global schema.'),
|
|
1596
|
+
schemaObjectSchema,
|
|
1597
|
+
]);
|
|
1598
|
+
/** Render body section. */
|
|
1599
|
+
const renderBodySectionSchema = z.object({
|
|
1600
|
+
/** Key path in the template context to render. */
|
|
1601
|
+
path: z.string().min(1).describe('Key path in template context to render.'),
|
|
1602
|
+
/** Markdown heading level for this section (1-6). */
|
|
1603
|
+
heading: z.number().min(1).max(6).describe('Markdown heading level (1-6).'),
|
|
1604
|
+
/** Override heading text (default: titlecased path). */
|
|
1605
|
+
label: z.string().optional().describe('Override heading text.'),
|
|
1606
|
+
/** Name of a registered Handlebars helper used as a format handler. */
|
|
1607
|
+
format: z
|
|
1608
|
+
.string()
|
|
1609
|
+
.optional()
|
|
1610
|
+
.describe('Name of a registered Handlebars helper used as a format handler.'),
|
|
1611
|
+
/** Additional args passed to the format helper. */
|
|
1612
|
+
formatArgs: z
|
|
1613
|
+
.array(z.unknown())
|
|
1614
|
+
.optional()
|
|
1615
|
+
.describe('Additional args passed to the format helper.'),
|
|
1616
|
+
/** If true, the value at path is treated as an array and iterated. */
|
|
1617
|
+
each: z
|
|
1618
|
+
.boolean()
|
|
1619
|
+
.optional()
|
|
1620
|
+
.describe('If true, the value at path is treated as an array and iterated.'),
|
|
1621
|
+
/** Handlebars template string for per-item heading text (used when each=true). */
|
|
1622
|
+
headingTemplate: z
|
|
1623
|
+
.string()
|
|
1624
|
+
.optional()
|
|
1625
|
+
.describe('Handlebars template string for per-item heading text (used when each=true).'),
|
|
1626
|
+
/** Key path within each item to use as renderable content (used when each=true). */
|
|
1627
|
+
contentPath: z
|
|
1628
|
+
.string()
|
|
1629
|
+
.optional()
|
|
1630
|
+
.describe('Key path within each item to use as renderable content (used when each=true).'),
|
|
1631
|
+
/** Key path within each item to sort by (used when each=true). */
|
|
1632
|
+
sort: z
|
|
1633
|
+
.string()
|
|
1634
|
+
.optional()
|
|
1635
|
+
.describe('Key path within each item to sort by (used when each=true).'),
|
|
1636
|
+
});
|
|
1637
|
+
/** Render config: YAML frontmatter + ordered body sections. */
|
|
1638
|
+
const renderConfigSchema = z.object({
|
|
1639
|
+
/** Keys or glob patterns to extract from context and include as YAML frontmatter. */
|
|
1640
|
+
frontmatter: z
|
|
1641
|
+
.array(z.string().min(1))
|
|
1642
|
+
.describe('Keys or glob patterns to include as YAML frontmatter. ' +
|
|
1643
|
+
'Supports picomatch globs (e.g. "*") and "!"-prefixed exclusion patterns (e.g. "!_*"). ' +
|
|
1644
|
+
'Explicit names preserve declaration order; glob-matched keys are sorted alphabetically.'),
|
|
1645
|
+
/** Ordered markdown body sections. */
|
|
1646
|
+
body: z
|
|
1647
|
+
.array(renderBodySectionSchema)
|
|
1648
|
+
.describe('Ordered markdown body sections.'),
|
|
1649
|
+
});
|
|
1848
1650
|
/**
|
|
1849
|
-
*
|
|
1850
|
-
*
|
|
1851
|
-
* Resolves strings in:
|
|
1852
|
-
* - `inferenceRules[*].map` when ending in `.json` (if 'files' in resolveTypes)
|
|
1853
|
-
* - `maps[*]` when a string (file path) (if 'files' in resolveTypes)
|
|
1854
|
-
* - `templates[*]` when a string (file path) (if 'files' in resolveTypes)
|
|
1855
|
-
* - `inferenceRules[*].schema` named references (if 'globals' in resolveTypes)
|
|
1856
|
-
*
|
|
1857
|
-
* @param doc - The document to resolve.
|
|
1858
|
-
* @param resolveTypes - Which resolution types to apply.
|
|
1859
|
-
* @returns The resolved document.
|
|
1651
|
+
* An inference rule that enriches document metadata.
|
|
1860
1652
|
*/
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1653
|
+
const inferenceRuleSchema = z
|
|
1654
|
+
.object({
|
|
1655
|
+
/** Unique name for this inference rule. */
|
|
1656
|
+
name: z
|
|
1657
|
+
.string()
|
|
1658
|
+
.min(1)
|
|
1659
|
+
.describe('Unique name identifying this inference rule.'),
|
|
1660
|
+
/** Human-readable description of what this rule does. */
|
|
1661
|
+
description: z
|
|
1662
|
+
.string()
|
|
1663
|
+
.min(1)
|
|
1664
|
+
.describe('Human-readable description of what this rule does.'),
|
|
1665
|
+
/** JSON Schema object to match against document metadata. */
|
|
1666
|
+
match: z
|
|
1667
|
+
.record(z.string(), z.unknown())
|
|
1668
|
+
.describe('JSON Schema object to match against file attributes.'),
|
|
1669
|
+
/** Array of schema references to merge (named refs and/or inline objects). */
|
|
1670
|
+
schema: z
|
|
1671
|
+
.array(schemaReferenceSchema)
|
|
1672
|
+
.optional()
|
|
1673
|
+
.describe('Array of schema references (named schema refs or inline objects) merged left-to-right.'),
|
|
1674
|
+
/** JsonMap transformation (inline or reference to named map). */
|
|
1675
|
+
map: z
|
|
1676
|
+
.union([jsonMapMapSchema, z.string()])
|
|
1677
|
+
.optional()
|
|
1678
|
+
.describe('JsonMap transformation (inline definition, named map reference, or .json file path).'),
|
|
1679
|
+
/** Handlebars template (inline string, named ref, or .hbs/.handlebars file path). */
|
|
1680
|
+
template: z
|
|
1681
|
+
.string()
|
|
1682
|
+
.optional()
|
|
1683
|
+
.describe('Handlebars content template (inline string, named ref, or .hbs/.handlebars file path).'),
|
|
1684
|
+
/** Declarative structured renderer configuration (mutually exclusive with template). */
|
|
1685
|
+
render: renderConfigSchema
|
|
1686
|
+
.optional()
|
|
1687
|
+
.describe('Declarative render configuration for frontmatter + structured Markdown output (mutually exclusive with template).'),
|
|
1688
|
+
/** Output file extension override (e.g. "md", "html", "txt"). Requires template or render. */
|
|
1689
|
+
renderAs: z
|
|
1690
|
+
.string()
|
|
1691
|
+
.regex(/^[a-z0-9]{1,10}$/, 'renderAs must be 1-10 lowercase alphanumeric characters')
|
|
1692
|
+
.optional()
|
|
1693
|
+
.describe('Output file extension override (without dot). Requires template or render.'),
|
|
1694
|
+
})
|
|
1695
|
+
.superRefine((val, ctx) => {
|
|
1696
|
+
if (val.render && val.template) {
|
|
1697
|
+
ctx.addIssue({
|
|
1698
|
+
code: 'custom',
|
|
1699
|
+
path: ['render'],
|
|
1700
|
+
message: 'render is mutually exclusive with template',
|
|
1887
1701
|
});
|
|
1888
1702
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
if (typeof value === 'string') {
|
|
1896
|
-
maps[key] = readFileReference(value);
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
resolved['maps'] = maps;
|
|
1900
|
-
}
|
|
1901
|
-
// Resolve templates[*] file references (if 'files' requested)
|
|
1902
|
-
if (resolveTypes.includes('files') &&
|
|
1903
|
-
resolved['templates'] &&
|
|
1904
|
-
typeof resolved['templates'] === 'object') {
|
|
1905
|
-
const templates = {
|
|
1906
|
-
...resolved['templates'],
|
|
1907
|
-
};
|
|
1908
|
-
for (const [key, value] of Object.entries(templates)) {
|
|
1909
|
-
if (typeof value === 'string') {
|
|
1910
|
-
templates[key] = readFileReference(value);
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
resolved['templates'] = templates;
|
|
1703
|
+
if (val.renderAs && !val.template && !val.render) {
|
|
1704
|
+
ctx.addIssue({
|
|
1705
|
+
code: 'custom',
|
|
1706
|
+
path: ['renderAs'],
|
|
1707
|
+
message: 'renderAs requires template or render',
|
|
1708
|
+
});
|
|
1914
1709
|
}
|
|
1915
|
-
|
|
1916
|
-
}
|
|
1710
|
+
});
|
|
1917
1711
|
|
|
1918
1712
|
/**
|
|
1919
|
-
* @module
|
|
1920
|
-
*
|
|
1921
|
-
* optionally filtered by JSONPath via the `path` query parameter.
|
|
1922
|
-
*
|
|
1923
|
-
* Delegates JSON querying to `createConfigQueryHandler` from `@karmaniverous/jeeves` core.
|
|
1713
|
+
* @module config/schemas/services
|
|
1714
|
+
* Service configuration schemas: embedding and vector store.
|
|
1924
1715
|
*/
|
|
1925
1716
|
/**
|
|
1926
|
-
*
|
|
1927
|
-
*
|
|
1928
|
-
* Uses `createConfigQueryHandler` from core to handle JSONPath querying.
|
|
1929
|
-
* Invalid JSONPath expressions are client errors and return 400.
|
|
1930
|
-
*
|
|
1931
|
-
* @param deps - Route dependencies.
|
|
1717
|
+
* Embedding model configuration.
|
|
1932
1718
|
*/
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1719
|
+
const embeddingConfigSchema = z.object({
|
|
1720
|
+
/** The embedding model provider. */
|
|
1721
|
+
provider: z
|
|
1722
|
+
.string()
|
|
1723
|
+
.default('gemini')
|
|
1724
|
+
.describe('Embedding provider name (e.g., "gemini", "openai").'),
|
|
1725
|
+
/** The embedding model name. */
|
|
1726
|
+
model: z
|
|
1727
|
+
.string()
|
|
1728
|
+
.default('gemini-embedding-001')
|
|
1729
|
+
.describe('Embedding model identifier (e.g., "gemini-embedding-001", "text-embedding-3-small").'),
|
|
1730
|
+
/** Maximum tokens per chunk for splitting. */
|
|
1731
|
+
chunkSize: z
|
|
1732
|
+
.number()
|
|
1733
|
+
.optional()
|
|
1734
|
+
.describe('Maximum chunk size in characters for text splitting.'),
|
|
1735
|
+
/** Overlap between chunks in tokens. */
|
|
1736
|
+
chunkOverlap: z
|
|
1737
|
+
.number()
|
|
1738
|
+
.optional()
|
|
1739
|
+
.describe('Character overlap between consecutive chunks.'),
|
|
1740
|
+
/** Embedding vector dimensions. */
|
|
1741
|
+
dimensions: z
|
|
1742
|
+
.number()
|
|
1743
|
+
.optional()
|
|
1744
|
+
.describe('Embedding vector dimensions (must match model output).'),
|
|
1745
|
+
/** API key for the embedding provider. */
|
|
1746
|
+
apiKey: z
|
|
1747
|
+
.string()
|
|
1748
|
+
.optional()
|
|
1749
|
+
.describe('API key for embedding provider (supports ${ENV_VAR} substitution).'),
|
|
1750
|
+
/** Maximum embedding requests per minute. */
|
|
1751
|
+
rateLimitPerMinute: z
|
|
1752
|
+
.number()
|
|
1753
|
+
.optional()
|
|
1754
|
+
.describe('Maximum embedding API requests per minute (rate limiting).'),
|
|
1755
|
+
/** Maximum concurrent embedding requests. */
|
|
1756
|
+
concurrency: z
|
|
1757
|
+
.number()
|
|
1758
|
+
.optional()
|
|
1759
|
+
.describe('Maximum concurrent embedding requests.'),
|
|
1760
|
+
});
|
|
1761
|
+
/**
|
|
1762
|
+
* Vector store configuration for Qdrant.
|
|
1763
|
+
*/
|
|
1764
|
+
const vectorStoreConfigSchema = z.object({
|
|
1765
|
+
/** Qdrant server URL. */
|
|
1766
|
+
url: z
|
|
1767
|
+
.string()
|
|
1768
|
+
.describe('Qdrant server URL (e.g., "http://localhost:6333").'),
|
|
1769
|
+
/** Qdrant collection name. */
|
|
1770
|
+
collectionName: z
|
|
1771
|
+
.string()
|
|
1772
|
+
.describe('Qdrant collection name for vector storage.'),
|
|
1773
|
+
/** Qdrant API key. */
|
|
1774
|
+
apiKey: z
|
|
1775
|
+
.string()
|
|
1776
|
+
.optional()
|
|
1777
|
+
.describe('Qdrant API key for authentication (supports ${ENV_VAR} substitution).'),
|
|
1778
|
+
});
|
|
1959
1779
|
|
|
1960
1780
|
/**
|
|
1961
|
-
* @module
|
|
1962
|
-
*
|
|
1781
|
+
* @module config/schemas/root
|
|
1782
|
+
* Root configuration schema combining all sub-schemas.
|
|
1963
1783
|
*/
|
|
1964
1784
|
/**
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
1967
|
-
* @param deps - Route dependencies.
|
|
1785
|
+
* Top-level configuration for jeeves-watcher.
|
|
1968
1786
|
*/
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
:
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
1787
|
+
const jeevesWatcherConfigSchema = z.object({
|
|
1788
|
+
/** Optional description of this watcher deployment's organizational strategy. */
|
|
1789
|
+
description: z
|
|
1790
|
+
.string()
|
|
1791
|
+
.optional()
|
|
1792
|
+
.describe("Human-readable description of this deployment's organizational strategy and content domains."),
|
|
1793
|
+
/** Global named schema collection. */
|
|
1794
|
+
schemas: z
|
|
1795
|
+
.record(z.string(), schemaEntrySchema)
|
|
1796
|
+
.optional()
|
|
1797
|
+
.describe('Global named schema definitions (inline objects or file paths) referenced by inference rules.'),
|
|
1798
|
+
/** File system watch configuration. */
|
|
1799
|
+
watch: watchConfigSchema.describe('File system watch configuration.'),
|
|
1800
|
+
/** Configuration file watch settings. */
|
|
1801
|
+
configWatch: configWatchConfigSchema
|
|
1802
|
+
.optional()
|
|
1803
|
+
.describe('Configuration file watch settings.'),
|
|
1804
|
+
/** Embedding model configuration. */
|
|
1805
|
+
embedding: embeddingConfigSchema.describe('Embedding model configuration.'),
|
|
1806
|
+
/** Vector store configuration. */
|
|
1807
|
+
vectorStore: vectorStoreConfigSchema.describe('Qdrant vector store configuration.'),
|
|
1808
|
+
/** API server configuration. */
|
|
1809
|
+
api: apiConfigSchema.optional().describe('API server configuration.'),
|
|
1810
|
+
/** Directory for persistent state files (issues.json, values.json, enrichments.sqlite). */
|
|
1811
|
+
stateDir: z
|
|
1812
|
+
.string()
|
|
1813
|
+
.optional()
|
|
1814
|
+
.describe('Directory for persistent state files (issues.json, values.json, enrichments.sqlite). Defaults to .jeeves-metadata.'),
|
|
1815
|
+
/** Rules for inferring metadata from document properties (inline objects or file paths). */
|
|
1816
|
+
inferenceRules: z
|
|
1817
|
+
.array(z.union([inferenceRuleSchema, z.string()]))
|
|
1818
|
+
.optional()
|
|
1819
|
+
.describe('Rules for inferring metadata from file attributes. Each entry may be an inline rule object or a file path to a JSON rule file (resolved relative to config directory).'),
|
|
1820
|
+
/** Reusable named JsonMap transformations (inline objects or .json file paths). */
|
|
1821
|
+
maps: z
|
|
1822
|
+
.record(z.string(), z.union([
|
|
1823
|
+
jsonMapMapSchema,
|
|
1824
|
+
z.string(),
|
|
1825
|
+
z.object({
|
|
1826
|
+
/** The JsonMap definition (inline object or file path). */
|
|
1827
|
+
map: jsonMapMapSchema.or(z.string()),
|
|
1828
|
+
/** Optional human-readable description of this map. */
|
|
1829
|
+
description: z.string().optional(),
|
|
1830
|
+
}),
|
|
1831
|
+
]))
|
|
1832
|
+
.optional()
|
|
1833
|
+
.describe('Reusable named JsonMap transformations (inline definition or .json file path resolved relative to config directory).'),
|
|
1834
|
+
/** Reusable named Handlebars templates (inline strings or .hbs/.handlebars file paths). */
|
|
1835
|
+
templates: z
|
|
1836
|
+
.record(z.string(), z.union([
|
|
1837
|
+
z.string(),
|
|
1838
|
+
z.object({
|
|
1839
|
+
/** The Handlebars template source (inline string or file path). */
|
|
1840
|
+
template: z.string(),
|
|
1841
|
+
/** Optional human-readable description of this template. */
|
|
1842
|
+
description: z.string().optional(),
|
|
1843
|
+
}),
|
|
1844
|
+
]))
|
|
1845
|
+
.optional()
|
|
1846
|
+
.describe('Named reusable Handlebars templates (inline strings or .hbs/.handlebars file paths).'),
|
|
1847
|
+
/** Custom Handlebars helper registration. */
|
|
1848
|
+
templateHelpers: z
|
|
1849
|
+
.record(z.string(), z.object({
|
|
1850
|
+
/** File path to the helper module (resolved relative to config directory). */
|
|
1851
|
+
path: z.string(),
|
|
1852
|
+
/** Optional human-readable description of this helper. */
|
|
1853
|
+
description: z.string().optional(),
|
|
1854
|
+
}))
|
|
1855
|
+
.optional()
|
|
1856
|
+
.describe('Custom Handlebars helper registration.'),
|
|
1857
|
+
/** Custom JsonMap lib function registration. */
|
|
1858
|
+
mapHelpers: z
|
|
1859
|
+
.record(z.string(), z.object({
|
|
1860
|
+
/** File path to the helper module (resolved relative to config directory). */
|
|
1861
|
+
path: z.string(),
|
|
1862
|
+
/** Optional human-readable description of this helper. */
|
|
1863
|
+
description: z.string().optional(),
|
|
1864
|
+
}))
|
|
1865
|
+
.optional()
|
|
1866
|
+
.describe('Custom JsonMap lib function registration.'),
|
|
1867
|
+
/** Reindex configuration. */
|
|
1868
|
+
reindex: z
|
|
1869
|
+
.object({
|
|
1870
|
+
/** URL to call when reindex completes. */
|
|
1871
|
+
callbackUrl: z.url().optional(),
|
|
1872
|
+
/** Maximum concurrent file operations during reindex. */
|
|
1873
|
+
concurrency: z
|
|
1874
|
+
.number()
|
|
1875
|
+
.int()
|
|
1876
|
+
.min(1)
|
|
1877
|
+
.default(50)
|
|
1878
|
+
.describe('Maximum concurrent file operations during reindex (default 50).'),
|
|
1879
|
+
})
|
|
1880
|
+
.optional()
|
|
1881
|
+
.describe('Reindex configuration.'),
|
|
1882
|
+
/** Named Qdrant filter patterns for skill-activated behaviors. */
|
|
1883
|
+
/** Search configuration including score thresholds and hybrid search. */
|
|
1884
|
+
search: z
|
|
1885
|
+
.object({
|
|
1886
|
+
/** Score thresholds for categorizing search result quality. */
|
|
1887
|
+
scoreThresholds: z
|
|
1888
|
+
.object({
|
|
1889
|
+
/** Minimum score for a result to be considered a strong match. */
|
|
1890
|
+
strong: z.number().min(-1).max(1),
|
|
1891
|
+
/** Minimum score for a result to be considered relevant. */
|
|
1892
|
+
relevant: z.number().min(-1).max(1),
|
|
1893
|
+
/** Maximum score below which results are considered noise. */
|
|
1894
|
+
noise: z.number().min(-1).max(1),
|
|
1895
|
+
})
|
|
1896
|
+
.optional(),
|
|
1897
|
+
/** Hybrid search configuration combining vector and full-text search. */
|
|
1898
|
+
hybrid: z
|
|
1899
|
+
.object({
|
|
1900
|
+
/** Enable hybrid search with RRF fusion. Default: false. */
|
|
1901
|
+
enabled: z.boolean().default(false),
|
|
1902
|
+
/** Weight for text (BM25) results in RRF fusion. Default: 0.3. */
|
|
1903
|
+
textWeight: z.number().min(0).max(1).default(0.3),
|
|
1904
|
+
})
|
|
1905
|
+
.optional(),
|
|
1906
|
+
})
|
|
1907
|
+
.optional()
|
|
1908
|
+
.describe('Search configuration including score thresholds and hybrid search.'),
|
|
1909
|
+
/** Logging configuration. */
|
|
1910
|
+
logging: loggingConfigSchema.optional().describe('Logging configuration.'),
|
|
1911
|
+
/** Timeout in milliseconds for graceful shutdown. */
|
|
1912
|
+
shutdownTimeoutMs: z
|
|
1913
|
+
.number()
|
|
1914
|
+
.optional()
|
|
1915
|
+
.describe('Timeout in milliseconds for graceful shutdown.'),
|
|
1916
|
+
/** Maximum consecutive system-level failures before triggering fatal error. Default: Infinity. */
|
|
1917
|
+
maxRetries: z
|
|
1918
|
+
.number()
|
|
1919
|
+
.optional()
|
|
1920
|
+
.describe('Maximum consecutive system-level failures before triggering fatal error. Default: Infinity.'),
|
|
1921
|
+
/** Maximum backoff delay in milliseconds for system errors. Default: 60000. */
|
|
1922
|
+
maxBackoffMs: z
|
|
1923
|
+
.number()
|
|
1924
|
+
.optional()
|
|
1925
|
+
.describe('Maximum backoff delay in milliseconds for system errors. Default: 60000.'),
|
|
1926
|
+
});
|
|
2029
1927
|
|
|
2030
1928
|
/**
|
|
2031
1929
|
* @module api/handlers/configSchema
|
|
@@ -2610,9 +2508,10 @@ function renderDoc(context, config, hbs) {
|
|
|
2610
2508
|
*
|
|
2611
2509
|
* @param configDir - Optional config directory for resolving relative file paths in lookups.
|
|
2612
2510
|
* @param customLib - Optional custom lib functions to merge.
|
|
2511
|
+
* @param extractText - Optional callback to extract text from a file path. Returns extracted text or undefined on failure.
|
|
2613
2512
|
* @returns The lib object.
|
|
2614
2513
|
*/
|
|
2615
|
-
function createJsonMapLib(configDir, customLib) {
|
|
2514
|
+
function createJsonMapLib(configDir, customLib, extractText) {
|
|
2616
2515
|
// Cache loaded JSON files within a single applyRules invocation.
|
|
2617
2516
|
const jsonCache = new Map();
|
|
2618
2517
|
const loadJson = (filePath) => {
|
|
@@ -2683,6 +2582,66 @@ function createJsonMapLib(configDir, customLib) {
|
|
|
2683
2582
|
}
|
|
2684
2583
|
return results;
|
|
2685
2584
|
},
|
|
2585
|
+
/**
|
|
2586
|
+
* Retrieve extracted text from neighboring files in the same directory.
|
|
2587
|
+
*
|
|
2588
|
+
* @param filePath - The current file path.
|
|
2589
|
+
* @param options - Optional windowing and sort options.
|
|
2590
|
+
* @returns Flat array of extracted text from siblings, in sort order.
|
|
2591
|
+
*/
|
|
2592
|
+
fetchSiblings: (filePath, options) => {
|
|
2593
|
+
if (!extractText)
|
|
2594
|
+
return [];
|
|
2595
|
+
const { before = 3, after = 1, sort = 'name' } = options ?? {};
|
|
2596
|
+
const dir = dirname(filePath);
|
|
2597
|
+
const ext = extname(filePath);
|
|
2598
|
+
// List all files in the directory with the same extension
|
|
2599
|
+
let entries;
|
|
2600
|
+
try {
|
|
2601
|
+
entries = readdirSync(dir).filter((name) => extname(name) === ext);
|
|
2602
|
+
}
|
|
2603
|
+
catch {
|
|
2604
|
+
return [];
|
|
2605
|
+
}
|
|
2606
|
+
// Sort by filename (default) or mtime
|
|
2607
|
+
if (sort === 'mtime') {
|
|
2608
|
+
entries.sort((a, b) => {
|
|
2609
|
+
try {
|
|
2610
|
+
const aStat = statSync(join(dir, a));
|
|
2611
|
+
const bStat = statSync(join(dir, b));
|
|
2612
|
+
return aStat.mtimeMs - bStat.mtimeMs;
|
|
2613
|
+
}
|
|
2614
|
+
catch {
|
|
2615
|
+
return 0;
|
|
2616
|
+
}
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
else {
|
|
2620
|
+
entries.sort();
|
|
2621
|
+
}
|
|
2622
|
+
// Find current file's position
|
|
2623
|
+
const currentName = basename(filePath);
|
|
2624
|
+
const currentIndex = entries.indexOf(currentName);
|
|
2625
|
+
if (currentIndex === -1)
|
|
2626
|
+
return [];
|
|
2627
|
+
// Slice before/after window (excluding current file)
|
|
2628
|
+
const beforeEntries = entries.slice(Math.max(0, currentIndex - before), currentIndex);
|
|
2629
|
+
const afterEntries = entries.slice(currentIndex + 1, currentIndex + 1 + after);
|
|
2630
|
+
const window = [...beforeEntries, ...afterEntries];
|
|
2631
|
+
// Extract text from each sibling, silently skipping failures
|
|
2632
|
+
const results = [];
|
|
2633
|
+
for (const name of window) {
|
|
2634
|
+
try {
|
|
2635
|
+
const text = extractText(join(dir, name));
|
|
2636
|
+
if (text !== undefined)
|
|
2637
|
+
results.push(text);
|
|
2638
|
+
}
|
|
2639
|
+
catch {
|
|
2640
|
+
// Silently skip files that fail extraction
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return results;
|
|
2644
|
+
},
|
|
2686
2645
|
...customLib,
|
|
2687
2646
|
};
|
|
2688
2647
|
}
|
|
@@ -2703,9 +2662,12 @@ function coerceType(value, type) {
|
|
|
2703
2662
|
if (value === null || value === undefined) {
|
|
2704
2663
|
return undefined;
|
|
2705
2664
|
}
|
|
2665
|
+
// Empty strings are only valid for the 'string' type
|
|
2666
|
+
if (value === '' && type !== 'string') {
|
|
2667
|
+
return undefined;
|
|
2668
|
+
}
|
|
2706
2669
|
switch (type) {
|
|
2707
2670
|
case 'string': {
|
|
2708
|
-
// Empty strings are valid for string type
|
|
2709
2671
|
if (typeof value === 'string')
|
|
2710
2672
|
return value;
|
|
2711
2673
|
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
@@ -2719,9 +2681,6 @@ function coerceType(value, type) {
|
|
|
2719
2681
|
return undefined;
|
|
2720
2682
|
}
|
|
2721
2683
|
case 'integer': {
|
|
2722
|
-
// Empty strings are not valid integers
|
|
2723
|
-
if (value === '')
|
|
2724
|
-
return undefined;
|
|
2725
2684
|
if (typeof value === 'string') {
|
|
2726
2685
|
// For strings, check that parsing yields an integer that matches the trimmed input
|
|
2727
2686
|
const trimmed = value.trim();
|
|
@@ -2737,16 +2696,10 @@ function coerceType(value, type) {
|
|
|
2737
2696
|
return undefined;
|
|
2738
2697
|
}
|
|
2739
2698
|
case 'number': {
|
|
2740
|
-
// Empty strings are not valid numbers
|
|
2741
|
-
if (value === '')
|
|
2742
|
-
return undefined;
|
|
2743
2699
|
const num = typeof value === 'string' ? parseFloat(value) : Number(value);
|
|
2744
2700
|
return Number.isFinite(num) ? num : undefined;
|
|
2745
2701
|
}
|
|
2746
2702
|
case 'boolean': {
|
|
2747
|
-
// Empty strings are not valid booleans
|
|
2748
|
-
if (value === '')
|
|
2749
|
-
return undefined;
|
|
2750
2703
|
if (typeof value === 'boolean')
|
|
2751
2704
|
return value;
|
|
2752
2705
|
if (typeof value === 'string') {
|
|
@@ -2759,9 +2712,6 @@ function coerceType(value, type) {
|
|
|
2759
2712
|
return undefined;
|
|
2760
2713
|
}
|
|
2761
2714
|
case 'array': {
|
|
2762
|
-
// Empty strings are not valid arrays
|
|
2763
|
-
if (value === '')
|
|
2764
|
-
return undefined;
|
|
2765
2715
|
if (Array.isArray(value))
|
|
2766
2716
|
return value;
|
|
2767
2717
|
if (typeof value === 'string') {
|
|
@@ -2777,9 +2727,6 @@ function coerceType(value, type) {
|
|
|
2777
2727
|
return undefined;
|
|
2778
2728
|
}
|
|
2779
2729
|
case 'object': {
|
|
2780
|
-
// Empty strings are not valid objects
|
|
2781
|
-
if (value === '')
|
|
2782
|
-
return undefined;
|
|
2783
2730
|
if (typeof value === 'string') {
|
|
2784
2731
|
try {
|
|
2785
2732
|
const parsed = JSON.parse(value);
|
|
@@ -3011,11 +2958,11 @@ async function loadCustomMapHelpers(helpers, configDir) {
|
|
|
3011
2958
|
* @returns The merged metadata and optional rendered content.
|
|
3012
2959
|
*/
|
|
3013
2960
|
async function applyRules(compiledRules, attributes, options = {}) {
|
|
3014
|
-
const { namedMaps, logger, templateEngine, configDir, customMapLib, globalSchemas, } = options;
|
|
2961
|
+
const { namedMaps, logger, templateEngine, configDir, customMapLib, globalSchemas, extractText, } = options;
|
|
3015
2962
|
const hbs = templateEngine?.hbs ?? createHandlebarsInstance();
|
|
3016
2963
|
// JsonMap's type definitions expect a generic JsonMapLib shape with unary functions.
|
|
3017
2964
|
// Our helper functions accept multiple args, which JsonMap supports at runtime.
|
|
3018
|
-
const lib = createJsonMapLib(configDir, customMapLib);
|
|
2965
|
+
const lib = createJsonMapLib(configDir, customMapLib, extractText);
|
|
3019
2966
|
let merged = {};
|
|
3020
2967
|
let renderedContent = null;
|
|
3021
2968
|
let renderAs = null;
|
|
@@ -3135,6 +3082,80 @@ async function applyRules(compiledRules, attributes, options = {}) {
|
|
|
3135
3082
|
return { metadata: merged, renderedContent, matchedRules, renderAs };
|
|
3136
3083
|
}
|
|
3137
3084
|
|
|
3085
|
+
/**
|
|
3086
|
+
* @module api/handlers/configMerge
|
|
3087
|
+
* Shared config merge utilities.
|
|
3088
|
+
*/
|
|
3089
|
+
/**
|
|
3090
|
+
* Merge inference rules by name: submitted rules replace existing by name, new ones are appended.
|
|
3091
|
+
*/
|
|
3092
|
+
function mergeInferenceRules(existing, incoming) {
|
|
3093
|
+
if (!incoming)
|
|
3094
|
+
return existing ?? [];
|
|
3095
|
+
if (!existing)
|
|
3096
|
+
return incoming;
|
|
3097
|
+
const merged = [...existing];
|
|
3098
|
+
for (const rule of incoming) {
|
|
3099
|
+
const name = rule['name'];
|
|
3100
|
+
if (!name) {
|
|
3101
|
+
merged.push(rule);
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
const idx = merged.findIndex((r) => r['name'] === name);
|
|
3105
|
+
if (idx >= 0) {
|
|
3106
|
+
merged[idx] = rule;
|
|
3107
|
+
}
|
|
3108
|
+
else {
|
|
3109
|
+
merged.push(rule);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
return merged;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
/**
|
|
3116
|
+
* @module api/handlers/mergeAndValidate
|
|
3117
|
+
* Shared config merge and validation logic used by both validate and apply handlers.
|
|
3118
|
+
*/
|
|
3119
|
+
/**
|
|
3120
|
+
* Merge a submitted partial config into the current config and validate against schema.
|
|
3121
|
+
*
|
|
3122
|
+
* @param currentConfig - The current running config.
|
|
3123
|
+
* @param submittedPartial - The partial config to merge in.
|
|
3124
|
+
* @returns The merge/validation result.
|
|
3125
|
+
*/
|
|
3126
|
+
function mergeAndValidateConfig(currentConfig, submittedPartial) {
|
|
3127
|
+
let candidateRaw = {
|
|
3128
|
+
...currentConfig,
|
|
3129
|
+
};
|
|
3130
|
+
if (submittedPartial) {
|
|
3131
|
+
const mergedRules = mergeInferenceRules(candidateRaw['inferenceRules'], submittedPartial['inferenceRules']);
|
|
3132
|
+
candidateRaw = {
|
|
3133
|
+
...candidateRaw,
|
|
3134
|
+
...submittedPartial,
|
|
3135
|
+
inferenceRules: mergedRules,
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
const parseResult = jeevesWatcherConfigSchema.safeParse(candidateRaw);
|
|
3139
|
+
const errors = [];
|
|
3140
|
+
if (!parseResult.success) {
|
|
3141
|
+
for (const issue of parseResult.error.issues) {
|
|
3142
|
+
errors.push({
|
|
3143
|
+
path: issue.path.join('.'),
|
|
3144
|
+
message: issue.message,
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
3147
|
+
return { candidateRaw, errors };
|
|
3148
|
+
}
|
|
3149
|
+
// Note: When accepting external config via API, string rule references are NOT resolved
|
|
3150
|
+
// (no configDir context). API-submitted rules must always be inline objects.
|
|
3151
|
+
// The cast is safe because API config never contains string rule refs.
|
|
3152
|
+
return {
|
|
3153
|
+
candidateRaw,
|
|
3154
|
+
parsed: parseResult.data,
|
|
3155
|
+
errors,
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3138
3159
|
/**
|
|
3139
3160
|
* @module api/handlers/configValidate
|
|
3140
3161
|
* Fastify route handler for POST /config/validate. Validates config against schema with optional test paths.
|
|
@@ -3651,11 +3672,11 @@ function createRenderHandler(deps) {
|
|
|
3651
3672
|
});
|
|
3652
3673
|
}
|
|
3653
3674
|
catch (error) {
|
|
3654
|
-
const
|
|
3675
|
+
const { message: msg } = normalizeError(error);
|
|
3655
3676
|
if (msg.includes('ENOENT') || msg.includes('no such file')) {
|
|
3656
3677
|
return reply.status(404).send({ error: 'File not found' });
|
|
3657
3678
|
}
|
|
3658
|
-
logger.error({ filePath,
|
|
3679
|
+
logger.error({ filePath, err: normalizeError(error) }, 'Render failed');
|
|
3659
3680
|
return reply.status(422).send({ error: msg });
|
|
3660
3681
|
}
|
|
3661
3682
|
};
|
|
@@ -3715,7 +3736,7 @@ function createRulesReapplyHandler(deps) {
|
|
|
3715
3736
|
* Create handler for POST /rules/register.
|
|
3716
3737
|
*/
|
|
3717
3738
|
function createRulesRegisterHandler(deps) {
|
|
3718
|
-
return wrapHandler(
|
|
3739
|
+
return wrapHandler((request) => {
|
|
3719
3740
|
const { source, rules } = request.body;
|
|
3720
3741
|
if (!source || typeof source !== 'string') {
|
|
3721
3742
|
throw new Error('Missing required field: source');
|
|
@@ -3726,11 +3747,11 @@ function createRulesRegisterHandler(deps) {
|
|
|
3726
3747
|
deps.virtualRuleStore.register(source, rules);
|
|
3727
3748
|
deps.onRulesChanged();
|
|
3728
3749
|
deps.logger.info({ source, ruleCount: rules.length }, 'Virtual rules registered');
|
|
3729
|
-
return
|
|
3750
|
+
return {
|
|
3730
3751
|
source,
|
|
3731
3752
|
registered: rules.length,
|
|
3732
3753
|
totalVirtualRules: deps.virtualRuleStore.size,
|
|
3733
|
-
}
|
|
3754
|
+
};
|
|
3734
3755
|
}, deps.logger, 'RulesRegister');
|
|
3735
3756
|
}
|
|
3736
3757
|
|
|
@@ -3755,16 +3776,16 @@ function unregisterSource(deps, source) {
|
|
|
3755
3776
|
* Create handler for DELETE /rules/unregister (body-based).
|
|
3756
3777
|
*/
|
|
3757
3778
|
function createRulesUnregisterHandler(deps) {
|
|
3758
|
-
return wrapHandler(
|
|
3759
|
-
return
|
|
3779
|
+
return wrapHandler((request) => {
|
|
3780
|
+
return unregisterSource(deps, request.body.source);
|
|
3760
3781
|
}, deps.logger, 'RulesUnregister');
|
|
3761
3782
|
}
|
|
3762
3783
|
/**
|
|
3763
3784
|
* Create handler for DELETE /rules/unregister/:source (param-based).
|
|
3764
3785
|
*/
|
|
3765
3786
|
function createRulesUnregisterParamHandler(deps) {
|
|
3766
|
-
return wrapHandler(
|
|
3767
|
-
return
|
|
3787
|
+
return wrapHandler((request) => {
|
|
3788
|
+
return unregisterSource(deps, request.params.source);
|
|
3768
3789
|
}, deps.logger, 'RulesUnregister');
|
|
3769
3790
|
}
|
|
3770
3791
|
|
|
@@ -4444,7 +4465,7 @@ class InitialScanTracker {
|
|
|
4444
4465
|
* @returns A configured Fastify instance.
|
|
4445
4466
|
*/
|
|
4446
4467
|
function createApiServer(options) {
|
|
4447
|
-
const { processor, vectorStore, embeddingProvider, queue, logger, config, issuesManager, valuesManager, configPath, helperIntrospection, virtualRuleStore, gitignoreFilter, version, initialScanTracker, } = options;
|
|
4468
|
+
const { descriptor, processor, vectorStore, embeddingProvider, queue, logger, config, issuesManager, valuesManager, configPath, helperIntrospection, virtualRuleStore, gitignoreFilter, version, initialScanTracker, } = options;
|
|
4448
4469
|
const getConfig = options.getConfig ?? (() => config);
|
|
4449
4470
|
const getFileSystemWatcher = options.getFileSystemWatcher ?? (() => options.fileSystemWatcher);
|
|
4450
4471
|
const reindexTracker = options.reindexTracker ?? new ReindexTracker();
|
|
@@ -4559,13 +4580,20 @@ function createApiServer(options) {
|
|
|
4559
4580
|
logger,
|
|
4560
4581
|
configDir: dirname(configPath),
|
|
4561
4582
|
}));
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4583
|
+
const coreConfigApplyHandler = createConfigApplyHandler({
|
|
4584
|
+
...descriptor,
|
|
4585
|
+
onConfigApply: () => {
|
|
4586
|
+
const cfg = getConfig();
|
|
4587
|
+
const reindexScope = cfg.configWatch?.reindex ?? 'issues';
|
|
4588
|
+
triggerReindex(reindexScope);
|
|
4589
|
+
return Promise.resolve();
|
|
4590
|
+
},
|
|
4591
|
+
});
|
|
4592
|
+
app.post('/config/apply', async (request, reply) => {
|
|
4593
|
+
const { patch, replace } = request.body;
|
|
4594
|
+
const result = await coreConfigApplyHandler({ patch, replace });
|
|
4595
|
+
return reply.status(result.status).send(result.body);
|
|
4596
|
+
});
|
|
4569
4597
|
// Virtual rules and points deletion routes
|
|
4570
4598
|
if (virtualRuleStore) {
|
|
4571
4599
|
const onRulesChanged = createOnRulesChanged({
|
|
@@ -4613,7 +4641,7 @@ function createApiServer(options) {
|
|
|
4613
4641
|
* @returns The normalized path string.
|
|
4614
4642
|
*/
|
|
4615
4643
|
function normalizePath(filePath, stripDriveLetter = false) {
|
|
4616
|
-
let result = filePath
|
|
4644
|
+
let result = normalizeSlashes(filePath).toLowerCase();
|
|
4617
4645
|
if (stripDriveLetter) {
|
|
4618
4646
|
result = result.replace(/^([a-z]):/, (_m, letter) => letter);
|
|
4619
4647
|
}
|
|
@@ -5214,7 +5242,7 @@ const CONFIG_WATCH_DEFAULTS = {
|
|
|
5214
5242
|
/** Default API values. */
|
|
5215
5243
|
const API_DEFAULTS = {
|
|
5216
5244
|
host: '127.0.0.1',
|
|
5217
|
-
port:
|
|
5245
|
+
port: DEFAULT_PORTS.watcher,
|
|
5218
5246
|
cacheTtlMs: 30000,
|
|
5219
5247
|
};
|
|
5220
5248
|
/** Default logging values. */
|
|
@@ -5682,6 +5710,7 @@ function extractMarkdownFrontmatter(markdown) {
|
|
|
5682
5710
|
: undefined;
|
|
5683
5711
|
return { frontmatter, body };
|
|
5684
5712
|
}
|
|
5713
|
+
/** Well-known JSON fields that contain meaningful text content. */
|
|
5685
5714
|
const JSON_TEXT_FIELDS = [
|
|
5686
5715
|
'content',
|
|
5687
5716
|
'body',
|
|
@@ -5782,6 +5811,25 @@ async function extractText(filePath, extension, additionalExtractors) {
|
|
|
5782
5811
|
* @module processor/buildMetadata
|
|
5783
5812
|
* Builds merged metadata from file content, inference rules, and enrichment store. I/O: reads files, extracts text, queries SQLite enrichment.
|
|
5784
5813
|
*/
|
|
5814
|
+
/**
|
|
5815
|
+
* Synchronously extract text from a file. Returns undefined on failure.
|
|
5816
|
+
* Used by fetchSiblings in JsonMap lib for sibling context extraction.
|
|
5817
|
+
*/
|
|
5818
|
+
function syncExtractText(filePath) {
|
|
5819
|
+
try {
|
|
5820
|
+
const raw = readFileSync(filePath, 'utf-8').replace(/^\uFEFF/, '');
|
|
5821
|
+
const ext = extname(filePath).toLowerCase();
|
|
5822
|
+
if (ext === '.json') {
|
|
5823
|
+
const parsed = JSON.parse(raw);
|
|
5824
|
+
return extractJsonText(parsed);
|
|
5825
|
+
}
|
|
5826
|
+
// All other supported text formats: return raw content
|
|
5827
|
+
return raw;
|
|
5828
|
+
}
|
|
5829
|
+
catch {
|
|
5830
|
+
return undefined;
|
|
5831
|
+
}
|
|
5832
|
+
}
|
|
5785
5833
|
/**
|
|
5786
5834
|
* Build merged metadata for a file by applying inference rules and merging with enrichment metadata.
|
|
5787
5835
|
*
|
|
@@ -5803,6 +5851,7 @@ async function buildMergedMetadata(options) {
|
|
|
5803
5851
|
configDir,
|
|
5804
5852
|
customMapLib,
|
|
5805
5853
|
globalSchemas,
|
|
5854
|
+
extractText: syncExtractText,
|
|
5806
5855
|
});
|
|
5807
5856
|
// 3. Read enrichment metadata from store (composable merge)
|
|
5808
5857
|
const enrichment = enrichmentStore?.get(filePath) ?? null;
|
|
@@ -7701,7 +7750,7 @@ function installShutdownHandlers(stop) {
|
|
|
7701
7750
|
* When provided, the file is loaded as-is (no migration).
|
|
7702
7751
|
* @returns The running JeevesWatcher instance.
|
|
7703
7752
|
*/
|
|
7704
|
-
async function startFromConfig(configPath) {
|
|
7753
|
+
async function startFromConfig(configPath, descriptor) {
|
|
7705
7754
|
let resolvedPath = configPath;
|
|
7706
7755
|
// Auto-migrate only when no explicit path was given.
|
|
7707
7756
|
if (!configPath) {
|
|
@@ -7724,7 +7773,10 @@ async function startFromConfig(configPath) {
|
|
|
7724
7773
|
}
|
|
7725
7774
|
}
|
|
7726
7775
|
const config = await loadConfig(resolvedPath);
|
|
7727
|
-
|
|
7776
|
+
// Dynamic import breaks the circular dependency between this module
|
|
7777
|
+
// and app/index.ts (which defines JeevesWatcher and re-exports startFromConfig).
|
|
7778
|
+
const { JeevesWatcher } = await Promise.resolve().then(function () { return index; });
|
|
7779
|
+
const app = new JeevesWatcher(config, resolvedPath, descriptor);
|
|
7728
7780
|
installShutdownHandlers(() => app.stop());
|
|
7729
7781
|
await app.start();
|
|
7730
7782
|
return app;
|
|
@@ -7740,6 +7792,7 @@ async function startFromConfig(configPath) {
|
|
|
7740
7792
|
class JeevesWatcher {
|
|
7741
7793
|
config;
|
|
7742
7794
|
configPath;
|
|
7795
|
+
descriptor;
|
|
7743
7796
|
factories;
|
|
7744
7797
|
runtimeOptions;
|
|
7745
7798
|
logger;
|
|
@@ -7760,9 +7813,13 @@ class JeevesWatcher {
|
|
|
7760
7813
|
initialScanTracker;
|
|
7761
7814
|
version;
|
|
7762
7815
|
/** Create a new JeevesWatcher instance. */
|
|
7763
|
-
constructor(config, configPath, factories = {}, runtimeOptions = {}) {
|
|
7816
|
+
constructor(config, configPath, descriptor, factories = {}, runtimeOptions = {}) {
|
|
7817
|
+
if (!descriptor) {
|
|
7818
|
+
throw new Error('JeevesWatcher requires a component descriptor. Pass watcherDescriptor from the descriptor module.');
|
|
7819
|
+
}
|
|
7764
7820
|
this.config = config;
|
|
7765
7821
|
this.configPath = configPath;
|
|
7822
|
+
this.descriptor = descriptor;
|
|
7766
7823
|
this.factories = { ...defaultFactories, ...factories };
|
|
7767
7824
|
this.runtimeOptions = runtimeOptions;
|
|
7768
7825
|
this.virtualRuleStore = new VirtualRuleStore();
|
|
@@ -7852,6 +7909,7 @@ class JeevesWatcher {
|
|
|
7852
7909
|
}
|
|
7853
7910
|
async startApiServer() {
|
|
7854
7911
|
const server = this.factories.createApiServer({
|
|
7912
|
+
descriptor: this.descriptor,
|
|
7855
7913
|
processor: this.processor,
|
|
7856
7914
|
vectorStore: this.vectorStore,
|
|
7857
7915
|
embeddingProvider: this.embeddingProvider,
|
|
@@ -7873,7 +7931,7 @@ class JeevesWatcher {
|
|
|
7873
7931
|
});
|
|
7874
7932
|
await server.listen({
|
|
7875
7933
|
host: this.config.api?.host ?? getBindAddress('watcher'),
|
|
7876
|
-
port: this.config.api?.port ??
|
|
7934
|
+
port: this.config.api?.port ?? DEFAULT_PORTS.watcher,
|
|
7877
7935
|
});
|
|
7878
7936
|
return server;
|
|
7879
7937
|
}
|
|
@@ -7931,12 +7989,18 @@ class JeevesWatcher {
|
|
|
7931
7989
|
}
|
|
7932
7990
|
}
|
|
7933
7991
|
|
|
7992
|
+
var index = /*#__PURE__*/Object.freeze({
|
|
7993
|
+
__proto__: null,
|
|
7994
|
+
JeevesWatcher: JeevesWatcher,
|
|
7995
|
+
startFromConfig: startFromConfig
|
|
7996
|
+
});
|
|
7997
|
+
|
|
7934
7998
|
/**
|
|
7935
7999
|
* @module cli/jeeves-watcher/customCommands
|
|
7936
8000
|
* Domain-specific CLI commands registered via the descriptor's customCliCommands.
|
|
7937
8001
|
* Uses fetchJson/postJson from core instead of hand-rolled API helpers.
|
|
7938
8002
|
*/
|
|
7939
|
-
const DEFAULT_PORT =
|
|
8003
|
+
const DEFAULT_PORT = String(DEFAULT_PORTS.watcher);
|
|
7940
8004
|
const DEFAULT_HOST = '127.0.0.1';
|
|
7941
8005
|
/** Build the API base URL from host and port options. */
|
|
7942
8006
|
function baseUrl(opts) {
|
|
@@ -8032,8 +8096,8 @@ function registerCustomCommands(program) {
|
|
|
8032
8096
|
.option('-s, --scope <scope>', 'Reindex scope (issues|full|rules|path|prune)', 'rules')
|
|
8033
8097
|
.option('-t, --path <paths...>', 'Target path(s) for path or rules scope')).action(async (opts) => {
|
|
8034
8098
|
await handleErrors(async () => {
|
|
8035
|
-
if (!
|
|
8036
|
-
console.error(`Invalid scope "${opts.scope}". Must be one of: ${
|
|
8099
|
+
if (!VALID_REINDEX_SCOPES.includes(opts.scope)) {
|
|
8100
|
+
console.error(`Invalid scope "${opts.scope}". Must be one of: ${VALID_REINDEX_SCOPES.join(', ')}`);
|
|
8037
8101
|
process.exitCode = 1;
|
|
8038
8102
|
return;
|
|
8039
8103
|
}
|
|
@@ -8091,8 +8155,6 @@ function registerCustomCommands(program) {
|
|
|
8091
8155
|
})();
|
|
8092
8156
|
});
|
|
8093
8157
|
}
|
|
8094
|
-
// --- shared constants and helpers ---
|
|
8095
|
-
const VALID_SCOPES = ['issues', 'full', 'rules', 'path', 'prune'];
|
|
8096
8158
|
/** Parse --json and --key options into a metadata object. Returns null on validation failure. */
|
|
8097
8159
|
function parseMetadataArgs(json, keys) {
|
|
8098
8160
|
let metadata = {};
|
|
@@ -8176,16 +8238,14 @@ const watcherDescriptor = {
|
|
|
8176
8238
|
version,
|
|
8177
8239
|
servicePackage: '@karmaniverous/jeeves-watcher',
|
|
8178
8240
|
pluginPackage: '@karmaniverous/jeeves-watcher-openclaw',
|
|
8179
|
-
defaultPort:
|
|
8241
|
+
defaultPort: DEFAULT_PORTS.watcher,
|
|
8180
8242
|
// Config
|
|
8181
8243
|
configSchema: jeevesWatcherConfigSchema,
|
|
8182
8244
|
configFileName: 'config.json',
|
|
8183
8245
|
initTemplate: () => ({ ...INIT_CONFIG_TEMPLATE }),
|
|
8184
|
-
//
|
|
8185
|
-
// where it has access to the live
|
|
8186
|
-
|
|
8187
|
-
await Promise.resolve();
|
|
8188
|
-
},
|
|
8246
|
+
// onConfigApply is overridden in createApiServer (api/index.ts) via the
|
|
8247
|
+
// descriptor passed as a dependency, where it has access to the live
|
|
8248
|
+
// reindex tracker and config getter.
|
|
8189
8249
|
customMerge: (target, source) => {
|
|
8190
8250
|
const mergedRules = mergeInferenceRules(target['inferenceRules'], source['inferenceRules']);
|
|
8191
8251
|
return {
|
|
@@ -8202,7 +8262,7 @@ const watcherDescriptor = {
|
|
|
8202
8262
|
configPath,
|
|
8203
8263
|
],
|
|
8204
8264
|
run: async (configPath) => {
|
|
8205
|
-
await startFromConfig(configPath);
|
|
8265
|
+
await startFromConfig(configPath, watcherDescriptor);
|
|
8206
8266
|
},
|
|
8207
8267
|
// Content — generateToolsContent is wired in the plugin package
|
|
8208
8268
|
// (watcherComponent.ts) where it has access to the API URL for menu generation.
|