conductor-bridge 1.0.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 +69 -0
- package/dist/bridge/index.d.ts +8 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +8 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/bridge/json-interchange.d.ts +94 -0
- package/dist/bridge/json-interchange.d.ts.map +1 -0
- package/dist/bridge/json-interchange.js +301 -0
- package/dist/bridge/json-interchange.js.map +1 -0
- package/dist/cli/commands/amend.d.ts +12 -0
- package/dist/cli/commands/amend.d.ts.map +1 -0
- package/dist/cli/commands/amend.js +205 -0
- package/dist/cli/commands/amend.js.map +1 -0
- package/dist/cli/commands/daemon.d.ts +12 -0
- package/dist/cli/commands/daemon.d.ts.map +1 -0
- package/dist/cli/commands/daemon.js +60 -0
- package/dist/cli/commands/daemon.js.map +1 -0
- package/dist/cli/commands/dispatch.d.ts +12 -0
- package/dist/cli/commands/dispatch.d.ts.map +1 -0
- package/dist/cli/commands/dispatch.js +207 -0
- package/dist/cli/commands/dispatch.js.map +1 -0
- package/dist/cli/commands/handoff.d.ts +31 -0
- package/dist/cli/commands/handoff.d.ts.map +1 -0
- package/dist/cli/commands/handoff.js +273 -0
- package/dist/cli/commands/handoff.js.map +1 -0
- package/dist/cli/commands/init.d.ts +12 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +301 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/status.d.ts +12 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +206 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +148 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/handoff/encryption.d.ts +85 -0
- package/dist/handoff/encryption.d.ts.map +1 -0
- package/dist/handoff/encryption.js +308 -0
- package/dist/handoff/encryption.js.map +1 -0
- package/dist/handoff/index.d.ts +8 -0
- package/dist/handoff/index.d.ts.map +1 -0
- package/dist/handoff/index.js +10 -0
- package/dist/handoff/index.js.map +1 -0
- package/dist/handoff/mycelium-arc.d.ts +116 -0
- package/dist/handoff/mycelium-arc.d.ts.map +1 -0
- package/dist/handoff/mycelium-arc.js +410 -0
- package/dist/handoff/mycelium-arc.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/index.d.ts +10 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +12 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/plan-parser.d.ts +29 -0
- package/dist/parsers/plan-parser.d.ts.map +1 -0
- package/dist/parsers/plan-parser.js +503 -0
- package/dist/parsers/plan-parser.js.map +1 -0
- package/dist/parsers/spec-parser.d.ts +10 -0
- package/dist/parsers/spec-parser.d.ts.map +1 -0
- package/dist/parsers/spec-parser.js +382 -0
- package/dist/parsers/spec-parser.js.map +1 -0
- package/dist/parsers/state-parser.d.ts +21 -0
- package/dist/parsers/state-parser.d.ts.map +1 -0
- package/dist/parsers/state-parser.js +378 -0
- package/dist/parsers/state-parser.js.map +1 -0
- package/dist/parsers/types.d.ts +190 -0
- package/dist/parsers/types.d.ts.map +1 -0
- package/dist/parsers/types.js +7 -0
- package/dist/parsers/types.js.map +1 -0
- package/dist/schemas/cognitive-state.d.ts +1523 -0
- package/dist/schemas/cognitive-state.d.ts.map +1 -0
- package/dist/schemas/cognitive-state.js +328 -0
- package/dist/schemas/cognitive-state.js.map +1 -0
- package/dist/schemas/enums.d.ts +124 -0
- package/dist/schemas/enums.d.ts.map +1 -0
- package/dist/schemas/enums.js +108 -0
- package/dist/schemas/enums.js.map +1 -0
- package/dist/schemas/index.d.ts +9 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +9 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/sync/adhd-continuity.d.ts +91 -0
- package/dist/sync/adhd-continuity.d.ts.map +1 -0
- package/dist/sync/adhd-continuity.js +302 -0
- package/dist/sync/adhd-continuity.js.map +1 -0
- package/dist/sync/convergence-tracker.d.ts +111 -0
- package/dist/sync/convergence-tracker.d.ts.map +1 -0
- package/dist/sync/convergence-tracker.js +299 -0
- package/dist/sync/convergence-tracker.js.map +1 -0
- package/dist/sync/index.d.ts +11 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +15 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/state-sync.d.ts +105 -0
- package/dist/sync/state-sync.d.ts.map +1 -0
- package/dist/sync/state-sync.js +403 -0
- package/dist/sync/state-sync.js.map +1 -0
- package/dist/sync/watcher.d.ts +90 -0
- package/dist/sync/watcher.d.ts.map +1 -0
- package/dist/sync/watcher.js +281 -0
- package/dist/sync/watcher.js.map +1 -0
- package/dist/utils/atomic-write.d.ts +31 -0
- package/dist/utils/atomic-write.d.ts.map +1 -0
- package/dist/utils/atomic-write.js +69 -0
- package/dist/utils/atomic-write.js.map +1 -0
- package/dist/utils/error-handling.d.ts +70 -0
- package/dist/utils/error-handling.d.ts.map +1 -0
- package/dist/utils/error-handling.js +109 -0
- package/dist/utils/error-handling.js.map +1 -0
- package/dist/utils/file-lock.d.ts +46 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +117 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/rlm-navigator.d.ts +160 -0
- package/dist/utils/rlm-navigator.d.ts.map +1 -0
- package/dist/utils/rlm-navigator.js +368 -0
- package/dist/utils/rlm-navigator.js.map +1 -0
- package/dist/utils/safe-path.d.ts +44 -0
- package/dist/utils/safe-path.d.ts.map +1 -0
- package/dist/utils/safe-path.js +96 -0
- package/dist/utils/safe-path.js.map +1 -0
- package/dist/utils/timed-io.d.ts +51 -0
- package/dist/utils/timed-io.d.ts.map +1 -0
- package/dist/utils/timed-io.js +128 -0
- package/dist/utils/timed-io.js.map +1 -0
- package/package.json +88 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Lock Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides cross-platform file locking with retry logic.
|
|
5
|
+
* Uses proper-lockfile for reliable locking behavior.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdir } from 'fs/promises';
|
|
8
|
+
import { dirname } from 'path';
|
|
9
|
+
const DEFAULT_OPTIONS = {
|
|
10
|
+
timeout: 10000,
|
|
11
|
+
retryInterval: 100,
|
|
12
|
+
maxRetries: 50,
|
|
13
|
+
stale: 60000,
|
|
14
|
+
};
|
|
15
|
+
// Simple in-memory lock registry for same-process coordination
|
|
16
|
+
const activeLocks = new Map();
|
|
17
|
+
export class FileLockError extends Error {
|
|
18
|
+
filePath;
|
|
19
|
+
code;
|
|
20
|
+
constructor(message, filePath, code) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.filePath = filePath;
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.name = 'FileLockError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Acquire a lock on a file, execute an operation, then release the lock.
|
|
29
|
+
*
|
|
30
|
+
* @param filePath - Path to the file to lock
|
|
31
|
+
* @param operation - Async operation to perform while holding the lock
|
|
32
|
+
* @param options - Lock options
|
|
33
|
+
* @returns Result of the operation
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const result = await withFileLock('/path/to/file.json', async () => {
|
|
38
|
+
* const data = await readFile('/path/to/file.json', 'utf-8');
|
|
39
|
+
* const parsed = JSON.parse(data);
|
|
40
|
+
* parsed.count++;
|
|
41
|
+
* await writeFile('/path/to/file.json', JSON.stringify(parsed));
|
|
42
|
+
* return parsed;
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export async function withFileLock(filePath, operation, options = {}) {
|
|
47
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
48
|
+
// Ensure directory exists for lock file
|
|
49
|
+
try {
|
|
50
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Directory might already exist
|
|
54
|
+
}
|
|
55
|
+
// Try to acquire lock with retries
|
|
56
|
+
const lockPath = `${filePath}.lock`;
|
|
57
|
+
let acquired = false;
|
|
58
|
+
let attempts = 0;
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
while (!acquired) {
|
|
61
|
+
// Check timeout
|
|
62
|
+
if (Date.now() - startTime > opts.timeout) {
|
|
63
|
+
throw new FileLockError(`Timeout waiting for lock on ${filePath}`, filePath, 'ETIMEOUT');
|
|
64
|
+
}
|
|
65
|
+
// Check max retries
|
|
66
|
+
if (attempts >= opts.maxRetries) {
|
|
67
|
+
throw new FileLockError(`Max retries exceeded waiting for lock on ${filePath}`, filePath, 'ETIMEOUT');
|
|
68
|
+
}
|
|
69
|
+
// Try to acquire
|
|
70
|
+
const existingLock = activeLocks.get(lockPath);
|
|
71
|
+
if (existingLock) {
|
|
72
|
+
// Check if stale
|
|
73
|
+
if (Date.now() - existingLock.timestamp > opts.stale) {
|
|
74
|
+
// Force release stale lock
|
|
75
|
+
existingLock.release();
|
|
76
|
+
activeLocks.delete(lockPath);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Wait and retry
|
|
80
|
+
await sleep(opts.retryInterval);
|
|
81
|
+
attempts++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Acquire lock
|
|
86
|
+
const release = () => {
|
|
87
|
+
activeLocks.delete(lockPath);
|
|
88
|
+
};
|
|
89
|
+
activeLocks.set(lockPath, {
|
|
90
|
+
timestamp: Date.now(),
|
|
91
|
+
release,
|
|
92
|
+
});
|
|
93
|
+
acquired = true;
|
|
94
|
+
}
|
|
95
|
+
// Execute operation with lock held
|
|
96
|
+
try {
|
|
97
|
+
return await operation();
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
// Release lock
|
|
101
|
+
const lock = activeLocks.get(lockPath);
|
|
102
|
+
if (lock) {
|
|
103
|
+
lock.release();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check if a file is currently locked.
|
|
109
|
+
*/
|
|
110
|
+
export function isFileLocked(filePath) {
|
|
111
|
+
const lockPath = `${filePath}.lock`;
|
|
112
|
+
return activeLocks.has(lockPath);
|
|
113
|
+
}
|
|
114
|
+
function sleep(ms) {
|
|
115
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=file-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock.js","sourceRoot":"","sources":["../../src/utils/file-lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security and Navigation utilities for Conductor Bridge
|
|
3
|
+
*/
|
|
4
|
+
export { atomicWriteFile, fileExists, type AtomicWriteOptions, } from './atomic-write.js';
|
|
5
|
+
export { validatePath, sanitizeFilename, type SafePathResult, } from './safe-path.js';
|
|
6
|
+
export { withFileLock, isFileLocked, FileLockError, type LockOptions, } from './file-lock.js';
|
|
7
|
+
export { readFileWithTimeout, readFileBufferWithTimeout, writeFileWithTimeout, withIOTimeout, FileIOTimeoutError, } from './timed-io.js';
|
|
8
|
+
export { withFallback, withFallbackSync, withRetry, createFallbackWrapper, type FallbackContext, type FallbackOptions, } from './error-handling.js';
|
|
9
|
+
export { RLMNavigator, FileNavigator, MultiFileSearch, PriorBasedFilter, rlmNavigator, SIZE_THRESHOLD_BYTES, DEFAULT_EXTENSIONS, SKIP_DIRS, type NavigationResult, type FileNavigatorOptions, type MultiFileSearchOptions, } from './rlm-navigator.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EACL,eAAe,EACf,UAAU,EACV,KAAK,kBAAkB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,KAAK,WAAW,GACjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,mBAAmB,EACnB,yBAAyB,EACzB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,GACnB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,SAAS,EACT,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,YAAY,EACZ,aAAa,EACb,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,EAClB,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,GAC5B,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security and Navigation utilities for Conductor Bridge
|
|
3
|
+
*/
|
|
4
|
+
// Security utilities
|
|
5
|
+
export { atomicWriteFile, fileExists, } from './atomic-write.js';
|
|
6
|
+
export { validatePath, sanitizeFilename, } from './safe-path.js';
|
|
7
|
+
export { withFileLock, isFileLocked, FileLockError, } from './file-lock.js';
|
|
8
|
+
export { readFileWithTimeout, readFileBufferWithTimeout, writeFileWithTimeout, withIOTimeout, FileIOTimeoutError, } from './timed-io.js';
|
|
9
|
+
export { withFallback, withFallbackSync, withRetry, createFallbackWrapper, } from './error-handling.js';
|
|
10
|
+
// RLM Navigator (Zhang, Kraska, Khattab 2025)
|
|
11
|
+
export { RLMNavigator, FileNavigator, MultiFileSearch, PriorBasedFilter, rlmNavigator, SIZE_THRESHOLD_BYTES, DEFAULT_EXTENSIONS, SKIP_DIRS, } from './rlm-navigator.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,qBAAqB;AACrB,OAAO,EACL,eAAe,EACf,UAAU,GAEX,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,YAAY,EACZ,gBAAgB,GAEjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,aAAa,GAEd,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,mBAAmB,EACnB,yBAAyB,EACzB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,GACnB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,SAAS,EACT,qBAAqB,GAGtB,MAAM,qBAAqB,CAAC;AAE7B,8CAA8C;AAC9C,OAAO,EACL,YAAY,EACZ,aAAa,EACb,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,EAClB,SAAS,GAIV,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RLM Navigator: Program-Environment Paradigm for Large File Navigation
|
|
3
|
+
*
|
|
4
|
+
* Implements the Recursive Language Model paradigm from Zhang, Kraska, Khattab (2025):
|
|
5
|
+
* "Treat long prompts as part of an external environment and allow the LLM
|
|
6
|
+
* to programmatically examine, decompose, and recursively call itself over snippets"
|
|
7
|
+
*
|
|
8
|
+
* Key Principle: Never load large data INTO context. Navigate it FROM context.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* Traditional: [Entire file in context] → "Find X"
|
|
12
|
+
* RLM: [File as external variable] → Write code to find X → Execute → Get result
|
|
13
|
+
*
|
|
14
|
+
* Integration with Conductor Bridge:
|
|
15
|
+
* - Navigates large spec.md/plan.md files
|
|
16
|
+
* - Navigates cognitive state documents
|
|
17
|
+
* - Navigates amendment files
|
|
18
|
+
* - Supports watcher for large file changes
|
|
19
|
+
*
|
|
20
|
+
* Reference: https://arxiv.org/abs/2512.24601
|
|
21
|
+
*/
|
|
22
|
+
export interface NavigationResult {
|
|
23
|
+
/** Original query/pattern */
|
|
24
|
+
query: string;
|
|
25
|
+
/** Matches: [lineNumber, content] */
|
|
26
|
+
matches: Array<[number, string]>;
|
|
27
|
+
/** Context snippets around matches */
|
|
28
|
+
contextSnippets: string[];
|
|
29
|
+
/** Source file path */
|
|
30
|
+
filePath?: string;
|
|
31
|
+
/** Total lines scanned */
|
|
32
|
+
totalLinesScanned: number;
|
|
33
|
+
/** Pattern used for search */
|
|
34
|
+
patternUsed?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface FileNavigatorOptions {
|
|
37
|
+
/** Maximum matches to return */
|
|
38
|
+
maxMatches?: number;
|
|
39
|
+
/** Lines of context around matches */
|
|
40
|
+
contextLines?: number;
|
|
41
|
+
/** Case-sensitive search */
|
|
42
|
+
caseSensitive?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface MultiFileSearchOptions extends FileNavigatorOptions {
|
|
45
|
+
/** File extensions to include */
|
|
46
|
+
extensions?: string[];
|
|
47
|
+
/** Maximum total results across all files */
|
|
48
|
+
maxResults?: number;
|
|
49
|
+
}
|
|
50
|
+
/** Size threshold above which RLM approach should be used (~50K tokens) */
|
|
51
|
+
export declare const SIZE_THRESHOLD_BYTES = 200000;
|
|
52
|
+
/** Default file extensions to search */
|
|
53
|
+
export declare const DEFAULT_EXTENSIONS: string[];
|
|
54
|
+
/** Directories to skip when searching */
|
|
55
|
+
export declare const SKIP_DIRS: Set<string>;
|
|
56
|
+
export declare class FileNavigator {
|
|
57
|
+
private filePath;
|
|
58
|
+
private lineCountCache?;
|
|
59
|
+
constructor(filePath: string);
|
|
60
|
+
/**
|
|
61
|
+
* Get total line count (cached).
|
|
62
|
+
*/
|
|
63
|
+
getLineCount(): Promise<number>;
|
|
64
|
+
/**
|
|
65
|
+
* Find pattern in file with surrounding context.
|
|
66
|
+
*/
|
|
67
|
+
findPattern(pattern: string | RegExp, options?: FileNavigatorOptions): Promise<NavigationResult>;
|
|
68
|
+
/**
|
|
69
|
+
* Get specific line range from file.
|
|
70
|
+
*/
|
|
71
|
+
getLines(start: number, end?: number): Promise<string[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Get context around a specific line.
|
|
74
|
+
*/
|
|
75
|
+
getContext(lineNum: number, window?: number): Promise<string>;
|
|
76
|
+
/**
|
|
77
|
+
* Count occurrences of pattern.
|
|
78
|
+
*/
|
|
79
|
+
countPattern(pattern: string | RegExp, caseSensitive?: boolean): Promise<number>;
|
|
80
|
+
/**
|
|
81
|
+
* Stream lines from file.
|
|
82
|
+
*/
|
|
83
|
+
streamLines(): AsyncGenerator<{
|
|
84
|
+
lineNum: number;
|
|
85
|
+
content: string;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
export declare class PriorBasedFilter {
|
|
89
|
+
private pattern;
|
|
90
|
+
private priors;
|
|
91
|
+
constructor(priors: string[]);
|
|
92
|
+
/**
|
|
93
|
+
* Filter file using priors.
|
|
94
|
+
*/
|
|
95
|
+
filterFile(filePath: string, maxMatches?: number): Promise<NavigationResult>;
|
|
96
|
+
/**
|
|
97
|
+
* Create filter from natural language query.
|
|
98
|
+
*/
|
|
99
|
+
static fromQuery(query: string): PriorBasedFilter;
|
|
100
|
+
}
|
|
101
|
+
export declare class MultiFileSearch {
|
|
102
|
+
private rootDir;
|
|
103
|
+
private extensions;
|
|
104
|
+
constructor(rootDir: string, extensions?: string[]);
|
|
105
|
+
/**
|
|
106
|
+
* Search across all files matching extensions.
|
|
107
|
+
*/
|
|
108
|
+
search(pattern: string | RegExp, options?: MultiFileSearchOptions): Promise<NavigationResult[]>;
|
|
109
|
+
/**
|
|
110
|
+
* Search using prior-based filtering.
|
|
111
|
+
*/
|
|
112
|
+
searchWithPriors(priors: string[], maxResults?: number): Promise<NavigationResult[]>;
|
|
113
|
+
/**
|
|
114
|
+
* Iterate over files, skipping ignored directories.
|
|
115
|
+
*/
|
|
116
|
+
private iterFiles;
|
|
117
|
+
}
|
|
118
|
+
export declare class RLMNavigator {
|
|
119
|
+
private basePath;
|
|
120
|
+
constructor(basePath?: string);
|
|
121
|
+
/**
|
|
122
|
+
* Determine if RLM approach should be used for this file.
|
|
123
|
+
*/
|
|
124
|
+
shouldUseRLM(filePath: string): Promise<boolean>;
|
|
125
|
+
/**
|
|
126
|
+
* Get navigator for a specific file.
|
|
127
|
+
*/
|
|
128
|
+
navigateFile(filePath: string): FileNavigator;
|
|
129
|
+
/**
|
|
130
|
+
* Search across codebase from base path.
|
|
131
|
+
*/
|
|
132
|
+
searchCodebase(pattern: string | RegExp, extensions?: string[]): Promise<NavigationResult[]>;
|
|
133
|
+
/**
|
|
134
|
+
* Navigate a cognitive state document.
|
|
135
|
+
*/
|
|
136
|
+
findInCognitiveState(statePath: string, query: string): Promise<NavigationResult>;
|
|
137
|
+
/**
|
|
138
|
+
* Navigate a Conductor markdown file (spec.md, plan.md, state.md).
|
|
139
|
+
*/
|
|
140
|
+
findInConductorFile(filePath: string, pattern: string, options?: FileNavigatorOptions): Promise<NavigationResult>;
|
|
141
|
+
/**
|
|
142
|
+
* Analyze a log file without loading into context.
|
|
143
|
+
*/
|
|
144
|
+
analyzeLog(logPath: string, errorOnly?: boolean): Promise<{
|
|
145
|
+
errors: {
|
|
146
|
+
count: number;
|
|
147
|
+
samples: string[];
|
|
148
|
+
};
|
|
149
|
+
warnings: {
|
|
150
|
+
count: number;
|
|
151
|
+
samples: string[];
|
|
152
|
+
};
|
|
153
|
+
info: {
|
|
154
|
+
count: number;
|
|
155
|
+
samples: string[];
|
|
156
|
+
};
|
|
157
|
+
}>;
|
|
158
|
+
}
|
|
159
|
+
export declare const rlmNavigator: RLMNavigator;
|
|
160
|
+
//# sourceMappingURL=rlm-navigator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rlm-navigator.d.ts","sourceRoot":"","sources":["../../src/utils/rlm-navigator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAWH,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjC,sCAAsC;IACtC,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,uBAAuB;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4BAA4B;IAC5B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,sBAAuB,SAAQ,oBAAoB;IAClE,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAMD,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,SAAU,CAAC;AAE5C,wCAAwC;AACxC,eAAO,MAAM,kBAAkB,UAAyC,CAAC;AAEzE,yCAAyC;AACzC,eAAO,MAAM,SAAS,aAOpB,CAAC;AAMH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAC,CAAS;gBAEpB,QAAQ,EAAE,MAAM;IAI5B;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAarC;;OAEG;IACG,WAAW,CACf,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,gBAAgB,CAAC;IA6C5B;;OAEG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAgB9D;;OAEG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAOtE;;OAEG;IACG,YAAY,CAChB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,aAAa,UAAQ,GACpB,OAAO,CAAC,MAAM,CAAC;IAiBlB;;OAEG;IACI,WAAW,IAAI,cAAc,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAY3E;AAMD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAW;gBAEb,MAAM,EAAE,MAAM,EAAE;IAM5B;;OAEG;IACG,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,UAAU,GAAE,MAAY,GACvB,OAAO,CAAC,gBAAgB,CAAC;IAsB5B;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB;CAmBlD;AAMD,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAW;gBAEjB,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE;IAKlD;;OAEG;IACG,MAAM,CACV,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,gBAAgB,EAAE,CAAC;IA0B9B;;OAEG;IACG,gBAAgB,CACpB,MAAM,EAAE,MAAM,EAAE,EAChB,UAAU,GAAE,MAAY,GACvB,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAqB9B;;OAEG;YACY,SAAS;CAwBzB;AAMD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,CAAC,EAAE,MAAM;IAI7B;;OAEG;IACG,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAStD;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa;IAI7C;;OAEG;IACG,cAAc,CAClB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAK9B;;OAEG;IACG,oBAAoB,CACxB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,gBAAgB,CAAC;IAK5B;;OAEG;IACG,mBAAmB,CACvB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,gBAAgB,CAAC;IAK5B;;OAEG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,OAAe,GACzB,OAAO,CAAC;QACT,MAAM,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC;QAC7C,QAAQ,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC;QAC/C,IAAI,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC;KAC5C,CAAC;CA8BH;AAMD,eAAO,MAAM,YAAY,cAAqB,CAAC"}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RLM Navigator: Program-Environment Paradigm for Large File Navigation
|
|
3
|
+
*
|
|
4
|
+
* Implements the Recursive Language Model paradigm from Zhang, Kraska, Khattab (2025):
|
|
5
|
+
* "Treat long prompts as part of an external environment and allow the LLM
|
|
6
|
+
* to programmatically examine, decompose, and recursively call itself over snippets"
|
|
7
|
+
*
|
|
8
|
+
* Key Principle: Never load large data INTO context. Navigate it FROM context.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* Traditional: [Entire file in context] → "Find X"
|
|
12
|
+
* RLM: [File as external variable] → Write code to find X → Execute → Get result
|
|
13
|
+
*
|
|
14
|
+
* Integration with Conductor Bridge:
|
|
15
|
+
* - Navigates large spec.md/plan.md files
|
|
16
|
+
* - Navigates cognitive state documents
|
|
17
|
+
* - Navigates amendment files
|
|
18
|
+
* - Supports watcher for large file changes
|
|
19
|
+
*
|
|
20
|
+
* Reference: https://arxiv.org/abs/2512.24601
|
|
21
|
+
*/
|
|
22
|
+
import { createReadStream } from 'fs';
|
|
23
|
+
import { stat, readdir } from 'fs/promises';
|
|
24
|
+
import { createInterface } from 'readline';
|
|
25
|
+
import { join, extname } from 'path';
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Constants
|
|
28
|
+
// ============================================================================
|
|
29
|
+
/** Size threshold above which RLM approach should be used (~50K tokens) */
|
|
30
|
+
export const SIZE_THRESHOLD_BYTES = 200_000;
|
|
31
|
+
/** Default file extensions to search */
|
|
32
|
+
export const DEFAULT_EXTENSIONS = ['.ts', '.js', '.md', '.json', '.txt'];
|
|
33
|
+
/** Directories to skip when searching */
|
|
34
|
+
export const SKIP_DIRS = new Set([
|
|
35
|
+
'.git',
|
|
36
|
+
'node_modules',
|
|
37
|
+
'dist',
|
|
38
|
+
'build',
|
|
39
|
+
'coverage',
|
|
40
|
+
'.cache',
|
|
41
|
+
]);
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// FileNavigator: Single File Navigation
|
|
44
|
+
// ============================================================================
|
|
45
|
+
export class FileNavigator {
|
|
46
|
+
filePath;
|
|
47
|
+
lineCountCache;
|
|
48
|
+
constructor(filePath) {
|
|
49
|
+
this.filePath = filePath;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get total line count (cached).
|
|
53
|
+
*/
|
|
54
|
+
async getLineCount() {
|
|
55
|
+
if (this.lineCountCache !== undefined) {
|
|
56
|
+
return this.lineCountCache;
|
|
57
|
+
}
|
|
58
|
+
let count = 0;
|
|
59
|
+
for await (const _ of this.streamLines()) {
|
|
60
|
+
count++;
|
|
61
|
+
}
|
|
62
|
+
this.lineCountCache = count;
|
|
63
|
+
return count;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Find pattern in file with surrounding context.
|
|
67
|
+
*/
|
|
68
|
+
async findPattern(pattern, options = {}) {
|
|
69
|
+
const { maxMatches = 100, contextLines = 3, caseSensitive = false, } = options;
|
|
70
|
+
const regex = pattern instanceof RegExp
|
|
71
|
+
? pattern
|
|
72
|
+
: new RegExp(pattern, caseSensitive ? '' : 'i');
|
|
73
|
+
const matches = [];
|
|
74
|
+
const contextSnippets = [];
|
|
75
|
+
const allLines = [];
|
|
76
|
+
let totalLines = 0;
|
|
77
|
+
// First pass: collect all lines and find matches
|
|
78
|
+
for await (const { lineNum, content } of this.streamLines()) {
|
|
79
|
+
allLines.push(content);
|
|
80
|
+
totalLines = lineNum;
|
|
81
|
+
if (regex.test(content) && matches.length < maxMatches) {
|
|
82
|
+
matches.push([lineNum, content.trim()]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Second pass: get context for matches
|
|
86
|
+
for (const [lineNum] of matches) {
|
|
87
|
+
const start = Math.max(0, lineNum - contextLines - 1);
|
|
88
|
+
const end = Math.min(allLines.length, lineNum + contextLines);
|
|
89
|
+
const context = allLines.slice(start, end).join('\n');
|
|
90
|
+
contextSnippets.push(context);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
query: pattern.toString(),
|
|
94
|
+
matches,
|
|
95
|
+
contextSnippets,
|
|
96
|
+
filePath: this.filePath,
|
|
97
|
+
totalLinesScanned: totalLines,
|
|
98
|
+
patternUsed: regex.source,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get specific line range from file.
|
|
103
|
+
*/
|
|
104
|
+
async getLines(start, end) {
|
|
105
|
+
const targetEnd = end ?? start;
|
|
106
|
+
const lines = [];
|
|
107
|
+
for await (const { lineNum, content } of this.streamLines()) {
|
|
108
|
+
if (lineNum >= start && lineNum <= targetEnd) {
|
|
109
|
+
lines.push(content);
|
|
110
|
+
}
|
|
111
|
+
if (lineNum > targetEnd) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get context around a specific line.
|
|
119
|
+
*/
|
|
120
|
+
async getContext(lineNum, window = 5) {
|
|
121
|
+
const start = Math.max(1, lineNum - window);
|
|
122
|
+
const end = lineNum + window;
|
|
123
|
+
const lines = await this.getLines(start, end);
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Count occurrences of pattern.
|
|
128
|
+
*/
|
|
129
|
+
async countPattern(pattern, caseSensitive = false) {
|
|
130
|
+
const regex = pattern instanceof RegExp
|
|
131
|
+
? pattern
|
|
132
|
+
: new RegExp(pattern, caseSensitive ? 'g' : 'gi');
|
|
133
|
+
let count = 0;
|
|
134
|
+
for await (const { content } of this.streamLines()) {
|
|
135
|
+
const matches = content.match(regex);
|
|
136
|
+
if (matches) {
|
|
137
|
+
count += matches.length;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return count;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Stream lines from file.
|
|
144
|
+
*/
|
|
145
|
+
async *streamLines() {
|
|
146
|
+
const rl = createInterface({
|
|
147
|
+
input: createReadStream(this.filePath, { encoding: 'utf-8' }),
|
|
148
|
+
crlfDelay: Infinity,
|
|
149
|
+
});
|
|
150
|
+
let lineNum = 0;
|
|
151
|
+
for await (const line of rl) {
|
|
152
|
+
lineNum++;
|
|
153
|
+
yield { lineNum, content: line };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// PriorBasedFilter: Filter Using Expectations
|
|
159
|
+
// ============================================================================
|
|
160
|
+
export class PriorBasedFilter {
|
|
161
|
+
pattern;
|
|
162
|
+
priors;
|
|
163
|
+
constructor(priors) {
|
|
164
|
+
this.priors = priors;
|
|
165
|
+
const escaped = priors.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
166
|
+
this.pattern = new RegExp(escaped.join('|'), 'i');
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Filter file using priors.
|
|
170
|
+
*/
|
|
171
|
+
async filterFile(filePath, maxMatches = 100) {
|
|
172
|
+
const nav = new FileNavigator(filePath);
|
|
173
|
+
const matches = [];
|
|
174
|
+
let totalLines = 0;
|
|
175
|
+
for await (const { lineNum, content } of nav.streamLines()) {
|
|
176
|
+
totalLines = lineNum;
|
|
177
|
+
if (this.pattern.test(content) && matches.length < maxMatches) {
|
|
178
|
+
matches.push([lineNum, content.trim()]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
query: this.priors.join('|'),
|
|
183
|
+
matches,
|
|
184
|
+
contextSnippets: [],
|
|
185
|
+
filePath,
|
|
186
|
+
totalLinesScanned: totalLines,
|
|
187
|
+
patternUsed: this.pattern.source,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Create filter from natural language query.
|
|
192
|
+
*/
|
|
193
|
+
static fromQuery(query) {
|
|
194
|
+
// Extract words longer than 3 chars as potential priors
|
|
195
|
+
const words = query.toLowerCase().match(/\b\w{4,}\b/g) || [];
|
|
196
|
+
// Remove common stop words
|
|
197
|
+
const stopWords = new Set([
|
|
198
|
+
'what',
|
|
199
|
+
'where',
|
|
200
|
+
'when',
|
|
201
|
+
'which',
|
|
202
|
+
'that',
|
|
203
|
+
'this',
|
|
204
|
+
'have',
|
|
205
|
+
'from',
|
|
206
|
+
'with',
|
|
207
|
+
'about',
|
|
208
|
+
]);
|
|
209
|
+
const priors = words.filter((w) => !stopWords.has(w));
|
|
210
|
+
return new PriorBasedFilter(priors);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// MultiFileSearch: Search Across Multiple Files
|
|
215
|
+
// ============================================================================
|
|
216
|
+
export class MultiFileSearch {
|
|
217
|
+
rootDir;
|
|
218
|
+
extensions;
|
|
219
|
+
constructor(rootDir, extensions) {
|
|
220
|
+
this.rootDir = rootDir;
|
|
221
|
+
this.extensions = extensions ?? DEFAULT_EXTENSIONS;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Search across all files matching extensions.
|
|
225
|
+
*/
|
|
226
|
+
async search(pattern, options = {}) {
|
|
227
|
+
const { maxResults = 100, ...fileOptions } = options;
|
|
228
|
+
const results = [];
|
|
229
|
+
let totalMatches = 0;
|
|
230
|
+
for await (const filePath of this.iterFiles()) {
|
|
231
|
+
if (totalMatches >= maxResults) {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
const nav = new FileNavigator(filePath);
|
|
235
|
+
const result = await nav.findPattern(pattern, {
|
|
236
|
+
...fileOptions,
|
|
237
|
+
maxMatches: maxResults - totalMatches,
|
|
238
|
+
});
|
|
239
|
+
if (result.matches.length > 0) {
|
|
240
|
+
results.push(result);
|
|
241
|
+
totalMatches += result.matches.length;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return results;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Search using prior-based filtering.
|
|
248
|
+
*/
|
|
249
|
+
async searchWithPriors(priors, maxResults = 100) {
|
|
250
|
+
const filter = new PriorBasedFilter(priors);
|
|
251
|
+
const results = [];
|
|
252
|
+
let totalMatches = 0;
|
|
253
|
+
for await (const filePath of this.iterFiles()) {
|
|
254
|
+
if (totalMatches >= maxResults) {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
const result = await filter.filterFile(filePath, maxResults - totalMatches);
|
|
258
|
+
if (result.matches.length > 0) {
|
|
259
|
+
results.push(result);
|
|
260
|
+
totalMatches += result.matches.length;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return results;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Iterate over files, skipping ignored directories.
|
|
267
|
+
*/
|
|
268
|
+
async *iterFiles(dir) {
|
|
269
|
+
const currentDir = dir ?? this.rootDir;
|
|
270
|
+
try {
|
|
271
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
const fullPath = join(currentDir, entry.name);
|
|
274
|
+
if (entry.isDirectory()) {
|
|
275
|
+
if (!SKIP_DIRS.has(entry.name)) {
|
|
276
|
+
yield* this.iterFiles(fullPath);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else if (entry.isFile()) {
|
|
280
|
+
const ext = extname(entry.name);
|
|
281
|
+
if (this.extensions.includes(ext)) {
|
|
282
|
+
yield fullPath;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// Skip unreadable directories
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// RLMNavigator: High-Level Navigation Interface
|
|
294
|
+
// ============================================================================
|
|
295
|
+
export class RLMNavigator {
|
|
296
|
+
basePath;
|
|
297
|
+
constructor(basePath) {
|
|
298
|
+
this.basePath = basePath ?? process.cwd();
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Determine if RLM approach should be used for this file.
|
|
302
|
+
*/
|
|
303
|
+
async shouldUseRLM(filePath) {
|
|
304
|
+
try {
|
|
305
|
+
const stats = await stat(filePath);
|
|
306
|
+
return stats.size > SIZE_THRESHOLD_BYTES;
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get navigator for a specific file.
|
|
314
|
+
*/
|
|
315
|
+
navigateFile(filePath) {
|
|
316
|
+
return new FileNavigator(filePath);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Search across codebase from base path.
|
|
320
|
+
*/
|
|
321
|
+
async searchCodebase(pattern, extensions) {
|
|
322
|
+
const searcher = new MultiFileSearch(this.basePath, extensions);
|
|
323
|
+
return searcher.search(pattern);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Navigate a cognitive state document.
|
|
327
|
+
*/
|
|
328
|
+
async findInCognitiveState(statePath, query) {
|
|
329
|
+
const filter = PriorBasedFilter.fromQuery(query);
|
|
330
|
+
return filter.filterFile(statePath);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Navigate a Conductor markdown file (spec.md, plan.md, state.md).
|
|
334
|
+
*/
|
|
335
|
+
async findInConductorFile(filePath, pattern, options) {
|
|
336
|
+
const nav = new FileNavigator(filePath);
|
|
337
|
+
return nav.findPattern(pattern, options);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Analyze a log file without loading into context.
|
|
341
|
+
*/
|
|
342
|
+
async analyzeLog(logPath, errorOnly = false) {
|
|
343
|
+
const nav = new FileNavigator(logPath);
|
|
344
|
+
const patterns = {
|
|
345
|
+
errors: '(ERROR|FATAL|Exception|Traceback)',
|
|
346
|
+
warnings: '(WARNING|WARN)',
|
|
347
|
+
info: '(INFO)',
|
|
348
|
+
};
|
|
349
|
+
const results = {};
|
|
350
|
+
for (const [key, pattern] of Object.entries(patterns)) {
|
|
351
|
+
if (errorOnly && !['errors', 'warnings'].includes(key)) {
|
|
352
|
+
results[key] = { count: 0, samples: [] };
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const result = await nav.findPattern(pattern, { maxMatches: 50 });
|
|
356
|
+
results[key] = {
|
|
357
|
+
count: result.matches.length,
|
|
358
|
+
samples: result.matches.slice(0, 5).map(([, content]) => content.slice(0, 100)),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return results;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// ============================================================================
|
|
365
|
+
// Convenience Exports
|
|
366
|
+
// ============================================================================
|
|
367
|
+
export const rlmNavigator = new RLMNavigator();
|
|
368
|
+
//# sourceMappingURL=rlm-navigator.js.map
|