ctxman 3.0.0 → 3.1.1
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 +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +381 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -67
- package/bin/cm-audit.js +121 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +687 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +772 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +116 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +312 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +53 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/ask.js +122 -0
- package/lib/rag/sources/FileDataSource.js +12 -9
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +246 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +257 -0
- package/lib/wizards/project-detector.js +572 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
package/lib/api/mcp/resources.js
CHANGED
|
@@ -1,269 +1,328 @@
|
|
|
1
|
-
import { getLogger } from
|
|
2
|
-
import { Scanner } from
|
|
3
|
-
import path from
|
|
4
|
-
import fs from
|
|
1
|
+
import { getLogger } from '../../utils/logger.js';
|
|
2
|
+
import { Scanner } from '../../core/Scanner.js';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import FileUtils from '../../utils/file-utils.js';
|
|
6
|
+
import { redactSecrets } from '../../utils/secret-redactor.js';
|
|
7
|
+
import { loadPolicy } from '../../policy/policy.js';
|
|
5
8
|
|
|
6
|
-
const logger = getLogger(
|
|
9
|
+
const logger = getLogger('MCP:Resources');
|
|
10
|
+
const MAX_CACHED_ANALYSES = 10;
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* ResourceProvider manages MCP resources for codebase access
|
|
10
14
|
* Provides list, read, and template operations for resources
|
|
11
15
|
*/
|
|
12
16
|
export class ResourceProvider {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
constructor(projectPath) {
|
|
18
|
+
this.scanner = null;
|
|
19
|
+
this.projectPath = projectPath;
|
|
20
|
+
this.cache = new Map();
|
|
21
|
+
this.supportedTypes = {
|
|
22
|
+
FILE: 'file',
|
|
23
|
+
ANALYSIS: 'analysis',
|
|
24
|
+
CONTEXT: 'context',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get projectPath() {
|
|
29
|
+
return this._projectPath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// analyze_codebase/generate_context switch projects; drop the scanner of the previous one
|
|
33
|
+
set projectPath(value) {
|
|
34
|
+
if (value !== this._projectPath) {
|
|
35
|
+
this._projectPath = value;
|
|
36
|
+
this.scanner = null;
|
|
22
37
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return { resources };
|
|
65
|
-
} catch (error) {
|
|
66
|
-
logger.error(`Error listing resources: ${error.message}`);
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Read a specific resource by URI
|
|
73
|
-
* @param {string} uri - Resource URI
|
|
74
|
-
* @returns {Promise<{contents: Array}>}
|
|
75
|
-
*/
|
|
76
|
-
async readResource(uri) {
|
|
77
|
-
try {
|
|
78
|
-
logger.info(`Reading resource: ${uri}`);
|
|
79
|
-
|
|
80
|
-
// Parse URI
|
|
81
|
-
const parsed = this.parseUri(uri);
|
|
82
|
-
|
|
83
|
-
switch (parsed.type) {
|
|
84
|
-
case this.supportedTypes.FILE:
|
|
85
|
-
return await this.readFileResource(parsed.path);
|
|
86
|
-
|
|
87
|
-
case this.supportedTypes.ANALYSIS:
|
|
88
|
-
return await this.readAnalysisResource(parsed.id);
|
|
89
|
-
|
|
90
|
-
case this.supportedTypes.CONTEXT:
|
|
91
|
-
return await this.readContextResource(parsed.name);
|
|
92
|
-
|
|
93
|
-
default:
|
|
94
|
-
throw new Error(`Unsupported resource type: ${parsed.type}`);
|
|
95
|
-
}
|
|
96
|
-
} catch (error) {
|
|
97
|
-
logger.error(`Error reading resource ${uri}: ${error.message}`);
|
|
98
|
-
throw error;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* List available resources with optional pagination
|
|
42
|
+
* @param {string} cursor - Optional cursor for pagination
|
|
43
|
+
* @returns {Promise<{resources: Array, nextCursor?: string}>}
|
|
44
|
+
*/
|
|
45
|
+
async listResources(cursor = null) {
|
|
46
|
+
try {
|
|
47
|
+
logger.info(`Listing resources${cursor ? ` from cursor: ${cursor}` : ''}`);
|
|
48
|
+
|
|
49
|
+
// Initialize scanner if needed
|
|
50
|
+
if (!this.scanner && this.projectPath) {
|
|
51
|
+
this.scanner = new Scanner(this.projectPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const resources = [];
|
|
55
|
+
|
|
56
|
+
// Analysis ids are generated by the server, so listing is the only way a client can
|
|
57
|
+
// find analysis://recent/{id}. The cursor pages through files, so they go on page one.
|
|
58
|
+
if (!cursor) {
|
|
59
|
+
resources.push(...this.listCachedAnalyses());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Add file resources if project path is set
|
|
63
|
+
if (this.scanner) {
|
|
64
|
+
const files = this.scanner.scan();
|
|
65
|
+
const PAGE_SIZE = 100;
|
|
66
|
+
const startIdx = cursor ? parseInt(cursor, 10) : 0;
|
|
67
|
+
const endIdx = Math.min(startIdx + PAGE_SIZE, files.length);
|
|
68
|
+
|
|
69
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
70
|
+
const file = files[i];
|
|
71
|
+
resources.push({
|
|
72
|
+
uri: `file://codebase/${file.relativePath}`,
|
|
73
|
+
name: path.basename(file.relativePath),
|
|
74
|
+
description: `Source file: ${file.relativePath}`,
|
|
75
|
+
mimeType: this.getMimeType(file.relativePath),
|
|
76
|
+
});
|
|
99
77
|
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Get resource templates for dynamic resources
|
|
104
|
-
* @returns {Array}
|
|
105
|
-
*/
|
|
106
|
-
getResourceTemplates() {
|
|
107
|
-
return [
|
|
108
|
-
{
|
|
109
|
-
uriTemplate: "file://codebase/{path}",
|
|
110
|
-
name: "Codebase Files",
|
|
111
|
-
description: "Access source files in the project",
|
|
112
|
-
mimeType: "text/plain"
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
uriTemplate: "analysis://recent/{id}",
|
|
116
|
-
name: "Analysis Results",
|
|
117
|
-
description: "Cached analysis results",
|
|
118
|
-
mimeType: "application/json"
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
uriTemplate: "context://template/{name}",
|
|
122
|
-
name: "Context Templates",
|
|
123
|
-
description: "Pre-generated context for common workflows",
|
|
124
|
-
mimeType: "text/plain"
|
|
125
|
-
}
|
|
126
|
-
];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Read a file resource
|
|
131
|
-
* @private
|
|
132
|
-
*/
|
|
133
|
-
async readFileResource(relativePath) {
|
|
134
|
-
const fullPath = path.join(this.projectPath || "", relativePath);
|
|
135
78
|
|
|
136
|
-
//
|
|
137
|
-
const
|
|
138
|
-
const resolvedProject = path.resolve(this.projectPath || "");
|
|
79
|
+
// Return next cursor if there are more results
|
|
80
|
+
const nextCursor = endIdx < files.length ? endIdx.toString() : undefined;
|
|
139
81
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
82
|
+
logger.info(`Listed ${resources.length} resources`);
|
|
83
|
+
return { resources, nextCursor };
|
|
84
|
+
}
|
|
145
85
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
86
|
+
return { resources };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.error(`Error listing resources: ${error.message}`);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Read a specific resource by URI
|
|
95
|
+
* @param {string} uri - Resource URI
|
|
96
|
+
* @returns {Promise<{contents: Array}>}
|
|
97
|
+
*/
|
|
98
|
+
async readResource(uri) {
|
|
99
|
+
try {
|
|
100
|
+
logger.info(`Reading resource: ${uri}`);
|
|
101
|
+
|
|
102
|
+
// Parse URI
|
|
103
|
+
const parsed = this.parseUri(uri);
|
|
104
|
+
|
|
105
|
+
switch (parsed.type) {
|
|
106
|
+
case this.supportedTypes.FILE:
|
|
107
|
+
return await this.readFileResource(parsed.path);
|
|
108
|
+
|
|
109
|
+
case this.supportedTypes.ANALYSIS:
|
|
110
|
+
return await this.readAnalysisResource(parsed.id);
|
|
111
|
+
|
|
112
|
+
case this.supportedTypes.CONTEXT:
|
|
113
|
+
return await this.readContextResource(parsed.name);
|
|
114
|
+
|
|
115
|
+
default:
|
|
116
|
+
throw new Error(`Unsupported resource type: ${parsed.type}`);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
logger.error(`Error reading resource ${uri}: ${error.message}`);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get resource templates for dynamic resources
|
|
126
|
+
* @returns {Array}
|
|
127
|
+
*/
|
|
128
|
+
getResourceTemplates() {
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
uriTemplate: 'file://codebase/{path}',
|
|
132
|
+
name: 'Codebase Files',
|
|
133
|
+
description: 'Access source files in the project',
|
|
134
|
+
mimeType: 'text/plain',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
uriTemplate: 'analysis://recent/{id}',
|
|
138
|
+
name: 'Analysis Results',
|
|
139
|
+
description: 'Cached analysis results',
|
|
140
|
+
mimeType: 'application/json',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
uriTemplate: 'context://template/{name}',
|
|
144
|
+
name: 'Context Templates',
|
|
145
|
+
description: 'Pre-generated context for common workflows',
|
|
146
|
+
mimeType: 'text/plain',
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Read a file resource
|
|
153
|
+
* @private
|
|
154
|
+
*/
|
|
155
|
+
async readFileResource(relativePath) {
|
|
156
|
+
// Security check: the path must stay inside the project (a prefix check would let
|
|
157
|
+
// /proj-other through for /proj)
|
|
158
|
+
const resolvedProject = path.resolve(this.projectPath || '');
|
|
159
|
+
const resolvedPath = path.resolve(resolvedProject, relativePath);
|
|
160
|
+
const inside = path.relative(resolvedProject, resolvedPath);
|
|
161
|
+
if (inside === '' || inside.startsWith('..') || path.isAbsolute(inside)) {
|
|
162
|
+
throw new Error('Access denied: path outside project directory');
|
|
153
163
|
}
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
async readAnalysisResource(id) {
|
|
160
|
-
const cached = this.cache.get(`analysis:${id}`);
|
|
161
|
-
|
|
162
|
-
if (!cached) {
|
|
163
|
-
throw new Error(`Analysis result not found: ${id}`);
|
|
164
|
-
}
|
|
165
|
+
// Same rule as the scanner: no binary or secret-bearing files such as .env or *.pem
|
|
166
|
+
if (!FileUtils.isText(resolvedPath)) {
|
|
167
|
+
throw new Error('Access denied: not a text source file');
|
|
168
|
+
}
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
uri: `analysis://recent/${id}`,
|
|
169
|
-
mimeType: "application/json",
|
|
170
|
-
text: JSON.stringify(cached, null, 2)
|
|
171
|
-
}]
|
|
172
|
-
};
|
|
170
|
+
if (loadPolicy(resolvedProject).isDenied(inside)) {
|
|
171
|
+
throw new Error('Access denied: the export policy denies this file');
|
|
173
172
|
}
|
|
174
173
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
174
|
+
const { text } = redactSecrets(fs.readFileSync(resolvedPath, 'utf-8'));
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
contents: [
|
|
178
|
+
{
|
|
179
|
+
uri: `file://codebase/${relativePath}`,
|
|
180
|
+
mimeType: this.getMimeType(relativePath),
|
|
181
|
+
text,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Read an analysis resource from cache
|
|
189
|
+
* @private
|
|
190
|
+
*/
|
|
191
|
+
async readAnalysisResource(id) {
|
|
192
|
+
const cached = this.cache.get(`analysis:${id}`);
|
|
193
|
+
|
|
194
|
+
if (!cached) {
|
|
195
|
+
throw new Error(`Analysis result not found: ${id}`);
|
|
196
|
+
}
|
|
181
197
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
198
|
+
return {
|
|
199
|
+
contents: [
|
|
200
|
+
{
|
|
201
|
+
uri: `analysis://recent/${id}`,
|
|
202
|
+
mimeType: 'application/json',
|
|
203
|
+
text: JSON.stringify(cached, null, 2),
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Read a context template resource
|
|
211
|
+
* @private
|
|
212
|
+
*/
|
|
213
|
+
async readContextResource(name) {
|
|
214
|
+
const cached = this.cache.get(`context:${name}`);
|
|
215
|
+
|
|
216
|
+
if (!cached) {
|
|
217
|
+
throw new Error(`Context template not found: ${name}`);
|
|
218
|
+
}
|
|
185
219
|
|
|
220
|
+
return {
|
|
221
|
+
contents: [
|
|
222
|
+
{
|
|
223
|
+
uri: `context://template/${name}`,
|
|
224
|
+
mimeType: 'text/plain',
|
|
225
|
+
text: cached,
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Cache an analysis result
|
|
233
|
+
* @param {string} id - Resource ID
|
|
234
|
+
* @param {Object} data - Analysis data
|
|
235
|
+
*/
|
|
236
|
+
cacheAnalysis(id, data) {
|
|
237
|
+
this.cache.set(`analysis:${id}`, data);
|
|
238
|
+
// Every analyze_codebase call adds a full analysis; a long-running server keeps the latest
|
|
239
|
+
const analyses = [...this.cache.keys()].filter((key) => key.startsWith('analysis:'));
|
|
240
|
+
for (const key of analyses.slice(0, -MAX_CACHED_ANALYSES)) {
|
|
241
|
+
this.cache.delete(key);
|
|
242
|
+
}
|
|
243
|
+
logger.info(`Cached analysis: ${id}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Resource entries for the cached analysis results
|
|
248
|
+
* @private
|
|
249
|
+
*/
|
|
250
|
+
listCachedAnalyses() {
|
|
251
|
+
return [...this.cache.keys()]
|
|
252
|
+
.filter((key) => key.startsWith('analysis:'))
|
|
253
|
+
.map((key) => {
|
|
254
|
+
const id = key.slice('analysis:'.length);
|
|
186
255
|
return {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}]
|
|
256
|
+
uri: `analysis://recent/${id}`,
|
|
257
|
+
name: `Analysis ${id}`,
|
|
258
|
+
description: 'Cached analyze_codebase result',
|
|
259
|
+
mimeType: 'application/json',
|
|
192
260
|
};
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Cache a context template
|
|
266
|
+
* @param {string} name - Template name
|
|
267
|
+
* @param {string} content - Context content
|
|
268
|
+
*/
|
|
269
|
+
cacheContext(name, content) {
|
|
270
|
+
this.cache.set(`context:${name}`, content);
|
|
271
|
+
logger.info(`Cached context: ${name}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Parse a resource URI
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
parseUri(uri) {
|
|
279
|
+
const fileMatch = uri.match(/^file:\/\/codebase\/(.+)$/);
|
|
280
|
+
if (fileMatch) {
|
|
281
|
+
return { type: this.supportedTypes.FILE, path: fileMatch[1] };
|
|
193
282
|
}
|
|
194
283
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
* @param {Object} data - Analysis data
|
|
199
|
-
*/
|
|
200
|
-
cacheAnalysis(id, data) {
|
|
201
|
-
this.cache.set(`analysis:${id}`, data);
|
|
202
|
-
logger.info(`Cached analysis: ${id}`);
|
|
284
|
+
const analysisMatch = uri.match(/^analysis:\/\/recent\/(.+)$/);
|
|
285
|
+
if (analysisMatch) {
|
|
286
|
+
return { type: this.supportedTypes.ANALYSIS, id: analysisMatch[1] };
|
|
203
287
|
}
|
|
204
288
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
* @param {string} content - Context content
|
|
209
|
-
*/
|
|
210
|
-
cacheContext(name, content) {
|
|
211
|
-
this.cache.set(`context:${name}`, content);
|
|
212
|
-
logger.info(`Cached context: ${name}`);
|
|
289
|
+
const contextMatch = uri.match(/^context:\/\/template\/(.+)$/);
|
|
290
|
+
if (contextMatch) {
|
|
291
|
+
return { type: this.supportedTypes.CONTEXT, name: contextMatch[1] };
|
|
213
292
|
}
|
|
214
293
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
".tsx": "text/typescript",
|
|
250
|
-
".py": "text/x-python",
|
|
251
|
-
".java": "text/x-java",
|
|
252
|
-
".go": "text/x-go",
|
|
253
|
-
".rs": "text/x-rust",
|
|
254
|
-
".c": "text/x-c",
|
|
255
|
-
".cpp": "text/x-c++",
|
|
256
|
-
".h": "text/x-c",
|
|
257
|
-
".json": "application/json",
|
|
258
|
-
".md": "text/markdown",
|
|
259
|
-
".txt": "text/plain",
|
|
260
|
-
".html": "text/html",
|
|
261
|
-
".css": "text/css",
|
|
262
|
-
".xml": "application/xml",
|
|
263
|
-
".yaml": "text/yaml",
|
|
264
|
-
".yml": "text/yaml"
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
return mimeTypes[ext] || "text/plain";
|
|
268
|
-
}
|
|
294
|
+
throw new Error(`Invalid resource URI: ${uri}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get MIME type for a file
|
|
299
|
+
* @private
|
|
300
|
+
*/
|
|
301
|
+
getMimeType(filePath) {
|
|
302
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
303
|
+
|
|
304
|
+
const mimeTypes = {
|
|
305
|
+
'.js': 'text/javascript',
|
|
306
|
+
'.ts': 'text/typescript',
|
|
307
|
+
'.jsx': 'text/javascript',
|
|
308
|
+
'.tsx': 'text/typescript',
|
|
309
|
+
'.py': 'text/x-python',
|
|
310
|
+
'.java': 'text/x-java',
|
|
311
|
+
'.go': 'text/x-go',
|
|
312
|
+
'.rs': 'text/x-rust',
|
|
313
|
+
'.c': 'text/x-c',
|
|
314
|
+
'.cpp': 'text/x-c++',
|
|
315
|
+
'.h': 'text/x-c',
|
|
316
|
+
'.json': 'application/json',
|
|
317
|
+
'.md': 'text/markdown',
|
|
318
|
+
'.txt': 'text/plain',
|
|
319
|
+
'.html': 'text/html',
|
|
320
|
+
'.css': 'text/css',
|
|
321
|
+
'.xml': 'application/xml',
|
|
322
|
+
'.yaml': 'text/yaml',
|
|
323
|
+
'.yml': 'text/yaml',
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return mimeTypes[ext] || 'text/plain';
|
|
327
|
+
}
|
|
269
328
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path checks for MCP tool and prompt arguments.
|
|
3
|
+
*
|
|
4
|
+
* Every `path` a client passes must name an existing directory. When the server runs with a
|
|
5
|
+
* root (`ctxman mcp --root <dir>` or CTXMAN_MCP_ROOT), the path must also lie inside that
|
|
6
|
+
* root once symlinks are resolved, and relative paths are taken relative to the root.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the sandbox root to its real path
|
|
14
|
+
* @param {string} root - Directory given with --root or CTXMAN_MCP_ROOT
|
|
15
|
+
* @returns {string} Absolute real path of the directory
|
|
16
|
+
* @throws {Error} When the directory does not exist
|
|
17
|
+
*/
|
|
18
|
+
export function resolveRootDir(root) {
|
|
19
|
+
const resolved = path.resolve(root);
|
|
20
|
+
const stat = fs.statSync(resolved, { throwIfNoEntry: false });
|
|
21
|
+
if (!stat?.isDirectory()) {
|
|
22
|
+
throw new Error(`MCP root is not an existing directory: ${resolved}`);
|
|
23
|
+
}
|
|
24
|
+
return fs.realpathSync(resolved);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether `target` is `root` itself or lies below it (both absolute and already resolved)
|
|
29
|
+
* @param {string} root
|
|
30
|
+
* @param {string} target
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isInsideRoot(root, target) {
|
|
34
|
+
const relative = path.relative(root, target);
|
|
35
|
+
return (
|
|
36
|
+
relative === '' ||
|
|
37
|
+
(relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check a client-supplied path.
|
|
43
|
+
*
|
|
44
|
+
* Without a root the value is returned as given, so tools behave as before; it only has to
|
|
45
|
+
* exist. With a root, a missing value means the root itself, and the real path is returned.
|
|
46
|
+
*
|
|
47
|
+
* @param {string|undefined} value - The `path` (or `projectPath`, `filePath`) argument
|
|
48
|
+
* @param {Object} [options]
|
|
49
|
+
* @param {string|null} [options.root] - Real path of the sandbox root, if any
|
|
50
|
+
* @param {'directory'|'file'|'any'} [options.expect='directory'] - What the path must name
|
|
51
|
+
* @param {string} [options.name='path'] - Argument name, for error messages
|
|
52
|
+
* @returns {string} Path to hand to the tool
|
|
53
|
+
* @throws {Error} When the path is missing, has the wrong type or lies outside the root
|
|
54
|
+
*/
|
|
55
|
+
export function resolveSandboxPath(
|
|
56
|
+
value,
|
|
57
|
+
{ root = null, expect = 'directory', name = 'path' } = {}
|
|
58
|
+
) {
|
|
59
|
+
if (value === undefined || value === null || value === '') {
|
|
60
|
+
if (root && expect !== 'file') return root;
|
|
61
|
+
throw new Error(`Missing required argument: ${name}`);
|
|
62
|
+
}
|
|
63
|
+
if (typeof value !== 'string') {
|
|
64
|
+
throw new Error(`Argument "${name}" must be a string`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const candidate = root ? path.resolve(root, value) : path.resolve(value);
|
|
68
|
+
const stat = fs.statSync(candidate, { throwIfNoEntry: false });
|
|
69
|
+
if (!stat) {
|
|
70
|
+
throw new Error(`${name} does not exist: ${value}`);
|
|
71
|
+
}
|
|
72
|
+
if (expect === 'directory' && !stat.isDirectory()) {
|
|
73
|
+
throw new Error(`${name} is not a directory: ${value}`);
|
|
74
|
+
}
|
|
75
|
+
if (expect === 'file' && !stat.isFile()) {
|
|
76
|
+
throw new Error(`${name} is not a file: ${value}`);
|
|
77
|
+
}
|
|
78
|
+
if (expect === 'any' && !stat.isDirectory() && !stat.isFile()) {
|
|
79
|
+
throw new Error(`${name} is neither a file nor a directory: ${value}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!root) return value;
|
|
83
|
+
|
|
84
|
+
const real = fs.realpathSync(candidate);
|
|
85
|
+
if (!isInsideRoot(root, real)) {
|
|
86
|
+
throw new Error(`${name} is outside the MCP root ${root}: ${value}`);
|
|
87
|
+
}
|
|
88
|
+
return real;
|
|
89
|
+
}
|