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
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile Manager
|
|
3
|
+
* Manages team configuration profiles for consistent context generation
|
|
4
|
+
* FEAT-004: Team Configuration Profiles
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname } from 'path';
|
|
11
|
+
import { DEFAULT_TARGET_MODEL } from './llm-detector.js';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Built-in profiles defined in code for reliability
|
|
18
|
+
*/
|
|
19
|
+
const BUILTIN_PROFILES = {
|
|
20
|
+
'frontend-team': {
|
|
21
|
+
name: 'Frontend Team Profile',
|
|
22
|
+
description: 'Configuration for frontend developers',
|
|
23
|
+
createdBy: 'ctxman',
|
|
24
|
+
createdAt: new Date().toISOString(),
|
|
25
|
+
config: {
|
|
26
|
+
exclude: ['backend/**', 'server/**', 'api/**', '**/*.test.js', '**/*.spec.js'],
|
|
27
|
+
include: [
|
|
28
|
+
'src/**/*.js',
|
|
29
|
+
'src/**/*.jsx',
|
|
30
|
+
'src/**/*.ts',
|
|
31
|
+
'src/**/*.tsx',
|
|
32
|
+
'components/**',
|
|
33
|
+
'pages/**',
|
|
34
|
+
],
|
|
35
|
+
targetModel: DEFAULT_TARGET_MODEL,
|
|
36
|
+
methodLevel: true,
|
|
37
|
+
methodInclude: ['*Component', '*Hook', '*Context', '*Provider'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
'backend-team': {
|
|
41
|
+
name: 'Backend Team Profile',
|
|
42
|
+
description: 'Configuration for backend developers',
|
|
43
|
+
createdBy: 'ctxman',
|
|
44
|
+
createdAt: new Date().toISOString(),
|
|
45
|
+
config: {
|
|
46
|
+
exclude: ['frontend/**', 'src/components/**', 'pages/**', '**/*.test.js', '**/*.spec.js'],
|
|
47
|
+
include: ['api/**', 'server/**', 'lib/**', 'db/**', 'services/**'],
|
|
48
|
+
targetModel: DEFAULT_TARGET_MODEL,
|
|
49
|
+
methodLevel: true,
|
|
50
|
+
methodInclude: ['*Controller', '*Service', '*Repository', '*Handler', '*Middleware'],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
'devops-team': {
|
|
54
|
+
name: 'DevOps Team Profile',
|
|
55
|
+
description: 'Configuration for DevOps engineers',
|
|
56
|
+
createdBy: 'ctxman',
|
|
57
|
+
createdAt: new Date().toISOString(),
|
|
58
|
+
config: {
|
|
59
|
+
exclude: ['**/*.test.js', 'src/**/*.js', 'frontend/**'],
|
|
60
|
+
include: [
|
|
61
|
+
'**/*.yaml',
|
|
62
|
+
'**/*.yml',
|
|
63
|
+
'**/Dockerfile*',
|
|
64
|
+
'**/*.sh',
|
|
65
|
+
'scripts/**',
|
|
66
|
+
'.github/**',
|
|
67
|
+
'terraform/**',
|
|
68
|
+
'k8s/**',
|
|
69
|
+
],
|
|
70
|
+
targetModel: DEFAULT_TARGET_MODEL,
|
|
71
|
+
methodLevel: false,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
default: {
|
|
75
|
+
name: 'Default Profile',
|
|
76
|
+
description: 'Standard configuration for general development',
|
|
77
|
+
createdBy: 'ctxman',
|
|
78
|
+
createdAt: new Date().toISOString(),
|
|
79
|
+
config: {
|
|
80
|
+
exclude: ['**/*.test.js', '**/*.spec.js', 'node_modules/**', 'coverage/**'],
|
|
81
|
+
include: ['src/**', 'lib/**'],
|
|
82
|
+
targetModel: DEFAULT_TARGET_MODEL,
|
|
83
|
+
methodLevel: false,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
class ProfileManager {
|
|
89
|
+
/**
|
|
90
|
+
* Create a ProfileManager instance
|
|
91
|
+
* @param {string} projectRoot - Project root directory
|
|
92
|
+
*/
|
|
93
|
+
constructor(projectRoot = process.cwd()) {
|
|
94
|
+
this.projectRoot = projectRoot;
|
|
95
|
+
this.profilesPath = path.join(projectRoot, '.ctxman', 'profiles');
|
|
96
|
+
this.builtinProfilesPath = path.join(__dirname, '..', '..', '.ctxman', 'profiles');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* List all available profiles (built-in + custom)
|
|
101
|
+
* @returns {Array} Array of profile objects
|
|
102
|
+
*/
|
|
103
|
+
list() {
|
|
104
|
+
const profiles = [];
|
|
105
|
+
|
|
106
|
+
// Add built-in profiles
|
|
107
|
+
for (const [id, profile] of Object.entries(BUILTIN_PROFILES)) {
|
|
108
|
+
profiles.push({
|
|
109
|
+
id,
|
|
110
|
+
...profile,
|
|
111
|
+
source: 'builtin',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Add custom profiles from project .ctxman/profiles/
|
|
116
|
+
const customProfiles = this.loadCustomProfiles();
|
|
117
|
+
profiles.push(...customProfiles);
|
|
118
|
+
|
|
119
|
+
return profiles;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get a specific profile by ID
|
|
124
|
+
* @param {string} profileId - Profile identifier
|
|
125
|
+
* @returns {object|null} Profile object or null if not found
|
|
126
|
+
*/
|
|
127
|
+
get(profileId) {
|
|
128
|
+
// Check built-in first
|
|
129
|
+
if (BUILTIN_PROFILES[profileId]) {
|
|
130
|
+
return {
|
|
131
|
+
id: profileId,
|
|
132
|
+
...BUILTIN_PROFILES[profileId],
|
|
133
|
+
source: 'builtin',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check custom profiles
|
|
138
|
+
const customProfiles = this.loadCustomProfiles();
|
|
139
|
+
const custom = customProfiles.find((p) => p.id === profileId);
|
|
140
|
+
if (custom) {
|
|
141
|
+
return custom;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Load custom profiles from .ctxman/profiles/
|
|
149
|
+
* @returns {Array} Array of custom profile objects
|
|
150
|
+
*/
|
|
151
|
+
loadCustomProfiles() {
|
|
152
|
+
const profiles = [];
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
if (!fs.existsSync(this.profilesPath)) {
|
|
156
|
+
return profiles;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const files = fs.readdirSync(this.profilesPath);
|
|
160
|
+
|
|
161
|
+
for (const file of files) {
|
|
162
|
+
if (!file.endsWith('.json')) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const filePath = path.join(this.profilesPath, file);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
170
|
+
const profile = JSON.parse(content);
|
|
171
|
+
const id = path.basename(file, '.json');
|
|
172
|
+
|
|
173
|
+
// Skip if already defined as built-in (built-ins take precedence)
|
|
174
|
+
if (BUILTIN_PROFILES[id]) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
profiles.push({
|
|
179
|
+
id,
|
|
180
|
+
...profile,
|
|
181
|
+
source: 'custom',
|
|
182
|
+
});
|
|
183
|
+
} catch (parseError) {
|
|
184
|
+
console.error(`Warning: Failed to parse profile ${file}: ${parseError.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error(`Warning: Failed to load custom profiles: ${error.message}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return profiles;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Apply a profile and get configuration options
|
|
196
|
+
* @param {string} profileId - Profile identifier
|
|
197
|
+
* @param {object} options - Additional options to merge
|
|
198
|
+
* @returns {object} Merged configuration options
|
|
199
|
+
*/
|
|
200
|
+
apply(profileId, options = {}) {
|
|
201
|
+
const profile = this.get(profileId);
|
|
202
|
+
|
|
203
|
+
if (!profile) {
|
|
204
|
+
throw new Error(`Profile '${profileId}' not found`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Build configuration from profile
|
|
208
|
+
const config = {
|
|
209
|
+
profileId,
|
|
210
|
+
profileName: profile.name,
|
|
211
|
+
profileDescription: profile.description,
|
|
212
|
+
profileSource: profile.source,
|
|
213
|
+
// Convert include/exclude patterns to ignore patterns for TokenAnalyzer
|
|
214
|
+
ignorePatterns: profile.config.exclude || [],
|
|
215
|
+
includePatterns: profile.config.include || [],
|
|
216
|
+
methodInclude: profile.config.methodInclude || [],
|
|
217
|
+
methodLevel: profile.config.methodLevel || false,
|
|
218
|
+
targetModel: profile.config.targetModel || options.targetModel,
|
|
219
|
+
...options,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return config;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Create a custom profile
|
|
227
|
+
* @param {string} id - Profile identifier
|
|
228
|
+
* @param {object} profile - Profile configuration
|
|
229
|
+
* @returns {string} Path to created profile file
|
|
230
|
+
*/
|
|
231
|
+
createCustom(id, profile) {
|
|
232
|
+
// Ensure .ctxman/profiles directory exists
|
|
233
|
+
if (!fs.existsSync(this.profilesPath)) {
|
|
234
|
+
fs.mkdirSync(this.profilesPath, { recursive: true });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const filePath = path.join(this.profilesPath, `${id}.json`);
|
|
238
|
+
|
|
239
|
+
// Validate profile structure
|
|
240
|
+
this.validateProfile(profile);
|
|
241
|
+
|
|
242
|
+
// Add metadata
|
|
243
|
+
const profileWithMeta = {
|
|
244
|
+
...profile,
|
|
245
|
+
createdAt: new Date().toISOString(),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Write profile file
|
|
249
|
+
try {
|
|
250
|
+
// 'wx' fails when the file exists, without a separate check that could go stale
|
|
251
|
+
fs.writeFileSync(filePath, JSON.stringify(profileWithMeta, null, 2), { flag: 'wx' });
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error.code === 'EEXIST') throw new Error(`Profile '${id}' already exists`);
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return filePath;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Validate profile structure
|
|
262
|
+
* @param {object} profile - Profile to validate
|
|
263
|
+
* @throws {Error} If profile is invalid
|
|
264
|
+
*/
|
|
265
|
+
validateProfile(profile) {
|
|
266
|
+
if (!profile.name || typeof profile.name !== 'string') {
|
|
267
|
+
throw new Error('Profile must have a "name" string property');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!profile.description || typeof profile.description !== 'string') {
|
|
271
|
+
throw new Error('Profile must have a "description" string property');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!profile.config || typeof profile.config !== 'object') {
|
|
275
|
+
throw new Error('Profile must have a "config" object property');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (profile.config.exclude && !Array.isArray(profile.config.exclude)) {
|
|
279
|
+
throw new Error('Profile "config.exclude" must be an array');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (profile.config.include && !Array.isArray(profile.config.include)) {
|
|
283
|
+
throw new Error('Profile "config.include" must be an array');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (profile.config.methodInclude && !Array.isArray(profile.config.methodInclude)) {
|
|
287
|
+
throw new Error('Profile "config.methodInclude" must be an array');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Export a profile to JSON
|
|
293
|
+
* @param {string} profileId - Profile identifier
|
|
294
|
+
* @returns {object} Profile object ready for export
|
|
295
|
+
*/
|
|
296
|
+
export(profileId) {
|
|
297
|
+
const profile = this.get(profileId);
|
|
298
|
+
|
|
299
|
+
if (!profile) {
|
|
300
|
+
throw new Error(`Profile '${profileId}' not found`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Return clean profile object without metadata
|
|
304
|
+
return {
|
|
305
|
+
name: profile.name,
|
|
306
|
+
description: profile.description,
|
|
307
|
+
createdBy: profile.createdBy || 'unknown',
|
|
308
|
+
createdAt: profile.createdAt || new Date().toISOString(),
|
|
309
|
+
config: profile.config,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Delete a custom profile
|
|
315
|
+
* @param {string} id - Profile identifier
|
|
316
|
+
* @returns {boolean} True if deleted, false if not found
|
|
317
|
+
*/
|
|
318
|
+
deleteCustom(id) {
|
|
319
|
+
// Prevent deletion of built-in profiles
|
|
320
|
+
if (BUILTIN_PROFILES[id]) {
|
|
321
|
+
throw new Error('Cannot delete built-in profiles');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const filePath = path.join(this.profilesPath, `${id}.json`);
|
|
325
|
+
|
|
326
|
+
if (!fs.existsSync(filePath)) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
fs.unlinkSync(filePath);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Format profiles for display
|
|
336
|
+
* @returns {string} Formatted profile list
|
|
337
|
+
*/
|
|
338
|
+
formatList() {
|
|
339
|
+
const profiles = this.list();
|
|
340
|
+
const lines = [];
|
|
341
|
+
|
|
342
|
+
lines.push('\n📋 Available Team Configuration Profiles:\n');
|
|
343
|
+
lines.push('═'.repeat(70));
|
|
344
|
+
|
|
345
|
+
// Group by source
|
|
346
|
+
const builtin = profiles.filter((p) => p.source === 'builtin');
|
|
347
|
+
const custom = profiles.filter((p) => p.source === 'custom');
|
|
348
|
+
|
|
349
|
+
if (builtin.length > 0) {
|
|
350
|
+
lines.push('\nBuilt-in Profiles:');
|
|
351
|
+
builtin.forEach((p) => {
|
|
352
|
+
lines.push(` ${p.id.padEnd(15)} ${p.description}`);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (custom.length > 0) {
|
|
357
|
+
lines.push('\nCustom Profiles:');
|
|
358
|
+
custom.forEach((p) => {
|
|
359
|
+
lines.push(` ${p.id.padEnd(15)} ${p.description}`);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
lines.push('\n' + '═'.repeat(70));
|
|
364
|
+
lines.push('\nUsage:');
|
|
365
|
+
lines.push(' ctxman --profile <profile-id>');
|
|
366
|
+
lines.push(' ctxman --profile frontend-team --cli');
|
|
367
|
+
lines.push('\nProfile Management:');
|
|
368
|
+
lines.push(' ctxman --list-profiles List all profiles');
|
|
369
|
+
lines.push(' ctxman --create-profile <name> Create a new profile');
|
|
370
|
+
lines.push(' ctxman --export-profile <name> Export a profile to JSON');
|
|
371
|
+
lines.push('\nCustom Profiles:');
|
|
372
|
+
lines.push(' Add profiles to .ctxman/profiles/<name>.json');
|
|
373
|
+
lines.push('');
|
|
374
|
+
|
|
375
|
+
return lines.join('\n');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export default ProfileManager;
|
|
380
|
+
export { BUILTIN_PROFILES };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a file through a single descriptor, so the type, size and location checks hold for
|
|
3
|
+
* the bytes that are read even when the path is replaced between the checks and the read
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
const sameFile = (a, b) => a.ino === b.ino && a.dev === b.dev;
|
|
10
|
+
|
|
11
|
+
const isInside = (root, target) => {
|
|
12
|
+
const relative = path.relative(root, target);
|
|
13
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} filePath
|
|
18
|
+
* @param {Object} [options]
|
|
19
|
+
* @param {number} [options.maxBytes=Infinity] - Larger files are not read
|
|
20
|
+
* @param {boolean} [options.followLinks=true] - false: a symbolic link is not a file
|
|
21
|
+
* @param {string} [options.within] - Real path of a directory the file must resolve inside
|
|
22
|
+
* @returns {{status: 'ok', buffer: Buffer, stat: fs.Stats}
|
|
23
|
+
* | {status: 'not-file' | 'too-large', stat: fs.Stats}
|
|
24
|
+
* | {status: 'outside' | 'missing'}}
|
|
25
|
+
*/
|
|
26
|
+
export function readFileChecked(
|
|
27
|
+
filePath,
|
|
28
|
+
{ maxBytes = Infinity, followLinks = true, within } = {}
|
|
29
|
+
) {
|
|
30
|
+
let fd;
|
|
31
|
+
try {
|
|
32
|
+
// Checked before opening: opening a FIFO would block
|
|
33
|
+
const before = followLinks ? fs.statSync(filePath) : fs.lstatSync(filePath);
|
|
34
|
+
if (!before.isFile()) return { status: 'not-file', stat: before };
|
|
35
|
+
|
|
36
|
+
fd = fs.openSync(filePath, 'r');
|
|
37
|
+
const stat = fs.fstatSync(fd);
|
|
38
|
+
if (!sameFile(stat, before)) return { status: 'missing' };
|
|
39
|
+
if (within) {
|
|
40
|
+
const real = fs.realpathSync(filePath);
|
|
41
|
+
if (!isInside(within, real) || !sameFile(fs.statSync(real), stat)) {
|
|
42
|
+
return { status: 'outside' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (stat.size > maxBytes) return { status: 'too-large', stat };
|
|
46
|
+
|
|
47
|
+
// At most the size checked above, even if the file grows while it is read
|
|
48
|
+
const buffer = Buffer.alloc(stat.size);
|
|
49
|
+
let length = 0;
|
|
50
|
+
while (length < buffer.length) {
|
|
51
|
+
const read = fs.readSync(fd, buffer, length, buffer.length - length, length);
|
|
52
|
+
if (read === 0) break;
|
|
53
|
+
length += read;
|
|
54
|
+
}
|
|
55
|
+
return { status: 'ok', buffer: buffer.subarray(0, length), stat };
|
|
56
|
+
} catch (_error) {
|
|
57
|
+
return { status: 'missing' };
|
|
58
|
+
} finally {
|
|
59
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret Redaction
|
|
3
|
+
* Masks credentials before file contents are exported for an LLM.
|
|
4
|
+
*
|
|
5
|
+
* Only high-precision patterns (fixed prefixes, documented formats): a false
|
|
6
|
+
* positive silently changes the code the model sees, so generic heuristics such
|
|
7
|
+
* as `password = "..."` or entropy checks are deliberately left out.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const SECRET_PATTERNS = [
|
|
11
|
+
{
|
|
12
|
+
type: 'private-key',
|
|
13
|
+
regex:
|
|
14
|
+
/-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g,
|
|
15
|
+
},
|
|
16
|
+
{ type: 'aws-access-key-id', regex: /\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/g },
|
|
17
|
+
{
|
|
18
|
+
type: 'github-token',
|
|
19
|
+
regex: /\b(?:gh[pousr]_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{22,255})\b/g,
|
|
20
|
+
},
|
|
21
|
+
{ type: 'gitlab-token', regex: /\bglpat-[A-Za-z0-9_-]{20,}/g },
|
|
22
|
+
{ type: 'slack-token', regex: /\bxox[abposr]-[A-Za-z0-9-]{10,}/g },
|
|
23
|
+
{ type: 'stripe-secret-key', regex: /\b[rs]k_live_[A-Za-z0-9]{20,}\b/g },
|
|
24
|
+
{ type: 'google-api-key', regex: /\bAIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/g },
|
|
25
|
+
{ type: 'anthropic-api-key', regex: /\bsk-ant-[A-Za-z0-9_-]{20,}/g },
|
|
26
|
+
// OpenAI keys embed the base64 of "OpenAI" (T3BlbkFJ)
|
|
27
|
+
{ type: 'openai-api-key', regex: /\bsk-[A-Za-z0-9_-]{20,}T3BlbkFJ[A-Za-z0-9_-]{20,}/g },
|
|
28
|
+
{ type: 'npm-token', regex: /\bnpm_[A-Za-z0-9]{36}\b/g },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Replace known secret formats with `[REDACTED:<type>]`
|
|
33
|
+
* @param {string} text - Content to scan
|
|
34
|
+
* @returns {{text: string, findings: string[]}} Redacted text and one entry per match
|
|
35
|
+
*/
|
|
36
|
+
export function redactSecrets(text) {
|
|
37
|
+
const findings = [];
|
|
38
|
+
let redacted = text;
|
|
39
|
+
|
|
40
|
+
for (const { type, regex } of SECRET_PATTERNS) {
|
|
41
|
+
redacted = redacted.replace(regex, () => {
|
|
42
|
+
findings.push(type);
|
|
43
|
+
return `[REDACTED:${type}]`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { text: redacted, findings };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format redaction counts for console output, e.g. "2 aws-access-key-id, 1 private-key"
|
|
52
|
+
* @param {Object<string, number>} counts - Matches per secret type
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
export function formatRedactionSummary(counts) {
|
|
56
|
+
return Object.entries(counts)
|
|
57
|
+
.map(([type, count]) => `${count} ${type}`)
|
|
58
|
+
.join(', ');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default { redactSecrets, formatRedactionSummary };
|