ctxman 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/.ctxman/llm-profiles.json +1067 -39
  2. package/.ctxman/profiles/backend-team.json +13 -0
  3. package/.ctxman/profiles/default.json +12 -0
  4. package/.ctxman/profiles/devops-team.json +21 -0
  5. package/.ctxman/profiles/frontend-team.json +20 -0
  6. package/.ctxman/templates/bug-fix.json +17 -0
  7. package/.ctxman/templates/code-review.json +9 -0
  8. package/.ctxman/templates/documentation.json +9 -0
  9. package/.ctxman/templates/feature.json +9 -0
  10. package/.ctxman/templates/full-context.json +9 -0
  11. package/.ctxman/templates/refactoring.json +18 -0
  12. package/.ctxman/templates/testing.json +9 -0
  13. package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
  14. package/LICENSE +21 -0
  15. package/README.md +380 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -13
  18. package/bin/cm-audit.js +124 -0
  19. package/bin/cm-count.js +151 -0
  20. package/bin/cm-gitingest.js +128 -115
  21. package/bin/cm-map.js +242 -0
  22. package/bin/cm-mcp.js +293 -0
  23. package/bin/cm-pack.js +405 -0
  24. package/bin/cm-policy.js +170 -0
  25. package/bin/cm-pr.js +248 -0
  26. package/bin/cm-search.js +178 -0
  27. package/bin/cm-update.js +128 -128
  28. package/bin/cm-why.js +197 -0
  29. package/bin/mcp-server.js +8 -24
  30. package/ctxman.js +237 -183
  31. package/examples/README.md +19 -4
  32. package/examples/github-actions/ctxman-check.yml +49 -0
  33. package/examples/wizard-profiles/security-audit/profile.json +7 -1
  34. package/index.d.ts +1422 -0
  35. package/index.js +55 -31
  36. package/lib/agents/pr-failure-agent.js +48 -45
  37. package/lib/analyzers/ai-suggester.js +451 -0
  38. package/lib/analyzers/dependency-scanner.js +505 -0
  39. package/lib/analyzers/duplicate-detector.js +264 -0
  40. package/lib/analyzers/go-method-analyzer.js +151 -110
  41. package/lib/analyzers/import-tracker.js +431 -0
  42. package/lib/analyzers/method-analyzer.js +1055 -434
  43. package/lib/analyzers/performance-dashboard.js +683 -0
  44. package/lib/analyzers/token-calculator.js +895 -504
  45. package/lib/analyzers/unused-exports-detector.js +336 -0
  46. package/lib/api/mcp/install.js +424 -0
  47. package/lib/api/mcp/prompts.js +281 -281
  48. package/lib/api/mcp/resources.js +300 -241
  49. package/lib/api/mcp/sandbox.js +89 -0
  50. package/lib/api/mcp/server.js +675 -312
  51. package/lib/api/mcp/start.js +125 -0
  52. package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
  53. package/lib/api/rest/server.js +105 -28
  54. package/lib/cache/CacheManager.js +9 -17
  55. package/lib/core/Analyzer.js +16 -10
  56. package/lib/core/ContextBuilder.js +19 -28
  57. package/lib/core/Reporter.js +15 -14
  58. package/lib/core/Scanner.js +81 -12
  59. package/lib/count/index.js +100 -0
  60. package/lib/formatters/format-registry.js +394 -372
  61. package/lib/formatters/gitingest-formatter.js +656 -621
  62. package/lib/formatters/toon-formatter-v1.3.js +637 -632
  63. package/lib/formatters/toon-formatter.js +239 -240
  64. package/lib/integrations/git/BlameTracker.js +4 -4
  65. package/lib/integrations/git/DiffAnalyzer.js +38 -31
  66. package/lib/integrations/git/GitClient.js +71 -53
  67. package/lib/lsp/server.js +311 -0
  68. package/lib/map/graph.js +299 -0
  69. package/lib/map/imports.js +233 -0
  70. package/lib/map/index.js +33 -0
  71. package/lib/map/rank.js +253 -0
  72. package/lib/map/render.js +346 -0
  73. package/lib/map/resolver.js +574 -0
  74. package/lib/pack/budget.js +111 -0
  75. package/lib/pack/code-lexer.js +360 -0
  76. package/lib/pack/compress.js +414 -0
  77. package/lib/pack/index.js +354 -0
  78. package/lib/pack/renderers/common.js +134 -0
  79. package/lib/pack/renderers/index.js +24 -0
  80. package/lib/pack/renderers/json.js +47 -0
  81. package/lib/pack/renderers/markdown.js +54 -0
  82. package/lib/pack/renderers/plain.js +38 -0
  83. package/lib/pack/renderers/xml.js +85 -0
  84. package/lib/pack/select.js +177 -0
  85. package/lib/pack/split.js +182 -0
  86. package/lib/pack/tree.js +35 -0
  87. package/lib/parsers/gitignore-parser.js +113 -107
  88. package/lib/parsers/method-filter-parser.js +50 -33
  89. package/lib/plugins/ExporterPlugin.js +3 -3
  90. package/lib/plugins/LanguagePlugin.js +3 -3
  91. package/lib/plugins/PluginManager.js +7 -13
  92. package/lib/policy/audit.js +294 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +52 -0
  96. package/lib/policy/policy.js +489 -0
  97. package/lib/pr/diff-parser.js +267 -0
  98. package/lib/pr/method-mapper.js +148 -0
  99. package/lib/pr/pr-context.js +829 -0
  100. package/lib/pr/pr-git.js +149 -0
  101. package/lib/pr/pr-renderer.js +644 -0
  102. package/lib/pr/related-tests.js +296 -0
  103. package/lib/rag/DataSourcePlugin.js +5 -5
  104. package/lib/rag/EmbeddingProvider.js +19 -8
  105. package/lib/rag/Indexer.js +14 -5
  106. package/lib/rag/VectorStoreFactory.js +3 -3
  107. package/lib/rag/sources/FileDataSource.js +6 -6
  108. package/lib/rag/stores/BaseVectorStore.js +5 -5
  109. package/lib/rag/stores/ChromaDBStore.js +10 -10
  110. package/lib/rag/stores/LanceDBStore.js +29 -19
  111. package/lib/rag/stores/PineconeStore.js +11 -9
  112. package/lib/search/bm25.js +433 -0
  113. package/lib/search/code-units.js +201 -0
  114. package/lib/search/symbols.js +189 -0
  115. package/lib/ui/dashboard.js +120 -89
  116. package/lib/ui/index.js +1 -6
  117. package/lib/ui/init-wizard-ui.js +550 -0
  118. package/lib/ui/progress-bar.js +94 -88
  119. package/lib/ui/select-input.js +40 -32
  120. package/lib/ui/template-selector.js +99 -0
  121. package/lib/ui/wizard.js +374 -329
  122. package/lib/utils/claude-token-counter.js +151 -0
  123. package/lib/utils/cli-output.js +17 -0
  124. package/lib/utils/clipboard-utils.js +49 -50
  125. package/lib/utils/config-utils.js +81 -66
  126. package/lib/utils/context-versioning.js +532 -0
  127. package/lib/utils/error-handler.js +112 -112
  128. package/lib/utils/file-utils.js +153 -69
  129. package/lib/utils/format-converter.js +356 -283
  130. package/lib/utils/git-utils.js +366 -294
  131. package/lib/utils/llm-cost-estimator.js +281 -0
  132. package/lib/utils/llm-detector.js +242 -78
  133. package/lib/utils/logger.js +243 -237
  134. package/lib/utils/messages.js +355 -0
  135. package/lib/utils/multi-repo-manager.js +364 -0
  136. package/lib/utils/profile-manager.js +380 -0
  137. package/lib/utils/safe-read.js +61 -0
  138. package/lib/utils/secret-redactor.js +61 -0
  139. package/lib/utils/snapshot-manager.js +679 -0
  140. package/lib/utils/template-manager.js +378 -0
  141. package/lib/utils/token-utils.js +201 -170
  142. package/lib/utils/tokenizer-adapter.js +421 -377
  143. package/lib/utils/updater.js +539 -506
  144. package/lib/watch/FileWatcher.js +12 -8
  145. package/lib/watch/IncrementalAnalyzer.js +7 -7
  146. package/lib/why/coupling.js +81 -0
  147. package/lib/why/errors.js +23 -0
  148. package/lib/why/git.js +169 -0
  149. package/lib/why/history.js +108 -0
  150. package/lib/why/index.js +720 -0
  151. package/lib/why/ownership.js +79 -0
  152. package/lib/why/paths.js +207 -0
  153. package/lib/why/references.js +259 -0
  154. package/lib/why/render.js +722 -0
  155. package/lib/why/search.js +358 -0
  156. package/lib/why/target.js +389 -0
  157. package/lib/wizards/config-generator.js +255 -0
  158. package/lib/wizards/project-detector.js +571 -0
  159. package/package.json +44 -10
  160. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  161. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  223. 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 };