@karmaniverous/jeeves-watcher 0.18.7 → 0.18.9
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/config.schema.json +228 -180
- package/dist/cli/jeeves-watcher/{index-CS1wrtes.js → index-xP2XrV4W.js} +347 -305
- package/dist/cli/jeeves-watcher/index.js +148 -23
- package/dist/index.d.ts +42 -27
- package/dist/index.js +487 -322
- package/package.json +2 -2
|
@@ -7,10 +7,12 @@ import { packageDirectorySync } from 'package-directory';
|
|
|
7
7
|
import { readFileSync, existsSync, mkdirSync, renameSync, statSync, readdirSync } from 'node:fs';
|
|
8
8
|
import { cosmiconfig } from 'cosmiconfig';
|
|
9
9
|
import { ZodError } from 'zod';
|
|
10
|
-
import { readdir, stat } from 'node:fs/promises';
|
|
10
|
+
import { readdir, stat, access } from 'node:fs/promises';
|
|
11
11
|
import { parallel } from 'radash';
|
|
12
12
|
import ignore from 'ignore';
|
|
13
13
|
import picomatch from 'picomatch';
|
|
14
|
+
import { execFile } from 'node:child_process';
|
|
15
|
+
import { promisify } from 'node:util';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* @module api/handlers/configMerge
|
|
@@ -224,7 +226,7 @@ async function startFromConfig(configPath, descriptor) {
|
|
|
224
226
|
const config = await loadConfig(resolvedPath);
|
|
225
227
|
// Dynamic import breaks the circular dependency between this module
|
|
226
228
|
// and app/index.ts (which defines JeevesWatcher and re-exports startFromConfig).
|
|
227
|
-
const { JeevesWatcher } = await import('./index-
|
|
229
|
+
const { JeevesWatcher } = await import('./index-xP2XrV4W.js');
|
|
228
230
|
const app = new JeevesWatcher(config, resolvedPath, descriptor);
|
|
229
231
|
installShutdownHandlers(() => app.stop());
|
|
230
232
|
await app.start();
|
|
@@ -613,6 +615,77 @@ async function retry(fn, options) {
|
|
|
613
615
|
throw lastError;
|
|
614
616
|
}
|
|
615
617
|
|
|
618
|
+
/**
|
|
619
|
+
* @module vcs/gitExec
|
|
620
|
+
* Shared git execution utilities.
|
|
621
|
+
*/
|
|
622
|
+
/** Promisified execFile for git commands. */
|
|
623
|
+
const execFileAsync = promisify(execFile);
|
|
624
|
+
/**
|
|
625
|
+
* Stage files via stdin to avoid ENAMETOOLONG on Windows.
|
|
626
|
+
*
|
|
627
|
+
* Uses `git add --pathspec-from-file=- --pathspec-file-nul` so the file list is piped
|
|
628
|
+
* through stdin (NUL-delimited) instead of passed as command-line
|
|
629
|
+
* arguments. This sidesteps the Windows CreateProcessW 32 767-char
|
|
630
|
+
* limit that triggers ENAMETOOLONG when batches contain many files
|
|
631
|
+
* with long absolute paths.
|
|
632
|
+
*
|
|
633
|
+
* @param files - Absolute paths of files to stage.
|
|
634
|
+
* @param cwd - Repository root (working directory for git).
|
|
635
|
+
*/
|
|
636
|
+
function gitAddViaStdin(files, cwd) {
|
|
637
|
+
return new Promise((resolve, reject) => {
|
|
638
|
+
const child = execFile('git', ['add', '--pathspec-from-file=-', '--pathspec-file-nul'], { cwd }, (error) => {
|
|
639
|
+
if (error)
|
|
640
|
+
reject(error);
|
|
641
|
+
else
|
|
642
|
+
resolve();
|
|
643
|
+
});
|
|
644
|
+
if (!child.stdin) {
|
|
645
|
+
reject(new Error('Failed to open stdin for git add'));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
// Suppress EPIPE — expected if git exits before we finish writing
|
|
649
|
+
child.stdin.on('error', (err) => {
|
|
650
|
+
if (err.code !== 'EPIPE') {
|
|
651
|
+
reject(err);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
child.stdin.end(files.join('\0'));
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Normalize a path for case-insensitive comparison on Windows.
|
|
659
|
+
* On Windows, lowercases the entire path; on other platforms, returns as-is.
|
|
660
|
+
*
|
|
661
|
+
* @param p - The path string to normalize.
|
|
662
|
+
* @param platform - The platform to check against (default: `process.platform`).
|
|
663
|
+
* @returns The normalized path.
|
|
664
|
+
*/
|
|
665
|
+
function normalizePathCase(p, platform = process.platform) {
|
|
666
|
+
return platform === 'win32' ? p.toLowerCase() : p;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Find the longest-prefix-match root for a normalized path.
|
|
670
|
+
* Roots must be sorted longest-first for correct nested matching.
|
|
671
|
+
*
|
|
672
|
+
* @param roots - Root paths sorted longest-first.
|
|
673
|
+
* @param normalizedPath - Normalized absolute path (forward slashes).
|
|
674
|
+
* @param platform - The platform to check against (default: `process.platform`).
|
|
675
|
+
* @returns The matching root, or undefined.
|
|
676
|
+
*/
|
|
677
|
+
function findRootForPath(roots, normalizedPath, platform = process.platform) {
|
|
678
|
+
const comparePath = normalizePathCase(normalizedPath, platform);
|
|
679
|
+
for (const root of roots) {
|
|
680
|
+
const compareRoot = normalizePathCase(root, platform);
|
|
681
|
+
if (comparePath === compareRoot ||
|
|
682
|
+
comparePath.startsWith(compareRoot + '/')) {
|
|
683
|
+
return root;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
688
|
+
|
|
616
689
|
/**
|
|
617
690
|
* Best-effort base directory inference for a glob pattern.
|
|
618
691
|
*
|
|
@@ -830,7 +903,7 @@ function groupByRoot(filePaths, watchPaths) {
|
|
|
830
903
|
/** Default page size for prune scroll (smaller than search to reduce connection strain). */
|
|
831
904
|
const PRUNE_SCROLL_PAGE_SIZE = 500;
|
|
832
905
|
/** Max retry attempts for scroll page failures. */
|
|
833
|
-
const SCROLL_RETRY_ATTEMPTS =
|
|
906
|
+
const SCROLL_RETRY_ATTEMPTS = 5;
|
|
834
907
|
/** Base delay for scroll retry backoff (ms). */
|
|
835
908
|
const SCROLL_RETRY_BASE_DELAY_MS = 1000;
|
|
836
909
|
/**
|
|
@@ -856,26 +929,42 @@ async function scrollPageWithRetry(vectorStore, cursor, logger) {
|
|
|
856
929
|
}
|
|
857
930
|
throw lastError;
|
|
858
931
|
}
|
|
932
|
+
/** Concurrency limit for fs.access() disk-existence checks. */
|
|
933
|
+
const FS_ACCESS_CONCURRENCY = 100;
|
|
859
934
|
/**
|
|
860
|
-
* Compute the plan for a prune operation.
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
935
|
+
* Compute the plan for a prune operation using a filter-first approach.
|
|
936
|
+
*
|
|
937
|
+
* 1. Enumerate all watched files from the filesystem → `watchedFiles` set.
|
|
938
|
+
* 2. Scroll Qdrant points. For each point:
|
|
939
|
+
* - No `file_path` → orphan.
|
|
940
|
+
* - `file_path` not in `watchedFiles` → orphan (out of watch scope).
|
|
941
|
+
* - `file_path` in `watchedFiles` → check disk existence via `fs.access()`.
|
|
942
|
+
* If missing from disk → orphan (deleted file still in index).
|
|
943
|
+
* 3. Batch `fs.access()` checks with bounded concurrency.
|
|
864
944
|
*/
|
|
865
945
|
async function computePrunePlan(deps) {
|
|
866
946
|
const { config, vectorStore, gitignoreFilter, logger } = deps;
|
|
867
947
|
if (!vectorStore) {
|
|
868
948
|
throw new Error('vectorStore is required for prune scope');
|
|
869
949
|
}
|
|
950
|
+
// Step 1: Build set of watched files from filesystem
|
|
951
|
+
const isGitignored = createIsGitignored(gitignoreFilter);
|
|
952
|
+
const watchedFileList = await listFilesFromGlobs(extractWatchPathStrings(config.watch.paths), config.watch.ignored, isGitignored);
|
|
953
|
+
const watchedFiles = new Set(watchedFileList.map((f) => normalizePathCase(normalizeSlashes(f))));
|
|
954
|
+
logger.info({ watchedFileCount: watchedFiles.size }, 'Prune: enumerated watched files from filesystem');
|
|
955
|
+
// Step 2: Scroll Qdrant and classify points
|
|
870
956
|
const seenFiles = new Set();
|
|
871
957
|
const orphanedFiles = new Set();
|
|
958
|
+
const diskCheckFiles = new Set();
|
|
872
959
|
const orphanedIds = [];
|
|
960
|
+
// Map from file_path → point IDs, for points needing disk-existence check
|
|
961
|
+
const diskCheckPointIds = new Map();
|
|
873
962
|
let totalPoints = 0;
|
|
874
963
|
let incomplete = false;
|
|
875
964
|
let cursor;
|
|
876
965
|
try {
|
|
877
|
-
|
|
878
|
-
while (
|
|
966
|
+
let hasMore = true;
|
|
967
|
+
while (hasMore) {
|
|
879
968
|
const page = await scrollPageWithRetry(vectorStore, cursor, logger);
|
|
880
969
|
for (const point of page.points) {
|
|
881
970
|
totalPoints++;
|
|
@@ -884,32 +973,62 @@ async function computePrunePlan(deps) {
|
|
|
884
973
|
orphanedIds.push(point.id);
|
|
885
974
|
continue;
|
|
886
975
|
}
|
|
887
|
-
|
|
888
|
-
|
|
976
|
+
const normalized = normalizePathCase(normalizeSlashes(filePath));
|
|
977
|
+
if (seenFiles.has(normalized)) {
|
|
978
|
+
if (orphanedFiles.has(normalized)) {
|
|
889
979
|
orphanedIds.push(point.id);
|
|
890
980
|
}
|
|
981
|
+
else if (diskCheckFiles.has(normalized)) {
|
|
982
|
+
// Already queued for disk check — track its ID
|
|
983
|
+
const ids = diskCheckPointIds.get(normalized);
|
|
984
|
+
if (ids)
|
|
985
|
+
ids.push(point.id);
|
|
986
|
+
}
|
|
891
987
|
continue;
|
|
892
988
|
}
|
|
893
|
-
seenFiles.add(
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
: false;
|
|
898
|
-
if (!watched || gitignored) {
|
|
899
|
-
orphanedFiles.add(filePath);
|
|
989
|
+
seenFiles.add(normalized);
|
|
990
|
+
if (!watchedFiles.has(normalized)) {
|
|
991
|
+
// Not in watch scope → definitely orphaned
|
|
992
|
+
orphanedFiles.add(normalized);
|
|
900
993
|
orphanedIds.push(point.id);
|
|
901
994
|
}
|
|
995
|
+
else {
|
|
996
|
+
// In watch scope → needs disk-existence check
|
|
997
|
+
diskCheckFiles.add(normalized);
|
|
998
|
+
diskCheckPointIds.set(normalized, [point.id]);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// Progress logging every 100K points
|
|
1002
|
+
if (totalPoints > 0 && totalPoints % 100000 === 0) {
|
|
1003
|
+
logger.info({ totalPoints, orphanedCount: orphanedIds.length }, 'Prune scroll progress');
|
|
1004
|
+
}
|
|
1005
|
+
if (!page.nextCursor) {
|
|
1006
|
+
hasMore = false;
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
cursor = page.nextCursor;
|
|
902
1010
|
}
|
|
903
|
-
if (!page.nextCursor)
|
|
904
|
-
break;
|
|
905
|
-
cursor = page.nextCursor;
|
|
906
1011
|
}
|
|
907
1012
|
}
|
|
908
1013
|
catch (error) {
|
|
909
|
-
// All retries exhausted - return partial results
|
|
910
1014
|
logger.error({ err: normalizeError(error), totalPoints, cursor }, 'Prune scroll failed after retries; returning partial plan');
|
|
911
1015
|
incomplete = true;
|
|
912
1016
|
}
|
|
1017
|
+
// Step 3: Batch fs.access() checks for files in watch scope
|
|
1018
|
+
const filesToCheck = [...diskCheckFiles];
|
|
1019
|
+
await parallel(FS_ACCESS_CONCURRENCY, filesToCheck, async (filePath) => {
|
|
1020
|
+
try {
|
|
1021
|
+
await access(filePath);
|
|
1022
|
+
}
|
|
1023
|
+
catch {
|
|
1024
|
+
// File doesn't exist on disk → orphaned
|
|
1025
|
+
orphanedFiles.add(filePath);
|
|
1026
|
+
const pointIds = diskCheckPointIds.get(filePath);
|
|
1027
|
+
if (pointIds) {
|
|
1028
|
+
orphanedIds.push(...pointIds);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
913
1032
|
const orphanedFileList = [...orphanedFiles];
|
|
914
1033
|
const byRoot = groupByRoot(orphanedFileList, extractWatchPathStrings(config.watch.paths));
|
|
915
1034
|
return {
|
|
@@ -967,6 +1086,12 @@ async function executeReindex(deps, scope, path, dryRun = false) {
|
|
|
967
1086
|
deps.queue?.resume();
|
|
968
1087
|
return { filesProcessed: 0, durationMs: 0, errors: 0, plan };
|
|
969
1088
|
}
|
|
1089
|
+
// Abort live prune when plan is incomplete — never silently delete partial data
|
|
1090
|
+
if (plan.incomplete) {
|
|
1091
|
+
deps.queue?.resume();
|
|
1092
|
+
logger.error({ totalPoints: plan.total, pointsToDelete: plan.toDelete }, 'Aborting live prune — scroll was incomplete; partial deletes are unsafe');
|
|
1093
|
+
return { filesProcessed: 0, durationMs: 0, errors: 1, plan };
|
|
1094
|
+
}
|
|
970
1095
|
// Execute prune
|
|
971
1096
|
reindexTracker?.start(scope);
|
|
972
1097
|
const startTime = Date.now();
|
|
@@ -1456,4 +1581,4 @@ const watcherDescriptor = {
|
|
|
1456
1581
|
const program = createServiceCli(watcherDescriptor);
|
|
1457
1582
|
program.parse();
|
|
1458
1583
|
|
|
1459
|
-
export { CONFIG_WATCH_VALID_SCOPES as C, GitignoreFilter as G, VALID_REINDEX_SCOPES as V,
|
|
1584
|
+
export { CONFIG_WATCH_VALID_SCOPES as C, GitignoreFilter as G, VALID_REINDEX_SCOPES as V, executeReindex as a, gitAddViaStdin as b, normalizePathCase as c, normalizeSlashes as d, execFileAsync as e, findRootForPath as f, getWatchRootBases as g, startFromConfig as h, isPathWatched as i, loadConfig as l, mergeInferenceRules as m, normalizeError as n, retry as r, sleep as s };
|
package/dist/index.d.ts
CHANGED
|
@@ -1252,10 +1252,9 @@ declare class CommitMessageGenerator {
|
|
|
1252
1252
|
}
|
|
1253
1253
|
|
|
1254
1254
|
/**
|
|
1255
|
-
* @module vcs/
|
|
1256
|
-
*
|
|
1255
|
+
* @module vcs/types
|
|
1256
|
+
* Shared types for the VCS subsystem.
|
|
1257
1257
|
*/
|
|
1258
|
-
|
|
1259
1258
|
/** Metadata for a pending reversion to include in the commit message. */
|
|
1260
1259
|
interface PendingReversion {
|
|
1261
1260
|
glob: string;
|
|
@@ -1267,6 +1266,12 @@ interface PushError {
|
|
|
1267
1266
|
timestamp: string;
|
|
1268
1267
|
message: string;
|
|
1269
1268
|
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* @module vcs/VcsManager
|
|
1272
|
+
* Per-root VCS manager for git-backed content versioning.
|
|
1273
|
+
*/
|
|
1274
|
+
|
|
1270
1275
|
/**
|
|
1271
1276
|
* Per-root VCS manager for git-backed content versioning.
|
|
1272
1277
|
*
|
|
@@ -1283,13 +1288,14 @@ declare class VcsManager {
|
|
|
1283
1288
|
readonly remoteUrl: string | undefined;
|
|
1284
1289
|
private readonly accessToken;
|
|
1285
1290
|
private readonly logger;
|
|
1286
|
-
private readonly
|
|
1291
|
+
private readonly commitMessageBuilder;
|
|
1287
1292
|
private readonly pending;
|
|
1288
1293
|
private readonly pendingReversions;
|
|
1289
1294
|
private readonly _pushErrors;
|
|
1290
1295
|
private readonly squashManager;
|
|
1291
1296
|
private debounceTimer;
|
|
1292
1297
|
private commitInFlight;
|
|
1298
|
+
private consecutiveCommitFailures;
|
|
1293
1299
|
private started;
|
|
1294
1300
|
private isBaseline;
|
|
1295
1301
|
private _lastPushTime;
|
|
@@ -1297,15 +1303,18 @@ declare class VcsManager {
|
|
|
1297
1303
|
get lastPushTime(): string | null;
|
|
1298
1304
|
get pushErrors(): readonly PushError[];
|
|
1299
1305
|
/**
|
|
1300
|
-
* Begin accepting file changes. Sets
|
|
1301
|
-
*
|
|
1306
|
+
* Begin accepting file changes. Sets the manager to started state and
|
|
1307
|
+
* starts the squash manager if configured.
|
|
1308
|
+
*
|
|
1309
|
+
* Baseline mode (commit prefix `"baseline:"`) remains active until
|
|
1310
|
+
* {@link endBaseline} is called by the coordinator after the initial scan.
|
|
1302
1311
|
*/
|
|
1303
|
-
start():
|
|
1312
|
+
start(): void;
|
|
1304
1313
|
/**
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
1314
|
+
* Signal that the initial filesystem scan is complete.
|
|
1315
|
+
* Clears the baseline flag so subsequent commits use normal "watcher: batch" messages.
|
|
1307
1316
|
*/
|
|
1308
|
-
|
|
1317
|
+
endBaseline(): void;
|
|
1309
1318
|
/**
|
|
1310
1319
|
* Add a file to the pending set and reset the debounce timer.
|
|
1311
1320
|
* If the pending set exceeds maxBatchSize, flush immediately.
|
|
@@ -1347,26 +1356,14 @@ declare class VcsManager {
|
|
|
1347
1356
|
*/
|
|
1348
1357
|
private resetDebounce;
|
|
1349
1358
|
/**
|
|
1350
|
-
*
|
|
1359
|
+
* Check for and remove stale index.lock files before retrying.
|
|
1351
1360
|
*/
|
|
1352
|
-
private
|
|
1361
|
+
private removeStaleLock;
|
|
1353
1362
|
/**
|
|
1354
|
-
*
|
|
1355
|
-
|
|
1356
|
-
private buildTemplateMessage;
|
|
1357
|
-
/**
|
|
1358
|
-
* Build the commit message, using AI when available with template fallback.
|
|
1359
|
-
*/
|
|
1360
|
-
private buildCommitMessage;
|
|
1361
|
-
/**
|
|
1362
|
-
* Commit a batch of files to the git repo with index.lock retry.
|
|
1363
|
+
* Commit a batch of files to the git repo with index.lock retry,
|
|
1364
|
+
* stale lock detection, circuit breaker, and re-queue cap.
|
|
1363
1365
|
*/
|
|
1364
1366
|
private commitBatch;
|
|
1365
|
-
/**
|
|
1366
|
-
* Push to the configured remote if remoteUrl is set.
|
|
1367
|
-
* On failure: logs the error and records it in pushErrors; never throws.
|
|
1368
|
-
*/
|
|
1369
|
-
private pushIfConfigured;
|
|
1370
1367
|
}
|
|
1371
1368
|
|
|
1372
1369
|
/**
|
|
@@ -1386,7 +1383,7 @@ declare class VcsCoordinator {
|
|
|
1386
1383
|
/**
|
|
1387
1384
|
* Start all VcsManager instances.
|
|
1388
1385
|
*/
|
|
1389
|
-
start():
|
|
1386
|
+
start(): void;
|
|
1390
1387
|
/**
|
|
1391
1388
|
* Route a file event to the correct root's VcsManager.
|
|
1392
1389
|
*
|
|
@@ -1394,6 +1391,15 @@ declare class VcsCoordinator {
|
|
|
1394
1391
|
* @param event - The event type: add, change, or unlink.
|
|
1395
1392
|
*/
|
|
1396
1393
|
onFileChange(filePath: string, event: 'add' | 'change' | 'unlink'): void;
|
|
1394
|
+
/**
|
|
1395
|
+
* Signal that the initial filesystem scan is complete.
|
|
1396
|
+
*
|
|
1397
|
+
* Flushes each manager's debounce buffer before ending baseline mode, ensuring
|
|
1398
|
+
* any files still pending in the VCS debounce are committed with a
|
|
1399
|
+
* `"baseline:"` prefix rather than `"watcher:"`. AI generation is not enabled
|
|
1400
|
+
* until all managers have completed their flush.
|
|
1401
|
+
*/
|
|
1402
|
+
onInitialScanComplete(): Promise<void>;
|
|
1397
1403
|
/**
|
|
1398
1404
|
* Flush all managers and clean up.
|
|
1399
1405
|
*/
|
|
@@ -1539,6 +1545,8 @@ interface FileSystemWatcherOptions {
|
|
|
1539
1545
|
contentHashCache?: ContentHashCache;
|
|
1540
1546
|
/** Optional callback for VCS file change notifications. */
|
|
1541
1547
|
onVcsFileChange?: (filePath: string, event: 'add' | 'change' | 'unlink') => void;
|
|
1548
|
+
/** Optional callback invoked when the initial filesystem scan completes. */
|
|
1549
|
+
onInitialScanComplete?: () => void | Promise<void>;
|
|
1542
1550
|
}
|
|
1543
1551
|
/**
|
|
1544
1552
|
* Filesystem watcher that maps chokidar events to the processing queue.
|
|
@@ -1553,6 +1561,7 @@ declare class FileSystemWatcher {
|
|
|
1553
1561
|
private readonly initialScanTracker?;
|
|
1554
1562
|
private readonly contentHashCache?;
|
|
1555
1563
|
private readonly onVcsFileChange?;
|
|
1564
|
+
private readonly onInitialScanComplete?;
|
|
1556
1565
|
private moveCorrelator?;
|
|
1557
1566
|
private globMatches;
|
|
1558
1567
|
private watcher;
|
|
@@ -1808,6 +1817,12 @@ declare class JeevesWatcher {
|
|
|
1808
1817
|
* Start the watcher, API server, and all components.
|
|
1809
1818
|
*/
|
|
1810
1819
|
start(): Promise<void>;
|
|
1820
|
+
/** Set up logger, VCS, embedding provider, and vector store. */
|
|
1821
|
+
private initInfrastructure;
|
|
1822
|
+
/** Build the processing pipeline: rules, templates, processor, queue, VCS. */
|
|
1823
|
+
private initPipeline;
|
|
1824
|
+
/** Wire up watcher and VCS coordinator, then start all services. */
|
|
1825
|
+
private startServicesAndWatcher;
|
|
1811
1826
|
/**
|
|
1812
1827
|
* Gracefully stop all components.
|
|
1813
1828
|
*/
|