ctxman 3.1.1 → 3.2.8
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/.ctxman/llm-profiles.json +6 -6
- package/README.md +16 -10
- package/bin/cli.js +109 -10
- package/bin/cm-ask.js +64 -6
- package/bin/cm-audit.js +76 -0
- package/bin/cm-count.js +45 -6
- package/bin/cm-diagnose.js +199 -0
- package/bin/cm-focus.js +322 -0
- package/bin/cm-map.js +71 -18
- package/bin/cm-pack.js +36 -5
- package/bin/cm-policy.js +107 -2
- package/bin/cm-pr.js +18 -3
- package/bin/cm-search.js +84 -19
- package/bin/cm-task.js +192 -0
- package/bin/cm-who.js +119 -0
- package/bin/cm-why.js +41 -5
- package/index.d.ts +940 -1
- package/index.js +105 -1
- package/lib/analyzers/token-calculator.js +76 -4
- package/lib/api/mcp/server.js +843 -29
- package/lib/cache/CacheManager.js +161 -47
- package/lib/core/Analyzer.js +61 -5
- package/lib/count/index.js +74 -14
- package/lib/diagnose/index.js +712 -0
- package/lib/focus/index.js +790 -0
- package/lib/map/call-graph.js +570 -0
- package/lib/map/graph.js +30 -5
- package/lib/map/index.js +12 -4
- package/lib/map/rank.js +45 -7
- package/lib/map/render.js +89 -28
- package/lib/pack/index.js +20 -1
- package/lib/pack/select.js +8 -1
- package/lib/policy/audit.js +3 -1
- package/lib/policy/index.js +5 -0
- package/lib/policy/workspace.js +254 -0
- package/lib/pr/pr-context.js +292 -31
- package/lib/pr/pr-renderer.js +25 -10
- package/lib/rag/EmbeddingProvider.js +139 -2
- package/lib/rag/VectorStoreFactory.js +10 -4
- package/lib/rag/ask.js +186 -17
- package/lib/rag/stores/SqliteVectorStore.js +595 -0
- package/lib/search/hybrid.js +324 -0
- package/lib/search/symbols.js +75 -11
- package/lib/task/index.js +630 -0
- package/lib/utils/file-utils.js +1 -0
- package/lib/utils/multi-repo-manager.js +4 -1
- package/lib/watch/FileWatcher.js +121 -31
- package/lib/watch/IncrementalAnalyzer.js +964 -16
- package/lib/who/index.js +893 -0
- package/lib/why/analyzer-bridge.js +223 -0
- package/lib/why/discussions.js +314 -0
- package/lib/why/index.js +356 -30
- package/lib/why/render.js +154 -9
- package/lib/workspace/index.js +1274 -0
- package/package.json +1 -1
package/lib/watch/FileWatcher.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FileWatcher - Real-Time File Watching Module
|
|
3
|
-
* v3.0.0 - Watch mode
|
|
3
|
+
* v3.0.0 - Watch mode (with Cross-Repo Multi-Root Workspace support)
|
|
4
4
|
*
|
|
5
5
|
* Responsibilities:
|
|
6
|
-
* - Watch file system for changes
|
|
6
|
+
* - Watch file system for changes across one or more repositories
|
|
7
7
|
* - Debounce rapid changes
|
|
8
|
-
* - Emit change events
|
|
9
|
-
* - Support ignore patterns
|
|
8
|
+
* - Emit change events (namespaced as <alias>/<relativePath> in multi-repo mode)
|
|
9
|
+
* - Support per-repository ignore patterns (.gitignore, .contextignore, .contextinclude)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import fs from 'fs';
|
|
@@ -14,6 +14,7 @@ import path from 'path';
|
|
|
14
14
|
import { EventEmitter } from 'events';
|
|
15
15
|
import GitIgnoreParser from '../parsers/gitignore-parser.js';
|
|
16
16
|
import FileUtils from '../utils/file-utils.js';
|
|
17
|
+
import { resolveWorkspaceRepos } from '../workspace/index.js';
|
|
17
18
|
import { getLogger } from '../utils/logger.js';
|
|
18
19
|
|
|
19
20
|
const logger = getLogger('FileWatcher');
|
|
@@ -22,20 +23,29 @@ export class FileWatcher extends EventEmitter {
|
|
|
22
23
|
constructor(rootPath, options = {}) {
|
|
23
24
|
super();
|
|
24
25
|
|
|
25
|
-
this.rootPath = rootPath;
|
|
26
|
+
this.rootPath = path.resolve(rootPath);
|
|
26
27
|
this.options = {
|
|
27
28
|
debounce: 1000, // 1 second debounce
|
|
28
29
|
recursive: true,
|
|
29
30
|
ignorePatterns: [],
|
|
31
|
+
repos: [],
|
|
30
32
|
...options,
|
|
31
33
|
};
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const extraRepos = Array.isArray(this.options.repos) ? this.options.repos.filter(Boolean) : [];
|
|
36
|
+
this.isMultiRepo = extraRepos.length > 0;
|
|
37
|
+
this.repos = this.isMultiRepo
|
|
38
|
+
? resolveWorkspaceRepos([this.rootPath, ...extraRepos])
|
|
39
|
+
: [{ root: this.rootPath, alias: null }];
|
|
40
|
+
|
|
41
|
+
// Initialize GitIgnoreParser for primary root and per-repo parsers
|
|
42
|
+
this.gitIgnoreByRoot = new Map();
|
|
43
|
+
for (const repo of this.repos) {
|
|
44
|
+
this.gitIgnoreByRoot.set(repo.root, this._createGitIgnoreParser(repo.root));
|
|
45
|
+
}
|
|
46
|
+
this.gitIgnore =
|
|
47
|
+
this.gitIgnoreByRoot.get(this.rootPath) || this._createGitIgnoreParser(this.rootPath);
|
|
37
48
|
|
|
38
|
-
this.gitIgnore = new GitIgnoreParser(gitignorePath, contextIgnorePath, contextIncludePath);
|
|
39
49
|
this.watchers = [];
|
|
40
50
|
this.debounceTimers = new Map();
|
|
41
51
|
this.isWatching = false;
|
|
@@ -48,7 +58,19 @@ export class FileWatcher extends EventEmitter {
|
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
/**
|
|
51
|
-
*
|
|
61
|
+
* Create a GitIgnoreParser for a repository root
|
|
62
|
+
* @param {string} repoRoot
|
|
63
|
+
* @returns {GitIgnoreParser}
|
|
64
|
+
*/
|
|
65
|
+
_createGitIgnoreParser(repoRoot) {
|
|
66
|
+
const gitignorePath = path.join(repoRoot, '.gitignore');
|
|
67
|
+
const contextIgnorePath = path.join(repoRoot, '.contextignore');
|
|
68
|
+
const contextIncludePath = path.join(repoRoot, '.contextinclude');
|
|
69
|
+
return new GitIgnoreParser(gitignorePath, contextIgnorePath, contextIncludePath);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Start watching all configured repositories
|
|
52
74
|
*/
|
|
53
75
|
start() {
|
|
54
76
|
if (this.isWatching) {
|
|
@@ -56,23 +78,40 @@ export class FileWatcher extends EventEmitter {
|
|
|
56
78
|
return;
|
|
57
79
|
}
|
|
58
80
|
|
|
59
|
-
logger.info(
|
|
81
|
+
logger.info(
|
|
82
|
+
this.isMultiRepo
|
|
83
|
+
? `Starting multi-repo file watch (${this.repos.map((r) => r.alias).join(', ')})`
|
|
84
|
+
: `Starting file watch: ${this.rootPath}`
|
|
85
|
+
);
|
|
60
86
|
|
|
61
87
|
try {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
88
|
+
for (const repo of this.repos) {
|
|
89
|
+
const watcher = fs.watch(
|
|
90
|
+
repo.root,
|
|
91
|
+
{ recursive: this.options.recursive },
|
|
92
|
+
(eventType, filename) => {
|
|
93
|
+
this.handleFileChange(eventType, filename, repo);
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
this.watchers.push(watcher);
|
|
97
|
+
}
|
|
69
98
|
|
|
70
|
-
this.watchers.push(watcher);
|
|
71
99
|
this.isWatching = true;
|
|
72
100
|
|
|
73
|
-
this.emit('watch:started', {
|
|
101
|
+
this.emit('watch:started', {
|
|
102
|
+
path: this.rootPath,
|
|
103
|
+
repos: this.repos.map((r) => ({ root: r.root, alias: r.alias })),
|
|
104
|
+
});
|
|
74
105
|
logger.info('Watch mode started');
|
|
75
106
|
} catch (error) {
|
|
107
|
+
for (const watcher of this.watchers) {
|
|
108
|
+
try {
|
|
109
|
+
watcher.close();
|
|
110
|
+
} catch {
|
|
111
|
+
// ignore cleanup errors
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
this.watchers = [];
|
|
76
115
|
logger.error(`Failed to start watch: ${error.message}`);
|
|
77
116
|
this.emit('watch:error', { error });
|
|
78
117
|
throw error;
|
|
@@ -111,19 +150,26 @@ export class FileWatcher extends EventEmitter {
|
|
|
111
150
|
* Handle file change event
|
|
112
151
|
* @param {string} eventType - 'rename' or 'change'
|
|
113
152
|
* @param {string} filename - Changed file name
|
|
153
|
+
* @param {Object} [repo] - Resolved repository entry { root, alias }
|
|
114
154
|
*/
|
|
115
|
-
handleFileChange(eventType, filename) {
|
|
155
|
+
handleFileChange(eventType, filename, repo = null) {
|
|
116
156
|
if (!filename) return;
|
|
117
157
|
|
|
118
|
-
const
|
|
119
|
-
const
|
|
158
|
+
const targetRepo = repo || this.repos[0] || { root: this.rootPath, alias: null };
|
|
159
|
+
const filePath = path.join(targetRepo.root, filename);
|
|
160
|
+
const repoRelativePath = path.relative(targetRepo.root, filePath);
|
|
120
161
|
|
|
121
|
-
// Check ignore patterns
|
|
122
|
-
if (this.shouldIgnore(
|
|
162
|
+
// Check ignore patterns against the owning repository
|
|
163
|
+
if (this.shouldIgnore(repoRelativePath, targetRepo.root)) {
|
|
123
164
|
this.stats.ignoredChanges++;
|
|
124
165
|
return;
|
|
125
166
|
}
|
|
126
167
|
|
|
168
|
+
const relativePath =
|
|
169
|
+
this.isMultiRepo && targetRepo.alias
|
|
170
|
+
? `${targetRepo.alias}/${repoRelativePath.split(path.sep).join('/')}`
|
|
171
|
+
: repoRelativePath;
|
|
172
|
+
|
|
127
173
|
this.stats.totalChanges++;
|
|
128
174
|
|
|
129
175
|
// Debounce rapid changes
|
|
@@ -133,7 +179,7 @@ export class FileWatcher extends EventEmitter {
|
|
|
133
179
|
|
|
134
180
|
const timer = setTimeout(() => {
|
|
135
181
|
this.debounceTimers.delete(filePath);
|
|
136
|
-
this.emitFileChange(eventType, filePath, relativePath);
|
|
182
|
+
this.emitFileChange(eventType, filePath, relativePath, targetRepo, repoRelativePath);
|
|
137
183
|
}, this.options.debounce);
|
|
138
184
|
|
|
139
185
|
this.debounceTimers.set(filePath, timer);
|
|
@@ -144,8 +190,10 @@ export class FileWatcher extends EventEmitter {
|
|
|
144
190
|
* @param {string} eventType
|
|
145
191
|
* @param {string} filePath
|
|
146
192
|
* @param {string} relativePath
|
|
193
|
+
* @param {Object} [repo]
|
|
194
|
+
* @param {string} [repoRelativePath]
|
|
147
195
|
*/
|
|
148
|
-
emitFileChange(eventType, filePath, relativePath) {
|
|
196
|
+
emitFileChange(eventType, filePath, relativePath, repo = null, repoRelativePath = null) {
|
|
149
197
|
logger.debug(`File ${eventType}: ${relativePath}`);
|
|
150
198
|
|
|
151
199
|
const event = {
|
|
@@ -156,6 +204,12 @@ export class FileWatcher extends EventEmitter {
|
|
|
156
204
|
exists: fs.existsSync(filePath),
|
|
157
205
|
};
|
|
158
206
|
|
|
207
|
+
if (this.isMultiRepo && repo?.alias) {
|
|
208
|
+
event.repo = repo.alias;
|
|
209
|
+
event.repoRoot = repo.root;
|
|
210
|
+
event.repoRelativePath = repoRelativePath || relativePath;
|
|
211
|
+
}
|
|
212
|
+
|
|
159
213
|
// Add file stats if exists
|
|
160
214
|
if (event.exists) {
|
|
161
215
|
try {
|
|
@@ -173,15 +227,50 @@ export class FileWatcher extends EventEmitter {
|
|
|
173
227
|
/**
|
|
174
228
|
* Check if file should be ignored
|
|
175
229
|
* @param {string} relativePath
|
|
230
|
+
* @param {string} [repoRoot]
|
|
176
231
|
* @returns {boolean}
|
|
177
232
|
*/
|
|
178
|
-
shouldIgnore(relativePath) {
|
|
233
|
+
shouldIgnore(relativePath, repoRoot = this.rootPath) {
|
|
234
|
+
const parts = relativePath.split(/[/\\]/);
|
|
235
|
+
const normalized = parts.join('/');
|
|
236
|
+
|
|
179
237
|
// node_modules, .git and build output are skipped like in the scanners, .gitignore or not
|
|
180
|
-
if (
|
|
238
|
+
if (parts.some((part) => FileUtils.isSkippedDirectory(part))) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Ignore .ctxman internal state (e.g. .ctxman/self-knowledge/index.sqlite) and SQLite journals
|
|
243
|
+
// so watch-mode RAG/graph writes never trigger a recursive watch feedback loop
|
|
244
|
+
if (
|
|
245
|
+
parts.includes('.ctxman') ||
|
|
246
|
+
normalized.endsWith('.sqlite') ||
|
|
247
|
+
normalized.endsWith('.sqlite-wal') ||
|
|
248
|
+
normalized.endsWith('.sqlite-shm') ||
|
|
249
|
+
normalized.endsWith('.sqlite-journal')
|
|
250
|
+
) {
|
|
181
251
|
return true;
|
|
182
252
|
}
|
|
253
|
+
|
|
254
|
+
// Check custom ignorePatterns if configured
|
|
255
|
+
if (Array.isArray(this.options.ignorePatterns) && this.options.ignorePatterns.length > 0) {
|
|
256
|
+
for (const pattern of this.options.ignorePatterns) {
|
|
257
|
+
if (
|
|
258
|
+
typeof pattern === 'string' &&
|
|
259
|
+
(normalized === pattern || normalized.startsWith(`${pattern}/`))
|
|
260
|
+
) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
if (pattern instanceof RegExp && pattern.test(normalized)) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const resolvedRoot = repoRoot ? path.resolve(repoRoot) : this.rootPath;
|
|
270
|
+
const parser = this.gitIgnoreByRoot.get(resolvedRoot) || this.gitIgnore;
|
|
271
|
+
|
|
183
272
|
// Use GitIgnoreParser for ignore rules
|
|
184
|
-
return
|
|
273
|
+
return parser.isIgnored(path.join(resolvedRoot, relativePath), relativePath);
|
|
185
274
|
}
|
|
186
275
|
|
|
187
276
|
/**
|
|
@@ -194,6 +283,7 @@ export class FileWatcher extends EventEmitter {
|
|
|
194
283
|
isWatching: this.isWatching,
|
|
195
284
|
activeWatchers: this.watchers.length,
|
|
196
285
|
pendingDebounce: this.debounceTimers.size,
|
|
286
|
+
repos: this.repos.map((r) => ({ root: r.root, alias: r.alias })),
|
|
197
287
|
};
|
|
198
288
|
}
|
|
199
289
|
|