@karmaniverous/jeeves-watcher 0.2.3 → 0.2.5
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 +15 -0
- package/config.schema.json +117 -81
- package/dist/cjs/index.js +422 -10
- package/dist/cli/jeeves-watcher/index.js +421 -11
- package/dist/index.d.ts +150 -5
- package/dist/index.iife.js +422 -12
- package/dist/index.iife.min.js +1 -1
- package/dist/mjs/index.js +422 -12
- package/package.json +2 -1
package/dist/index.iife.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function (exports, Fastify, promises, node_path, picomatch, radash, node_crypto, cosmiconfig, zod, jsonmap, googleGenai, pino, uuid, cheerio, yaml, mammoth, Ajv, addFormats, textsplitters, jsClientRest, chokidar) {
|
|
1
|
+
(function (exports, Fastify, promises, node_path, picomatch, radash, node_crypto, cosmiconfig, zod, jsonmap, googleGenai, node_fs, ignore, pino, uuid, cheerio, yaml, mammoth, Ajv, addFormats, textsplitters, jsClientRest, chokidar) {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
function _interopNamespaceDefault(e) {
|
|
@@ -434,6 +434,7 @@
|
|
|
434
434
|
stabilityThresholdMs: 500,
|
|
435
435
|
usePolling: false,
|
|
436
436
|
pollIntervalMs: 1000,
|
|
437
|
+
respectGitignore: true,
|
|
437
438
|
};
|
|
438
439
|
/** Default embedding configuration. */
|
|
439
440
|
const EMBEDDING_DEFAULTS = {
|
|
@@ -478,6 +479,11 @@
|
|
|
478
479
|
.number()
|
|
479
480
|
.optional()
|
|
480
481
|
.describe('Time in milliseconds a file must remain unchanged before processing.'),
|
|
482
|
+
/** Whether to respect .gitignore files when processing. */
|
|
483
|
+
respectGitignore: zod.z
|
|
484
|
+
.boolean()
|
|
485
|
+
.optional()
|
|
486
|
+
.describe('Skip files ignored by .gitignore in git repositories. Only applies to repos with a .git directory. Default: true.'),
|
|
481
487
|
});
|
|
482
488
|
/**
|
|
483
489
|
* Configuration watch settings.
|
|
@@ -645,6 +651,16 @@
|
|
|
645
651
|
.number()
|
|
646
652
|
.optional()
|
|
647
653
|
.describe('Timeout in milliseconds for graceful shutdown.'),
|
|
654
|
+
/** Maximum consecutive system-level failures before triggering fatal error. Default: Infinity. */
|
|
655
|
+
maxRetries: zod.z
|
|
656
|
+
.number()
|
|
657
|
+
.optional()
|
|
658
|
+
.describe('Maximum consecutive system-level failures before triggering fatal error. Default: Infinity.'),
|
|
659
|
+
/** Maximum backoff delay in milliseconds for system errors. Default: 60000. */
|
|
660
|
+
maxBackoffMs: zod.z
|
|
661
|
+
.number()
|
|
662
|
+
.optional()
|
|
663
|
+
.describe('Maximum backoff delay in milliseconds for system errors. Default: 60000.'),
|
|
648
664
|
});
|
|
649
665
|
|
|
650
666
|
/**
|
|
@@ -933,6 +949,212 @@
|
|
|
933
949
|
return factory(config, logger);
|
|
934
950
|
}
|
|
935
951
|
|
|
952
|
+
/**
|
|
953
|
+
* @module gitignore
|
|
954
|
+
* Processor-level gitignore filtering. Scans watched paths for `.gitignore` files in git repos, caches parsed patterns, and exposes `isIgnored()` for path checking.
|
|
955
|
+
*/
|
|
956
|
+
/**
|
|
957
|
+
* Find the git repo root by walking up from `startDir` looking for `.git/`.
|
|
958
|
+
* Returns `undefined` if no repo is found.
|
|
959
|
+
*/
|
|
960
|
+
function findRepoRoot(startDir) {
|
|
961
|
+
let dir = node_path.resolve(startDir);
|
|
962
|
+
const root = node_path.resolve('/');
|
|
963
|
+
while (dir !== root) {
|
|
964
|
+
if (node_fs.existsSync(node_path.join(dir, '.git')) &&
|
|
965
|
+
node_fs.statSync(node_path.join(dir, '.git')).isDirectory()) {
|
|
966
|
+
return dir;
|
|
967
|
+
}
|
|
968
|
+
const parent = node_path.dirname(dir);
|
|
969
|
+
if (parent === dir)
|
|
970
|
+
break;
|
|
971
|
+
dir = parent;
|
|
972
|
+
}
|
|
973
|
+
return undefined;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Convert a watch path (directory, file path, or glob) to a concrete directory
|
|
977
|
+
* that can be scanned for a repo root.
|
|
978
|
+
*/
|
|
979
|
+
function watchPathToScanDir(watchPath) {
|
|
980
|
+
const absPath = node_path.resolve(watchPath);
|
|
981
|
+
try {
|
|
982
|
+
return node_fs.statSync(absPath).isDirectory() ? absPath : node_path.dirname(absPath);
|
|
983
|
+
}
|
|
984
|
+
catch {
|
|
985
|
+
// ignore
|
|
986
|
+
}
|
|
987
|
+
// If this is a glob, fall back to the non-glob prefix.
|
|
988
|
+
const globMatch = /[*?[{]/.exec(watchPath);
|
|
989
|
+
if (!globMatch)
|
|
990
|
+
return undefined;
|
|
991
|
+
const prefix = watchPath.slice(0, globMatch.index);
|
|
992
|
+
const trimmed = prefix.trim();
|
|
993
|
+
const baseDir = trimmed.length === 0
|
|
994
|
+
? '.'
|
|
995
|
+
: trimmed.endsWith('/') || trimmed.endsWith('\\')
|
|
996
|
+
? trimmed
|
|
997
|
+
: node_path.dirname(trimmed);
|
|
998
|
+
const resolved = node_path.resolve(baseDir);
|
|
999
|
+
if (!node_fs.existsSync(resolved))
|
|
1000
|
+
return undefined;
|
|
1001
|
+
return resolved;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Recursively find all `.gitignore` files under `dir`.
|
|
1005
|
+
* Skips `.git` and `node_modules` directories for performance.
|
|
1006
|
+
*/
|
|
1007
|
+
function findGitignoreFiles(dir) {
|
|
1008
|
+
const results = [];
|
|
1009
|
+
const gitignorePath = node_path.join(dir, '.gitignore');
|
|
1010
|
+
if (node_fs.existsSync(gitignorePath)) {
|
|
1011
|
+
results.push(gitignorePath);
|
|
1012
|
+
}
|
|
1013
|
+
let entries;
|
|
1014
|
+
try {
|
|
1015
|
+
entries = node_fs.readdirSync(dir);
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
return results;
|
|
1019
|
+
}
|
|
1020
|
+
for (const entry of entries) {
|
|
1021
|
+
if (entry === '.git' || entry === 'node_modules')
|
|
1022
|
+
continue;
|
|
1023
|
+
const fullPath = node_path.join(dir, entry);
|
|
1024
|
+
try {
|
|
1025
|
+
if (node_fs.statSync(fullPath).isDirectory()) {
|
|
1026
|
+
results.push(...findGitignoreFiles(fullPath));
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
catch {
|
|
1030
|
+
// Skip inaccessible entries
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return results;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Parse a `.gitignore` file into an `ignore` instance.
|
|
1037
|
+
*/
|
|
1038
|
+
function parseGitignore(gitignorePath) {
|
|
1039
|
+
const content = node_fs.readFileSync(gitignorePath, 'utf8');
|
|
1040
|
+
return ignore().add(content);
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Normalize a path to use forward slashes (required by `ignore` package).
|
|
1044
|
+
*/
|
|
1045
|
+
function toForwardSlash(p) {
|
|
1046
|
+
return p.replace(/\\/g, '/');
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Processor-level gitignore filter. Checks file paths against the nearest
|
|
1050
|
+
* `.gitignore` chain in git repositories.
|
|
1051
|
+
*/
|
|
1052
|
+
class GitignoreFilter {
|
|
1053
|
+
repos = new Map();
|
|
1054
|
+
/**
|
|
1055
|
+
* Create a GitignoreFilter by scanning watched paths for `.gitignore` files.
|
|
1056
|
+
*
|
|
1057
|
+
* @param watchPaths - Absolute paths being watched (directories or globs resolved to roots).
|
|
1058
|
+
*/
|
|
1059
|
+
constructor(watchPaths) {
|
|
1060
|
+
this.scan(watchPaths);
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Scan paths for git repos and their `.gitignore` files.
|
|
1064
|
+
*/
|
|
1065
|
+
scan(watchPaths) {
|
|
1066
|
+
this.repos.clear();
|
|
1067
|
+
const scannedDirs = new Set();
|
|
1068
|
+
for (const watchPath of watchPaths) {
|
|
1069
|
+
const scanDir = watchPathToScanDir(watchPath);
|
|
1070
|
+
if (!scanDir)
|
|
1071
|
+
continue;
|
|
1072
|
+
if (scannedDirs.has(scanDir))
|
|
1073
|
+
continue;
|
|
1074
|
+
scannedDirs.add(scanDir);
|
|
1075
|
+
const repoRoot = findRepoRoot(scanDir);
|
|
1076
|
+
if (!repoRoot)
|
|
1077
|
+
continue;
|
|
1078
|
+
if (this.repos.has(repoRoot))
|
|
1079
|
+
continue;
|
|
1080
|
+
const gitignoreFiles = findGitignoreFiles(repoRoot);
|
|
1081
|
+
const entries = gitignoreFiles.map((gf) => ({
|
|
1082
|
+
dir: node_path.dirname(gf),
|
|
1083
|
+
ig: parseGitignore(gf),
|
|
1084
|
+
}));
|
|
1085
|
+
// Sort deepest-first so nested `.gitignore` files are checked first
|
|
1086
|
+
entries.sort((a, b) => b.dir.length - a.dir.length);
|
|
1087
|
+
this.repos.set(repoRoot, { root: repoRoot, entries });
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Check whether a file path is ignored by any applicable `.gitignore`.
|
|
1092
|
+
*
|
|
1093
|
+
* @param filePath - Absolute file path to check.
|
|
1094
|
+
* @returns `true` if the file should be ignored.
|
|
1095
|
+
*/
|
|
1096
|
+
isIgnored(filePath) {
|
|
1097
|
+
const absPath = node_path.resolve(filePath);
|
|
1098
|
+
for (const [, repo] of this.repos) {
|
|
1099
|
+
// Check if file is within this repo
|
|
1100
|
+
const relToRepo = node_path.relative(repo.root, absPath);
|
|
1101
|
+
if (relToRepo.startsWith('..') || relToRepo.startsWith(node_path.resolve('/'))) {
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
// Check each `.gitignore` entry (deepest-first)
|
|
1105
|
+
for (const entry of repo.entries) {
|
|
1106
|
+
const relToEntry = node_path.relative(entry.dir, absPath);
|
|
1107
|
+
if (relToEntry.startsWith('..'))
|
|
1108
|
+
continue;
|
|
1109
|
+
const normalized = toForwardSlash(relToEntry);
|
|
1110
|
+
if (entry.ig.ignores(normalized)) {
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Invalidate and re-parse a specific `.gitignore` file.
|
|
1119
|
+
* Call when a `.gitignore` file is added, changed, or removed.
|
|
1120
|
+
*
|
|
1121
|
+
* @param gitignorePath - Absolute path to the `.gitignore` file that changed.
|
|
1122
|
+
*/
|
|
1123
|
+
invalidate(gitignorePath) {
|
|
1124
|
+
const absPath = node_path.resolve(gitignorePath);
|
|
1125
|
+
const gitignoreDir = node_path.dirname(absPath);
|
|
1126
|
+
for (const [, repo] of this.repos) {
|
|
1127
|
+
const relToRepo = node_path.relative(repo.root, gitignoreDir);
|
|
1128
|
+
if (relToRepo.startsWith('..'))
|
|
1129
|
+
continue;
|
|
1130
|
+
// Remove old entry for this directory
|
|
1131
|
+
repo.entries = repo.entries.filter((e) => e.dir !== gitignoreDir);
|
|
1132
|
+
// Re-parse if file still exists
|
|
1133
|
+
if (node_fs.existsSync(absPath)) {
|
|
1134
|
+
repo.entries.push({ dir: gitignoreDir, ig: parseGitignore(absPath) });
|
|
1135
|
+
// Re-sort deepest-first
|
|
1136
|
+
repo.entries.sort((a, b) => b.dir.length - a.dir.length);
|
|
1137
|
+
}
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
// If not in any known repo, check if it's in a repo we haven't scanned
|
|
1141
|
+
const repoRoot = findRepoRoot(gitignoreDir);
|
|
1142
|
+
if (repoRoot && node_fs.existsSync(absPath)) {
|
|
1143
|
+
const entries = [
|
|
1144
|
+
{ dir: gitignoreDir, ig: parseGitignore(absPath) },
|
|
1145
|
+
];
|
|
1146
|
+
if (this.repos.has(repoRoot)) {
|
|
1147
|
+
const repo = this.repos.get(repoRoot);
|
|
1148
|
+
repo.entries.push(entries[0]);
|
|
1149
|
+
repo.entries.sort((a, b) => b.dir.length - a.dir.length);
|
|
1150
|
+
}
|
|
1151
|
+
else {
|
|
1152
|
+
this.repos.set(repoRoot, { root: repoRoot, entries });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
936
1158
|
/**
|
|
937
1159
|
* @module logger
|
|
938
1160
|
* Creates pino logger instances. I/O: optionally writes logs to file via pino/file transport. Defaults to stdout at info level.
|
|
@@ -1049,11 +1271,11 @@
|
|
|
1049
1271
|
}
|
|
1050
1272
|
async function extractPlaintext(filePath) {
|
|
1051
1273
|
const raw = await promises.readFile(filePath, 'utf8');
|
|
1052
|
-
return { text: raw };
|
|
1274
|
+
return { text: raw.replace(/^\uFEFF/, '') };
|
|
1053
1275
|
}
|
|
1054
1276
|
async function extractJson(filePath) {
|
|
1055
1277
|
const raw = await promises.readFile(filePath, 'utf8');
|
|
1056
|
-
const parsed = JSON.parse(raw);
|
|
1278
|
+
const parsed = JSON.parse(raw.replace(/^\uFEFF/, ''));
|
|
1057
1279
|
const json = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
1058
1280
|
? parsed
|
|
1059
1281
|
: undefined;
|
|
@@ -1075,7 +1297,7 @@
|
|
|
1075
1297
|
}
|
|
1076
1298
|
async function extractHtml(filePath) {
|
|
1077
1299
|
const raw = await promises.readFile(filePath, 'utf8');
|
|
1078
|
-
const $ = cheerio__namespace.load(raw);
|
|
1300
|
+
const $ = cheerio__namespace.load(raw.replace(/^\uFEFF/, ''));
|
|
1079
1301
|
$('script, style').remove();
|
|
1080
1302
|
const text = $('body').text().trim() || $.text().trim();
|
|
1081
1303
|
return { text };
|
|
@@ -1905,6 +2127,112 @@
|
|
|
1905
2127
|
}
|
|
1906
2128
|
}
|
|
1907
2129
|
|
|
2130
|
+
/**
|
|
2131
|
+
* @module health
|
|
2132
|
+
* Tracks consecutive system-level failures and applies exponential backoff.
|
|
2133
|
+
* Triggers fatal error callback when maxRetries is exceeded.
|
|
2134
|
+
*/
|
|
2135
|
+
/**
|
|
2136
|
+
* Tracks system health via consecutive failure count and exponential backoff.
|
|
2137
|
+
*/
|
|
2138
|
+
class SystemHealth {
|
|
2139
|
+
consecutiveFailures = 0;
|
|
2140
|
+
maxRetries;
|
|
2141
|
+
maxBackoffMs;
|
|
2142
|
+
baseDelayMs;
|
|
2143
|
+
onFatalError;
|
|
2144
|
+
logger;
|
|
2145
|
+
constructor(options) {
|
|
2146
|
+
this.maxRetries = options.maxRetries ?? Number.POSITIVE_INFINITY;
|
|
2147
|
+
this.maxBackoffMs = options.maxBackoffMs ?? 60_000;
|
|
2148
|
+
this.baseDelayMs = options.baseDelayMs ?? 1000;
|
|
2149
|
+
this.onFatalError = options.onFatalError;
|
|
2150
|
+
this.logger = options.logger;
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Record a successful system operation. Resets the failure counter.
|
|
2154
|
+
*/
|
|
2155
|
+
recordSuccess() {
|
|
2156
|
+
if (this.consecutiveFailures > 0) {
|
|
2157
|
+
this.logger.info({ previousFailures: this.consecutiveFailures }, 'System health recovered');
|
|
2158
|
+
}
|
|
2159
|
+
this.consecutiveFailures = 0;
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Record a system-level failure. If maxRetries is exceeded, triggers fatal error.
|
|
2163
|
+
*
|
|
2164
|
+
* @param error - The error that occurred.
|
|
2165
|
+
* @returns Whether the watcher should continue (false = fatal).
|
|
2166
|
+
*/
|
|
2167
|
+
recordFailure(error) {
|
|
2168
|
+
this.consecutiveFailures += 1;
|
|
2169
|
+
this.logger.error({
|
|
2170
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
2171
|
+
maxRetries: this.maxRetries,
|
|
2172
|
+
err: normalizeError(error),
|
|
2173
|
+
}, 'System-level failure recorded');
|
|
2174
|
+
if (this.consecutiveFailures >= this.maxRetries) {
|
|
2175
|
+
this.logger.fatal({ consecutiveFailures: this.consecutiveFailures }, 'Maximum retries exceeded, triggering fatal error');
|
|
2176
|
+
if (this.onFatalError) {
|
|
2177
|
+
this.onFatalError(error);
|
|
2178
|
+
return false;
|
|
2179
|
+
}
|
|
2180
|
+
throw error instanceof Error
|
|
2181
|
+
? error
|
|
2182
|
+
: new Error(`Fatal system error: ${String(error)}`);
|
|
2183
|
+
}
|
|
2184
|
+
return true;
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Compute the current backoff delay based on consecutive failures.
|
|
2188
|
+
*
|
|
2189
|
+
* @returns Delay in milliseconds.
|
|
2190
|
+
*/
|
|
2191
|
+
get currentBackoffMs() {
|
|
2192
|
+
if (this.consecutiveFailures === 0)
|
|
2193
|
+
return 0;
|
|
2194
|
+
const exp = Math.max(0, this.consecutiveFailures - 1);
|
|
2195
|
+
return Math.min(this.maxBackoffMs, this.baseDelayMs * 2 ** exp);
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Sleep for the current backoff duration.
|
|
2199
|
+
*
|
|
2200
|
+
* @param signal - Optional abort signal.
|
|
2201
|
+
*/
|
|
2202
|
+
async backoff(signal) {
|
|
2203
|
+
const delay = this.currentBackoffMs;
|
|
2204
|
+
if (delay <= 0)
|
|
2205
|
+
return;
|
|
2206
|
+
this.logger.warn({ delayMs: delay, consecutiveFailures: this.consecutiveFailures }, 'Backing off before next attempt');
|
|
2207
|
+
await new Promise((resolve, reject) => {
|
|
2208
|
+
const timer = setTimeout(() => {
|
|
2209
|
+
cleanup();
|
|
2210
|
+
resolve();
|
|
2211
|
+
}, delay);
|
|
2212
|
+
const onAbort = () => {
|
|
2213
|
+
cleanup();
|
|
2214
|
+
reject(new Error('Backoff aborted'));
|
|
2215
|
+
};
|
|
2216
|
+
const cleanup = () => {
|
|
2217
|
+
clearTimeout(timer);
|
|
2218
|
+
if (signal)
|
|
2219
|
+
signal.removeEventListener('abort', onAbort);
|
|
2220
|
+
};
|
|
2221
|
+
if (signal) {
|
|
2222
|
+
if (signal.aborted) {
|
|
2223
|
+
onAbort();
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
/** Current consecutive failure count. */
|
|
2231
|
+
get failures() {
|
|
2232
|
+
return this.consecutiveFailures;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
1908
2236
|
/**
|
|
1909
2237
|
* @module watcher
|
|
1910
2238
|
* Filesystem watcher wrapping chokidar. I/O: watches files/directories for add/change/unlink events, enqueues to processing queue.
|
|
@@ -1917,6 +2245,8 @@
|
|
|
1917
2245
|
queue;
|
|
1918
2246
|
processor;
|
|
1919
2247
|
logger;
|
|
2248
|
+
health;
|
|
2249
|
+
gitignoreFilter;
|
|
1920
2250
|
watcher;
|
|
1921
2251
|
/**
|
|
1922
2252
|
* Create a new FileSystemWatcher.
|
|
@@ -1925,12 +2255,21 @@
|
|
|
1925
2255
|
* @param queue - The event queue.
|
|
1926
2256
|
* @param processor - The document processor.
|
|
1927
2257
|
* @param logger - The logger instance.
|
|
2258
|
+
* @param options - Optional health/fatal error options.
|
|
1928
2259
|
*/
|
|
1929
|
-
constructor(config, queue, processor, logger) {
|
|
2260
|
+
constructor(config, queue, processor, logger, options = {}) {
|
|
1930
2261
|
this.config = config;
|
|
1931
2262
|
this.queue = queue;
|
|
1932
2263
|
this.processor = processor;
|
|
1933
2264
|
this.logger = logger;
|
|
2265
|
+
this.gitignoreFilter = options.gitignoreFilter;
|
|
2266
|
+
const healthOptions = {
|
|
2267
|
+
maxRetries: options.maxRetries,
|
|
2268
|
+
maxBackoffMs: options.maxBackoffMs,
|
|
2269
|
+
onFatalError: options.onFatalError,
|
|
2270
|
+
logger,
|
|
2271
|
+
};
|
|
2272
|
+
this.health = new SystemHealth(healthOptions);
|
|
1934
2273
|
}
|
|
1935
2274
|
/**
|
|
1936
2275
|
* Start watching the filesystem and processing events.
|
|
@@ -1946,19 +2285,29 @@
|
|
|
1946
2285
|
ignoreInitial: false,
|
|
1947
2286
|
});
|
|
1948
2287
|
this.watcher.on('add', (path) => {
|
|
2288
|
+
this.handleGitignoreChange(path);
|
|
2289
|
+
if (this.isGitignored(path))
|
|
2290
|
+
return;
|
|
1949
2291
|
this.logger.debug({ path }, 'File added');
|
|
1950
|
-
this.queue.enqueue({ type: 'create', path, priority: 'normal' }, () => this.processor.processFile(path));
|
|
2292
|
+
this.queue.enqueue({ type: 'create', path, priority: 'normal' }, () => this.wrapProcessing(() => this.processor.processFile(path)));
|
|
1951
2293
|
});
|
|
1952
2294
|
this.watcher.on('change', (path) => {
|
|
2295
|
+
this.handleGitignoreChange(path);
|
|
2296
|
+
if (this.isGitignored(path))
|
|
2297
|
+
return;
|
|
1953
2298
|
this.logger.debug({ path }, 'File changed');
|
|
1954
|
-
this.queue.enqueue({ type: 'modify', path, priority: 'normal' }, () => this.processor.processFile(path));
|
|
2299
|
+
this.queue.enqueue({ type: 'modify', path, priority: 'normal' }, () => this.wrapProcessing(() => this.processor.processFile(path)));
|
|
1955
2300
|
});
|
|
1956
2301
|
this.watcher.on('unlink', (path) => {
|
|
2302
|
+
this.handleGitignoreChange(path);
|
|
2303
|
+
if (this.isGitignored(path))
|
|
2304
|
+
return;
|
|
1957
2305
|
this.logger.debug({ path }, 'File removed');
|
|
1958
|
-
this.queue.enqueue({ type: 'delete', path, priority: 'normal' }, () => this.processor.deleteFile(path));
|
|
2306
|
+
this.queue.enqueue({ type: 'delete', path, priority: 'normal' }, () => this.wrapProcessing(() => this.processor.deleteFile(path)));
|
|
1959
2307
|
});
|
|
1960
2308
|
this.watcher.on('error', (error) => {
|
|
1961
2309
|
this.logger.error({ err: normalizeError(error) }, 'Watcher error');
|
|
2310
|
+
this.health.recordFailure(error);
|
|
1962
2311
|
});
|
|
1963
2312
|
this.queue.process();
|
|
1964
2313
|
this.logger.info({ paths: this.config.paths }, 'Filesystem watcher started');
|
|
@@ -1973,6 +2322,53 @@
|
|
|
1973
2322
|
this.logger.info('Filesystem watcher stopped');
|
|
1974
2323
|
}
|
|
1975
2324
|
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Get the system health tracker.
|
|
2327
|
+
*/
|
|
2328
|
+
get systemHealth() {
|
|
2329
|
+
return this.health;
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Check if a path is gitignored and should be skipped.
|
|
2333
|
+
*/
|
|
2334
|
+
isGitignored(path) {
|
|
2335
|
+
if (!this.gitignoreFilter)
|
|
2336
|
+
return false;
|
|
2337
|
+
const ignored = this.gitignoreFilter.isIgnored(path);
|
|
2338
|
+
if (ignored) {
|
|
2339
|
+
this.logger.debug({ path }, 'Skipping gitignored file');
|
|
2340
|
+
}
|
|
2341
|
+
return ignored;
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* If the changed file is a `.gitignore`, invalidate the filter cache.
|
|
2345
|
+
*/
|
|
2346
|
+
handleGitignoreChange(path) {
|
|
2347
|
+
if (!this.gitignoreFilter)
|
|
2348
|
+
return;
|
|
2349
|
+
if (path.endsWith('.gitignore')) {
|
|
2350
|
+
this.logger.info({ path }, 'Gitignore file changed, refreshing filter');
|
|
2351
|
+
this.gitignoreFilter.invalidate(path);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Wrap a processing operation with health tracking.
|
|
2356
|
+
* On success, resets the failure counter.
|
|
2357
|
+
* On failure, records the failure and applies backoff.
|
|
2358
|
+
*/
|
|
2359
|
+
async wrapProcessing(fn) {
|
|
2360
|
+
try {
|
|
2361
|
+
await this.health.backoff();
|
|
2362
|
+
await fn();
|
|
2363
|
+
this.health.recordSuccess();
|
|
2364
|
+
}
|
|
2365
|
+
catch (error) {
|
|
2366
|
+
const shouldContinue = this.health.recordFailure(error);
|
|
2367
|
+
if (!shouldContinue) {
|
|
2368
|
+
await this.stop();
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
1976
2372
|
}
|
|
1977
2373
|
|
|
1978
2374
|
/**
|
|
@@ -2048,7 +2444,7 @@
|
|
|
2048
2444
|
compileRules,
|
|
2049
2445
|
createDocumentProcessor: (config, embeddingProvider, vectorStore, compiledRules, logger) => new DocumentProcessor(config, embeddingProvider, vectorStore, compiledRules, logger),
|
|
2050
2446
|
createEventQueue: (options) => new EventQueue(options),
|
|
2051
|
-
createFileSystemWatcher: (config, queue, processor, logger) => new FileSystemWatcher(config, queue, processor, logger),
|
|
2447
|
+
createFileSystemWatcher: (config, queue, processor, logger, options) => new FileSystemWatcher(config, queue, processor, logger, options),
|
|
2052
2448
|
createApiServer,
|
|
2053
2449
|
};
|
|
2054
2450
|
/**
|
|
@@ -2058,6 +2454,7 @@
|
|
|
2058
2454
|
config;
|
|
2059
2455
|
configPath;
|
|
2060
2456
|
factories;
|
|
2457
|
+
runtimeOptions;
|
|
2061
2458
|
logger;
|
|
2062
2459
|
watcher;
|
|
2063
2460
|
queue;
|
|
@@ -2070,11 +2467,13 @@
|
|
|
2070
2467
|
* @param config - The application configuration.
|
|
2071
2468
|
* @param configPath - Optional config file path to watch for changes.
|
|
2072
2469
|
* @param factories - Optional component factories (for dependency injection).
|
|
2470
|
+
* @param runtimeOptions - Optional runtime-only options (e.g., onFatalError).
|
|
2073
2471
|
*/
|
|
2074
|
-
constructor(config, configPath, factories = {}) {
|
|
2472
|
+
constructor(config, configPath, factories = {}, runtimeOptions = {}) {
|
|
2075
2473
|
this.config = config;
|
|
2076
2474
|
this.configPath = configPath;
|
|
2077
2475
|
this.factories = { ...defaultFactories, ...factories };
|
|
2476
|
+
this.runtimeOptions = runtimeOptions;
|
|
2078
2477
|
}
|
|
2079
2478
|
/**
|
|
2080
2479
|
* Start the watcher, API server, and all components.
|
|
@@ -2107,7 +2506,16 @@
|
|
|
2107
2506
|
rateLimitPerMinute: this.config.embedding.rateLimitPerMinute,
|
|
2108
2507
|
});
|
|
2109
2508
|
this.queue = queue;
|
|
2110
|
-
const
|
|
2509
|
+
const respectGitignore = this.config.watch.respectGitignore ?? true;
|
|
2510
|
+
const gitignoreFilter = respectGitignore
|
|
2511
|
+
? new GitignoreFilter(this.config.watch.paths)
|
|
2512
|
+
: undefined;
|
|
2513
|
+
const watcher = this.factories.createFileSystemWatcher(this.config.watch, queue, processor, logger, {
|
|
2514
|
+
maxRetries: this.config.maxRetries,
|
|
2515
|
+
maxBackoffMs: this.config.maxBackoffMs,
|
|
2516
|
+
onFatalError: this.runtimeOptions.onFatalError,
|
|
2517
|
+
gitignoreFilter,
|
|
2518
|
+
});
|
|
2111
2519
|
this.watcher = watcher;
|
|
2112
2520
|
const server = this.factories.createApiServer({
|
|
2113
2521
|
processor,
|
|
@@ -2215,7 +2623,9 @@
|
|
|
2215
2623
|
exports.DocumentProcessor = DocumentProcessor;
|
|
2216
2624
|
exports.EventQueue = EventQueue;
|
|
2217
2625
|
exports.FileSystemWatcher = FileSystemWatcher;
|
|
2626
|
+
exports.GitignoreFilter = GitignoreFilter;
|
|
2218
2627
|
exports.JeevesWatcher = JeevesWatcher;
|
|
2628
|
+
exports.SystemHealth = SystemHealth;
|
|
2219
2629
|
exports.VectorStoreClient = VectorStoreClient;
|
|
2220
2630
|
exports.apiConfigSchema = apiConfigSchema;
|
|
2221
2631
|
exports.applyRules = applyRules;
|
|
@@ -2241,4 +2651,4 @@
|
|
|
2241
2651
|
exports.watchConfigSchema = watchConfigSchema;
|
|
2242
2652
|
exports.writeMetadata = writeMetadata;
|
|
2243
2653
|
|
|
2244
|
-
})(this["jeeves-watcher"] = this["jeeves-watcher"] || {}, Fastify, promises, node_path, picomatch, radash, node_crypto, cosmiconfig, zod, jsonmap, googleGenai, pino, uuid, cheerio, yaml, mammoth, Ajv, addFormats, textsplitters, jsClientRest, chokidar);
|
|
2654
|
+
})(this["jeeves-watcher"] = this["jeeves-watcher"] || {}, Fastify, promises, node_path, picomatch, radash, node_crypto, cosmiconfig, zod, jsonmap, googleGenai, node_fs, ignore, pino, uuid, cheerio, yaml, mammoth, Ajv, addFormats, textsplitters, jsClientRest, chokidar);
|