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.
- 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 +380 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -13
- package/bin/cm-audit.js +124 -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 +683 -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 +675 -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 +113 -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 +294 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +52 -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/sources/FileDataSource.js +6 -6
- 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 +243 -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 +255 -0
- package/lib/wizards/project-detector.js +571 -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
|
@@ -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 };
|
package/lib/map/graph.js
ADDED
|
@@ -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
|
+
}
|