ctxman 3.0.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/custom-profiles.example.json +20 -0
- package/.ctxman/llm-profiles.json +131 -0
- package/.ctxman/logs/ctxman-2026-07-02.log +156 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
- package/.ctxman/wizard-profiles/code-review/.contextignore +41 -0
- package/.ctxman/wizard-profiles/code-review/.contextinclude +30 -0
- package/.ctxman/wizard-profiles/code-review/.methodignore +26 -0
- package/.ctxman/wizard-profiles/code-review/.methodinclude +47 -0
- package/.ctxman/wizard-profiles/code-review/profile.json +26 -0
- package/.ctxman/wizard-profiles/documentation/.contextignore +21 -0
- package/.ctxman/wizard-profiles/documentation/.contextinclude +24 -0
- package/.ctxman/wizard-profiles/documentation/.methodignore +19 -0
- package/.ctxman/wizard-profiles/documentation/.methodinclude +44 -0
- package/.ctxman/wizard-profiles/documentation/profile.json +27 -0
- package/.ctxman/wizard-profiles/full/.contextignore +23 -0
- package/.ctxman/wizard-profiles/full/.contextinclude +38 -0
- package/.ctxman/wizard-profiles/full/.methodignore +5 -0
- package/.ctxman/wizard-profiles/full/.methodinclude +5 -0
- package/.ctxman/wizard-profiles/full/profile.json +28 -0
- package/.ctxman/wizard-profiles/llm-explain/.contextignore +25 -0
- package/.ctxman/wizard-profiles/llm-explain/.contextinclude +33 -0
- package/.ctxman/wizard-profiles/llm-explain/.methodignore +19 -0
- package/.ctxman/wizard-profiles/llm-explain/.methodinclude +48 -0
- package/.ctxman/wizard-profiles/llm-explain/profile.json +27 -0
- package/.ctxman/wizard-profiles/minimal/.contextignore +27 -0
- package/.ctxman/wizard-profiles/minimal/.contextinclude +14 -0
- package/.ctxman/wizard-profiles/minimal/.methodignore +22 -0
- package/.ctxman/wizard-profiles/minimal/.methodinclude +18 -0
- package/.ctxman/wizard-profiles/minimal/profile.json +26 -0
- package/.ctxman/wizard-profiles/security-audit/.contextignore +36 -0
- package/.ctxman/wizard-profiles/security-audit/.contextinclude +37 -0
- package/.ctxman/wizard-profiles/security-audit/.methodignore +29 -0
- package/.ctxman/wizard-profiles/security-audit/.methodinclude +60 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +34 -0
- package/README.md +1167 -0
- package/bin/cli.js +650 -0
- package/bin/cm-ask.js +95 -0
- package/bin/cm-gitingest.js +134 -0
- package/bin/cm-update.js +175 -0
- package/bin/mcp-server.js +31 -0
- package/ctxman.js +236 -0
- package/examples/README.md +298 -0
- package/examples/custom-llm-profiles.example.json +19 -0
- package/examples/wizard-profiles/code-review/.contextignore +41 -0
- package/examples/wizard-profiles/code-review/.contextinclude +30 -0
- package/examples/wizard-profiles/code-review/.methodignore +26 -0
- package/examples/wizard-profiles/code-review/.methodinclude +47 -0
- package/examples/wizard-profiles/code-review/profile.json +26 -0
- package/examples/wizard-profiles/documentation/.contextignore +21 -0
- package/examples/wizard-profiles/documentation/.contextinclude +24 -0
- package/examples/wizard-profiles/documentation/.methodignore +19 -0
- package/examples/wizard-profiles/documentation/.methodinclude +44 -0
- package/examples/wizard-profiles/documentation/profile.json +27 -0
- package/examples/wizard-profiles/full/.contextignore +23 -0
- package/examples/wizard-profiles/full/.contextinclude +38 -0
- package/examples/wizard-profiles/full/.methodignore +5 -0
- package/examples/wizard-profiles/full/.methodinclude +5 -0
- package/examples/wizard-profiles/full/profile.json +28 -0
- package/examples/wizard-profiles/llm-explain/.contextignore +25 -0
- package/examples/wizard-profiles/llm-explain/.contextinclude +33 -0
- package/examples/wizard-profiles/llm-explain/.methodignore +19 -0
- package/examples/wizard-profiles/llm-explain/.methodinclude +48 -0
- package/examples/wizard-profiles/llm-explain/profile.json +27 -0
- package/examples/wizard-profiles/minimal/.contextignore +27 -0
- package/examples/wizard-profiles/minimal/.contextinclude +14 -0
- package/examples/wizard-profiles/minimal/.methodignore +22 -0
- package/examples/wizard-profiles/minimal/.methodinclude +18 -0
- package/examples/wizard-profiles/minimal/profile.json +26 -0
- package/examples/wizard-profiles/security-audit/.contextignore +36 -0
- package/examples/wizard-profiles/security-audit/.contextinclude +37 -0
- package/examples/wizard-profiles/security-audit/.methodignore +29 -0
- package/examples/wizard-profiles/security-audit/.methodinclude +60 -0
- package/examples/wizard-profiles/security-audit/profile.json +34 -0
- package/index.js +68 -0
- package/lib/agents/pr-failure-agent.js +201 -0
- package/lib/analyzers/go-method-analyzer.js +126 -0
- package/lib/analyzers/method-analyzer.js +457 -0
- package/lib/analyzers/token-calculator.js +577 -0
- package/lib/api/mcp/prompts.js +319 -0
- package/lib/api/mcp/resources.js +269 -0
- package/lib/api/mcp/server.js +339 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +63 -0
- package/lib/api/rest/server.js +405 -0
- package/lib/cache/CacheManager.js +388 -0
- package/lib/core/Analyzer.js +180 -0
- package/lib/core/ContextBuilder.js +325 -0
- package/lib/core/Reporter.js +270 -0
- package/lib/core/Scanner.js +129 -0
- package/lib/formatters/format-registry.js +419 -0
- package/lib/formatters/gitingest-formatter.js +746 -0
- package/lib/formatters/toon-formatter-v1.3.js +731 -0
- package/lib/formatters/toon-formatter.js +291 -0
- package/lib/integrations/git/BlameTracker.js +142 -0
- package/lib/integrations/git/DiffAnalyzer.js +260 -0
- package/lib/integrations/git/GitClient.js +336 -0
- package/lib/parsers/gitignore-parser.js +127 -0
- package/lib/parsers/method-filter-parser.js +50 -0
- package/lib/plugins/ExporterPlugin.js +102 -0
- package/lib/plugins/LanguagePlugin.js +100 -0
- package/lib/plugins/PluginManager.js +302 -0
- package/lib/rag/DataSourcePlugin.js +31 -0
- package/lib/rag/EmbeddingProvider.js +75 -0
- package/lib/rag/Indexer.js +83 -0
- package/lib/rag/VectorStoreFactory.js +24 -0
- package/lib/rag/sources/FileDataSource.js +70 -0
- package/lib/rag/stores/BaseVectorStore.js +43 -0
- package/lib/rag/stores/ChromaDBStore.js +85 -0
- package/lib/rag/stores/LanceDBStore.js +109 -0
- package/lib/rag/stores/PineconeStore.js +74 -0
- package/lib/ui/dashboard.js +109 -0
- package/lib/ui/index.js +15 -0
- package/lib/ui/progress-bar.js +110 -0
- package/lib/ui/select-input.js +51 -0
- package/lib/ui/wizard.js +359 -0
- package/lib/utils/clipboard-utils.js +66 -0
- package/lib/utils/config-utils.js +94 -0
- package/lib/utils/error-handler.js +145 -0
- package/lib/utils/file-utils.js +89 -0
- package/lib/utils/format-converter.js +325 -0
- package/lib/utils/git-utils.js +337 -0
- package/lib/utils/llm-detector.js +409 -0
- package/lib/utils/logger.js +278 -0
- package/lib/utils/token-utils.js +200 -0
- package/lib/utils/tokenizer-adapter.js +454 -0
- package/lib/utils/updater.js +565 -0
- package/lib/watch/FileWatcher.js +208 -0
- package/lib/watch/IncrementalAnalyzer.js +206 -0
- package/package.json +118 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Detector
|
|
3
|
+
* Auto-detects target LLM model and provides optimization recommendations
|
|
4
|
+
* v2.3.7 feature - JSON config based
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { resolve, dirname } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import ConfigUtils from './config-utils.js';
|
|
11
|
+
import { getLogger } from './logger.js';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Lazy-loaded profiles cache
|
|
17
|
+
let profilesCache = null;
|
|
18
|
+
|
|
19
|
+
// Logger instance
|
|
20
|
+
const logger = getLogger('LLMDetector');
|
|
21
|
+
|
|
22
|
+
export class LLMDetector {
|
|
23
|
+
/**
|
|
24
|
+
* Load LLM profiles from JSON config
|
|
25
|
+
* Merges built-in profiles with custom user profiles
|
|
26
|
+
* @returns {object} All LLM profiles
|
|
27
|
+
*/
|
|
28
|
+
static loadProfiles() {
|
|
29
|
+
// Return cached if available
|
|
30
|
+
if (profilesCache) {
|
|
31
|
+
return profilesCache;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Load built-in profiles
|
|
36
|
+
const profilesPath = resolve(__dirname, '../../.ctxman/llm-profiles.json');
|
|
37
|
+
const profilesData = JSON.parse(readFileSync(profilesPath, 'utf-8'));
|
|
38
|
+
const builtInProfiles = profilesData.profiles || {};
|
|
39
|
+
const defaultProfile = profilesData.default;
|
|
40
|
+
|
|
41
|
+
// Try to load custom profiles
|
|
42
|
+
let customProfiles = {};
|
|
43
|
+
try {
|
|
44
|
+
const customPath = resolve(__dirname, '../../.ctxman/custom-profiles.json');
|
|
45
|
+
const customData = JSON.parse(readFileSync(customPath, 'utf-8'));
|
|
46
|
+
customProfiles = customData.profiles || {};
|
|
47
|
+
logger.debug(`Loaded ${Object.keys(customProfiles).length} custom LLM profiles`);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// Custom profiles optional
|
|
50
|
+
logger.debug('No custom profiles found (optional)');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Merge profiles (custom overrides built-in)
|
|
54
|
+
const allProfiles = {
|
|
55
|
+
...builtInProfiles,
|
|
56
|
+
...customProfiles
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Cache the result
|
|
60
|
+
profilesCache = {
|
|
61
|
+
profiles: allProfiles,
|
|
62
|
+
default: defaultProfile
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
logger.debug(`Loaded ${Object.keys(allProfiles).length} total LLM profiles`);
|
|
66
|
+
return profilesCache;
|
|
67
|
+
|
|
68
|
+
} catch (error) {
|
|
69
|
+
logger.error(`Failed to load LLM profiles: ${error.message}`);
|
|
70
|
+
|
|
71
|
+
// Fallback to minimal built-in if JSON load fails
|
|
72
|
+
const fallback = {
|
|
73
|
+
profiles: {
|
|
74
|
+
'claude-sonnet-4.5': {
|
|
75
|
+
name: 'Claude Sonnet 4.5',
|
|
76
|
+
vendor: 'Anthropic',
|
|
77
|
+
contextWindow: 200000,
|
|
78
|
+
utilizationPercentage: 0.6,
|
|
79
|
+
maxRecommendedInput: 120000,
|
|
80
|
+
preferredFormat: 'toon',
|
|
81
|
+
chunkStrategy: 'smart'
|
|
82
|
+
},
|
|
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
|
+
},
|
|
93
|
+
default: {
|
|
94
|
+
name: 'Unknown Model',
|
|
95
|
+
contextWindow: 100000,
|
|
96
|
+
utilizationPercentage: 0.6,
|
|
97
|
+
maxRecommendedInput: 60000,
|
|
98
|
+
preferredFormat: 'json',
|
|
99
|
+
chunkStrategy: 'smart'
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
profilesCache = fallback;
|
|
104
|
+
return fallback;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get default profile for unknown models
|
|
110
|
+
* @returns {object} Default profile
|
|
111
|
+
*/
|
|
112
|
+
static getDefaultProfile() {
|
|
113
|
+
const data = this.loadProfiles();
|
|
114
|
+
return data.default;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get all profiles
|
|
119
|
+
* @returns {object} All LLM profiles
|
|
120
|
+
*/
|
|
121
|
+
static getAllProfiles() {
|
|
122
|
+
const data = this.loadProfiles();
|
|
123
|
+
return data.profiles;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Auto-detect LLM from environment and config
|
|
127
|
+
* Performance target: <100ms
|
|
128
|
+
* @returns {string} Detected model name or 'unknown'
|
|
129
|
+
*/
|
|
130
|
+
static detect() {
|
|
131
|
+
const startTime = Date.now();
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// 1. Check environment variables (fastest, <1ms)
|
|
135
|
+
const envModel = this.detectFromEnv();
|
|
136
|
+
if (envModel && envModel !== 'unknown') {
|
|
137
|
+
logger.debug(`LLM detected from environment: ${envModel}`);
|
|
138
|
+
return envModel;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 2. Check user config (slower but still fast, <50ms)
|
|
142
|
+
const configModel = this.detectFromConfig();
|
|
143
|
+
if (configModel && configModel !== 'unknown') {
|
|
144
|
+
logger.debug(`LLM detected from config: ${configModel}`);
|
|
145
|
+
return configModel;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
logger.debug('No LLM detected, using defaults');
|
|
149
|
+
return 'unknown';
|
|
150
|
+
|
|
151
|
+
} finally {
|
|
152
|
+
const elapsed = Date.now() - startTime;
|
|
153
|
+
if (elapsed > 100) {
|
|
154
|
+
logger.warn(`LLM detection took ${elapsed}ms (threshold: 100ms)`);
|
|
155
|
+
} else {
|
|
156
|
+
logger.debug(`LLM detection completed in ${elapsed}ms`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Detect from environment variables
|
|
163
|
+
* Checks for API keys and explicit overrides
|
|
164
|
+
* @returns {string|null} Model name or null
|
|
165
|
+
*/
|
|
166
|
+
static detectFromEnv() {
|
|
167
|
+
// Explicit override
|
|
168
|
+
if (process.env.CTXMAN_LLM) {
|
|
169
|
+
return process.env.CTXMAN_LLM;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Anthropic (Claude)
|
|
173
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
174
|
+
return 'claude-sonnet-4.5'; // Default to Sonnet
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// OpenAI (GPT-4)
|
|
178
|
+
if (process.env.OPENAI_API_KEY) {
|
|
179
|
+
return 'gpt-4o'; // Default to GPT-4o (most capable)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Google (Gemini)
|
|
183
|
+
if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) {
|
|
184
|
+
return 'gemini-2.0-flash'; // Default to Flash (fastest)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// DeepSeek
|
|
188
|
+
if (process.env.DEEPSEEK_API_KEY) {
|
|
189
|
+
return 'deepseek-chat'; // Default to Chat
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Detect from user configuration file
|
|
197
|
+
* @returns {string|null} Model name or null
|
|
198
|
+
*/
|
|
199
|
+
static detectFromConfig() {
|
|
200
|
+
try {
|
|
201
|
+
const config = ConfigUtils.loadUserConfig();
|
|
202
|
+
if (config && config.targetModel) {
|
|
203
|
+
return config.targetModel;
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
logger.debug(`Config file not found or invalid: ${error.message}`);
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get LLM profile by name
|
|
213
|
+
* Supports built-in and custom user models
|
|
214
|
+
* @param {string} modelName - Model identifier
|
|
215
|
+
* @returns {object} LLM profile
|
|
216
|
+
*/
|
|
217
|
+
static getProfile(modelName) {
|
|
218
|
+
// Unknown model
|
|
219
|
+
if (!modelName || modelName === 'unknown') {
|
|
220
|
+
return this.getDefaultProfile();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Load all profiles
|
|
224
|
+
const allProfiles = this.getAllProfiles();
|
|
225
|
+
|
|
226
|
+
// Check if profile exists
|
|
227
|
+
if (allProfiles[modelName]) {
|
|
228
|
+
return allProfiles[modelName];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Fallback to default
|
|
232
|
+
logger.warn(`Unknown LLM model: ${modelName}, using default profile`);
|
|
233
|
+
return {
|
|
234
|
+
...this.getDefaultProfile(),
|
|
235
|
+
name: modelName
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Recommend optimal configuration based on LLM profile and repository stats
|
|
241
|
+
* @param {object} profile - LLM profile
|
|
242
|
+
* @param {object} repoStats - Repository statistics
|
|
243
|
+
* @returns {object} Configuration recommendations
|
|
244
|
+
*/
|
|
245
|
+
static recommendConfiguration(profile, repoStats) {
|
|
246
|
+
const { totalTokens, totalFiles } = repoStats;
|
|
247
|
+
const { maxRecommendedInput, preferredFormat, chunkStrategy, utilizationPercentage } = profile;
|
|
248
|
+
|
|
249
|
+
const fitsInContext = totalTokens <= maxRecommendedInput;
|
|
250
|
+
const chunksNeeded = fitsInContext ? 1 : Math.ceil(totalTokens / maxRecommendedInput);
|
|
251
|
+
|
|
252
|
+
// Chunk size should also respect 60% rule
|
|
253
|
+
const recommendedChunkSize = maxRecommendedInput;
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
targetModel: profile.name,
|
|
257
|
+
outputFormat: preferredFormat,
|
|
258
|
+
chunkingEnabled: !fitsInContext,
|
|
259
|
+
chunkStrategy: chunkStrategy,
|
|
260
|
+
chunkSize: recommendedChunkSize,
|
|
261
|
+
chunksNeeded: chunksNeeded,
|
|
262
|
+
fitsInContext: fitsInContext,
|
|
263
|
+
utilizationPercentage: Math.round((utilizationPercentage || 0.6) * 100),
|
|
264
|
+
utilizationActual: (totalTokens / maxRecommendedInput) * 100
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Analyze context fit - detailed analysis for display
|
|
270
|
+
* @param {object} profile - LLM profile
|
|
271
|
+
* @param {object} repoStats - Repository statistics
|
|
272
|
+
* @returns {object} Context fit analysis
|
|
273
|
+
*/
|
|
274
|
+
static analyzeContextFit(profile, repoStats) {
|
|
275
|
+
const { totalTokens, totalFiles } = repoStats;
|
|
276
|
+
const { name, contextWindow, maxRecommendedInput, utilizationPercentage } = profile;
|
|
277
|
+
|
|
278
|
+
// Calculate usable context (60% of total by default)
|
|
279
|
+
const utilization = utilizationPercentage || 0.6;
|
|
280
|
+
const usableContext = maxRecommendedInput; // Already calculated as 60%
|
|
281
|
+
const reservedForSystem = contextWindow - usableContext; // 40% reserved
|
|
282
|
+
|
|
283
|
+
const fitsInOne = totalTokens <= usableContext;
|
|
284
|
+
const chunksNeeded = fitsInOne ? 1 : Math.ceil(totalTokens / usableContext);
|
|
285
|
+
const utilizationActual = (totalTokens / usableContext) * 100; // % of usable context
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
modelName: name,
|
|
289
|
+
contextWindow,
|
|
290
|
+
usableContext,
|
|
291
|
+
reservedForSystem,
|
|
292
|
+
utilizationPercentage: Math.round(utilization * 100),
|
|
293
|
+
repoTokens: totalTokens,
|
|
294
|
+
repoFiles: totalFiles,
|
|
295
|
+
fitsInOne,
|
|
296
|
+
chunksNeeded,
|
|
297
|
+
utilizationActual,
|
|
298
|
+
recommendation: this.getRecommendation(fitsInOne, chunksNeeded, utilizationActual)
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get recommendation message based on context fit
|
|
304
|
+
* @param {boolean} fitsInOne - Whether repo fits in one context
|
|
305
|
+
* @param {number} chunksNeeded - Number of chunks needed
|
|
306
|
+
* @param {number} utilizationActual - Actual utilization percentage
|
|
307
|
+
* @returns {string} Recommendation message
|
|
308
|
+
*/
|
|
309
|
+
static getRecommendation(fitsInOne, chunksNeeded, utilizationActual) {
|
|
310
|
+
if (fitsInOne) {
|
|
311
|
+
if (utilizationActual < 50) {
|
|
312
|
+
return 'Plenty of room for system prompts and responses.';
|
|
313
|
+
} else if (utilizationActual < 80) {
|
|
314
|
+
return 'Your entire codebase fits comfortably.';
|
|
315
|
+
} else if (utilizationActual <= 100) {
|
|
316
|
+
return 'Repository uses most of available context.';
|
|
317
|
+
} else {
|
|
318
|
+
// Should not happen if fitsInOne is true, but safety check
|
|
319
|
+
return 'Context limit exceeded. Enable chunking.';
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
if (chunksNeeded <= 3) {
|
|
323
|
+
return `Repository requires ${chunksNeeded} chunks. Enable chunking with --chunk flag.`;
|
|
324
|
+
} else if (chunksNeeded <= 10) {
|
|
325
|
+
return `Large repository needs ${chunksNeeded} chunks. Consider filtering with .contextinclude.`;
|
|
326
|
+
} else {
|
|
327
|
+
return `Very large repository (${chunksNeeded} chunks). Strongly recommend filtering to key files.`;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* List all available LLM profiles
|
|
334
|
+
* @returns {string[]} Array of model identifiers
|
|
335
|
+
*/
|
|
336
|
+
static listProfiles() {
|
|
337
|
+
const allProfiles = this.getAllProfiles();
|
|
338
|
+
return Object.keys(allProfiles);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get human-readable list of models with details
|
|
343
|
+
* @returns {object[]} Array of model details
|
|
344
|
+
*/
|
|
345
|
+
static getModelList() {
|
|
346
|
+
const allProfiles = this.getAllProfiles();
|
|
347
|
+
return Object.entries(allProfiles).map(([key, profile]) => ({
|
|
348
|
+
id: key,
|
|
349
|
+
name: profile.name,
|
|
350
|
+
vendor: profile.vendor,
|
|
351
|
+
contextWindow: profile.contextWindow,
|
|
352
|
+
preferredFormat: profile.preferredFormat
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Format context fit analysis for CLI display
|
|
358
|
+
* @param {object} analysis - Context fit analysis
|
|
359
|
+
* @returns {string} Formatted text
|
|
360
|
+
*/
|
|
361
|
+
static formatAnalysis(analysis) {
|
|
362
|
+
const lines = [];
|
|
363
|
+
|
|
364
|
+
lines.push('');
|
|
365
|
+
lines.push('📊 Context Window Analysis:');
|
|
366
|
+
lines.push('════════════════════════════════════════════════════');
|
|
367
|
+
lines.push(` Target Model: ${analysis.modelName}`);
|
|
368
|
+
lines.push(` Total Context: ${analysis.contextWindow.toLocaleString()} tokens`);
|
|
369
|
+
lines.push(` Usable Context: ${analysis.usableContext.toLocaleString()} tokens (${analysis.utilizationPercentage}% policy)`);
|
|
370
|
+
lines.push(` Reserved for System: ${analysis.reservedForSystem.toLocaleString()} tokens (${100 - analysis.utilizationPercentage}%)`);
|
|
371
|
+
lines.push('');
|
|
372
|
+
lines.push(` Your Repository: ${analysis.repoTokens.toLocaleString()} tokens (${analysis.repoFiles} files)`);
|
|
373
|
+
|
|
374
|
+
if (analysis.fitsInOne) {
|
|
375
|
+
// Show utilization only if it fits
|
|
376
|
+
lines.push(` Utilization: ${analysis.utilizationActual.toFixed(1)}% of usable context`);
|
|
377
|
+
lines.push('');
|
|
378
|
+
|
|
379
|
+
// Determine fit quality
|
|
380
|
+
let fitStatus = '';
|
|
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
|
+
}
|
|
388
|
+
|
|
389
|
+
lines.push(` ${fitStatus}`);
|
|
390
|
+
lines.push(` ${analysis.recommendation}`);
|
|
391
|
+
lines.push(' 💡 Recommendation: Use single-file export (no chunking needed)');
|
|
392
|
+
} else {
|
|
393
|
+
// Show excess instead of percentage over 100%
|
|
394
|
+
const excess = analysis.repoTokens - analysis.usableContext;
|
|
395
|
+
const excessMultiplier = (analysis.repoTokens / analysis.usableContext).toFixed(1);
|
|
396
|
+
|
|
397
|
+
lines.push(` Exceeds by: ${excess.toLocaleString()} tokens (${excessMultiplier}x usable context)`);
|
|
398
|
+
lines.push('');
|
|
399
|
+
lines.push(` ❌ TOO LARGE! Repository exceeds usable context.`);
|
|
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`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
lines.push('');
|
|
406
|
+
|
|
407
|
+
return lines.join('\n');
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger System
|
|
3
|
+
* Centralized logging with multiple levels and file output
|
|
4
|
+
* v2.3.6+
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
class Logger {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.level = options.level || process.env.LOG_LEVEL || 'info';
|
|
13
|
+
this.logToFile = options.logToFile !== false;
|
|
14
|
+
this.logDir = options.logDir || path.join(process.cwd(), '.ctxman', 'logs');
|
|
15
|
+
this.logFile = options.logFile || `ctxman-${this.getDateString()}.log`;
|
|
16
|
+
this.silent = options.silent || false;
|
|
17
|
+
|
|
18
|
+
// Log levels (higher number = more verbose)
|
|
19
|
+
this.levels = {
|
|
20
|
+
error: 0,
|
|
21
|
+
warn: 1,
|
|
22
|
+
info: 2,
|
|
23
|
+
debug: 3,
|
|
24
|
+
trace: 4
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Colors for terminal output
|
|
28
|
+
this.colors = {
|
|
29
|
+
error: '\x1b[31m', // Red
|
|
30
|
+
warn: '\x1b[33m', // Yellow
|
|
31
|
+
info: '\x1b[36m', // Cyan
|
|
32
|
+
debug: '\x1b[35m', // Magenta
|
|
33
|
+
trace: '\x1b[90m', // Gray
|
|
34
|
+
reset: '\x1b[0m'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Icons for each level
|
|
38
|
+
this.icons = {
|
|
39
|
+
error: '❌',
|
|
40
|
+
warn: '⚠️',
|
|
41
|
+
info: 'ℹ️',
|
|
42
|
+
debug: '🔍',
|
|
43
|
+
trace: '🔬'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.initializeLogDirectory();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize log directory
|
|
51
|
+
*/
|
|
52
|
+
initializeLogDirectory() {
|
|
53
|
+
if (this.logToFile) {
|
|
54
|
+
const fullLogDir = path.isAbsolute(this.logDir) ? this.logDir : path.join(process.cwd(), this.logDir);
|
|
55
|
+
if (!fs.existsSync(fullLogDir)) {
|
|
56
|
+
fs.mkdirSync(fullLogDir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get current date string for log file names
|
|
63
|
+
*/
|
|
64
|
+
getDateString() {
|
|
65
|
+
const now = new Date();
|
|
66
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get timestamp for log entries
|
|
71
|
+
*/
|
|
72
|
+
getTimestamp() {
|
|
73
|
+
return new Date().toISOString();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if should log based on level
|
|
78
|
+
*/
|
|
79
|
+
shouldLog(level) {
|
|
80
|
+
return this.levels[level] <= this.levels[this.level];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Format log message
|
|
85
|
+
*/
|
|
86
|
+
formatMessage(level, message, meta = {}) {
|
|
87
|
+
const timestamp = this.getTimestamp();
|
|
88
|
+
const icon = this.icons[level];
|
|
89
|
+
|
|
90
|
+
// Console format (with colors)
|
|
91
|
+
const consoleMessage = `${this.colors[level]}${icon} [${level.toUpperCase()}]${this.colors.reset} ${message}`;
|
|
92
|
+
|
|
93
|
+
// File format (plain text with metadata)
|
|
94
|
+
const metaString = Object.keys(meta).length > 0 ? ` | ${JSON.stringify(meta)}` : '';
|
|
95
|
+
const fileMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}${metaString}`;
|
|
96
|
+
|
|
97
|
+
return { consoleMessage, fileMessage };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Write log to file
|
|
102
|
+
*/
|
|
103
|
+
writeToFile(message) {
|
|
104
|
+
if (!this.logToFile) return;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const fullLogPath = path.isAbsolute(this.logDir)
|
|
108
|
+
? path.join(this.logDir, this.logFile)
|
|
109
|
+
: path.join(process.cwd(), this.logDir, this.logFile);
|
|
110
|
+
|
|
111
|
+
fs.appendFileSync(fullLogPath, message + '\n', 'utf8');
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// Fail silently to avoid infinite loop
|
|
114
|
+
console.error('Failed to write to log file:', error.message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Core logging method
|
|
120
|
+
*/
|
|
121
|
+
log(level, message, meta = {}) {
|
|
122
|
+
if (!this.shouldLog(level)) return;
|
|
123
|
+
|
|
124
|
+
const { consoleMessage, fileMessage } = this.formatMessage(level, message, meta);
|
|
125
|
+
|
|
126
|
+
// Output to console
|
|
127
|
+
if (!this.silent) {
|
|
128
|
+
console.log(consoleMessage);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Output to file
|
|
132
|
+
this.writeToFile(fileMessage);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Convenience methods
|
|
137
|
+
*/
|
|
138
|
+
error(message, meta = {}) {
|
|
139
|
+
this.log('error', message, meta);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
warn(message, meta = {}) {
|
|
143
|
+
this.log('warn', message, meta);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
info(message, meta = {}) {
|
|
147
|
+
this.log('info', message, meta);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
debug(message, meta = {}) {
|
|
151
|
+
this.log('debug', message, meta);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
trace(message, meta = {}) {
|
|
155
|
+
this.log('trace', message, meta);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Log with custom icon
|
|
160
|
+
*/
|
|
161
|
+
custom(icon, level, message, meta = {}) {
|
|
162
|
+
const originalIcon = this.icons[level];
|
|
163
|
+
this.icons[level] = icon;
|
|
164
|
+
this.log(level, message, meta);
|
|
165
|
+
this.icons[level] = originalIcon;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Group related logs
|
|
170
|
+
*/
|
|
171
|
+
group(title) {
|
|
172
|
+
if (!this.silent) {
|
|
173
|
+
console.group(title);
|
|
174
|
+
}
|
|
175
|
+
this.writeToFile(`\n--- ${title} ---`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
groupEnd() {
|
|
179
|
+
if (!this.silent) {
|
|
180
|
+
console.groupEnd();
|
|
181
|
+
}
|
|
182
|
+
this.writeToFile('--- End Group ---\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Log with timer
|
|
187
|
+
*/
|
|
188
|
+
time(label) {
|
|
189
|
+
console.time(label);
|
|
190
|
+
this.writeToFile(`[TIMER START] ${label}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
timeEnd(label) {
|
|
194
|
+
console.timeEnd(label);
|
|
195
|
+
this.writeToFile(`[TIMER END] ${label}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Clear old log files (keep last N days)
|
|
200
|
+
*/
|
|
201
|
+
clearOldLogs(daysToKeep = 7) {
|
|
202
|
+
try {
|
|
203
|
+
const fullLogDir = path.isAbsolute(this.logDir) ? this.logDir : path.join(process.cwd(), this.logDir);
|
|
204
|
+
if (!fs.existsSync(fullLogDir)) return;
|
|
205
|
+
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
const maxAge = daysToKeep * 24 * 60 * 60 * 1000;
|
|
208
|
+
|
|
209
|
+
const files = fs.readdirSync(fullLogDir);
|
|
210
|
+
files.forEach(file => {
|
|
211
|
+
const filePath = path.join(fullLogDir, file);
|
|
212
|
+
const stats = fs.statSync(filePath);
|
|
213
|
+
const age = now - stats.mtime.getTime();
|
|
214
|
+
|
|
215
|
+
if (age > maxAge) {
|
|
216
|
+
fs.unlinkSync(filePath);
|
|
217
|
+
this.debug(`Deleted old log file: ${file}`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
} catch (error) {
|
|
221
|
+
this.error('Failed to clear old logs', { error: error.message });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get log file path
|
|
227
|
+
*/
|
|
228
|
+
getLogFilePath() {
|
|
229
|
+
return path.isAbsolute(this.logDir)
|
|
230
|
+
? path.join(this.logDir, this.logFile)
|
|
231
|
+
: path.join(process.cwd(), this.logDir, this.logFile);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Read recent logs
|
|
236
|
+
*/
|
|
237
|
+
getRecentLogs(lines = 100) {
|
|
238
|
+
try {
|
|
239
|
+
const logPath = this.getLogFilePath();
|
|
240
|
+
if (!fs.existsSync(logPath)) {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
245
|
+
const allLines = content.split('\n').filter(line => line.trim());
|
|
246
|
+
return allLines.slice(-lines);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
this.error('Failed to read logs', { error: error.message });
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Create singleton instance
|
|
255
|
+
let defaultLogger = null;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get or create default logger instance
|
|
259
|
+
*/
|
|
260
|
+
function getLogger(options = {}) {
|
|
261
|
+
if (!defaultLogger) {
|
|
262
|
+
defaultLogger = new Logger(options);
|
|
263
|
+
}
|
|
264
|
+
return defaultLogger;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Create a new logger instance
|
|
269
|
+
*/
|
|
270
|
+
function createLogger(options = {}) {
|
|
271
|
+
return new Logger(options);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export {
|
|
275
|
+
Logger,
|
|
276
|
+
getLogger,
|
|
277
|
+
createLogger
|
|
278
|
+
};
|