ctxman 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +381 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -67
- package/bin/cm-audit.js +121 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +687 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +772 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +116 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +312 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +53 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/ask.js +122 -0
- package/lib/rag/sources/FileDataSource.js +12 -9
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +246 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +257 -0
- package/lib/wizards/project-detector.js +572 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
|
@@ -19,6 +19,12 @@ let profilesCache = null;
|
|
|
19
19
|
// Logger instance
|
|
20
20
|
const logger = getLogger('LLMDetector');
|
|
21
21
|
|
|
22
|
+
/** Model used when nothing is configured or detected (a current, widely available model) */
|
|
23
|
+
export const DEFAULT_TARGET_MODEL = 'claude-sonnet-5-5';
|
|
24
|
+
|
|
25
|
+
// Deprecated/retired notices already printed in this process
|
|
26
|
+
const warnedModels = new Set();
|
|
27
|
+
|
|
22
28
|
export class LLMDetector {
|
|
23
29
|
/**
|
|
24
30
|
* Load LLM profiles from JSON config
|
|
@@ -37,6 +43,7 @@ export class LLMDetector {
|
|
|
37
43
|
const profilesData = JSON.parse(readFileSync(profilesPath, 'utf-8'));
|
|
38
44
|
const builtInProfiles = profilesData.profiles || {};
|
|
39
45
|
const defaultProfile = profilesData.default;
|
|
46
|
+
let aliases = profilesData.aliases || {};
|
|
40
47
|
|
|
41
48
|
// Try to load custom profiles
|
|
42
49
|
let customProfiles = {};
|
|
@@ -44,8 +51,9 @@ export class LLMDetector {
|
|
|
44
51
|
const customPath = resolve(__dirname, '../../.ctxman/custom-profiles.json');
|
|
45
52
|
const customData = JSON.parse(readFileSync(customPath, 'utf-8'));
|
|
46
53
|
customProfiles = customData.profiles || {};
|
|
54
|
+
aliases = { ...aliases, ...(customData.aliases || {}) };
|
|
47
55
|
logger.debug(`Loaded ${Object.keys(customProfiles).length} custom LLM profiles`);
|
|
48
|
-
} catch (
|
|
56
|
+
} catch (_error) {
|
|
49
57
|
// Custom profiles optional
|
|
50
58
|
logger.debug('No custom profiles found (optional)');
|
|
51
59
|
}
|
|
@@ -53,51 +61,44 @@ export class LLMDetector {
|
|
|
53
61
|
// Merge profiles (custom overrides built-in)
|
|
54
62
|
const allProfiles = {
|
|
55
63
|
...builtInProfiles,
|
|
56
|
-
...customProfiles
|
|
64
|
+
...customProfiles,
|
|
57
65
|
};
|
|
58
66
|
|
|
59
67
|
// Cache the result
|
|
60
68
|
profilesCache = {
|
|
61
69
|
profiles: allProfiles,
|
|
62
|
-
default: defaultProfile
|
|
70
|
+
default: defaultProfile,
|
|
71
|
+
aliases,
|
|
72
|
+
lastUpdated: profilesData.lastUpdated,
|
|
63
73
|
};
|
|
64
74
|
|
|
65
75
|
logger.debug(`Loaded ${Object.keys(allProfiles).length} total LLM profiles`);
|
|
66
76
|
return profilesCache;
|
|
67
|
-
|
|
68
77
|
} catch (error) {
|
|
69
78
|
logger.error(`Failed to load LLM profiles: ${error.message}`);
|
|
70
79
|
|
|
71
80
|
// Fallback to minimal built-in if JSON load fails
|
|
72
81
|
const fallback = {
|
|
73
82
|
profiles: {
|
|
74
|
-
|
|
75
|
-
name: 'Claude Sonnet
|
|
83
|
+
[DEFAULT_TARGET_MODEL]: {
|
|
84
|
+
name: 'Claude Sonnet 5.5',
|
|
76
85
|
vendor: 'Anthropic',
|
|
77
|
-
contextWindow:
|
|
86
|
+
contextWindow: 1000000,
|
|
78
87
|
utilizationPercentage: 0.6,
|
|
79
|
-
maxRecommendedInput:
|
|
88
|
+
maxRecommendedInput: 600000,
|
|
80
89
|
preferredFormat: 'toon',
|
|
81
|
-
chunkStrategy: '
|
|
90
|
+
chunkStrategy: 'none',
|
|
82
91
|
},
|
|
83
|
-
'gpt-4o': {
|
|
84
|
-
name: 'GPT-4o',
|
|
85
|
-
vendor: 'OpenAI',
|
|
86
|
-
contextWindow: 128000,
|
|
87
|
-
utilizationPercentage: 0.6,
|
|
88
|
-
maxRecommendedInput: 76800,
|
|
89
|
-
preferredFormat: 'json',
|
|
90
|
-
chunkStrategy: 'smart'
|
|
91
|
-
}
|
|
92
92
|
},
|
|
93
|
+
aliases: {},
|
|
93
94
|
default: {
|
|
94
95
|
name: 'Unknown Model',
|
|
95
96
|
contextWindow: 100000,
|
|
96
97
|
utilizationPercentage: 0.6,
|
|
97
98
|
maxRecommendedInput: 60000,
|
|
98
99
|
preferredFormat: 'json',
|
|
99
|
-
chunkStrategy: 'smart'
|
|
100
|
-
}
|
|
100
|
+
chunkStrategy: 'smart',
|
|
101
|
+
},
|
|
101
102
|
};
|
|
102
103
|
|
|
103
104
|
profilesCache = fallback;
|
|
@@ -114,6 +115,14 @@ export class LLMDetector {
|
|
|
114
115
|
return data.default;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Id of the model used when none is configured
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
static getDefaultModelId() {
|
|
123
|
+
return DEFAULT_TARGET_MODEL;
|
|
124
|
+
}
|
|
125
|
+
|
|
117
126
|
/**
|
|
118
127
|
* Get all profiles
|
|
119
128
|
* @returns {object} All LLM profiles
|
|
@@ -147,7 +156,6 @@ export class LLMDetector {
|
|
|
147
156
|
|
|
148
157
|
logger.debug('No LLM detected, using defaults');
|
|
149
158
|
return 'unknown';
|
|
150
|
-
|
|
151
159
|
} finally {
|
|
152
160
|
const elapsed = Date.now() - startTime;
|
|
153
161
|
if (elapsed > 100) {
|
|
@@ -169,24 +177,21 @@ export class LLMDetector {
|
|
|
169
177
|
return process.env.CTXMAN_LLM;
|
|
170
178
|
}
|
|
171
179
|
|
|
172
|
-
//
|
|
180
|
+
// A provider's API key selects that provider's current balanced model
|
|
173
181
|
if (process.env.ANTHROPIC_API_KEY) {
|
|
174
|
-
return 'claude-sonnet-
|
|
182
|
+
return 'claude-sonnet-5-5';
|
|
175
183
|
}
|
|
176
184
|
|
|
177
|
-
// OpenAI (GPT-4)
|
|
178
185
|
if (process.env.OPENAI_API_KEY) {
|
|
179
|
-
return 'gpt-
|
|
186
|
+
return 'gpt-6.1-sol';
|
|
180
187
|
}
|
|
181
188
|
|
|
182
|
-
// Google (Gemini)
|
|
183
189
|
if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) {
|
|
184
|
-
return 'gemini-
|
|
190
|
+
return 'gemini-3.8-flash';
|
|
185
191
|
}
|
|
186
192
|
|
|
187
|
-
// DeepSeek
|
|
188
193
|
if (process.env.DEEPSEEK_API_KEY) {
|
|
189
|
-
return 'deepseek-
|
|
194
|
+
return 'deepseek-flash';
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
return null;
|
|
@@ -220,22 +225,60 @@ export class LLMDetector {
|
|
|
220
225
|
return this.getDefaultProfile();
|
|
221
226
|
}
|
|
222
227
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return allProfiles[modelName];
|
|
228
|
+
const id = this.resolveModelId(modelName);
|
|
229
|
+
if (id) {
|
|
230
|
+
const profile = { id, ...this.getAllProfiles()[id] };
|
|
231
|
+
this.warnIfOutdated(profile);
|
|
232
|
+
return profile;
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
// Fallback to default
|
|
232
|
-
logger.warn(`Unknown LLM model: ${modelName}, using default profile`);
|
|
236
|
+
logger.warn(`Unknown LLM model: ${modelName}, using default profile (see --list-llms)`);
|
|
233
237
|
return {
|
|
234
238
|
...this.getDefaultProfile(),
|
|
235
|
-
name: modelName
|
|
239
|
+
name: modelName,
|
|
236
240
|
};
|
|
237
241
|
}
|
|
238
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Map a model name to the id of its profile: the id itself, an alias (older ctxman
|
|
245
|
+
* ids such as `claude-sonnet-4.5`, provider aliases) or a dated snapshot id
|
|
246
|
+
* (`claude-haiku-4-5-20251001`, `gpt-5-2025-08-07`)
|
|
247
|
+
* @param {string} modelName - Model identifier as given by the user
|
|
248
|
+
* @returns {string|null} Profile id, or null for unknown models
|
|
249
|
+
*/
|
|
250
|
+
static resolveModelId(modelName) {
|
|
251
|
+
if (!modelName) return null;
|
|
252
|
+
const { profiles, aliases = {} } = this.loadProfiles();
|
|
253
|
+
const candidates = [modelName, modelName.toLowerCase()];
|
|
254
|
+
for (const name of candidates) {
|
|
255
|
+
if (profiles[name]) return name;
|
|
256
|
+
if (aliases[name] && profiles[aliases[name]]) return aliases[name];
|
|
257
|
+
}
|
|
258
|
+
const undated = modelName
|
|
259
|
+
.toLowerCase()
|
|
260
|
+
.replace(/-\d{4}-\d{2}-\d{2}$/, '')
|
|
261
|
+
.replace(/-\d{8}$/, '');
|
|
262
|
+
if (undated !== modelName.toLowerCase()) {
|
|
263
|
+
return this.resolveModelId(undated);
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Warn once per process when a profile's model is deprecated or retired by its provider
|
|
270
|
+
* @param {object} profile - Resolved profile (with id)
|
|
271
|
+
*/
|
|
272
|
+
static warnIfOutdated(profile) {
|
|
273
|
+
if (!['deprecated', 'retired'].includes(profile.status) || warnedModels.has(profile.id)) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
warnedModels.add(profile.id);
|
|
277
|
+
const when = profile.retires ? ` (retires ${profile.retires})` : '';
|
|
278
|
+
const instead = profile.replacement ? `; consider ${profile.replacement}` : '';
|
|
279
|
+
logger.warn(`${profile.name} is ${profile.status} by ${profile.vendor}${when}${instead}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
239
282
|
/**
|
|
240
283
|
* Recommend optimal configuration based on LLM profile and repository stats
|
|
241
284
|
* @param {object} profile - LLM profile
|
|
@@ -243,7 +286,7 @@ export class LLMDetector {
|
|
|
243
286
|
* @returns {object} Configuration recommendations
|
|
244
287
|
*/
|
|
245
288
|
static recommendConfiguration(profile, repoStats) {
|
|
246
|
-
const { totalTokens
|
|
289
|
+
const { totalTokens } = repoStats;
|
|
247
290
|
const { maxRecommendedInput, preferredFormat, chunkStrategy, utilizationPercentage } = profile;
|
|
248
291
|
|
|
249
292
|
const fitsInContext = totalTokens <= maxRecommendedInput;
|
|
@@ -261,7 +304,7 @@ export class LLMDetector {
|
|
|
261
304
|
chunksNeeded: chunksNeeded,
|
|
262
305
|
fitsInContext: fitsInContext,
|
|
263
306
|
utilizationPercentage: Math.round((utilizationPercentage || 0.6) * 100),
|
|
264
|
-
utilizationActual: (totalTokens / maxRecommendedInput) * 100
|
|
307
|
+
utilizationActual: (totalTokens / maxRecommendedInput) * 100,
|
|
265
308
|
};
|
|
266
309
|
}
|
|
267
310
|
|
|
@@ -269,9 +312,10 @@ export class LLMDetector {
|
|
|
269
312
|
* Analyze context fit - detailed analysis for display
|
|
270
313
|
* @param {object} profile - LLM profile
|
|
271
314
|
* @param {object} repoStats - Repository statistics
|
|
315
|
+
* @param {object[]} largestFiles - Optional array of largest files for suggestions
|
|
272
316
|
* @returns {object} Context fit analysis
|
|
273
317
|
*/
|
|
274
|
-
static analyzeContextFit(profile, repoStats) {
|
|
318
|
+
static analyzeContextFit(profile, repoStats, largestFiles = []) {
|
|
275
319
|
const { totalTokens, totalFiles } = repoStats;
|
|
276
320
|
const { name, contextWindow, maxRecommendedInput, utilizationPercentage } = profile;
|
|
277
321
|
|
|
@@ -284,6 +328,22 @@ export class LLMDetector {
|
|
|
284
328
|
const chunksNeeded = fitsInOne ? 1 : Math.ceil(totalTokens / usableContext);
|
|
285
329
|
const utilizationActual = (totalTokens / usableContext) * 100; // % of usable context
|
|
286
330
|
|
|
331
|
+
// Calculate percentage of total context window
|
|
332
|
+
const percentageOfTotal = (totalTokens / contextWindow) * 100;
|
|
333
|
+
|
|
334
|
+
// Determine warning level based on thresholds
|
|
335
|
+
let warningLevel = 'ok';
|
|
336
|
+
if (percentageOfTotal >= 100) {
|
|
337
|
+
warningLevel = 'overflow';
|
|
338
|
+
} else if (percentageOfTotal >= 90) {
|
|
339
|
+
warningLevel = 'critical';
|
|
340
|
+
} else if (percentageOfTotal >= 80) {
|
|
341
|
+
warningLevel = 'warning';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Generate file suggestions if needed
|
|
345
|
+
const suggestions = this.generateFileSuggestions(largestFiles, totalTokens, usableContext);
|
|
346
|
+
|
|
287
347
|
return {
|
|
288
348
|
modelName: name,
|
|
289
349
|
contextWindow,
|
|
@@ -295,10 +355,88 @@ export class LLMDetector {
|
|
|
295
355
|
fitsInOne,
|
|
296
356
|
chunksNeeded,
|
|
297
357
|
utilizationActual,
|
|
298
|
-
|
|
358
|
+
percentageOfTotal,
|
|
359
|
+
warningLevel,
|
|
360
|
+
suggestions,
|
|
361
|
+
recommendation: this.getRecommendation(fitsInOne, chunksNeeded, utilizationActual),
|
|
299
362
|
};
|
|
300
363
|
}
|
|
301
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Generate file exclusion suggestions based on largest files
|
|
367
|
+
* @param {object[]} largestFiles - Array of largest files sorted by tokens
|
|
368
|
+
* @param {number} totalTokens - Total repository tokens
|
|
369
|
+
* @param {number} usableContext - Usable context limit
|
|
370
|
+
* @returns {object[]} Array of file suggestions
|
|
371
|
+
*/
|
|
372
|
+
static generateFileSuggestions(largestFiles, totalTokens, usableContext) {
|
|
373
|
+
if (!largestFiles || largestFiles.length === 0) {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const suggestions = [];
|
|
378
|
+
const excessTokens = totalTokens - usableContext;
|
|
379
|
+
|
|
380
|
+
// If we're over budget, suggest files to exclude
|
|
381
|
+
if (excessTokens > 0) {
|
|
382
|
+
let accumulatedSavings = 0;
|
|
383
|
+
|
|
384
|
+
for (const file of largestFiles) {
|
|
385
|
+
if (accumulatedSavings >= excessTokens) break;
|
|
386
|
+
|
|
387
|
+
suggestions.push({
|
|
388
|
+
path: file.relativePath || file.path,
|
|
389
|
+
tokens: file.tokens,
|
|
390
|
+
percentage: ((file.tokens / totalTokens) * 100).toFixed(1),
|
|
391
|
+
reason: this.getFileSuggestionReason(file),
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
accumulatedSavings += file.tokens;
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
// Even if under budget, show top files that could be excluded if needed
|
|
398
|
+
const topFiles = largestFiles.slice(0, 3);
|
|
399
|
+
for (const file of topFiles) {
|
|
400
|
+
suggestions.push({
|
|
401
|
+
path: file.relativePath || file.path,
|
|
402
|
+
tokens: file.tokens,
|
|
403
|
+
percentage: ((file.tokens / totalTokens) * 100).toFixed(1),
|
|
404
|
+
reason: this.getFileSuggestionReason(file),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return suggestions;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Determine reason for file suggestion
|
|
414
|
+
* @param {object} file - File info object
|
|
415
|
+
* @returns {string} Suggestion reason
|
|
416
|
+
*/
|
|
417
|
+
static getFileSuggestionReason(file) {
|
|
418
|
+
const path = file.relativePath || file.path;
|
|
419
|
+
const ext = path.split('.').pop()?.toLowerCase();
|
|
420
|
+
|
|
421
|
+
if (path.includes('.test.') || path.includes('.spec.') || path.includes('__tests__')) {
|
|
422
|
+
return 'Test file';
|
|
423
|
+
}
|
|
424
|
+
if (path.includes('node_modules')) {
|
|
425
|
+
return 'Dependency';
|
|
426
|
+
}
|
|
427
|
+
if (ext === 'md' || path.includes('docs/') || path.includes('documentation/')) {
|
|
428
|
+
return 'Documentation';
|
|
429
|
+
}
|
|
430
|
+
if (ext === 'json' && !path.includes('package.json') && !path.includes('tsconfig.json')) {
|
|
431
|
+
return 'Data file';
|
|
432
|
+
}
|
|
433
|
+
if (path.includes('dist/') || path.includes('build/') || path.includes('coverage/')) {
|
|
434
|
+
return 'Build output';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return 'Large file';
|
|
438
|
+
}
|
|
439
|
+
|
|
302
440
|
/**
|
|
303
441
|
* Get recommendation message based on context fit
|
|
304
442
|
* @param {boolean} fitsInOne - Whether repo fits in one context
|
|
@@ -340,17 +478,32 @@ export class LLMDetector {
|
|
|
340
478
|
|
|
341
479
|
/**
|
|
342
480
|
* Get human-readable list of models with details
|
|
481
|
+
* @param {object} [options]
|
|
482
|
+
* @param {boolean} [options.includeRetired=false] - Also list models the provider has retired
|
|
343
483
|
* @returns {object[]} Array of model details
|
|
344
484
|
*/
|
|
345
|
-
static getModelList() {
|
|
485
|
+
static getModelList({ includeRetired = false } = {}) {
|
|
346
486
|
const allProfiles = this.getAllProfiles();
|
|
347
|
-
return Object.entries(allProfiles)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
487
|
+
return Object.entries(allProfiles)
|
|
488
|
+
.filter(([, profile]) => includeRetired || profile.status !== 'retired')
|
|
489
|
+
.map(([key, profile]) => ({
|
|
490
|
+
id: key,
|
|
491
|
+
name: profile.name,
|
|
492
|
+
vendor: profile.vendor,
|
|
493
|
+
contextWindow: profile.contextWindow,
|
|
494
|
+
outputWindow: profile.outputWindow,
|
|
495
|
+
preferredFormat: profile.preferredFormat,
|
|
496
|
+
status: profile.status || 'active',
|
|
497
|
+
pricing: profile.pricing || null,
|
|
498
|
+
}));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Date the built-in model data was last checked against the providers' docs
|
|
503
|
+
* @returns {string|undefined} ISO date
|
|
504
|
+
*/
|
|
505
|
+
static getDataDate() {
|
|
506
|
+
return this.loadProfiles().lastUpdated;
|
|
354
507
|
}
|
|
355
508
|
|
|
356
509
|
/**
|
|
@@ -365,43 +518,54 @@ export class LLMDetector {
|
|
|
365
518
|
lines.push('📊 Context Window Analysis:');
|
|
366
519
|
lines.push('════════════════════════════════════════════════════');
|
|
367
520
|
lines.push(` Target Model: ${analysis.modelName}`);
|
|
368
|
-
lines.push(`
|
|
369
|
-
|
|
370
|
-
|
|
521
|
+
lines.push(` Available Context: ${analysis.contextWindow.toLocaleString()} tokens`);
|
|
522
|
+
|
|
523
|
+
// Show actual percentage of total context window
|
|
524
|
+
const percentageDisplay = analysis.percentageOfTotal.toFixed(1);
|
|
525
|
+
lines.push(
|
|
526
|
+
` Your Repository: ${analysis.repoTokens.toLocaleString()} tokens (${percentageDisplay}%)`
|
|
527
|
+
);
|
|
371
528
|
lines.push('');
|
|
372
|
-
lines.push(` Your Repository: ${analysis.repoTokens.toLocaleString()} tokens (${analysis.repoFiles} files)`);
|
|
373
529
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
530
|
+
// Warning level display with colors (using ANSI codes)
|
|
531
|
+
if (analysis.warningLevel === 'overflow') {
|
|
532
|
+
// Red for overflow (100%+)
|
|
533
|
+
lines.push(" \x1b[31m⚠️ OVERFLOW: You've exceeded the context window!\x1b[0m");
|
|
534
|
+
lines.push(
|
|
535
|
+
` Exceeds by: ${(analysis.repoTokens - analysis.contextWindow).toLocaleString()} tokens`
|
|
536
|
+
);
|
|
537
|
+
} else if (analysis.warningLevel === 'critical') {
|
|
538
|
+
// Red for critical (90-99%)
|
|
539
|
+
lines.push(" \x1b[31m⚠️ CRITICAL: You're using 90%+ of context window\x1b[0m");
|
|
540
|
+
} else if (analysis.warningLevel === 'warning') {
|
|
541
|
+
// Yellow for warning (80-89%)
|
|
542
|
+
lines.push(" \x1b[33m⚠️ WARNING: You're using 80%+ of context window\x1b[0m");
|
|
543
|
+
} else {
|
|
544
|
+
// Green for OK
|
|
545
|
+
lines.push(' \x1b[32m✅ Context usage is within safe limits\x1b[0m');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Show suggestions if available
|
|
549
|
+
if (analysis.suggestions && analysis.suggestions.length > 0) {
|
|
377
550
|
lines.push('');
|
|
551
|
+
lines.push(' 💡 Consider excluding these large files:');
|
|
378
552
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (analysis.utilizationActual < 50) {
|
|
382
|
-
fitStatus = '✅ EXCELLENT FIT!';
|
|
383
|
-
} else if (analysis.utilizationActual < 80) {
|
|
384
|
-
fitStatus = '✅ GOOD FIT!';
|
|
385
|
-
} else {
|
|
386
|
-
fitStatus = '⚠️ ACCEPTABLE FIT';
|
|
387
|
-
}
|
|
553
|
+
const maxSuggestions = analysis.warningLevel === 'overflow' ? 5 : 3;
|
|
554
|
+
const suggestionsToShow = analysis.suggestions.slice(0, maxSuggestions);
|
|
388
555
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const excess = analysis.repoTokens - analysis.usableContext;
|
|
395
|
-
const excessMultiplier = (analysis.repoTokens / analysis.usableContext).toFixed(1);
|
|
556
|
+
for (const suggestion of suggestionsToShow) {
|
|
557
|
+
lines.push(
|
|
558
|
+
` - ${suggestion.path} (${suggestion.tokens.toLocaleString()} tokens, ${suggestion.percentage}%)`
|
|
559
|
+
);
|
|
560
|
+
}
|
|
396
561
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
lines.push(` 📦 Chunks Needed: ${analysis.chunksNeeded}`);
|
|
401
|
-
lines.push(' 💡 Recommendation: Enable chunking with --chunk flag');
|
|
402
|
-
lines.push(` 💡 Or: Filter files with .contextinclude to fit in ${analysis.usableContext.toLocaleString()} tokens`);
|
|
562
|
+
if (analysis.suggestions.length > maxSuggestions) {
|
|
563
|
+
lines.push(` ... and ${analysis.suggestions.length - maxSuggestions} more`);
|
|
564
|
+
}
|
|
403
565
|
}
|
|
404
566
|
|
|
567
|
+
lines.push('');
|
|
568
|
+
lines.push(` ${analysis.recommendation}`);
|
|
405
569
|
lines.push('');
|
|
406
570
|
|
|
407
571
|
return lines.join('\n');
|