ctxman 3.0.0 → 3.1.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.
Files changed (223) hide show
  1. package/.ctxman/llm-profiles.json +1067 -39
  2. package/.ctxman/profiles/backend-team.json +13 -0
  3. package/.ctxman/profiles/default.json +12 -0
  4. package/.ctxman/profiles/devops-team.json +21 -0
  5. package/.ctxman/profiles/frontend-team.json +20 -0
  6. package/.ctxman/templates/bug-fix.json +17 -0
  7. package/.ctxman/templates/code-review.json +9 -0
  8. package/.ctxman/templates/documentation.json +9 -0
  9. package/.ctxman/templates/feature.json +9 -0
  10. package/.ctxman/templates/full-context.json +9 -0
  11. package/.ctxman/templates/refactoring.json +18 -0
  12. package/.ctxman/templates/testing.json +9 -0
  13. package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
  14. package/LICENSE +21 -0
  15. package/README.md +380 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -13
  18. package/bin/cm-audit.js +124 -0
  19. package/bin/cm-count.js +151 -0
  20. package/bin/cm-gitingest.js +128 -115
  21. package/bin/cm-map.js +242 -0
  22. package/bin/cm-mcp.js +293 -0
  23. package/bin/cm-pack.js +405 -0
  24. package/bin/cm-policy.js +170 -0
  25. package/bin/cm-pr.js +248 -0
  26. package/bin/cm-search.js +178 -0
  27. package/bin/cm-update.js +128 -128
  28. package/bin/cm-why.js +197 -0
  29. package/bin/mcp-server.js +8 -24
  30. package/ctxman.js +237 -183
  31. package/examples/README.md +19 -4
  32. package/examples/github-actions/ctxman-check.yml +49 -0
  33. package/examples/wizard-profiles/security-audit/profile.json +7 -1
  34. package/index.d.ts +1422 -0
  35. package/index.js +55 -31
  36. package/lib/agents/pr-failure-agent.js +48 -45
  37. package/lib/analyzers/ai-suggester.js +451 -0
  38. package/lib/analyzers/dependency-scanner.js +505 -0
  39. package/lib/analyzers/duplicate-detector.js +264 -0
  40. package/lib/analyzers/go-method-analyzer.js +151 -110
  41. package/lib/analyzers/import-tracker.js +431 -0
  42. package/lib/analyzers/method-analyzer.js +1055 -434
  43. package/lib/analyzers/performance-dashboard.js +683 -0
  44. package/lib/analyzers/token-calculator.js +895 -504
  45. package/lib/analyzers/unused-exports-detector.js +336 -0
  46. package/lib/api/mcp/install.js +424 -0
  47. package/lib/api/mcp/prompts.js +281 -281
  48. package/lib/api/mcp/resources.js +300 -241
  49. package/lib/api/mcp/sandbox.js +89 -0
  50. package/lib/api/mcp/server.js +675 -312
  51. package/lib/api/mcp/start.js +125 -0
  52. package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
  53. package/lib/api/rest/server.js +105 -28
  54. package/lib/cache/CacheManager.js +9 -17
  55. package/lib/core/Analyzer.js +16 -10
  56. package/lib/core/ContextBuilder.js +19 -28
  57. package/lib/core/Reporter.js +15 -14
  58. package/lib/core/Scanner.js +81 -12
  59. package/lib/count/index.js +100 -0
  60. package/lib/formatters/format-registry.js +394 -372
  61. package/lib/formatters/gitingest-formatter.js +656 -621
  62. package/lib/formatters/toon-formatter-v1.3.js +637 -632
  63. package/lib/formatters/toon-formatter.js +239 -240
  64. package/lib/integrations/git/BlameTracker.js +4 -4
  65. package/lib/integrations/git/DiffAnalyzer.js +38 -31
  66. package/lib/integrations/git/GitClient.js +71 -53
  67. package/lib/lsp/server.js +311 -0
  68. package/lib/map/graph.js +299 -0
  69. package/lib/map/imports.js +233 -0
  70. package/lib/map/index.js +33 -0
  71. package/lib/map/rank.js +253 -0
  72. package/lib/map/render.js +346 -0
  73. package/lib/map/resolver.js +574 -0
  74. package/lib/pack/budget.js +111 -0
  75. package/lib/pack/code-lexer.js +360 -0
  76. package/lib/pack/compress.js +414 -0
  77. package/lib/pack/index.js +354 -0
  78. package/lib/pack/renderers/common.js +134 -0
  79. package/lib/pack/renderers/index.js +24 -0
  80. package/lib/pack/renderers/json.js +47 -0
  81. package/lib/pack/renderers/markdown.js +54 -0
  82. package/lib/pack/renderers/plain.js +38 -0
  83. package/lib/pack/renderers/xml.js +85 -0
  84. package/lib/pack/select.js +177 -0
  85. package/lib/pack/split.js +182 -0
  86. package/lib/pack/tree.js +35 -0
  87. package/lib/parsers/gitignore-parser.js +113 -107
  88. package/lib/parsers/method-filter-parser.js +50 -33
  89. package/lib/plugins/ExporterPlugin.js +3 -3
  90. package/lib/plugins/LanguagePlugin.js +3 -3
  91. package/lib/plugins/PluginManager.js +7 -13
  92. package/lib/policy/audit.js +294 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +52 -0
  96. package/lib/policy/policy.js +489 -0
  97. package/lib/pr/diff-parser.js +267 -0
  98. package/lib/pr/method-mapper.js +148 -0
  99. package/lib/pr/pr-context.js +829 -0
  100. package/lib/pr/pr-git.js +149 -0
  101. package/lib/pr/pr-renderer.js +644 -0
  102. package/lib/pr/related-tests.js +296 -0
  103. package/lib/rag/DataSourcePlugin.js +5 -5
  104. package/lib/rag/EmbeddingProvider.js +19 -8
  105. package/lib/rag/Indexer.js +14 -5
  106. package/lib/rag/VectorStoreFactory.js +3 -3
  107. package/lib/rag/sources/FileDataSource.js +6 -6
  108. package/lib/rag/stores/BaseVectorStore.js +5 -5
  109. package/lib/rag/stores/ChromaDBStore.js +10 -10
  110. package/lib/rag/stores/LanceDBStore.js +29 -19
  111. package/lib/rag/stores/PineconeStore.js +11 -9
  112. package/lib/search/bm25.js +433 -0
  113. package/lib/search/code-units.js +201 -0
  114. package/lib/search/symbols.js +189 -0
  115. package/lib/ui/dashboard.js +120 -89
  116. package/lib/ui/index.js +1 -6
  117. package/lib/ui/init-wizard-ui.js +550 -0
  118. package/lib/ui/progress-bar.js +94 -88
  119. package/lib/ui/select-input.js +40 -32
  120. package/lib/ui/template-selector.js +99 -0
  121. package/lib/ui/wizard.js +374 -329
  122. package/lib/utils/claude-token-counter.js +151 -0
  123. package/lib/utils/cli-output.js +17 -0
  124. package/lib/utils/clipboard-utils.js +49 -50
  125. package/lib/utils/config-utils.js +81 -66
  126. package/lib/utils/context-versioning.js +532 -0
  127. package/lib/utils/error-handler.js +112 -112
  128. package/lib/utils/file-utils.js +153 -69
  129. package/lib/utils/format-converter.js +356 -283
  130. package/lib/utils/git-utils.js +366 -294
  131. package/lib/utils/llm-cost-estimator.js +281 -0
  132. package/lib/utils/llm-detector.js +242 -78
  133. package/lib/utils/logger.js +243 -237
  134. package/lib/utils/messages.js +355 -0
  135. package/lib/utils/multi-repo-manager.js +364 -0
  136. package/lib/utils/profile-manager.js +380 -0
  137. package/lib/utils/safe-read.js +61 -0
  138. package/lib/utils/secret-redactor.js +61 -0
  139. package/lib/utils/snapshot-manager.js +679 -0
  140. package/lib/utils/template-manager.js +378 -0
  141. package/lib/utils/token-utils.js +201 -170
  142. package/lib/utils/tokenizer-adapter.js +421 -377
  143. package/lib/utils/updater.js +539 -506
  144. package/lib/watch/FileWatcher.js +12 -8
  145. package/lib/watch/IncrementalAnalyzer.js +7 -7
  146. package/lib/why/coupling.js +81 -0
  147. package/lib/why/errors.js +23 -0
  148. package/lib/why/git.js +169 -0
  149. package/lib/why/history.js +108 -0
  150. package/lib/why/index.js +720 -0
  151. package/lib/why/ownership.js +79 -0
  152. package/lib/why/paths.js +207 -0
  153. package/lib/why/references.js +259 -0
  154. package/lib/why/render.js +722 -0
  155. package/lib/why/search.js +358 -0
  156. package/lib/why/target.js +389 -0
  157. package/lib/wizards/config-generator.js +255 -0
  158. package/lib/wizards/project-detector.js +571 -0
  159. package/package.json +44 -10
  160. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  161. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -1,269 +1,328 @@
1
- import { getLogger } from "../../utils/logger.js";
2
- import { Scanner } from "../../core/Scanner.js";
3
- import path from "path";
4
- import fs from "fs";
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("MCP:Resources");
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
- constructor(projectPath) {
14
- this.projectPath = projectPath;
15
- this.scanner = null;
16
- this.cache = new Map();
17
- this.supportedTypes = {
18
- FILE: "file",
19
- ANALYSIS: "analysis",
20
- CONTEXT: "context"
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
- * List available resources with optional pagination
26
- * @param {string} cursor - Optional cursor for pagination
27
- * @returns {Promise<{resources: Array, nextCursor?: string}>}
28
- */
29
- async listResources(cursor = null) {
30
- try {
31
- logger.info(`Listing resources${cursor ? ` from cursor: ${cursor}` : ""}`);
32
-
33
- // Initialize scanner if needed
34
- if (!this.scanner && this.projectPath) {
35
- this.scanner = new Scanner(this.projectPath);
36
- }
37
-
38
- const resources = [];
39
-
40
- // Add file resources if project path is set
41
- if (this.scanner) {
42
- const files = this.scanner.scan();
43
- const PAGE_SIZE = 100;
44
- const startIdx = cursor ? parseInt(cursor, 10) : 0;
45
- const endIdx = Math.min(startIdx + PAGE_SIZE, files.length);
46
-
47
- for (let i = startIdx; i < endIdx; i++) {
48
- const file = files[i];
49
- resources.push({
50
- uri: `file://codebase/${file.relativePath}`,
51
- name: path.basename(file.relativePath),
52
- description: `Source file: ${file.relativePath}`,
53
- mimeType: this.getMimeType(file.relativePath)
54
- });
55
- }
56
-
57
- // Return next cursor if there are more results
58
- const nextCursor = endIdx < files.length ? endIdx.toString() : undefined;
59
-
60
- logger.info(`Listed ${resources.length} resources`);
61
- return { resources, nextCursor };
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
- // Security check: ensure path is within project
137
- const resolvedPath = path.resolve(fullPath);
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
- if (!resolvedPath.startsWith(resolvedProject)) {
141
- throw new Error("Access denied: path outside project directory");
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
- return {
147
- contents: [{
148
- uri: `file://codebase/${relativePath}`,
149
- mimeType: this.getMimeType(relativePath),
150
- text: content
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
- * Read an analysis resource from cache
157
- * @private
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
- return {
167
- contents: [{
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
- * Read a context template resource
177
- * @private
178
- */
179
- async readContextResource(name) {
180
- const cached = this.cache.get(`context:${name}`);
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
- if (!cached) {
183
- throw new Error(`Context template not found: ${name}`);
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
- contents: [{
188
- uri: `context://template/${name}`,
189
- mimeType: "text/plain",
190
- text: cached
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
- * Cache an analysis result
197
- * @param {string} id - Resource ID
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
- * Cache a context template
207
- * @param {string} name - Template name
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
- * Parse a resource URI
217
- * @private
218
- */
219
- parseUri(uri) {
220
- const fileMatch = uri.match(/^file:\/\/codebase\/(.+)$/);
221
- if (fileMatch) {
222
- return { type: this.supportedTypes.FILE, path: fileMatch[1] };
223
- }
224
-
225
- const analysisMatch = uri.match(/^analysis:\/\/recent\/(.+)$/);
226
- if (analysisMatch) {
227
- return { type: this.supportedTypes.ANALYSIS, id: analysisMatch[1] };
228
- }
229
-
230
- const contextMatch = uri.match(/^context:\/\/template\/(.+)$/);
231
- if (contextMatch) {
232
- return { type: this.supportedTypes.CONTEXT, name: contextMatch[1] };
233
- }
234
-
235
- throw new Error(`Invalid resource URI: ${uri}`);
236
- }
237
-
238
- /**
239
- * Get MIME type for a file
240
- * @private
241
- */
242
- getMimeType(filePath) {
243
- const ext = path.extname(filePath).toLowerCase();
244
-
245
- const mimeTypes = {
246
- ".js": "text/javascript",
247
- ".ts": "text/typescript",
248
- ".jsx": "text/javascript",
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
+ }