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.
Files changed (224) 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 +381 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -67
  18. package/bin/cm-audit.js +121 -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 +687 -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 +772 -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 +116 -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 +312 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +53 -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/ask.js +122 -0
  108. package/lib/rag/sources/FileDataSource.js +12 -9
  109. package/lib/rag/stores/BaseVectorStore.js +5 -5
  110. package/lib/rag/stores/ChromaDBStore.js +10 -10
  111. package/lib/rag/stores/LanceDBStore.js +29 -19
  112. package/lib/rag/stores/PineconeStore.js +11 -9
  113. package/lib/search/bm25.js +433 -0
  114. package/lib/search/code-units.js +201 -0
  115. package/lib/search/symbols.js +189 -0
  116. package/lib/ui/dashboard.js +120 -89
  117. package/lib/ui/index.js +1 -6
  118. package/lib/ui/init-wizard-ui.js +550 -0
  119. package/lib/ui/progress-bar.js +94 -88
  120. package/lib/ui/select-input.js +40 -32
  121. package/lib/ui/template-selector.js +99 -0
  122. package/lib/ui/wizard.js +374 -329
  123. package/lib/utils/claude-token-counter.js +151 -0
  124. package/lib/utils/cli-output.js +17 -0
  125. package/lib/utils/clipboard-utils.js +49 -50
  126. package/lib/utils/config-utils.js +81 -66
  127. package/lib/utils/context-versioning.js +532 -0
  128. package/lib/utils/error-handler.js +112 -112
  129. package/lib/utils/file-utils.js +153 -69
  130. package/lib/utils/format-converter.js +356 -283
  131. package/lib/utils/git-utils.js +366 -294
  132. package/lib/utils/llm-cost-estimator.js +281 -0
  133. package/lib/utils/llm-detector.js +242 -78
  134. package/lib/utils/logger.js +246 -237
  135. package/lib/utils/messages.js +355 -0
  136. package/lib/utils/multi-repo-manager.js +364 -0
  137. package/lib/utils/profile-manager.js +380 -0
  138. package/lib/utils/safe-read.js +61 -0
  139. package/lib/utils/secret-redactor.js +61 -0
  140. package/lib/utils/snapshot-manager.js +679 -0
  141. package/lib/utils/template-manager.js +378 -0
  142. package/lib/utils/token-utils.js +201 -170
  143. package/lib/utils/tokenizer-adapter.js +421 -377
  144. package/lib/utils/updater.js +539 -506
  145. package/lib/watch/FileWatcher.js +12 -8
  146. package/lib/watch/IncrementalAnalyzer.js +7 -7
  147. package/lib/why/coupling.js +81 -0
  148. package/lib/why/errors.js +23 -0
  149. package/lib/why/git.js +169 -0
  150. package/lib/why/history.js +108 -0
  151. package/lib/why/index.js +720 -0
  152. package/lib/why/ownership.js +79 -0
  153. package/lib/why/paths.js +207 -0
  154. package/lib/why/references.js +259 -0
  155. package/lib/why/render.js +722 -0
  156. package/lib/why/search.js +358 -0
  157. package/lib/why/target.js +389 -0
  158. package/lib/wizards/config-generator.js +257 -0
  159. package/lib/wizards/project-detector.js +572 -0
  160. package/package.json +44 -10
  161. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  224. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Ctxman Language Server Protocol (LSP) Server
3
+ *
4
+ * This module provides LSP server functionality for IDE integrations.
5
+ * It enables real-time token counting and context generation features.
6
+ *
7
+ * This is a stub implementation. The full LSP server requires the
8
+ * vscode-languageserver package to be installed. When available, the
9
+ * server provides real-time token counting and context generation.
10
+ *
11
+ * @version 1.0.0
12
+ * @module lib/lsp/server
13
+ */
14
+
15
+ // Characters per token by model family, for the editor's live estimate; exact counts
16
+ // come from the CLI (`ctxman count --model <id>`). Claude's tokenizer yields more tokens
17
+ // for the same text than OpenAI's, hence the lower ratio.
18
+ const MODEL_TOKEN_RATIOS = {
19
+ claude: 3.5,
20
+ gpt: 4,
21
+ gemini: 4,
22
+ deepseek: 4,
23
+ mistral: 3.5,
24
+ codestral: 3.5,
25
+ grok: 4,
26
+ qwen: 4,
27
+ llama: 4,
28
+ };
29
+
30
+ let currentModel = 'claude-sonnet-5-5';
31
+
32
+ /**
33
+ * Estimate token count for text
34
+ * @param {string} text - Text to estimate
35
+ * @param {string} model - Target model id (e.g. claude-sonnet-5-5, gpt-6.1-sol)
36
+ * @returns {number} Estimated token count
37
+ */
38
+ function estimateTokens(text, model = currentModel) {
39
+ if (!text) return 0;
40
+ const family = Object.keys(MODEL_TOKEN_RATIOS).find((key) =>
41
+ String(model).toLowerCase().startsWith(key)
42
+ );
43
+ const ratio = MODEL_TOKEN_RATIOS[family] || 4;
44
+ return Math.ceil(text.length / ratio);
45
+ }
46
+
47
+ // Stub connection and documents for when vscode-languageserver is not available
48
+ let connection = null;
49
+ let documents = null;
50
+
51
+ // Try to import vscode-languageserver if available
52
+ try {
53
+ const vscodeLsp = await import('vscode-languageserver/node.js');
54
+ const vscodeTextDocument = await import('vscode-languageserver-textdocument');
55
+
56
+ const { createConnection, TextDocuments, ProposedFeatures, TextDocumentSyncKind } = vscodeLsp;
57
+ const { TextDocument } = vscodeTextDocument;
58
+
59
+ // Create a connection for the server
60
+ connection = createConnection(ProposedFeatures.all);
61
+
62
+ // Create a simple text document manager
63
+ documents = new TextDocuments(TextDocument);
64
+
65
+ // Token estimation cache
66
+ const tokenCache = new Map();
67
+
68
+ // Server configuration
69
+ let hasConfigurationCapability = false;
70
+ let hasWorkspaceFolderCapability = false;
71
+ let _hasDiagnosticRelatedInformationCapability = false;
72
+
73
+ /**
74
+ * Handle initialize request
75
+ */
76
+ connection.onInitialize((params) => {
77
+ const capabilities = params.capabilities;
78
+
79
+ // Does the client support the `workspace/configuration` request?
80
+ hasConfigurationCapability = !!(
81
+ capabilities.workspace && !!capabilities.workspace.configuration
82
+ );
83
+ hasWorkspaceFolderCapability = !!(
84
+ capabilities.workspace && !!capabilities.workspace.workspaceFolders
85
+ );
86
+ _hasDiagnosticRelatedInformationCapability = !!(
87
+ capabilities.textDocument &&
88
+ capabilities.textDocument.publishDiagnostics &&
89
+ capabilities.textDocument.publishDiagnostics.relatedInformation
90
+ );
91
+
92
+ const result = {
93
+ capabilities: {
94
+ textDocumentSync: TextDocumentSyncKind.Incremental,
95
+ // Tell the client that this server supports code completion
96
+ completionProvider: {
97
+ resolveProvider: true,
98
+ },
99
+ // Custom commands
100
+ executeCommandProvider: {
101
+ commands: ['ctxman.generateContext', 'ctxman.analyzeFile', 'ctxman.getTokenCount'],
102
+ },
103
+ },
104
+ };
105
+
106
+ if (hasWorkspaceFolderCapability) {
107
+ result.capabilities.workspace = {
108
+ workspaceFolders: {
109
+ supported: true,
110
+ },
111
+ };
112
+ }
113
+
114
+ return result;
115
+ });
116
+
117
+ /**
118
+ * Handle initialized notification
119
+ */
120
+ connection.onInitialized(() => {
121
+ if (hasConfigurationCapability) {
122
+ // Register for all configuration changes
123
+ connection.client.register(vscodeLsp.DidChangeConfigurationNotification.type, undefined);
124
+ }
125
+
126
+ connection.console.log('Ctxman LSP Server initialized');
127
+ });
128
+
129
+ /**
130
+ * Handle configuration changes
131
+ */
132
+ connection.onDidChangeConfiguration((_change) => {
133
+ if (hasConfigurationCapability) {
134
+ // Reset all cached document settings
135
+ tokenCache.clear();
136
+ }
137
+
138
+ // Revalidate all open text documents
139
+ documents.all().forEach(validateTextDocument);
140
+ });
141
+
142
+ /**
143
+ * Validate text document and compute token count
144
+ * @param {TextDocument} textDocument
145
+ */
146
+ async function validateTextDocument(textDocument) {
147
+ const text = textDocument.getText();
148
+ const tokens = estimateTokens(text);
149
+
150
+ // Cache the token count
151
+ tokenCache.set(textDocument.uri, {
152
+ tokens,
153
+ timestamp: Date.now(),
154
+ });
155
+
156
+ // Send token count notification
157
+ connection.sendNotification('ctxman/tokenCount', {
158
+ uri: textDocument.uri,
159
+ tokens,
160
+ model: currentModel,
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Handle document open
166
+ */
167
+ connection.onDidOpenTextDocument((params) => {
168
+ const document = documents.get(params.textDocument.uri);
169
+ if (document) {
170
+ validateTextDocument(document);
171
+ }
172
+ });
173
+
174
+ /**
175
+ * Handle document change
176
+ */
177
+ connection.onDidChangeTextDocument((params) => {
178
+ const document = documents.get(params.textDocument.uri);
179
+ if (document) {
180
+ validateTextDocument(document);
181
+ }
182
+ });
183
+
184
+ /**
185
+ * Handle document save
186
+ */
187
+ connection.onDidSaveTextDocument((params) => {
188
+ const document = documents.get(params.textDocument.uri);
189
+ if (document) {
190
+ validateTextDocument(document);
191
+ }
192
+ });
193
+
194
+ /**
195
+ * Handle custom command: generateContext
196
+ */
197
+ connection.onExecuteCommand((params) => {
198
+ if (params.command === 'ctxman.generateContext') {
199
+ const [uri, options = {}] = params.arguments || [];
200
+
201
+ // TODO: Integrate with ctxman CLI
202
+ // For now, return stub response
203
+ return {
204
+ success: true,
205
+ message: 'Context generation is a stub. Integration with ctxman CLI coming soon.',
206
+ uri,
207
+ options,
208
+ };
209
+ }
210
+
211
+ if (params.command === 'ctxman.getTokenCount') {
212
+ const [uri] = params.arguments || [];
213
+ const cached = tokenCache.get(uri);
214
+
215
+ if (cached) {
216
+ return {
217
+ uri,
218
+ tokens: cached.tokens,
219
+ model: currentModel,
220
+ timestamp: cached.timestamp,
221
+ };
222
+ }
223
+
224
+ const document = documents.get(uri);
225
+ if (document) {
226
+ const tokens = estimateTokens(document.getText());
227
+ return {
228
+ uri,
229
+ tokens,
230
+ model: currentModel,
231
+ };
232
+ }
233
+
234
+ return null;
235
+ }
236
+
237
+ if (params.command === 'ctxman.analyzeFile') {
238
+ const [uri] = params.arguments || [];
239
+ const document = documents.get(uri);
240
+
241
+ if (document) {
242
+ const text = document.getText();
243
+ const tokens = estimateTokens(text);
244
+ const lines = text.split('\n').length;
245
+ const chars = text.length;
246
+
247
+ return {
248
+ uri,
249
+ tokens,
250
+ lines,
251
+ characters: chars,
252
+ tokensPerLine: Math.round(tokens / lines),
253
+ model: currentModel,
254
+ };
255
+ }
256
+
257
+ return null;
258
+ }
259
+
260
+ return null;
261
+ });
262
+
263
+ /**
264
+ * Handle completion request
265
+ */
266
+ connection.onCompletion((textDocumentPosition) => {
267
+ // The pass parameter contains the position of the text document in
268
+ // which code complete got requested
269
+ const document = documents.get(textDocumentPosition.textDocument.uri);
270
+ if (!document) {
271
+ return [];
272
+ }
273
+
274
+ // Return stub completion items
275
+ // Future: provide context-aware suggestions
276
+ return [];
277
+ });
278
+
279
+ /**
280
+ * Handle completion resolve
281
+ */
282
+ connection.onCompletionResolve((item) => {
283
+ return item;
284
+ });
285
+
286
+ // Make the text document manager listen on the connection
287
+ // for open, change and close text document events
288
+ documents.listen(connection);
289
+
290
+ // Listen on the connection
291
+ connection.listen();
292
+ } catch (_error) {
293
+ // vscode-languageserver not available - running in stub mode
294
+ console.log('Ctxman LSP: Running in stub mode (vscode-languageserver not available)');
295
+ connection = {
296
+ onInitialize: () => {},
297
+ onInitialized: () => {},
298
+ listen: () => {},
299
+ console: { log: () => {} },
300
+ };
301
+ documents = {
302
+ get: () => null,
303
+ all: () => [],
304
+ listen: () => {},
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Export for testing
310
+ */
311
+ export { estimateTokens, MODEL_TOKEN_RATIOS, connection, documents };
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Repository graph for `ctxman map`
3
+ *
4
+ * One node per source file the scanner returns (so .gitignore, .contextignore and
5
+ * .contextinclude apply), one edge per resolved import, plus what ranking needs: the
6
+ * functions of each file, how often other files mention them, and how often git
7
+ * changed each file recently.
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { execFileSync } from 'child_process';
13
+ import { Scanner } from '../core/Scanner.js';
14
+ import MethodAnalyzer from '../analyzers/method-analyzer.js';
15
+ import { redactSecrets } from '../utils/secret-redactor.js';
16
+ import { languageOf, extractImports, extractDeclarations } from './imports.js';
17
+ import { ImportResolver } from './resolver.js';
18
+
19
+ /** Commits of git history mined by default */
20
+ export const DEFAULT_HISTORY = 300;
21
+ /** Larger files are left out: they are generated or vendored far more often than written */
22
+ export const DEFAULT_MAX_FILE_BYTES = 512 * 1024;
23
+ /** Average characters per line above which a file counts as minified */
24
+ const MINIFIED_LINE_LENGTH = 300;
25
+ /** Longest signature kept on one map line */
26
+ const MAX_SIGNATURE_LENGTH = 120;
27
+ const IDENTIFIER = /[A-Za-z_$][\w$]*/g;
28
+
29
+ /**
30
+ * @typedef {Object} MapMethod
31
+ * @property {string} name - Function or method name
32
+ * @property {number} line - 1-based line of the declaration
33
+ * @property {string} signature - Declaration text on one line (secrets redacted)
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} MapNode
38
+ * @property {string} path - Project-relative path with forward slashes
39
+ * @property {string} language
40
+ * @property {number} bytes
41
+ * @property {MapMethod[]} methods
42
+ * @property {number} symbolRefs - How much other files mention this file's function names
43
+ * @property {number} unresolvedImports - References that named no file of the project
44
+ */
45
+
46
+ /**
47
+ * @typedef {Object} RepoGraph
48
+ * @property {string} root - Absolute project root
49
+ * @property {MapNode[]} nodes - Sorted by path
50
+ * @property {Array<{from: string, to: string, weight: number}>} edges - Importer → imported
51
+ * @property {{available: boolean, requested: number, commits: number,
52
+ * counts: Object<string, number>}} history - Commits touching each file
53
+ * @property {Object} stats
54
+ */
55
+
56
+ /**
57
+ * Scan a project and build its file dependency graph
58
+ * @param {string} root - Project directory
59
+ * @param {Object} [options]
60
+ * @param {number} [options.history=300] - Commits to mine for change frequency (0 disables)
61
+ * @param {boolean} [options.redact=true] - Mask secrets in signatures
62
+ * @param {number} [options.maxFileBytes=524288] - Skip larger files
63
+ * @returns {RepoGraph}
64
+ */
65
+ export function buildRepoGraph(root, options = {}) {
66
+ const history = options.history ?? DEFAULT_HISTORY;
67
+ const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
68
+ const redact = options.redact !== false;
69
+ if (!Number.isInteger(history) || history < 0) {
70
+ throw new Error(`history must be a non-negative integer, got ${JSON.stringify(history)}`);
71
+ }
72
+
73
+ const absoluteRoot = path.resolve(root);
74
+ if (!fs.statSync(absoluteRoot, { throwIfNoEntry: false })?.isDirectory()) {
75
+ throw new Error(`Not a directory: ${root}`);
76
+ }
77
+
78
+ const timings = {};
79
+ let started = performance.now();
80
+ const scanned = new Scanner(absoluteRoot).scan();
81
+ timings.scan = performance.now() - started;
82
+
83
+ started = performance.now();
84
+ const analyzer = new MethodAnalyzer();
85
+ const redactions = {};
86
+ const skipped = [];
87
+ const nodes = [];
88
+ const identifiers = [];
89
+ const candidates = scanned
90
+ .map((file) => ({ file, relative: file.relativePath.split(path.sep).join('/') }))
91
+ .filter(({ relative }) => languageOf(relative))
92
+ .sort((a, b) => compare(a.relative, b.relative));
93
+
94
+ for (const { file, relative } of candidates) {
95
+ if (file.size > maxFileBytes) {
96
+ skipped.push({ path: relative, reason: 'too large' });
97
+ continue;
98
+ }
99
+ let content;
100
+ try {
101
+ content = fs.readFileSync(file.path, 'utf-8');
102
+ } catch (_error) {
103
+ skipped.push({ path: relative, reason: 'unreadable' });
104
+ continue;
105
+ }
106
+ const lines = content.split('\n');
107
+ if (content.length > 4096 && content.length / lines.length > MINIFIED_LINE_LENGTH) {
108
+ skipped.push({ path: relative, reason: 'minified' });
109
+ continue;
110
+ }
111
+
112
+ const language = languageOf(relative);
113
+ nodes.push({
114
+ path: relative,
115
+ language,
116
+ bytes: file.size,
117
+ methods: extractMethods(analyzer, content, lines, file.path, redact ? redactions : null),
118
+ symbolRefs: 0,
119
+ unresolvedImports: 0,
120
+ imports: extractImports(language, content),
121
+ declarations: extractDeclarations(language, content),
122
+ });
123
+ identifiers.push(new Set(content.match(IDENTIFIER)));
124
+ }
125
+ timings.parse = performance.now() - started;
126
+
127
+ started = performance.now();
128
+ const resolver = new ImportResolver(absoluteRoot, nodes);
129
+ const edges = [];
130
+ for (const node of nodes) {
131
+ const { edges: targets, unresolved } = resolver.resolve(node);
132
+ node.unresolvedImports = unresolved;
133
+ for (const { to, weight } of targets) edges.push({ from: node.path, to, weight });
134
+ }
135
+ edges.sort((a, b) => compare(a.from, b.from) || compare(a.to, b.to));
136
+ for (const node of nodes) {
137
+ delete node.imports;
138
+ delete node.declarations;
139
+ }
140
+ countSymbolReferences(nodes, identifiers);
141
+ timings.resolve = performance.now() - started;
142
+
143
+ started = performance.now();
144
+ const mined = mineHistory(absoluteRoot, history);
145
+ const known = new Set(nodes.map((node) => node.path));
146
+ mined.counts = Object.fromEntries(
147
+ Object.entries(mined.counts).filter(([file]) => known.has(file))
148
+ );
149
+ timings.history = performance.now() - started;
150
+
151
+ return {
152
+ root: absoluteRoot,
153
+ nodes,
154
+ edges,
155
+ history: mined,
156
+ stats: {
157
+ scannedFiles: scanned.length,
158
+ files: nodes.length,
159
+ edges: edges.length,
160
+ unresolvedImports: nodes.reduce((sum, node) => sum + node.unresolvedImports, 0),
161
+ skipped,
162
+ redactions,
163
+ timings,
164
+ },
165
+ };
166
+ }
167
+
168
+ /** Byte-order comparison, the same on every platform and locale */
169
+ function compare(a, b) {
170
+ return a < b ? -1 : a > b ? 1 : 0;
171
+ }
172
+
173
+ /**
174
+ * Functions of a file with a one-line signature each
175
+ * @returns {MapMethod[]}
176
+ */
177
+ function extractMethods(analyzer, content, lines, filePath, redactions) {
178
+ const found = analyzer
179
+ .extractMethods(content, filePath)
180
+ .sort((a, b) => a.line - b.line || compare(a.name, b.name));
181
+ return found.map((method) => {
182
+ let signature = signatureAt(lines, method.line);
183
+ if (redactions) {
184
+ const result = redactSecrets(signature);
185
+ signature = result.text;
186
+ for (const type of result.findings) redactions[type] = (redactions[type] || 0) + 1;
187
+ }
188
+ return { name: method.name, line: method.line, signature };
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Declaration starting at a line, joined onto one line until its parameter list closes,
194
+ * without the body
195
+ * @param {string[]} lines
196
+ * @param {number} lineNumber - 1-based
197
+ * @returns {string}
198
+ */
199
+ export function signatureAt(lines, lineNumber) {
200
+ let text = (lines[lineNumber - 1] || '').trim();
201
+ const depth = (value) => (value.match(/\(/g) || []).length - (value.match(/\)/g) || []).length;
202
+ for (let i = lineNumber; depth(text) > 0 && i < lineNumber + 6 && i < lines.length; i++) {
203
+ const next = lines[i].trim();
204
+ text += text.endsWith('(') || next.startsWith(')') ? next : ` ${next}`;
205
+ }
206
+ text = text.replace(/\s+/g, ' ');
207
+ // A one-line body (`f() { return 1; }`) is cut after the parameter list
208
+ const body = text.search(/\)\s*\{/);
209
+ if (body !== -1) text = text.slice(0, body + 1);
210
+ text = text.replace(/\s*\{\s*\}?\s*$/, '').trim();
211
+ if (text.length > MAX_SIGNATURE_LENGTH) text = `${text.slice(0, MAX_SIGNATURE_LENGTH - 1)}…`;
212
+ return text;
213
+ }
214
+
215
+ /**
216
+ * symbolRefs of each node: for each function name it defines, the number of other files
217
+ * that mention the name, shared between the files that define the same name (so a
218
+ * `constructor` or `render` defined everywhere earns little)
219
+ */
220
+ function countSymbolReferences(nodes, identifiers) {
221
+ const definedNames = nodes.map(
222
+ (node) =>
223
+ new Set(
224
+ node.methods.map((method) => method.name.split(' ').pop()).filter((name) => name.length > 2)
225
+ )
226
+ );
227
+ const definers = new Map();
228
+ for (const names of definedNames) {
229
+ for (const name of names) definers.set(name, (definers.get(name) || 0) + 1);
230
+ }
231
+ const mentions = new Map();
232
+ for (const ids of identifiers) {
233
+ for (const id of ids) {
234
+ if (definers.has(id)) mentions.set(id, (mentions.get(id) || 0) + 1);
235
+ }
236
+ }
237
+ nodes.forEach((node, i) => {
238
+ let score = 0;
239
+ for (const name of definedNames[i]) {
240
+ const defining = definers.get(name);
241
+ score += Math.max(0, (mentions.get(name) || 0) - defining) / defining;
242
+ }
243
+ node.symbolRefs = score;
244
+ });
245
+ }
246
+
247
+ /**
248
+ * How many of the last `commits` commits (merges excluded) touched each file
249
+ * @param {string} root - Absolute project root; paths come back relative to it
250
+ * @param {number} commits - 0 disables
251
+ * @returns {{available: boolean, requested: number, commits: number,
252
+ * counts: Object<string, number>, reason?: string}}
253
+ */
254
+ export function mineHistory(root, commits) {
255
+ const result = { available: false, requested: commits, commits: 0, counts: {} };
256
+ if (!commits) {
257
+ result.reason = 'disabled';
258
+ return result;
259
+ }
260
+ let output;
261
+ try {
262
+ output = execFileSync(
263
+ 'git',
264
+ [
265
+ '-c',
266
+ 'core.quotePath=false',
267
+ 'log',
268
+ `--max-count=${commits}`,
269
+ '--no-merges',
270
+ '--name-only',
271
+ '--relative',
272
+ '--format=%x00',
273
+ ],
274
+ {
275
+ cwd: root,
276
+ encoding: 'utf-8',
277
+ maxBuffer: 256 * 1024 * 1024,
278
+ stdio: ['ignore', 'pipe', 'ignore'],
279
+ }
280
+ );
281
+ } catch (_error) {
282
+ result.reason = 'not a git repository';
283
+ return result;
284
+ }
285
+
286
+ const chunks = output.split('\0').slice(1);
287
+ for (const chunk of chunks) {
288
+ const files = new Set(
289
+ chunk
290
+ .split('\n')
291
+ .map((line) => line.trim())
292
+ .filter(Boolean)
293
+ );
294
+ for (const file of files) result.counts[file] = (result.counts[file] || 0) + 1;
295
+ }
296
+ result.available = true;
297
+ result.commits = chunks.length;
298
+ return result;
299
+ }