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.
Files changed (224) 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 +381 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -67
  18. package/bin/cm-audit.js +121 -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 +687 -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 +772 -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 +116 -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 +312 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +53 -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/ask.js +122 -0
  108. package/lib/rag/sources/FileDataSource.js +12 -9
  109. package/lib/rag/stores/BaseVectorStore.js +5 -5
  110. package/lib/rag/stores/ChromaDBStore.js +10 -10
  111. package/lib/rag/stores/LanceDBStore.js +29 -19
  112. package/lib/rag/stores/PineconeStore.js +11 -9
  113. package/lib/search/bm25.js +433 -0
  114. package/lib/search/code-units.js +201 -0
  115. package/lib/search/symbols.js +189 -0
  116. package/lib/ui/dashboard.js +120 -89
  117. package/lib/ui/index.js +1 -6
  118. package/lib/ui/init-wizard-ui.js +550 -0
  119. package/lib/ui/progress-bar.js +94 -88
  120. package/lib/ui/select-input.js +40 -32
  121. package/lib/ui/template-selector.js +99 -0
  122. package/lib/ui/wizard.js +374 -329
  123. package/lib/utils/claude-token-counter.js +151 -0
  124. package/lib/utils/cli-output.js +17 -0
  125. package/lib/utils/clipboard-utils.js +49 -50
  126. package/lib/utils/config-utils.js +81 -66
  127. package/lib/utils/context-versioning.js +532 -0
  128. package/lib/utils/error-handler.js +112 -112
  129. package/lib/utils/file-utils.js +153 -69
  130. package/lib/utils/format-converter.js +356 -283
  131. package/lib/utils/git-utils.js +366 -294
  132. package/lib/utils/llm-cost-estimator.js +281 -0
  133. package/lib/utils/llm-detector.js +242 -78
  134. package/lib/utils/logger.js +246 -237
  135. package/lib/utils/messages.js +355 -0
  136. package/lib/utils/multi-repo-manager.js +364 -0
  137. package/lib/utils/profile-manager.js +380 -0
  138. package/lib/utils/safe-read.js +61 -0
  139. package/lib/utils/secret-redactor.js +61 -0
  140. package/lib/utils/snapshot-manager.js +679 -0
  141. package/lib/utils/template-manager.js +378 -0
  142. package/lib/utils/token-utils.js +201 -170
  143. package/lib/utils/tokenizer-adapter.js +421 -377
  144. package/lib/utils/updater.js +539 -506
  145. package/lib/watch/FileWatcher.js +12 -8
  146. package/lib/watch/IncrementalAnalyzer.js +7 -7
  147. package/lib/why/coupling.js +81 -0
  148. package/lib/why/errors.js +23 -0
  149. package/lib/why/git.js +169 -0
  150. package/lib/why/history.js +108 -0
  151. package/lib/why/index.js +720 -0
  152. package/lib/why/ownership.js +79 -0
  153. package/lib/why/paths.js +207 -0
  154. package/lib/why/references.js +259 -0
  155. package/lib/why/render.js +722 -0
  156. package/lib/why/search.js +358 -0
  157. package/lib/why/target.js +389 -0
  158. package/lib/wizards/config-generator.js +257 -0
  159. package/lib/wizards/project-detector.js +572 -0
  160. package/package.json +44 -10
  161. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  224. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
package/bin/cli.js CHANGED
@@ -3,15 +3,29 @@
3
3
  import { TokenAnalyzer } from '../index.js';
4
4
  import FormatRegistry from '../lib/formatters/format-registry.js';
5
5
  import FormatConverter from '../lib/utils/format-converter.js';
6
- import { LLMDetector } from '../lib/utils/llm-detector.js';
6
+ import { LLMDetector, DEFAULT_TARGET_MODEL } from '../lib/utils/llm-detector.js';
7
+ import { LLMCostEstimator } from '../lib/utils/llm-cost-estimator.js';
7
8
  import APIServer from '../lib/api/rest/server.js';
8
9
  import FileWatcher from '../lib/watch/FileWatcher.js';
9
10
  import IncrementalAnalyzer from '../lib/watch/IncrementalAnalyzer.js';
10
11
  import DiffAnalyzer from '../lib/integrations/git/DiffAnalyzer.js';
11
- import { execSync } from 'child_process';
12
+ import TemplateManager from '../lib/utils/template-manager.js';
13
+ import ProfileManager from '../lib/utils/profile-manager.js';
14
+ import { AISuggester } from '../lib/analyzers/ai-suggester.js';
15
+ import { DependencyScanner } from '../lib/analyzers/dependency-scanner.js';
16
+ import { ImportTracker } from '../lib/analyzers/import-tracker.js';
17
+ import MultiRepoManager from '../lib/utils/multi-repo-manager.js';
18
+ import SnapshotManager from '../lib/utils/snapshot-manager.js';
19
+ import ContextVersioning from '../lib/utils/context-versioning.js';
20
+ import { loadPolicy } from '../lib/policy/policy.js';
21
+ import { spawnSync } from 'child_process';
12
22
  import { fileURLToPath } from 'url';
13
- import { dirname, resolve } from 'path';
23
+ import { dirname, resolve, sep } from 'path';
14
24
  import { readFileSync } from 'fs';
25
+ import { exitOnBrokenPipe } from '../lib/utils/cli-output.js';
26
+
27
+ // `ctxman --cli | head` closes stdout early; stop quietly instead of throwing EPIPE
28
+ exitOnBrokenPipe();
15
29
 
16
30
  // ESM equivalents for __dirname and __filename
17
31
  const __filename = fileURLToPath(import.meta.url);
@@ -20,631 +34,1968 @@ const __dirname = dirname(__filename);
20
34
  // Load package.json
21
35
  const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
22
36
 
23
- async function main() {
24
- const args = process.argv.slice(2);
37
+ // Run one of the bin/cm-*.js subcommand scripts with the remaining arguments
38
+ function runSubcommand(script, scriptArgs) {
39
+ const result = spawnSync(process.execPath, [resolve(__dirname, script), ...scriptArgs], {
40
+ stdio: 'inherit',
41
+ });
42
+ process.exitCode = result.status ?? 1;
43
+ }
25
44
 
26
- if (args.includes('--help') || args.includes('-h')) {
27
- printHelp();
28
- return;
45
+ async function main() {
46
+ const args = process.argv.slice(2);
47
+ // Subcommands count only in first position, so a word inside an unquoted `ask` query or
48
+ // an option value (`--profile serve`) does not switch commands
49
+ const command = args[0]?.startsWith('-') ? null : args[0];
50
+
51
+ // Before --help: the github command prints its own help
52
+ if (command === 'github' || command === 'git') {
53
+ runSubcommand('./cm-gitingest.js', args.slice(1));
54
+ return;
55
+ }
56
+
57
+ if (command === 'count') {
58
+ runSubcommand('./cm-count.js', args.slice(1));
59
+ return;
60
+ }
61
+
62
+ if (command === 'map') {
63
+ runSubcommand('./cm-map.js', args.slice(1));
64
+ return;
65
+ }
66
+
67
+ if (command === 'pr') {
68
+ runSubcommand('./cm-pr.js', args.slice(1));
69
+ return;
70
+ }
71
+
72
+ if (command === 'mcp') {
73
+ runSubcommand('./cm-mcp.js', args.slice(1));
74
+ return;
75
+ }
76
+
77
+ if (command === 'search') {
78
+ runSubcommand('./cm-search.js', args.slice(1));
79
+ return;
80
+ }
81
+
82
+ if (command === 'policy') {
83
+ runSubcommand('./cm-policy.js', args.slice(1));
84
+ return;
85
+ }
86
+
87
+ if (command === 'audit') {
88
+ runSubcommand('./cm-audit.js', args.slice(1));
89
+ return;
90
+ }
91
+
92
+ if (command === 'why') {
93
+ runSubcommand('./cm-why.js', args.slice(1));
94
+ return;
95
+ }
96
+
97
+ if (command === 'pack') {
98
+ runSubcommand('./cm-pack.js', args.slice(1));
99
+ return;
100
+ }
101
+
102
+ if (args.includes('--help') || args.includes('-h')) {
103
+ printHelp();
104
+ return;
105
+ }
106
+
107
+ // Check for version flag
108
+ if (args.includes('--version')) {
109
+ console.log(`Ctxman v${pkg.version}`);
110
+ return;
111
+ }
112
+
113
+ // Check for RAG 'ask' mode (v3.1.0); the rest of the arguments are the query
114
+ if (command === 'ask') {
115
+ runSubcommand('./cm-ask.js', args.slice(1));
116
+ return;
117
+ }
118
+
119
+ if (command === 'update') {
120
+ runSubcommand('./cm-update.js', args.slice(1));
121
+ return;
122
+ }
123
+
124
+ // Digest from an existing report/context file, handled by the legacy entry point's code
125
+ if (args.includes('--gitingest-from-report') || args.includes('--gitingest-from-context')) {
126
+ const { main: runLegacy } = await import('../ctxman.js');
127
+ await runLegacy(args);
128
+ return;
129
+ }
130
+
131
+ // Check for profile listing (FEAT-004)
132
+ if (args.includes('--list-profiles')) {
133
+ listProfiles();
134
+ return;
135
+ }
136
+
137
+ // Check for profile creation (FEAT-004)
138
+ if (args.includes('--create-profile')) {
139
+ createProfile(args);
140
+ return;
141
+ }
142
+
143
+ // Check for profile export (FEAT-004)
144
+ if (args.includes('--export-profile')) {
145
+ exportProfile(args);
146
+ return;
147
+ }
148
+
149
+ // Check for template listing (FEAT-009)
150
+ if (args.includes('--list-templates')) {
151
+ listTemplates();
152
+ return;
153
+ }
154
+
155
+ // Check for format listing
156
+ if (args.includes('--list-formats')) {
157
+ listFormats();
158
+ return;
159
+ }
160
+
161
+ // Check for LLM model listing (v2.3.7)
162
+ if (args.includes('--list-llms')) {
163
+ listLLMs(args);
164
+ return;
165
+ }
166
+
167
+ // Check for cost estimation (FEAT-010)
168
+ if (args.includes('--estimate-cost')) {
169
+ runCostEstimation(args);
170
+ return;
171
+ }
172
+
173
+ // Check for multi-repo commands (FEAT-006)
174
+ if (args.includes('--add-repo')) {
175
+ addRepo(args);
176
+ return;
177
+ }
178
+
179
+ if (args.includes('--remove-repo')) {
180
+ removeRepo(args);
181
+ return;
182
+ }
183
+
184
+ if (args.includes('--list-repos')) {
185
+ listRepos();
186
+ return;
187
+ }
188
+
189
+ // Check for AI suggestions (FEAT-005)
190
+ if (args.includes('--ai-suggest')) {
191
+ await runAISuggest(args);
192
+ return;
193
+ }
194
+
195
+ // Check for snapshot commands (FEAT-003)
196
+ if (args.includes('--snapshot')) {
197
+ await runSnapshot(args);
198
+ return;
199
+ }
200
+
201
+ if (args.includes('--list-snapshots')) {
202
+ await listSnapshots();
203
+ return;
204
+ }
205
+
206
+ if (args.includes('--diff-last')) {
207
+ await runDiffLast(args);
208
+ return;
209
+ }
210
+
211
+ if (args.includes('--diff-snapshot')) {
212
+ await runDiffSnapshot(args);
213
+ return;
214
+ }
215
+
216
+ if (args.includes('--snapshot-trend')) {
217
+ await runSnapshotTrend(args);
218
+ return;
219
+ }
220
+
221
+ // Check for context versioning commands (FEAT-012)
222
+ if (args.includes('--list-versions')) {
223
+ await listVersions();
224
+ return;
225
+ }
226
+
227
+ if (args.includes('--restore-version')) {
228
+ await restoreVersion(args);
229
+ return;
230
+ }
231
+
232
+ if (args.includes('--compare-versions')) {
233
+ await compareVersions(args);
234
+ return;
235
+ }
236
+
237
+ // Check for performance dashboard (FEAT-001)
238
+ if (args.includes('--perf-dashboard')) {
239
+ await runPerfDashboard(args);
240
+ return;
241
+ }
242
+
243
+ // Check for dependency scanning (FEAT-002)
244
+ if (args.includes('--scan-dependencies')) {
245
+ await runDependencyScan(args);
246
+ return;
247
+ }
248
+
249
+ // Check for format conversion mode (v2.3.2)
250
+ if (command === 'convert') {
251
+ runFormatConversion(args);
252
+ return;
253
+ }
254
+
255
+ // Check for init command (FEAT-001: Configuration Wizard)
256
+ if (command === 'init') {
257
+ await runInitWizard(args);
258
+ return;
259
+ }
260
+
261
+ // Check for API server mode (v3.0.0)
262
+ if (command === 'serve') {
263
+ await runAPIServer(args);
264
+ return;
265
+ }
266
+
267
+ // Check for watch mode (v3.0.0)
268
+ if (command === 'watch') {
269
+ await runWatchMode(args);
270
+ return;
271
+ }
272
+
273
+ // Check for explicit dashboard mode
274
+ if (args.includes('--dashboard')) {
275
+ try {
276
+ await runDashboard();
277
+ return;
278
+ } catch (error) {
279
+ console.error('āš ļø Live dashboard failed.');
280
+ console.error(' Error:', error.message);
281
+ console.error(' Falling back to standard mode...\n');
29
282
  }
283
+ }
30
284
 
31
- // Check for version flag
32
- if (args.includes('--version')) {
33
- console.log(`Ctxman v${pkg.version}`);
34
- return;
35
- }
285
+ // Check if CLI mode is explicitly requested OR any analysis flags are present
286
+ const hasAnalysisFlags = args.some(
287
+ (arg) => arg.startsWith('-') && arg !== '--cli' && arg !== '--wizard' && arg !== '--dashboard'
288
+ );
36
289
 
37
- // Check for format listing
38
- if (args.includes('--list-formats')) {
39
- listFormats();
40
- return;
41
- }
290
+ const cliMode = args.includes('--cli') || hasAnalysisFlags;
42
291
 
43
- // Check for LLM model listing (v2.3.7)
44
- if (args.includes('--list-llms')) {
45
- listLLMs();
46
- return;
47
- }
48
-
49
- // Check for format conversion mode (v2.3.2)
50
- if (args.includes('convert')) {
51
- runFormatConversion(args);
52
- return;
292
+ // DEFAULT: Run wizard mode unless --cli flag or other flags are present
293
+ if (!cliMode) {
294
+ try {
295
+ await runWizard();
296
+ return;
297
+ } catch (error) {
298
+ // If wizard fails, fall through to normal mode
299
+ console.error('āš ļø Interactive wizard mode failed.');
300
+ console.error(' Error:', error.message);
301
+ console.error(' Falling back to CLI mode...\n');
302
+ console.error(' Tip: Use --cli flag to skip wizard mode\n');
53
303
  }
304
+ }
54
305
 
55
- // Check for GitHub GitIngest mode (v2.3.6+)
56
- if (args.includes('github') || args.includes('git')) {
57
- const commandPath = resolve(__dirname, './cm-gitingest.js');
58
- const gitArgs = args.filter(arg => arg !== 'github' && arg !== 'git');
59
- execSync(`node "${commandPath}" ${gitArgs.join(' ')}`, { stdio: 'inherit' });
60
- return;
61
- }
306
+ // CLI Mode: Run traditional command-line analysis
307
+ let options = parseArguments(args);
62
308
 
63
- // Check for RAG 'ask' mode (v3.1.0)
64
- if (args.includes('ask')) {
65
- const commandPath = resolve(__dirname, './cm-ask.js');
66
- const askArgs = args.filter(arg => arg !== 'ask');
67
- // Pass remaining args as the query
68
- execSync(`node "${commandPath}" "${askArgs.join(' ')}"`, { stdio: 'inherit' });
69
- return;
70
- }
309
+ // Apply template if specified (FEAT-009)
310
+ if (options.template) {
311
+ try {
312
+ const manager = new TemplateManager(process.cwd());
313
+ const templateConfig = manager.apply(options.template, {
314
+ targetModel: options.targetModel,
315
+ });
71
316
 
72
- // Check for API server mode (v3.0.0)
73
- if (args.includes('serve')) {
74
- await runAPIServer(args);
75
- return;
317
+ // Merge template config with options (options take precedence)
318
+ options = {
319
+ ...options,
320
+ templateConfig,
321
+ // Use template's target model if not explicitly set
322
+ targetModel: options.targetModel || templateConfig.targetModel,
323
+ };
324
+ } catch (error) {
325
+ console.error(`āŒ Template error: ${error.message}`);
326
+ process.exit(1);
76
327
  }
328
+ }
77
329
 
78
- // Check for watch mode (v3.0.0)
79
- if (args.includes('watch')) {
80
- await runWatchMode(args);
81
- return;
82
- }
330
+ // Apply profile if specified (FEAT-004)
331
+ if (options.profile) {
332
+ try {
333
+ const manager = new ProfileManager(process.cwd());
334
+ const profileConfig = manager.apply(options.profile, {
335
+ targetModel: options.targetModel,
336
+ });
83
337
 
84
- // Check for explicit dashboard mode
85
- if (args.includes('--dashboard')) {
86
- try {
87
- await runDashboard();
88
- return;
89
- } catch (error) {
90
- console.error('āš ļø Live dashboard failed.');
91
- console.error(' Error:', error.message);
92
- console.error(' Falling back to standard mode...\n');
93
- }
338
+ // Merge profile config with options (options take precedence)
339
+ options = {
340
+ ...options,
341
+ profileConfig,
342
+ // Use profile's target model if not explicitly set
343
+ targetModel: options.targetModel || profileConfig.targetModel,
344
+ methodLevel: options.methodLevel || profileConfig.methodLevel,
345
+ };
346
+ } catch (error) {
347
+ console.error(`āŒ Profile error: ${error.message}`);
348
+ process.exit(1);
94
349
  }
350
+ }
95
351
 
96
- // Check if CLI mode is explicitly requested OR any analysis flags are present
97
- const hasAnalysisFlags = args.some(arg =>
98
- arg.startsWith('-') &&
99
- arg !== '--cli' &&
100
- arg !== '--wizard' &&
101
- arg !== '--dashboard'
102
- );
352
+ // Export policy (.ctxman/policy.json, CTXMAN_POLICY): deny rules, export limits, audit log
353
+ const policy = loadCliPolicy(options.projectRoot);
103
354
 
104
- const cliMode = args.includes('--cli') || hasAnalysisFlags;
105
-
106
- // DEFAULT: Run wizard mode unless --cli flag or other flags are present
107
- if (!cliMode) {
108
- try {
109
- await runWizard();
110
- return;
111
- } catch (error) {
112
- // If wizard fails, fall through to normal mode
113
- console.error('āš ļø Interactive wizard mode failed.');
114
- console.error(' Error:', error.message);
115
- console.error(' Falling back to CLI mode...\n');
116
- console.error(' Tip: Use --cli flag to skip wizard mode\n');
117
- }
118
- }
355
+ // Git integration: Filter to changed files only (v3.0.0)
356
+ if (options.changedOnly || options.changedSince) {
357
+ await runChangedFilesAnalysis({ ...options, policy });
358
+ return;
359
+ }
119
360
 
120
- // CLI Mode: Run traditional command-line analysis
121
- const options = parseArguments(args);
361
+ // Multi-repository analysis (FEAT-006)
362
+ if (options.multiRepo) {
363
+ await runMultiRepoAnalysis(options);
364
+ return;
365
+ }
122
366
 
123
- // Git integration: Filter to changed files only (v3.0.0)
124
- if (options.changedOnly || options.changedSince) {
125
- await runChangedFilesAnalysis(options);
126
- return;
127
- }
367
+ printStartupInfo(options);
128
368
 
129
- printStartupInfo(options);
369
+ const analyzer = new TokenAnalyzer(options.projectRoot, { ...options, policy });
370
+ const stats = analyzer.run();
130
371
 
131
- const analyzer = new TokenAnalyzer(options.projectRoot, options);
132
- analyzer.run();
372
+ // Auto-create context version when using --cli (FEAT-012)
373
+ if (args.includes('--cli') && stats) {
374
+ await autoCreateVersion(stats, options);
375
+ }
133
376
  }
134
377
 
135
378
  function parseArguments(args) {
136
- return {
137
- // Output options
138
- saveReport: args.includes('--save-report') || args.includes('-s'),
139
- verbose: args.includes('--verbose') || args.includes('-v'),
140
- contextExport: args.includes('--context-export'),
141
- contextToClipboard: args.includes('--context-clipboard'),
142
- json: args.includes('--json'), // NEW: JSON output mode
143
-
144
- // Analysis options
145
- methodLevel: args.includes('--method-level') || args.includes('-m'),
146
- gitingest: args.includes('--gitingest') || args.includes('-g'),
147
-
148
- // Format options (v2.3.0)
149
- outputFormat: getOutputFormat(args),
150
-
151
- // LLM options (v2.3.7)
152
- targetModel: getTargetModel(args),
153
- autoDetectLLM: args.includes('--auto-detect-llm'),
154
-
155
- // Git options (v3.0.0)
156
- changedOnly: args.includes('--changed-only'),
157
- changedSince: getChangedSince(args),
158
- withAuthors: args.includes('--with-authors'),
159
- withHistory: args.includes('--with-history'),
160
-
161
- // UI options (v2.3.0)
162
- simple: args.includes('--simple'),
163
- dashboard: args.includes('--dashboard'),
164
-
165
- // Chunking options (v2.3.0)
166
- chunking: {
167
- enabled: args.includes('--chunk'),
168
- strategy: getChunkStrategy(args),
169
- maxTokensPerChunk: getChunkSize(args)
170
- },
171
-
172
- projectRoot: process.cwd()
173
- };
379
+ return {
380
+ // Output options
381
+ saveReport: args.includes('--save-report') || args.includes('-s'),
382
+ verbose: args.includes('--verbose') || args.includes('-v'),
383
+ contextExport: args.includes('--context-export'),
384
+ contextToClipboard: args.includes('--context-clipboard'),
385
+ json: args.includes('--json'), // NEW: JSON output mode
386
+
387
+ // Analysis options
388
+ methodLevel: args.includes('--method-level') || args.includes('-m'),
389
+ gitingest: args.includes('--gitingest') || args.includes('-g'),
390
+ redactSecrets: !args.includes('--no-redact'),
391
+ respectIgnoreFiles: getRespectIgnoreFiles(args),
392
+ aiSuggest: args.includes('--ai-suggest'), // FEAT-005: AI suggestions
393
+
394
+ // Profile options (FEAT-004)
395
+ profile: getProfile(args),
396
+
397
+ // Template options (FEAT-009)
398
+ template: getTemplate(args),
399
+
400
+ // Format options (v2.3.0)
401
+ outputFormat: getOutputFormat(args),
402
+
403
+ // LLM options (v2.3.7)
404
+ targetModel: getTargetModel(args),
405
+ autoDetectLLM: args.includes('--auto-detect-llm'),
406
+
407
+ // Git options (v3.0.0)
408
+ changedOnly: args.includes('--changed-only'),
409
+ changedSince: getChangedSince(args),
410
+
411
+ // Multi-repo options (FEAT-006)
412
+ multiRepo: args.includes('--multi-repo'),
413
+
414
+ // UI options (v2.3.0)
415
+ simple: args.includes('--simple'),
416
+ dashboard: args.includes('--dashboard'),
417
+
418
+ // Chunking options (v2.3.0)
419
+ chunking: {
420
+ enabled: args.includes('--chunk'),
421
+ strategy: getChunkStrategy(args),
422
+ maxTokensPerChunk: getChunkSize(args),
423
+ },
424
+
425
+ projectRoot: process.cwd(),
426
+ };
174
427
  }
175
428
 
176
429
  function getOutputFormat(args) {
177
- const formatIndex = args.findIndex(arg => arg === '--output' || arg === '-o');
178
- if (formatIndex !== -1 && args[formatIndex + 1]) {
179
- return args[formatIndex + 1];
180
- }
181
- return 'toon'; // Default to TOON format in v2.3.0
430
+ const formatIndex = args.findIndex((arg) => arg === '--output' || arg === '-o');
431
+ if (formatIndex !== -1 && args[formatIndex + 1]) {
432
+ return args[formatIndex + 1];
433
+ }
434
+ return null; // Not set: context exports stay JSON (llm-context.json)
435
+ }
436
+
437
+ // --respect-ignore-files <list|all>: also honour other AI tools' ignore files
438
+ function getRespectIgnoreFiles(args) {
439
+ const index = args.indexOf('--respect-ignore-files');
440
+ if (index === -1) return null;
441
+ const value = args[index + 1];
442
+ return value && !value.startsWith('-') ? value : 'all';
443
+ }
444
+
445
+ // An invalid policy file stops the command instead of being ignored
446
+ function loadCliPolicy(projectRoot) {
447
+ try {
448
+ return loadPolicy(projectRoot);
449
+ } catch (error) {
450
+ console.error(`āŒ ${error.message}`);
451
+ process.exit(error.exitCode ?? 2);
452
+ }
182
453
  }
183
454
 
184
455
  function getChunkStrategy(args) {
185
- const strategyIndex = args.findIndex(arg => arg === '--chunk-strategy');
186
- if (strategyIndex !== -1 && args[strategyIndex + 1]) {
187
- return args[strategyIndex + 1];
188
- }
189
- return 'smart'; // Default strategy
456
+ const strategyIndex = args.findIndex((arg) => arg === '--chunk-strategy');
457
+ if (strategyIndex !== -1 && args[strategyIndex + 1]) {
458
+ return args[strategyIndex + 1];
459
+ }
460
+ return 'smart'; // Default strategy
190
461
  }
191
462
 
192
463
  function getChunkSize(args) {
193
- const sizeIndex = args.findIndex(arg => arg === '--chunk-size');
194
- if (sizeIndex !== -1 && args[sizeIndex + 1]) {
195
- return parseInt(args[sizeIndex + 1], 10);
196
- }
197
- return 100000; // Default 100k tokens
464
+ const sizeIndex = args.findIndex((arg) => arg === '--chunk-size');
465
+ if (sizeIndex !== -1 && args[sizeIndex + 1]) {
466
+ return parseInt(args[sizeIndex + 1], 10);
467
+ }
468
+ return 100000; // Default 100k tokens
198
469
  }
199
470
 
200
471
  function getTargetModel(args) {
201
- // Check for explicit model flag
202
- const modelIndex = args.findIndex(arg => arg === '--target-model');
203
- if (modelIndex !== -1 && args[modelIndex + 1]) {
204
- return args[modelIndex + 1];
205
- }
206
-
207
- // Check for auto-detect flag
208
- if (args.includes('--auto-detect-llm')) {
209
- const detected = LLMDetector.detect();
210
- if (detected !== 'unknown') {
211
- console.log(`āœ… Auto-detected LLM: ${LLMDetector.getProfile(detected).name}`);
212
- return detected;
213
- }
472
+ // Check for explicit model flag
473
+ const modelIndex = args.findIndex((arg) => arg === '--target-model');
474
+ if (modelIndex !== -1 && args[modelIndex + 1]) {
475
+ return args[modelIndex + 1];
476
+ }
477
+
478
+ // Check for auto-detect flag
479
+ if (args.includes('--auto-detect-llm')) {
480
+ const detected = LLMDetector.detect();
481
+ if (detected !== 'unknown') {
482
+ console.log(`āœ… Auto-detected LLM: ${LLMDetector.getProfile(detected).name}`);
483
+ return detected;
214
484
  }
485
+ }
215
486
 
216
- return null; // No model specified
487
+ return null; // No model specified
217
488
  }
218
489
 
219
- function listLLMs() {
220
- console.log('\nšŸ“‹ Supported LLM Models (v2.3.7):\n');
221
- console.log('═'.repeat(70));
490
+ function listLLMs(args = []) {
491
+ const includeRetired = args.includes('--all');
492
+ const models = LLMDetector.getModelList({ includeRetired });
493
+ const checked = LLMDetector.getDataDate();
222
494
 
223
- const models = LLMDetector.getModelList();
224
-
225
- // Group by vendor
226
- const byVendor = {};
227
- models.forEach(model => {
228
- if (!byVendor[model.vendor]) {
229
- byVendor[model.vendor] = [];
230
- }
231
- byVendor[model.vendor].push(model);
232
- });
495
+ console.log(`\nšŸ“‹ Supported LLM Models${checked ? ` (checked ${checked})` : ''}:\n`);
496
+ console.log('═'.repeat(78));
233
497
 
234
- // Display by vendor
235
- Object.entries(byVendor).forEach(([vendor, models]) => {
236
- console.log(`\n${vendor}:`);
237
- models.forEach(model => {
238
- const contextDisplay = model.contextWindow >= 1000000
239
- ? `${(model.contextWindow / 1000000).toFixed(1)}M`
240
- : `${Math.floor(model.contextWindow / 1000)}k`;
241
- console.log(` ${model.id.padEnd(25)} ${model.name.padEnd(25)} (${contextDisplay} context)`);
242
- });
498
+ // Group by vendor
499
+ const byVendor = {};
500
+ models.forEach((model) => {
501
+ if (!byVendor[model.vendor]) {
502
+ byVendor[model.vendor] = [];
503
+ }
504
+ byVendor[model.vendor].push(model);
505
+ });
506
+
507
+ const size = (tokens) =>
508
+ !tokens
509
+ ? '-'
510
+ : tokens >= 1000000
511
+ ? `${+(tokens / 1000000).toFixed(2)}M`
512
+ : `${Math.floor(tokens / 1000)}K`;
513
+ const price = (pricing) => (pricing ? `$${pricing.input}/$${pricing.output}` : '-');
514
+
515
+ // Display by vendor
516
+ Object.entries(byVendor).forEach(([vendor, models]) => {
517
+ console.log(`\n${vendor}:`);
518
+ console.log(
519
+ ` ${'ID'.padEnd(26)} ${'Context'.padStart(7)} ${'Output'.padStart(7)} ${'$ in/out per 1M'.padEnd(16)} Status`
520
+ );
521
+ models.forEach((model) => {
522
+ const status = model.status === 'active' ? '' : model.status;
523
+ console.log(
524
+ ` ${model.id.padEnd(26)} ${size(model.contextWindow).padStart(7)} ${size(model.outputWindow).padStart(7)} ${price(model.pricing).padEnd(16)} ${status}`
525
+ );
243
526
  });
244
-
245
- console.log('\n' + '═'.repeat(70));
246
- console.log('\nUsage:');
247
- console.log(' ctxman --target-model <MODEL_ID>');
248
- console.log(' ctxman --auto-detect-llm');
249
- console.log('\nExample:');
250
- console.log(' ctxman --target-model claude-sonnet-4.5');
251
- console.log(' ctxman --auto-detect-llm --cli\n');
527
+ });
528
+
529
+ console.log('\n' + '═'.repeat(78));
530
+ console.log('\nUsage:');
531
+ console.log(' ctxman --target-model <MODEL_ID>');
532
+ console.log(' ctxman --auto-detect-llm');
533
+ console.log(' ctxman --list-llms --all Include models the provider has retired');
534
+ console.log('\nExample:');
535
+ console.log(` ctxman --target-model ${DEFAULT_TARGET_MODEL}`);
536
+ console.log(' ctxman --auto-detect-llm --cli\n');
252
537
  }
253
538
 
254
539
  function printStartupInfo(options) {
255
- console.log('šŸš€ Ctxman v3.0.0');
256
- console.log('='.repeat(50));
257
-
258
- // Only show active options if any are set
259
- const hasOptions = options.outputFormat || options.methodLevel || options.chunking?.enabled ||
260
- options.saveReport || options.verbose || options.contextExport ||
261
- options.contextToClipboard || options.gitingest;
262
-
263
- if (hasOptions) {
264
- console.log('šŸ“‹ Active options:');
265
- if (options.outputFormat) {
266
- console.log(` Output format: ${options.outputFormat}`);
267
- }
268
- if (options.methodLevel) {
269
- console.log(' Method-level analysis: enabled');
270
- }
271
- if (options.saveReport) {
272
- console.log(' Save report: enabled');
273
- }
274
- if (options.gitingest) {
275
- console.log(' GitIngest format: enabled');
276
- }
277
- if (options.contextExport) {
278
- console.log(' Context export: enabled');
279
- }
280
- if (options.contextToClipboard) {
281
- console.log(' Copy to clipboard: enabled');
282
- }
283
- if (options.chunking?.enabled) {
284
- console.log(` Chunking: ${options.chunking.strategy} (${options.chunking.maxTokensPerChunk.toLocaleString()} tokens/chunk)`);
285
- }
286
- console.log();
540
+ console.log(`šŸš€ Ctxman v${pkg.version}`);
541
+ console.log('='.repeat(50));
542
+
543
+ // Show profile info if used
544
+ if (options.profile) {
545
+ const manager = new ProfileManager(process.cwd());
546
+ const profile = manager.get(options.profile);
547
+ if (profile) {
548
+ console.log(`šŸ“‹ Using profile: ${profile.name}`);
549
+ console.log(` Description: ${profile.description}`);
550
+ console.log();
551
+ }
552
+ }
553
+
554
+ // Show template info if used
555
+ if (options.template) {
556
+ const manager = new TemplateManager(process.cwd());
557
+ const template = manager.get(options.template);
558
+ if (template) {
559
+ console.log(`šŸ“‹ Using template: ${template.name}`);
560
+ console.log(` Description: ${template.description}`);
561
+ console.log();
562
+ }
563
+ }
564
+
565
+ // Only show active options if any are set
566
+ const hasOptions =
567
+ options.outputFormat ||
568
+ options.methodLevel ||
569
+ options.chunking?.enabled ||
570
+ options.saveReport ||
571
+ options.verbose ||
572
+ options.contextExport ||
573
+ options.contextToClipboard ||
574
+ options.gitingest;
575
+
576
+ if (hasOptions) {
577
+ console.log('šŸ“‹ Active options:');
578
+ if (options.outputFormat) {
579
+ console.log(` Output format: ${options.outputFormat}`);
580
+ }
581
+ if (options.methodLevel) {
582
+ console.log(' Method-level analysis: enabled');
583
+ }
584
+ if (options.saveReport) {
585
+ console.log(' Save report: enabled');
586
+ }
587
+ if (options.gitingest) {
588
+ console.log(' GitIngest format: enabled');
287
589
  }
590
+ if (options.contextExport) {
591
+ console.log(' Context export: enabled');
592
+ }
593
+ if (options.contextToClipboard) {
594
+ console.log(' Copy to clipboard: enabled');
595
+ }
596
+ if (options.chunking?.enabled) {
597
+ console.log(
598
+ ` Chunking: ${options.chunking.strategy} (${options.chunking.maxTokensPerChunk.toLocaleString()} tokens/chunk)`
599
+ );
600
+ }
601
+ console.log();
602
+ }
288
603
  }
289
604
 
290
605
  function printHelp() {
291
- console.log('Ctxman v3.0.0 - AI Development Platform with Plugin Architecture and Git Integration');
292
- console.log();
293
- console.log('Usage: ctxman [options]');
294
- console.log();
295
- console.log('Default Mode:');
296
- console.log(' ctxman Launch interactive wizard (DEFAULT)');
297
- console.log(' --cli Use CLI mode instead of wizard');
298
- console.log();
299
- console.log('Analysis Options:');
300
- console.log(' -s, --save-report Save detailed JSON report');
301
- console.log(' -v, --verbose Show all included files');
302
- console.log(' -m, --method-level Enable method-level analysis');
303
- console.log(' -g, --gitingest Generate GitIngest-style digest');
304
- console.log();
305
- console.log('Output Options (v2.3.0):');
306
- console.log(' -o, --output FORMAT Output format (default: toon)');
307
- console.log(' Formats: toon, json, yaml, csv, xml, markdown, gitingest');
308
- console.log(' --context-export Generate LLM context file');
309
- console.log(' --context-clipboard Copy context to clipboard');
310
- console.log(' --list-formats List all available output formats');
311
- console.log();
312
- console.log('UI Options (v2.3.0):');
313
- console.log(' --simple Simple text-based output (no fancy UI)');
314
- console.log(' --dashboard Live dashboard mode');
315
- console.log(' --wizard Force interactive wizard mode');
316
- console.log();
317
- console.log('Chunking Options (v2.3.0):');
318
- console.log(' --chunk Enable smart chunking for large repos');
319
- console.log(' --chunk-strategy TYPE Chunking strategy (smart, size, file, directory)');
320
- console.log(' --chunk-size TOKENS Max tokens per chunk (default: 100000)');
321
- console.log();
322
- console.log('LLM Optimization (v2.3.7):');
323
- console.log(' --target-model MODEL Optimize for specific LLM (e.g., claude-sonnet-4.5)');
324
- console.log(' --auto-detect-llm Auto-detect LLM from environment variables');
325
- console.log(' --list-llms List all supported LLM models');
326
- console.log();
327
- console.log('Git Integration (v3.0.0):');
328
- console.log(' --changed-only Analyze only files with uncommitted changes');
329
- console.log(' --changed-since REF Analyze files changed since commit/branch');
330
- console.log(' --with-authors Include author information');
331
- console.log(' --with-history Include commit history');
332
- console.log();
333
- console.log('Platform Features (v3.0.0):');
334
- console.log(' serve [options] Start REST API server');
335
- console.log(' --port PORT Server port (default: 3000)');
336
- console.log(' --auth-token TOKEN API authentication token');
337
- console.log(' watch [options] Watch mode with auto-analysis');
338
- console.log(' --debounce MS Debounce delay (default: 1000ms)');
339
- console.log();
340
- console.log('General Options:');
341
- console.log(' -h, --help Show this help');
342
- console.log(' --version Show version number');
343
- console.log();
344
- console.log('Configuration Files:');
345
- console.log(' .contextinclude Include only specified files');
346
- console.log(' .contextignore Exclude specified files');
347
- console.log(' .methodinclude Include only specified methods');
348
- console.log(' .methodignore Exclude specified methods');
349
- console.log();
350
- console.log('Format Conversion (v2.3.2):');
351
- console.log(' convert INPUT --from FORMAT --to FORMAT');
352
- console.log(' Convert between formats');
353
- console.log(' Examples:');
354
- console.log(' ctxman convert report.json --from json --to toon');
355
- console.log(' ctxman convert data.toon --from toon --to yaml');
356
- console.log(' ctxman convert context.yaml --from yaml --to json');
357
- console.log();
358
- console.log('GitHub Integration (v2.3.6+):');
359
- console.log(' github URL [options] Generate GitIngest from GitHub repository');
360
- console.log(' git URL [options] Alias for github command');
361
- console.log(' Examples:');
362
- console.log(' ctxman github facebook/react');
363
- console.log(' ctxman github https://github.com/vercel/next.js --branch canary');
364
- console.log(' ctxman git angular/angular -o docs/angular.txt');
365
- console.log();
366
- console.log('Examples:');
367
- console.log(' ctxman # Launch interactive wizard (DEFAULT)');
368
- console.log(' ctxman --cli # Use CLI mode');
369
- console.log(' ctxman --cli -o json --save-report # CLI: JSON format + save report');
370
- console.log(' ctxman --cli -o toon --context-clipboard # CLI: TOON to clipboard');
371
- console.log(' ctxman --cli --gitingest --chunk # CLI: GitIngest with chunking');
372
- console.log(' ctxman --cli -m -o yaml # CLI: Method-level + YAML format');
373
- console.log(' ctxman --cli --chunk --chunk-strategy smart # CLI: Smart chunking');
374
- console.log(' ctxman convert data.json --from json --to toon # Convert formats');
375
- console.log();
376
- console.log('Format Comparison (token efficiency):');
377
- console.log(' TOON: 40-50% reduction (most efficient)');
378
- console.log(' JSON: Standard format (baseline)');
379
- console.log(' YAML: Human-readable (5-10% larger than JSON)');
380
- console.log(' Markdown: Documentation-friendly (20-30% larger)');
381
- console.log();
382
- console.log('For more information: https://github.com/hakkisagdic/ctxman');
606
+ console.log(
607
+ `Ctxman v${pkg.version} - AI Development Platform with Plugin Architecture and Git Integration`
608
+ );
609
+ console.log();
610
+ console.log('Usage: ctxman [options]');
611
+ console.log();
612
+ console.log('Initialization (FEAT-001):');
613
+ console.log(' init Initialize ctxman configuration with interactive wizard');
614
+ console.log(' --force, -f Overwrite existing configuration files');
615
+ console.log(' --minimal Create minimal configuration only');
616
+ console.log(' --yes, -y Skip prompts and use defaults');
617
+ console.log();
618
+ console.log('Default Mode:');
619
+ console.log(' ctxman Launch interactive wizard (DEFAULT)');
620
+ console.log(' --cli Use CLI mode instead of wizard');
621
+ console.log();
622
+ console.log('Profile Options (FEAT-004):');
623
+ console.log(' --profile <name> Use a team configuration profile');
624
+ console.log(' --list-profiles List available profiles');
625
+ console.log(' --create-profile <name> Create a new custom profile');
626
+ console.log(' --export-profile <name> Export a profile to JSON');
627
+ console.log();
628
+ console.log('Template Options (FEAT-009):');
629
+ console.log(' -t, --template <name> Use a pre-built context template');
630
+ console.log(' --list-templates List available templates');
631
+ console.log();
632
+ console.log('Analysis Options:');
633
+ console.log(' -s, --save-report Save detailed JSON report');
634
+ console.log(' -v, --verbose Show all included files');
635
+ console.log(' -m, --method-level Enable method-level analysis');
636
+ console.log(' -g, --gitingest Generate GitIngest-style digest');
637
+ console.log(' --no-redact Keep API keys/tokens/private keys in the digest');
638
+ console.log(' --respect-ignore-files LIST');
639
+ console.log(' Also honour AI-tool ignore files: "all" or a list such');
640
+ console.log(' as .cursorignore,.aiexclude (see ctxman policy)');
641
+ console.log(' --ai-suggest Get AI-powered context optimization suggestions');
642
+ console.log();
643
+ console.log('Output Options (v2.3.0):');
644
+ console.log(
645
+ ' -o, --output FORMAT Format of --context-export/--context-clipboard (default: json)'
646
+ );
647
+ console.log(
648
+ ' Formats: toon, json, yaml, csv, xml, markdown, gitingest'
649
+ );
650
+ console.log(' --context-export Generate LLM context file');
651
+ console.log(' --context-clipboard Copy context to clipboard');
652
+ console.log(' --list-formats List all available output formats');
653
+ console.log();
654
+ console.log('UI Options (v2.3.0):');
655
+ console.log(' --simple Simple text-based output (no fancy UI)');
656
+ console.log(' --dashboard Live dashboard mode');
657
+ console.log(' --wizard Force interactive wizard mode');
658
+ console.log();
659
+ console.log('Chunking Options (v2.3.0):');
660
+ console.log(' --chunk Enable smart chunking for large repos');
661
+ console.log(' --chunk-strategy TYPE Chunking strategy (smart, size, file, directory)');
662
+ console.log(' --chunk-size TOKENS Max tokens per chunk (default: 100000)');
663
+ console.log();
664
+ console.log('LLM Optimization (v2.3.7):');
665
+ console.log(
666
+ ' --target-model MODEL Optimize for specific LLM (e.g., claude-sonnet-5-5, gpt-5)'
667
+ );
668
+ console.log(' --auto-detect-llm Auto-detect LLM from environment variables');
669
+ console.log(' --list-llms List all supported LLM models');
670
+ console.log();
671
+ console.log('Cost Estimation (FEAT-010):');
672
+ console.log(' --estimate-cost Show cost estimates for all LLM providers');
673
+ console.log();
674
+ console.log('Snapshot & Diff (FEAT-003):');
675
+ console.log(' --snapshot [message] Create a snapshot of current token state');
676
+ console.log(' --list-snapshots List all saved snapshots');
677
+ console.log(' --diff-last Compare current state with last snapshot');
678
+ console.log(' --diff-snapshot <id1> <id2> Compare two specific snapshots');
679
+ console.log(' --snapshot-trend Show token growth trend across snapshots');
680
+ console.log(' --limit N Limit snapshots to analyze (default: 10)');
681
+ console.log(' --json Output in JSON format');
682
+ console.log();
683
+ console.log('Context Versioning (FEAT-012):');
684
+ console.log(' --cli Run analysis and auto-create context version');
685
+ console.log(' --list-versions List all saved context versions');
686
+ console.log(' --restore-version <id> Restore a previous context version');
687
+ console.log(' --compare-versions <id1> <id2> Compare two context versions');
688
+ console.log(' --json Output in JSON format (for compare)');
689
+ console.log();
690
+ console.log('Performance Dashboard (FEAT-001):');
691
+ console.log(' --perf-dashboard Display text-based performance dashboard');
692
+ console.log(' --period <period> Time period: 7d (default), 30d, all');
693
+ console.log(' --save-report Save dashboard output to file');
694
+ console.log();
695
+ console.log('Dependency Scanner (FEAT-002):');
696
+ console.log(' --scan-dependencies Analyze node_modules for token impact');
697
+ console.log(' --dependency-depth N How deep to scan (default: 1)');
698
+ console.log(' --include-types Include TypeScript definitions');
699
+ console.log(' --check-security Check for security vulnerabilities (npm audit)');
700
+ console.log();
701
+ console.log('Multi-Repository (FEAT-006):');
702
+ console.log(' --multi-repo Analyze all configured repositories');
703
+ console.log(' --add-repo <path> Add a repository to configuration');
704
+ console.log(' --remove-repo <path> Remove a repository from configuration');
705
+ console.log(' --list-repos List all configured repositories');
706
+ console.log();
707
+ console.log('Git Integration (v3.0.0):');
708
+ console.log(' --changed-only Analyze only files with uncommitted changes');
709
+ console.log(' --changed-since REF Analyze files changed since commit/branch');
710
+ console.log();
711
+ console.log('Platform Features (v3.0.0):');
712
+ console.log(' serve [options] Start REST API server');
713
+ console.log(' --port PORT Server port (default: 3000)');
714
+ console.log(' --auth-token TOKEN API authentication token');
715
+ console.log(' --host HOST Address to bind (default: localhost)');
716
+ console.log(' --cors Send CORS headers (off by default)');
717
+ console.log(' watch [options] Watch mode with auto-analysis');
718
+ console.log(' --debounce MS Debounce delay (default: 1000ms)');
719
+ console.log();
720
+ console.log('General Options:');
721
+ console.log(' -h, --help Show this help');
722
+ console.log(' --version Show version number');
723
+ console.log();
724
+ console.log('Template Examples:');
725
+ console.log(' ctxman --template bug-fix Use bug-fix template');
726
+ console.log(' ctxman -t feature --cli Feature template in CLI mode');
727
+ console.log(' ctxman --list-templates Show all templates');
728
+ console.log();
729
+ console.log('Configuration Files:');
730
+ console.log(' .contextinclude Include only specified files');
731
+ console.log(' .contextignore Exclude specified files');
732
+ console.log(' .methodinclude Include only specified methods');
733
+ console.log(' .methodignore Exclude specified methods');
734
+ console.log(' .ctxman/templates/*.json Custom template files');
735
+ console.log(' .ctxman/profiles/*.json Custom team profile files');
736
+ console.log();
737
+ console.log('Format Conversion (v2.3.2):');
738
+ console.log(' convert INPUT --from FORMAT --to FORMAT');
739
+ console.log(' Convert between formats');
740
+ console.log(' Examples:');
741
+ console.log(' ctxman convert report.json --from json --to toon');
742
+ console.log(' ctxman convert data.toon --from toon --to yaml');
743
+ console.log(' ctxman convert context.yaml --from yaml --to json');
744
+ console.log();
745
+ console.log('GitHub Integration (v2.3.6+):');
746
+ console.log(' github URL [options] Generate GitIngest from GitHub repository');
747
+ console.log(' git URL [options] Alias for github command');
748
+ console.log(' Examples:');
749
+ console.log(' ctxman github facebook/react');
750
+ console.log(' ctxman github https://github.com/vercel/next.js --branch canary');
751
+ console.log(' ctxman git angular/angular -o docs/angular.txt');
752
+ console.log();
753
+ console.log('Other Commands:');
754
+ console.log(' pack [path] [options] File contents in one LLM-ready document (xml, markdown,');
755
+ console.log(' plain, json); --compress, --budget, --split');
756
+ console.log(' (ctxman pack --help for options)');
757
+ console.log(' map [path] [options] Ranked repository map within a token budget');
758
+ console.log(' (ctxman map --help for options)');
759
+ console.log(' pr [--base REF] [--head REF] [path]');
760
+ console.log(' Review context for a branch: log, changed methods,');
761
+ console.log(' diff, related tests (ctxman pr --help)');
762
+ console.log(' why FILE[:SYMBOL|#L10-L40] [options]');
763
+ console.log(' Why the code looks like this: commits, references,');
764
+ console.log(' owners, co-changes, docs and tests (ctxman why --help)');
765
+ console.log(' search QUERY [path] Ranked (BM25) search over methods and code chunks');
766
+ console.log(' count [path|-] [--model ID] [--api]');
767
+ console.log(' Count tokens for a model (exact for Claude with --api)');
768
+ console.log(' mcp [install|uninstall|config]');
769
+ console.log(' Start the MCP server or add it to an MCP client');
770
+ console.log(' policy [check|init] Show or create the export policy (.ctxman/policy.json)');
771
+ console.log(' audit [--since DATE] Show the export audit log written under a policy');
772
+ console.log(' ask QUESTION Answer a question from the indexed codebase');
773
+ console.log(' update [check|install|rollback|channel NAME|info]');
774
+ console.log(' Check for and manage ctxman updates');
775
+ console.log();
776
+ console.log('Examples:');
777
+ console.log(' ctxman # Launch interactive wizard (DEFAULT)');
778
+ console.log(' ctxman --cli # Use CLI mode');
779
+ console.log(' ctxman --cli -o json --save-report # CLI: JSON format + save report');
780
+ console.log(' ctxman --cli -o toon --context-clipboard # CLI: TOON to clipboard');
781
+ console.log(' ctxman --cli --gitingest --chunk # CLI: GitIngest with chunking');
782
+ console.log(' ctxman --cli -m -o yaml # CLI: Method-level + YAML format');
783
+ console.log(' ctxman --cli --chunk --chunk-strategy smart # CLI: Smart chunking');
784
+ console.log(' ctxman convert data.json --from json --to toon # Convert formats');
785
+ console.log();
786
+ console.log('Format Comparison (token efficiency):');
787
+ console.log(' TOON: 40-50% reduction (most efficient)');
788
+ console.log(' JSON: Standard format (baseline)');
789
+ console.log(' YAML: Human-readable (5-10% larger than JSON)');
790
+ console.log(' Markdown: Documentation-friendly (20-30% larger)');
791
+ console.log();
792
+ console.log('For more information: https://github.com/hakkisagdic/ctxman');
383
793
  }
384
794
 
385
795
  function listFormats() {
386
- const registry = new FormatRegistry();
387
- const formats = registry.getAllInfo();
388
-
389
- console.log('šŸ“‹ Available Output Formats:\n');
390
- console.log('Format'.padEnd(15) + 'Description'.padEnd(50) + 'Extension');
391
- console.log('='.repeat(80));
392
-
393
- for (const [name, info] of Object.entries(formats)) {
394
- console.log(
395
- name.padEnd(15) +
396
- info.description.substring(0, 48).padEnd(50) +
397
- info.extension
398
- );
399
- }
796
+ const registry = new FormatRegistry();
797
+ const formats = registry.getAllInfo();
400
798
 
401
- console.log();
402
- console.log('Usage: ctxman --output <format>');
403
- console.log('Example: ctxman --output toon --context-clipboard');
799
+ console.log('šŸ“‹ Available Output Formats:\n');
800
+ console.log('Format'.padEnd(15) + 'Description'.padEnd(50) + 'Extension');
801
+ console.log('='.repeat(80));
802
+
803
+ for (const [name, info] of Object.entries(formats)) {
804
+ console.log(name.padEnd(15) + info.description.substring(0, 48).padEnd(50) + info.extension);
805
+ }
806
+
807
+ console.log();
808
+ console.log('Usage: ctxman --output <format>');
809
+ console.log('Example: ctxman --output toon --context-clipboard');
404
810
  }
405
811
 
406
812
  function getChangedSince(args) {
407
- const sinceIndex = args.findIndex(arg => arg === '--changed-since');
408
- if (sinceIndex !== -1 && args[sinceIndex + 1]) {
409
- return args[sinceIndex + 1];
410
- }
411
- return null;
813
+ const sinceIndex = args.findIndex((arg) => arg === '--changed-since');
814
+ if (sinceIndex !== -1 && args[sinceIndex + 1]) {
815
+ return args[sinceIndex + 1];
816
+ }
817
+ return null;
818
+ }
819
+
820
+ function getTemplate(args) {
821
+ const templateIndex = args.findIndex((arg) => arg === '--template' || arg === '-t');
822
+ if (templateIndex !== -1 && args[templateIndex + 1]) {
823
+ return args[templateIndex + 1];
824
+ }
825
+ return null;
826
+ }
827
+
828
+ function getProfile(args) {
829
+ const profileIndex = args.findIndex((arg) => arg === '--profile');
830
+ if (profileIndex !== -1 && args[profileIndex + 1]) {
831
+ return args[profileIndex + 1];
832
+ }
833
+ return null;
834
+ }
835
+
836
+ function listTemplates() {
837
+ const manager = new TemplateManager(process.cwd());
838
+ console.log(manager.formatList());
839
+ }
840
+
841
+ function listProfiles() {
842
+ const manager = new ProfileManager(process.cwd());
843
+ console.log(manager.formatList());
844
+ }
845
+
846
+ function createProfile(args) {
847
+ const profileIndex = args.findIndex((arg) => arg === '--create-profile');
848
+ const profileName = profileIndex !== -1 && args[profileIndex + 1] ? args[profileIndex + 1] : null;
849
+
850
+ if (!profileName) {
851
+ console.error('āŒ Profile name required');
852
+ console.error(' Usage: ctxman --create-profile <name>');
853
+ process.exit(1);
854
+ }
855
+
856
+ const manager = new ProfileManager(process.cwd());
857
+
858
+ // Create a basic profile template
859
+ const profile = {
860
+ name: `${profileName} Profile`,
861
+ description: `Configuration for ${profileName}`,
862
+ createdBy: process.env.USER || 'unknown',
863
+ config: {
864
+ exclude: ['**/*.test.js', '**/*.spec.js', 'node_modules/**'],
865
+ include: ['src/**', 'lib/**'],
866
+ targetModel: DEFAULT_TARGET_MODEL,
867
+ methodLevel: false,
868
+ },
869
+ };
870
+
871
+ try {
872
+ const filePath = manager.createCustom(profileName, profile);
873
+ console.log(`\nāœ… Created profile '${profileName}'`);
874
+ console.log(` Location: ${filePath}\n`);
875
+ console.log(' Edit the file to customize your team configuration.\n');
876
+ } catch (error) {
877
+ console.error(`āŒ Failed to create profile: ${error.message}`);
878
+ process.exit(1);
879
+ }
880
+ }
881
+
882
+ function exportProfile(args) {
883
+ const profileIndex = args.findIndex((arg) => arg === '--export-profile');
884
+ const profileName = profileIndex !== -1 && args[profileIndex + 1] ? args[profileIndex + 1] : null;
885
+
886
+ if (!profileName) {
887
+ console.error('āŒ Profile name required');
888
+ console.error(' Usage: ctxman --export-profile <name>');
889
+ process.exit(1);
890
+ }
891
+
892
+ const manager = new ProfileManager(process.cwd());
893
+
894
+ try {
895
+ const profile = manager.export(profileName);
896
+ console.log(JSON.stringify(profile, null, 2));
897
+ } catch (error) {
898
+ console.error(`āŒ Failed to export profile: ${error.message}`);
899
+ process.exit(1);
900
+ }
412
901
  }
413
902
 
414
903
  async function runAPIServer(args) {
415
- const portIndex = args.findIndex(arg => arg === '--port');
416
- const port = portIndex !== -1 && args[portIndex + 1]
417
- ? parseInt(args[portIndex + 1], 10)
418
- : 3000;
419
-
420
- const authTokenIndex = args.findIndex(arg => arg === '--auth-token');
421
- const authToken = authTokenIndex !== -1 && args[authTokenIndex + 1]
422
- ? args[authTokenIndex + 1]
423
- : null;
424
-
425
- const server = new APIServer({ port, authToken });
426
-
427
- // Handle shutdown
428
- process.on('SIGINT', () => {
429
- console.log('\n\nšŸ›‘ Shutting down server...');
430
- server.stop();
431
- process.exit(0);
432
- });
904
+ const portIndex = args.findIndex((arg) => arg === '--port');
905
+ const port = portIndex !== -1 && args[portIndex + 1] ? parseInt(args[portIndex + 1], 10) : 3000;
906
+
907
+ const authTokenIndex = args.findIndex((arg) => arg === '--auth-token');
908
+ const authToken =
909
+ authTokenIndex !== -1 && args[authTokenIndex + 1] ? args[authTokenIndex + 1] : null;
910
+
911
+ const hostIndex = args.findIndex((arg) => arg === '--host');
912
+ const host = hostIndex !== -1 && args[hostIndex + 1] ? args[hostIndex + 1] : 'localhost';
913
+ const cors = args.includes('--cors');
914
+
915
+ if (!authToken && cors) {
916
+ console.warn('āš ļø --cors without --auth-token: any web page you open can read the responses.');
917
+ }
918
+ if (!authToken && !['localhost', '127.0.0.1', '::1'].includes(host)) {
919
+ console.warn(
920
+ `āš ļø Listening on ${host} without --auth-token: anyone on the network can query it.`
921
+ );
922
+ }
923
+
924
+ const server = new APIServer({ port, host, authToken, cors });
925
+
926
+ // Handle shutdown
927
+ process.on('SIGINT', () => {
928
+ console.log('\n\nšŸ›‘ Shutting down server...');
929
+ server.stop();
930
+ process.exit(0);
931
+ });
433
932
 
434
- server.start();
933
+ server.start();
435
934
  }
436
935
 
437
936
  async function runChangedFilesAnalysis(options) {
438
- console.log('šŸ”€ Git Integration - Analyzing Changed Files');
439
- console.log('═'.repeat(60));
440
- console.log();
937
+ console.log('šŸ”€ Git Integration - Analyzing Changed Files');
938
+ console.log('═'.repeat(60));
939
+ console.log();
940
+
941
+ const diffAnalyzer = new DiffAnalyzer(options.projectRoot);
942
+ const changes = diffAnalyzer.analyzeChanges(options.changedSince);
943
+
944
+ console.log(`šŸ“ Found ${changes.totalChangedFiles} changed files`);
945
+ if (options.changedSince) {
946
+ console.log(` Since: ${options.changedSince}`);
947
+ }
948
+ console.log(` Impact: ${changes.impact.level.toUpperCase()} (score: ${changes.impact.score})`);
949
+ console.log();
950
+
951
+ if (changes.totalChangedFiles === 0) {
952
+ console.log('āœ… No changed files to analyze');
953
+ return;
954
+ }
955
+
956
+ // Analyze only changed files; git reports them relative to the repository root
957
+ const changedFiles = new Set(changes.changedFiles);
958
+ const analyzer = new TokenAnalyzer(options.projectRoot, {
959
+ ...options,
960
+ fileFilter: (relativePath) => changedFiles.has(relativePath.split(sep).join('/')),
961
+ });
962
+
963
+ analyzer.run();
964
+ }
965
+
966
+ async function runWatchMode(args) {
967
+ console.log('šŸ‘ļø Starting watch mode...\n');
968
+
969
+ const projectRoot = process.cwd();
970
+ const debounce = args.includes('--debounce')
971
+ ? parseInt(args[args.indexOf('--debounce') + 1], 10) || 1000
972
+ : 1000;
973
+
974
+ const watcher = new FileWatcher(projectRoot, { debounce });
975
+ const analyzer = new IncrementalAnalyzer({ methodLevel: args.includes('-m') });
976
+
977
+ // Handle file changes
978
+ watcher.on('file:changed', async (event) => {
979
+ console.log(`\nšŸ“ File ${event.type}: ${event.relativePath}`);
980
+ await analyzer.analyzeChange(event);
981
+ });
982
+
983
+ // Handle analysis complete
984
+ analyzer.on('analysis:complete', (event) => {
985
+ console.log(` āœ… Analysis complete: ${event.analysis.tokens} tokens (${event.elapsed}ms)`);
986
+
987
+ const stats = analyzer.getStats();
988
+ console.log(
989
+ ` šŸ“Š Total: ${stats.totalFiles} files, ${stats.totalTokens.toLocaleString()} tokens`
990
+ );
991
+ });
992
+
993
+ // Handle file deletion
994
+ analyzer.on('file:deleted', (event) => {
995
+ console.log(` šŸ—‘ļø File deleted: ${event.file} (-${event.oldTokens} tokens)`);
996
+ });
997
+
998
+ // Start watching
999
+ watcher.start();
1000
+
1001
+ console.log('āœ… Watch mode active');
1002
+ console.log(' Press Ctrl+C to stop\n');
1003
+
1004
+ // Keep process alive
1005
+ process.on('SIGINT', () => {
1006
+ console.log('\n\nšŸ›‘ Stopping watch mode...');
1007
+ watcher.stop();
1008
+ process.exit(0);
1009
+ });
1010
+ }
1011
+
1012
+ // Ink needs raw mode on a terminal; without one it prints an error and exits 0 having done
1013
+ // nothing, so the callers' fallbacks never ran
1014
+ function assertInteractiveTerminal() {
1015
+ if (!process.stdin.isTTY) {
1016
+ throw new Error('stdin is not an interactive terminal');
1017
+ }
1018
+ }
1019
+
1020
+ async function runWizard() {
1021
+ try {
1022
+ assertInteractiveTerminal();
1023
+
1024
+ // Dynamic imports for ESM modules
1025
+ const ReactModule = await import('react');
1026
+ const React = ReactModule.default || ReactModule;
1027
+ const { render } = await import('ink');
1028
+ const Wizard = (await import('../lib/ui/wizard.js')).default;
1029
+
1030
+ // Clear screen for clean wizard display
1031
+ console.clear();
1032
+ console.log('šŸ§™ Starting interactive wizard...\n');
1033
+
1034
+ const instance = render(
1035
+ React.createElement(Wizard, {
1036
+ onComplete: (answers) => {
1037
+ instance.unmount();
1038
+
1039
+ console.log('\n✨ Wizard complete! Running analysis with your configuration...\n');
1040
+
1041
+ // Show template info if profile is used
1042
+ if (answers.profile && answers.profile !== 'custom') {
1043
+ console.log(`šŸ“‹ Using template: ${answers.profile}`);
1044
+ if (answers.profileMetadata) {
1045
+ console.log(` Description: ${answers.profileMetadata.description}`);
1046
+ }
1047
+ console.log();
1048
+ }
1049
+
1050
+ // Run analyzer with wizard configuration
1051
+ const options = {
1052
+ outputFormat: answers.outputFormat,
1053
+ useCase: answers.useCase,
1054
+ targetModel: answers.targetModel,
1055
+ projectRoot: process.cwd(),
1056
+ simple: true, // No export menu
1057
+ contextExport: true, // Auto-export to file
1058
+ template: answers.profile !== 'custom' ? answers.profile : null,
1059
+ policy: loadCliPolicy(process.cwd()),
1060
+ };
1061
+
1062
+ const analyzer = new TokenAnalyzer(options.projectRoot, options);
1063
+ analyzer.run();
1064
+
1065
+ console.log('\nāœ… Analysis complete! Context exported to llm-context.json\n');
1066
+ },
1067
+ })
1068
+ );
1069
+ } catch (error) {
1070
+ throw error; // Re-throw to be caught by main()
1071
+ }
1072
+ }
1073
+
1074
+ async function runDashboard() {
1075
+ try {
1076
+ assertInteractiveTerminal();
1077
+
1078
+ // Dynamic imports for ESM modules
1079
+ const ReactModule = await import('react');
1080
+ const React = ReactModule.default || ReactModule;
1081
+ const { render } = await import('ink');
1082
+ const Dashboard = (await import('../lib/ui/dashboard.js')).default;
1083
+
1084
+ // Clear console and show loading message
1085
+ console.clear();
1086
+ console.log('šŸ“Š Analyzing project for dashboard...\n');
1087
+
1088
+ // Run analyzer silently (no console output)
1089
+ const originalLog = console.log;
1090
+ const logs = [];
1091
+ console.log = (...args) => logs.push(args); // Capture logs
1092
+
1093
+ const analyzer = new TokenAnalyzer(process.cwd(), {
1094
+ simple: true,
1095
+ verbose: false,
1096
+ dashboard: true, // Skip export handling
1097
+ });
1098
+ const stats = analyzer.run();
1099
+
1100
+ console.log = originalLog; // Restore console.log
441
1101
 
442
- const diffAnalyzer = new DiffAnalyzer(options.projectRoot);
443
- const changes = diffAnalyzer.analyzeChanges(options.changedSince);
1102
+ // Clear and render dashboard
1103
+ console.clear();
1104
+
1105
+ const instance = render(
1106
+ React.createElement(Dashboard, {
1107
+ stats,
1108
+ topFiles: stats.largestFiles || [],
1109
+ status: 'complete',
1110
+ onExit: () => {
1111
+ instance.unmount();
1112
+ process.exit(0);
1113
+ },
1114
+ })
1115
+ );
1116
+ } catch (error) {
1117
+ throw error; // Re-throw to be caught by main()
1118
+ }
1119
+ }
1120
+
1121
+ /**
1122
+ * Run init wizard for configuration setup (FEAT-001)
1123
+ * @param {string[]} args - Command line arguments
1124
+ */
1125
+ async function runInitWizard(args) {
1126
+ try {
1127
+ const options = {
1128
+ force: args.includes('--force') || args.includes('-f'),
1129
+ minimal: args.includes('--minimal'),
1130
+ yes: args.includes('--yes') || args.includes('-y'),
1131
+ };
444
1132
 
445
- console.log(`šŸ“ Found ${changes.totalChangedFiles} changed files`);
446
- if (options.changedSince) {
447
- console.log(` Since: ${options.changedSince}`);
1133
+ if (!options.yes && !options.minimal && !process.stdin.isTTY) {
1134
+ console.error('āŒ The init wizard needs an interactive terminal.');
1135
+ console.error(' Use --yes for the detected defaults or --minimal for a minimal config.');
1136
+ process.exitCode = 1;
1137
+ return;
448
1138
  }
449
- console.log(` Impact: ${changes.impact.level.toUpperCase()} (score: ${changes.impact.score})`);
450
- console.log();
451
1139
 
452
- if (changes.totalChangedFiles === 0) {
453
- console.log('āœ… No changed files to analyze');
454
- return;
1140
+ // Dynamic imports for ESM modules
1141
+ const ReactModule = await import('react');
1142
+ const React = ReactModule.default || ReactModule;
1143
+ const { render } = await import('ink');
1144
+ const InitWizard = (await import('../lib/ui/init-wizard-ui.js')).default;
1145
+
1146
+ // Clear screen for clean wizard display
1147
+ console.clear();
1148
+
1149
+ // If --yes flag, skip interactive UI and use defaults
1150
+ if (options.yes || options.minimal) {
1151
+ console.log('šŸ§™ Initializing ctxman configuration...\n');
1152
+
1153
+ const ProjectDetector = (await import('../lib/wizards/project-detector.js')).default;
1154
+ const ConfigGenerator = (await import('../lib/wizards/config-generator.js')).default;
1155
+
1156
+ const detector = new ProjectDetector(process.cwd());
1157
+ const generator = new ConfigGenerator(process.cwd());
1158
+
1159
+ // Detect project type
1160
+ const projects = await detector.detect();
1161
+
1162
+ let config;
1163
+ if (options.minimal) {
1164
+ config = {
1165
+ contextignore: ['node_modules/', '.git/', 'dist/', 'build/', 'coverage/'],
1166
+ contextinclude: ['src/**', 'lib/**'],
1167
+ methodinclude: [],
1168
+ };
1169
+ } else {
1170
+ const types = projects.map((p) => p.type);
1171
+ config = detector.getMergedConfig(types);
1172
+ }
1173
+
1174
+ // Generate config files
1175
+ const result = generator.generate(config, {
1176
+ force: options.force,
1177
+ minimal: options.minimal,
1178
+ });
1179
+
1180
+ // Display results
1181
+ console.log('Configuration generated:\n');
1182
+ result.created.forEach((f) => console.log(` āœ“ Created ${f.filename}`));
1183
+ result.overwritten.forEach((f) => console.log(` āœ“ Updated ${f.filename}`));
1184
+ result.skipped.forEach((f) => console.log(` ⊘ Skipped ${f.filename} (${f.reason})`));
1185
+
1186
+ console.log('\nāœ… Configuration complete! Try: ctxman --cli\n');
1187
+ return;
455
1188
  }
456
1189
 
457
- // Analyze only changed files
458
- const analyzer = new TokenAnalyzer(options.projectRoot, {
459
- ...options,
460
- fileFilter: (filePath) => changes.changedFiles.includes(filePath)
461
- });
1190
+ // Interactive wizard
1191
+ const instance = render(
1192
+ React.createElement(InitWizard, {
1193
+ options,
1194
+ onComplete: (result) => {
1195
+ instance.unmount();
462
1196
 
463
- analyzer.run();
1197
+ if (result) {
1198
+ console.log('\nāœ… Configuration complete!\n');
1199
+ }
1200
+ },
1201
+ })
1202
+ );
1203
+ } catch (error) {
1204
+ console.error('āŒ Init wizard failed:', error.message);
1205
+ console.error(error.stack);
1206
+ process.exit(1);
1207
+ }
464
1208
  }
465
1209
 
466
- async function runWatchMode(args) {
467
- console.log('šŸ‘ļø Starting watch mode...\n');
1210
+ /**
1211
+ * Run cost estimation for all LLM providers (FEAT-010)
1212
+ * @param {string[]} args - Command line arguments
1213
+ */
1214
+ function runCostEstimation(_args) {
1215
+ console.log('šŸ’° LLM Cost Estimator');
1216
+ console.log('═'.repeat(60));
1217
+ console.log();
1218
+ console.log('šŸ“Š Analyzing repository...');
1219
+ console.log();
1220
+
1221
+ // Run analyzer silently (no console output)
1222
+ const originalLog = console.log;
1223
+ const logs = [];
1224
+ console.log = (...args) => logs.push(args); // Capture logs
1225
+
1226
+ const analyzer = new TokenAnalyzer(process.cwd(), {
1227
+ simple: true,
1228
+ verbose: false,
1229
+ dashboard: true, // Skip export handling
1230
+ });
1231
+ const stats = analyzer.run();
1232
+
1233
+ console.log = originalLog; // Restore console.log
1234
+
1235
+ if (!stats || !stats.totalTokens) {
1236
+ console.error('āŒ Failed to analyze repository');
1237
+ process.exit(1);
1238
+ }
468
1239
 
469
- const projectRoot = process.cwd();
470
- const debounce = args.includes('--debounce')
471
- ? parseInt(args[args.indexOf('--debounce') + 1], 10) || 1000
472
- : 1000;
1240
+ // Create cost estimator
1241
+ const estimator = new LLMCostEstimator(stats.totalTokens);
473
1242
 
474
- const watcher = new FileWatcher(projectRoot, { debounce });
475
- const analyzer = new IncrementalAnalyzer({ methodLevel: args.includes('-m') });
1243
+ // Get comparisons and recommendation
1244
+ const comparisons = estimator.compareAll();
1245
+ const recommendation = estimator.getRecommendation(comparisons);
476
1246
 
477
- // Handle file changes
478
- watcher.on('file:changed', async (event) => {
479
- console.log(`\nšŸ“ File ${event.type}: ${event.relativePath}`);
480
- await analyzer.analyzeChange(event);
481
- });
1247
+ // Display formatted output
1248
+ console.log(estimator.formatEstimates(comparisons, recommendation));
1249
+ }
482
1250
 
483
- // Handle analysis complete
484
- analyzer.on('analysis:complete', (event) => {
485
- console.log(` āœ… Analysis complete: ${event.analysis.tokens} tokens (${event.elapsed}ms)`);
1251
+ /**
1252
+ * Add a repository to multi-repo configuration (FEAT-006)
1253
+ * @param {string[]} args - Command line arguments
1254
+ */
1255
+ function addRepo(args) {
1256
+ const repoIndex = args.findIndex((arg) => arg === '--add-repo');
1257
+ const repoPath = repoIndex !== -1 && args[repoIndex + 1] ? args[repoIndex + 1] : null;
1258
+
1259
+ if (!repoPath) {
1260
+ console.error('āŒ Repository path required');
1261
+ console.error(' Usage: ctxman --add-repo <path>');
1262
+ console.error(' Example: ctxman --add-repo ../frontend-app');
1263
+ process.exit(1);
1264
+ }
486
1265
 
487
- const stats = analyzer.getStats();
488
- console.log(` šŸ“Š Total: ${stats.totalFiles} files, ${stats.totalTokens.toLocaleString()} tokens`);
489
- });
1266
+ const manager = new MultiRepoManager(process.cwd());
490
1267
 
491
- // Handle file deletion
492
- analyzer.on('file:deleted', (event) => {
493
- console.log(` šŸ—‘ļø File deleted: ${event.file} (-${event.oldTokens} tokens)`);
494
- });
1268
+ // Parse optional alias from --alias flag
1269
+ const aliasIndex = args.findIndex((arg) => arg === '--alias');
1270
+ const alias = aliasIndex !== -1 && args[aliasIndex + 1] ? args[aliasIndex + 1] : null;
1271
+
1272
+ try {
1273
+ const repo = manager.addRepo(repoPath, { alias });
1274
+ console.log(`\nāœ… Added repository: ${repo.alias}`);
1275
+ console.log(` Path: ${repo.path}`);
1276
+ console.log(` ID: ${repo.id}\n`);
1277
+ } catch (error) {
1278
+ console.error(`āŒ Failed to add repository: ${error.message}`);
1279
+ process.exit(1);
1280
+ }
1281
+ }
495
1282
 
496
- // Start watching
497
- watcher.start();
1283
+ /**
1284
+ * Remove a repository from multi-repo configuration (FEAT-006)
1285
+ * @param {string[]} args - Command line arguments
1286
+ */
1287
+ function removeRepo(args) {
1288
+ const repoIndex = args.findIndex((arg) => arg === '--remove-repo');
1289
+ const repoPath = repoIndex !== -1 && args[repoIndex + 1] ? args[repoIndex + 1] : null;
1290
+
1291
+ if (!repoPath) {
1292
+ console.error('āŒ Repository path required');
1293
+ console.error(' Usage: ctxman --remove-repo <path>');
1294
+ console.error(' Example: ctxman --remove-repo ../frontend-app');
1295
+ process.exit(1);
1296
+ }
498
1297
 
499
- console.log('āœ… Watch mode active');
500
- console.log(' Press Ctrl+C to stop\n');
1298
+ const manager = new MultiRepoManager(process.cwd());
501
1299
 
502
- // Keep process alive
503
- process.on('SIGINT', () => {
504
- console.log('\n\nšŸ›‘ Stopping watch mode...');
505
- watcher.stop();
506
- process.exit(0);
507
- });
1300
+ if (manager.removeRepo(repoPath)) {
1301
+ console.log(`\nāœ… Removed repository: ${repoPath}\n`);
1302
+ } else {
1303
+ console.error(`āŒ Repository not found: ${repoPath}`);
1304
+ console.error(' Use --list-repos to see configured repositories.');
1305
+ process.exit(1);
1306
+ }
508
1307
  }
509
1308
 
510
- async function runWizard() {
511
- try {
512
- // Dynamic imports for ESM modules
513
- const ReactModule = await import('react');
514
- const React = ReactModule.default || ReactModule;
515
- const { render } = await import('ink');
516
- const Wizard = (await import('../lib/ui/wizard.js')).default;
517
-
518
- // Clear screen for clean wizard display
519
- console.clear();
520
- console.log('šŸ§™ Starting interactive wizard...\n');
521
-
522
- const instance = render(
523
- React.createElement(Wizard, {
524
- onComplete: (answers) => {
525
- instance.unmount();
526
-
527
- console.log('\n✨ Wizard complete! Running analysis with your configuration...\n');
528
-
529
- // Run analyzer with wizard configuration
530
- const options = {
531
- outputFormat: answers.outputFormat,
532
- useCase: answers.useCase,
533
- targetModel: answers.targetModel,
534
- projectRoot: process.cwd(),
535
- simple: true, // No export menu
536
- contextExport: true // Auto-export to file
537
- };
538
-
539
- const analyzer = new TokenAnalyzer(options.projectRoot, options);
540
- analyzer.run();
541
-
542
- console.log('\nāœ… Analysis complete! Context exported to llm-context.json\n');
543
- }
544
- })
545
- );
546
- } catch (error) {
547
- throw error; // Re-throw to be caught by main()
548
- }
1309
+ /**
1310
+ * List all configured repositories (FEAT-006)
1311
+ */
1312
+ function listRepos() {
1313
+ const manager = new MultiRepoManager(process.cwd());
1314
+ console.log(manager.formatList());
549
1315
  }
550
1316
 
551
- async function runDashboard() {
552
- try {
553
- // Dynamic imports for ESM modules
554
- const ReactModule = await import('react');
555
- const React = ReactModule.default || ReactModule;
556
- const { render } = await import('ink');
557
- const Dashboard = (await import('../lib/ui/dashboard.js')).default;
558
-
559
- // Clear console and show loading message
560
- console.clear();
561
- console.log('šŸ“Š Analyzing project for dashboard...\n');
562
-
563
- // Run analyzer silently (no console output)
564
- const originalLog = console.log;
565
- const logs = [];
566
- console.log = (...args) => logs.push(args); // Capture logs
567
-
568
- const analyzer = new TokenAnalyzer(process.cwd(), {
569
- simple: true,
570
- verbose: false,
571
- dashboard: true // Skip export handling
572
- });
573
- const stats = analyzer.run();
574
-
575
- console.log = originalLog; // Restore console.log
576
-
577
- // Clear and render dashboard
578
- console.clear();
579
-
580
- const instance = render(
581
- React.createElement(Dashboard, {
582
- stats,
583
- topFiles: stats.largestFiles || [],
584
- status: 'complete',
585
- onExit: () => {
586
- instance.unmount();
587
- process.exit(0);
588
- }
589
- })
590
- );
591
- } catch (error) {
592
- throw error; // Re-throw to be caught by main()
1317
+ /**
1318
+ * Run multi-repository analysis (FEAT-006)
1319
+ * @param {object} options - Analysis options
1320
+ */
1321
+ async function runMultiRepoAnalysis(options) {
1322
+ console.log('šŸ”€ Multi-Repository Context Analysis');
1323
+ console.log('═'.repeat(60));
1324
+ console.log();
1325
+
1326
+ const manager = new MultiRepoManager(process.cwd());
1327
+ const repos = manager.listRepos();
1328
+
1329
+ if (repos.length === 0) {
1330
+ console.log('āš ļø No repositories configured.');
1331
+ console.log();
1332
+ console.log(' Add repositories with:');
1333
+ console.log(' ctxman --add-repo <path>');
1334
+ console.log();
1335
+ console.log(' Example:');
1336
+ console.log(' ctxman --add-repo ../frontend');
1337
+ console.log(' ctxman --add-repo ../api --alias "API Service"');
1338
+ console.log();
1339
+ return;
1340
+ }
1341
+
1342
+ console.log(`šŸ“Š Analyzing ${repos.length} repositories...\n`);
1343
+
1344
+ // Run analysis
1345
+ const results = manager.analyzeAll(options);
1346
+
1347
+ // Display results
1348
+ console.log(manager.formatResults(results));
1349
+
1350
+ // Generate GitIngest digest if requested
1351
+ if (options.gitingest) {
1352
+ console.log('\nšŸ“ Generating GitIngest digest...\n');
1353
+ await generateMultiRepoDigest(results, options);
1354
+ }
1355
+ }
1356
+
1357
+ /**
1358
+ * Generate GitIngest digest for multi-repo results
1359
+ * @param {object} results - Multi-repo analysis results
1360
+ * @param {object} options - Generation options
1361
+ */
1362
+ async function generateMultiRepoDigest(results, _options) {
1363
+ const fs = await import('fs');
1364
+ const path = await import('path');
1365
+
1366
+ let digest = `# Multi-Repository Context\n\n`;
1367
+ digest += `Generated: ${new Date().toISOString()}\n\n`;
1368
+
1369
+ // Repository summary
1370
+ digest += `## Repository Summary\n\n`;
1371
+ digest += `| Repository | Files | Tokens | % of Total |\n`;
1372
+ digest += `|------------|-------|--------|------------|\n`;
1373
+
1374
+ for (const repo of results.repos) {
1375
+ if (!repo.error) {
1376
+ digest += `| ${repo.alias} | ${repo.files.toLocaleString()} | ${repo.tokens.toLocaleString()} | ${repo.percentage}% |\n`;
1377
+ }
1378
+ }
1379
+
1380
+ digest += `| **Total** | **${results.combined.totalFiles.toLocaleString()}** | **${results.combined.totalTokens.toLocaleString()}** | **100%** |\n\n`;
1381
+
1382
+ // File contents per repository
1383
+ digest += `## Repository Contents\n\n`;
1384
+
1385
+ for (const repo of results.repos) {
1386
+ if (repo.error) continue;
1387
+
1388
+ digest += `### ${repo.alias}\n\n`;
1389
+
1390
+ // Read and include file contents
1391
+ const repoPath = repo.resolvedPath;
1392
+ if (fs.existsSync(repoPath)) {
1393
+ digest += `Path: ${repo.path}\n\n`;
1394
+ digest += `Files: ${repo.files}, Tokens: ${repo.tokens.toLocaleString()}\n\n`;
593
1395
  }
1396
+ }
1397
+
1398
+ // Write digest
1399
+ const outputPath = path.join(process.cwd(), 'digest.txt');
1400
+ fs.writeFileSync(outputPath, digest);
1401
+ console.log(`šŸ“ Generated digest: ${outputPath}\n`);
1402
+ }
1403
+
1404
+ /**
1405
+ * Run AI-powered context suggestions (FEAT-005)
1406
+ * @param {string[]} args - Command line arguments
1407
+ */
1408
+ async function runAISuggest(args) {
1409
+ const json = args.includes('--json');
1410
+ // With --json, stdout carries only the JSON document
1411
+ const info = json ? console.error : console.log;
1412
+
1413
+ info('šŸ¤– AI Context Suggestions');
1414
+ info('═'.repeat(60));
1415
+ info();
1416
+ info('šŸ“Š Analyzing repository for optimization opportunities...');
1417
+ info();
1418
+
1419
+ const verbose = args.includes('--verbose') || args.includes('-v');
1420
+
1421
+ // Run analyzer silently (no console output)
1422
+ const originalLog = console.log;
1423
+ const logs = [];
1424
+ console.log = (...args) => logs.push(args); // Capture logs
1425
+
1426
+ const analyzer = new TokenAnalyzer(process.cwd(), {
1427
+ simple: true,
1428
+ verbose: false,
1429
+ dashboard: true, // Skip export handling
1430
+ });
1431
+ const stats = analyzer.run();
1432
+
1433
+ console.log = originalLog; // Restore console.log
1434
+
1435
+ if (!stats || !stats.totalTokens) {
1436
+ console.error('āŒ Failed to analyze repository');
1437
+ process.exit(1);
1438
+ }
1439
+
1440
+ // Get the files array from stats
1441
+ const files = stats.largestFiles || [];
1442
+
1443
+ // Create AI suggester
1444
+ const suggester = new AISuggester({
1445
+ projectRoot: process.cwd(),
1446
+ verbose,
1447
+ json,
1448
+ });
1449
+
1450
+ // Run analysis and get suggestions; the suggester's logger writes through console.log
1451
+ console.log = info;
1452
+ const result = await suggester.analyze(stats, files);
1453
+ console.log = originalLog;
1454
+
1455
+ // Display formatted output
1456
+ console.log(suggester.formatOutput(result));
1457
+
1458
+ // Save report if requested
1459
+ if (args.includes('--save-report') || args.includes('-s')) {
1460
+ const reportPath = resolve(process.cwd(), 'ai-suggestions-report.json');
1461
+ const fs = await import('fs');
1462
+ fs.writeFileSync(reportPath, JSON.stringify(result, null, 2));
1463
+ info(`šŸ’¾ Report saved to: ai-suggestions-report.json`);
1464
+ }
594
1465
  }
595
1466
 
596
1467
  function runFormatConversion(args) {
597
- // v2.3.2: Format conversion utility
598
- const converter = new FormatConverter();
599
-
600
- // Parse arguments
601
- const fromIndex = args.findIndex(arg => arg === '--from');
602
- const toIndex = args.findIndex(arg => arg === '--to');
603
- const inputIndex = args.findIndex(arg => arg === 'convert') + 1;
604
-
605
- if (fromIndex === -1 || toIndex === -1) {
606
- console.error('āŒ Format conversion requires --from and --to flags');
607
- console.error(' Usage: ctxman convert input.json --from json --to toon');
608
- process.exit(1);
1468
+ // v2.3.2: Format conversion utility
1469
+ const converter = new FormatConverter();
1470
+
1471
+ // Parse arguments
1472
+ const fromIndex = args.findIndex((arg) => arg === '--from');
1473
+ const toIndex = args.findIndex((arg) => arg === '--to');
1474
+ const inputIndex = args.findIndex((arg) => arg === 'convert') + 1;
1475
+
1476
+ if (fromIndex === -1 || toIndex === -1) {
1477
+ console.error('āŒ Format conversion requires --from and --to flags');
1478
+ console.error(' Usage: ctxman convert input.json --from json --to toon');
1479
+ process.exit(1);
1480
+ }
1481
+
1482
+ const inputFile = args[inputIndex];
1483
+ const fromFormat = args[fromIndex + 1];
1484
+ const toFormat = args[toIndex + 1];
1485
+
1486
+ if (!inputFile) {
1487
+ console.error('āŒ No input file specified');
1488
+ process.exit(1);
1489
+ }
1490
+
1491
+ // Generate output filename
1492
+ const outputFile = inputFile.replace(/\.[^.]+$/, `.${toFormat}`);
1493
+
1494
+ console.log('šŸ”„ Converting formats...');
1495
+ console.log(` Input: ${inputFile} (${fromFormat})`);
1496
+ console.log(` Output: ${outputFile} (${toFormat})`);
1497
+ console.log();
1498
+
1499
+ try {
1500
+ const result = converter.convertFile(inputFile, outputFile, fromFormat, toFormat);
1501
+
1502
+ console.log('āœ… Conversion successful!');
1503
+ console.log(` Input size: ${result.inputSize.toLocaleString()} chars`);
1504
+ console.log(` Output size: ${result.outputSize.toLocaleString()} chars`);
1505
+ console.log(` Savings: ${result.savingsPercent} (${result.savings} chars)`);
1506
+ console.log(` Output file: ${result.outputFile}`);
1507
+ } catch (error) {
1508
+ console.error('āŒ Conversion failed:', error.message);
1509
+ process.exit(1);
1510
+ }
1511
+ }
1512
+
1513
+ /**
1514
+ * Create a snapshot (FEAT-003)
1515
+ * @param {string[]} args - Command line arguments
1516
+ */
1517
+ async function runSnapshot(args) {
1518
+ console.log('šŸ“ø Creating Snapshot');
1519
+ console.log('═'.repeat(60));
1520
+ console.log();
1521
+
1522
+ // Get message from arguments
1523
+ const snapshotIndex = args.findIndex((arg) => arg === '--snapshot');
1524
+ let message = '';
1525
+ if (snapshotIndex !== -1 && args[snapshotIndex + 1] && !args[snapshotIndex + 1].startsWith('-')) {
1526
+ message = args[snapshotIndex + 1];
1527
+ }
1528
+
1529
+ // Run analyzer silently
1530
+ const originalLog = console.log;
1531
+ const logs = [];
1532
+ console.log = (...args) => logs.push(args);
1533
+
1534
+ const analyzer = new TokenAnalyzer(process.cwd(), {
1535
+ simple: true,
1536
+ verbose: false,
1537
+ dashboard: true,
1538
+ });
1539
+ const stats = analyzer.run();
1540
+
1541
+ console.log = originalLog;
1542
+
1543
+ if (!stats || !stats.totalTokens) {
1544
+ console.error('āŒ Failed to analyze repository');
1545
+ process.exit(1);
1546
+ }
1547
+
1548
+ // Create snapshot
1549
+ const manager = new SnapshotManager(process.cwd());
1550
+ const { id, snapshot } = await manager.createSnapshot(stats, message);
1551
+
1552
+ // Format output
1553
+ console.log('\nšŸ“ø Snapshot created:', id);
1554
+ console.log(' Files:', snapshot.summary.totalFiles.toLocaleString());
1555
+ console.log(' Tokens:', snapshot.summary.totalTokens.toLocaleString());
1556
+ if (message) {
1557
+ console.log(' Message:', message);
1558
+ }
1559
+ console.log();
1560
+ }
1561
+
1562
+ /**
1563
+ * List all snapshots (FEAT-003)
1564
+ */
1565
+ async function listSnapshots() {
1566
+ const manager = new SnapshotManager(process.cwd());
1567
+ const output = await manager.listSnapshots();
1568
+ console.log(output);
1569
+ }
1570
+
1571
+ /**
1572
+ * Compare with last snapshot (FEAT-003)
1573
+ * @param {string[]} args - Command line arguments
1574
+ */
1575
+ async function runDiffLast(args) {
1576
+ const json = args.includes('--json');
1577
+ // With --json, stdout carries only the JSON document
1578
+ const info = json ? console.error : console.log;
1579
+
1580
+ info('šŸ“Š Comparing with Last Snapshot');
1581
+ info('═'.repeat(60));
1582
+ info();
1583
+
1584
+ // Run current analysis
1585
+ const originalLog = console.log;
1586
+ const logs = [];
1587
+ console.log = (...args) => logs.push(args);
1588
+
1589
+ const analyzer = new TokenAnalyzer(process.cwd(), {
1590
+ simple: true,
1591
+ verbose: false,
1592
+ dashboard: true,
1593
+ });
1594
+ const stats = analyzer.run();
1595
+
1596
+ console.log = originalLog;
1597
+
1598
+ if (!stats || !stats.totalTokens) {
1599
+ console.error('āŒ Failed to analyze repository');
1600
+ process.exit(1);
1601
+ }
1602
+
1603
+ // Compare with last snapshot
1604
+ const manager = new SnapshotManager(process.cwd());
1605
+ const comparison = await manager.compareWithLast(stats);
1606
+
1607
+ if (!comparison) {
1608
+ info('\nāš ļø No previous snapshots found.');
1609
+ info(' Create one with: ctxman --snapshot "message"\n');
1610
+ return;
1611
+ }
1612
+
1613
+ if (json) {
1614
+ console.log(JSON.stringify(comparison, null, 2));
1615
+ } else {
1616
+ console.log(manager.formatDiff(comparison));
1617
+ }
1618
+ }
1619
+
1620
+ /**
1621
+ * Compare two specific snapshots (FEAT-003)
1622
+ * @param {string[]} args - Command line arguments
1623
+ */
1624
+ async function runDiffSnapshot(args) {
1625
+ const diffIndex = args.findIndex((arg) => arg === '--diff-snapshot');
1626
+ const id1 = diffIndex !== -1 && args[diffIndex + 1] ? args[diffIndex + 1] : null;
1627
+ const id2 = diffIndex !== -1 && args[diffIndex + 2] ? args[diffIndex + 2] : null;
1628
+
1629
+ const json = args.includes('--json');
1630
+
1631
+ if (!id1 || !id2) {
1632
+ console.error('āŒ Two snapshot IDs required');
1633
+ console.error(' Usage: ctxman --diff-snapshot <id1> <id2>');
1634
+ console.error(' Example: ctxman --diff-snapshot snap-001 snap-002');
1635
+ process.exit(1);
1636
+ }
1637
+
1638
+ const manager = new SnapshotManager(process.cwd());
1639
+ const comparison = await manager.compareSnapshots(id1, id2);
1640
+
1641
+ if (!comparison) {
1642
+ console.error('āŒ One or both snapshots not found');
1643
+ console.error(` ID1: ${id1}`);
1644
+ console.error(` ID2: ${id2}`);
1645
+ process.exit(1);
1646
+ }
1647
+
1648
+ if (json) {
1649
+ console.log(JSON.stringify(comparison, null, 2));
1650
+ } else {
1651
+ console.log(manager.formatDiff(comparison));
1652
+ }
1653
+ }
1654
+
1655
+ /**
1656
+ * Show snapshot trend (FEAT-003)
1657
+ * @param {string[]} args - Command line arguments
1658
+ */
1659
+ async function runSnapshotTrend(args) {
1660
+ const json = args.includes('--json');
1661
+ // With --json, stdout carries only the JSON document
1662
+ const info = json ? console.error : console.log;
1663
+
1664
+ info('šŸ“ˆ Snapshot Trend Analysis');
1665
+ info('═'.repeat(60));
1666
+ info();
1667
+
1668
+ const limitIndex = args.findIndex((arg) => arg === '--limit');
1669
+ const limit = limitIndex !== -1 && args[limitIndex + 1] ? parseInt(args[limitIndex + 1], 10) : 10;
1670
+
1671
+ const manager = new SnapshotManager(process.cwd());
1672
+ const trend = await manager.getTrend(limit);
1673
+
1674
+ if (!trend) {
1675
+ info('\nāš ļø Need at least 2 snapshots for trend analysis.');
1676
+ info(' Create snapshots with: ctxman --snapshot "message"\n');
1677
+ return;
1678
+ }
1679
+
1680
+ if (json) {
1681
+ console.log(JSON.stringify(trend, null, 2));
1682
+ } else {
1683
+ console.log(manager.formatTrend(trend));
1684
+ }
1685
+ }
1686
+
1687
+ /**
1688
+ * List all context versions (FEAT-012)
1689
+ */
1690
+ async function listVersions() {
1691
+ const manager = new ContextVersioning(process.cwd());
1692
+ console.log(await manager.listVersions());
1693
+ }
1694
+
1695
+ /**
1696
+ * Restore a context version (FEAT-012)
1697
+ * @param {string[]} args - Command line arguments
1698
+ */
1699
+ async function restoreVersion(args) {
1700
+ const restoreIndex = args.findIndex((arg) => arg === '--restore-version');
1701
+ const versionId = restoreIndex !== -1 && args[restoreIndex + 1] ? args[restoreIndex + 1] : null;
1702
+
1703
+ if (!versionId) {
1704
+ console.error('āŒ Version ID required');
1705
+ console.error(' Usage: ctxman --restore-version <id>');
1706
+ console.error(' Example: ctxman --restore-version ctx-v001');
1707
+ process.exit(1);
1708
+ }
1709
+
1710
+ const manager = new ContextVersioning(process.cwd());
1711
+ const version = await manager.restoreVersion(versionId);
1712
+
1713
+ if (!version) {
1714
+ console.error(`āŒ Version not found: ${versionId}`);
1715
+ console.error(' Use --list-versions to see available versions.');
1716
+ process.exit(1);
1717
+ }
1718
+
1719
+ console.log(`\nāœ… Restored context version: ${versionId}`);
1720
+ console.log(` Created: ${version.timestamp}`);
1721
+ console.log(` Files: ${version.summary.totalFiles.toLocaleString()}`);
1722
+ console.log(` Tokens: ${version.summary.totalTokens.toLocaleString()}`);
1723
+ if (version.config.targetModel) {
1724
+ console.log(` Model: ${version.config.targetModel}`);
1725
+ }
1726
+ if (version.gitCommit) {
1727
+ console.log(` Git: ${version.gitCommit}`);
1728
+ }
1729
+ console.log();
1730
+
1731
+ // Export restored context to file
1732
+ const fs = await import('fs');
1733
+ const outputPath = 'restored-context.json';
1734
+ fs.writeFileSync(outputPath, JSON.stringify(version, null, 2));
1735
+ console.log(`šŸ“ Saved to: ${outputPath}\n`);
1736
+ }
1737
+
1738
+ /**
1739
+ * Compare two context versions (FEAT-012)
1740
+ * @param {string[]} args - Command line arguments
1741
+ */
1742
+ async function compareVersions(args) {
1743
+ const compareIndex = args.findIndex((arg) => arg === '--compare-versions');
1744
+ const id1 = compareIndex !== -1 && args[compareIndex + 1] ? args[compareIndex + 1] : null;
1745
+ const id2 = compareIndex !== -1 && args[compareIndex + 2] ? args[compareIndex + 2] : null;
1746
+
1747
+ if (!id1 || !id2) {
1748
+ console.error('āŒ Two version IDs required');
1749
+ console.error(' Usage: ctxman --compare-versions <id1> <id2>');
1750
+ console.error(' Example: ctxman --compare-versions ctx-v001 ctx-v002');
1751
+ process.exit(1);
1752
+ }
1753
+
1754
+ const manager = new ContextVersioning(process.cwd());
1755
+ const comparison = await manager.compareVersions(id1, id2);
1756
+
1757
+ if (!comparison) {
1758
+ console.error('āŒ One or both versions not found');
1759
+ console.error(` ID1: ${id1}`);
1760
+ console.error(` ID2: ${id2}`);
1761
+ process.exit(1);
1762
+ }
1763
+
1764
+ const json = args.includes('--json');
1765
+ if (json) {
1766
+ console.log(JSON.stringify(comparison, null, 2));
1767
+ } else {
1768
+ console.log(manager.formatComparison(comparison));
1769
+ }
1770
+ }
1771
+
1772
+ /**
1773
+ * Run dependency scan (FEAT-002)
1774
+ * @param {string[]} args - Command line arguments
1775
+ */
1776
+ async function runDependencyScan(args) {
1777
+ console.log('šŸ“¦ Dependency Context Scanner');
1778
+ console.log('═'.repeat(60));
1779
+ console.log();
1780
+
1781
+ // Parse options
1782
+ const depthIndex = args.findIndex((arg) => arg === '--dependency-depth');
1783
+ const depth = depthIndex !== -1 && args[depthIndex + 1] ? parseInt(args[depthIndex + 1], 10) : 1;
1784
+
1785
+ const includeTypes = args.includes('--include-types');
1786
+ const checkSecurity = args.includes('--check-security');
1787
+
1788
+ try {
1789
+ // Initialize scanner
1790
+ const scanner = new DependencyScanner(process.cwd(), {
1791
+ depth,
1792
+ includeTypes,
1793
+ checkSecurity,
1794
+ });
1795
+
1796
+ // Initialize import tracker
1797
+ const tracker = new ImportTracker(process.cwd());
1798
+
1799
+ console.log('šŸ” Scanning source files for imports...');
1800
+ const activeImports = tracker.trackImports();
1801
+
1802
+ console.log(`šŸ“‹ Found ${activeImports.length} actively imported packages`);
1803
+ console.log();
1804
+
1805
+ console.log('šŸ” Analyzing dependencies...');
1806
+ const analysis = await scanner.analyze(activeImports);
1807
+
1808
+ // Display results
1809
+ console.log('═'.repeat(60));
1810
+ console.log('šŸ“Š DEPENDENCY ANALYSIS REPORT');
1811
+ console.log('═'.repeat(60));
1812
+ console.log();
1813
+
1814
+ console.log(`Total dependencies: ${analysis.summary.totalDependencies}`);
1815
+ console.log(`Installed: ${analysis.summary.installedDependencies}`);
1816
+ console.log(`Missing: ${analysis.summary.missingDependencies}`);
1817
+ console.log(`Active in codebase: ${analysis.summary.activeDependencies}`);
1818
+ console.log(`Total tokens: ${analysis.summary.totalTokens.toLocaleString()}`);
1819
+
1820
+ if (analysis.summary.securityIssues > 0) {
1821
+ console.log(`āš ļø Security issues: ${analysis.summary.securityIssues}`);
1822
+ }
1823
+ console.log();
1824
+
1825
+ // Display table
1826
+ console.log('─'.repeat(80));
1827
+ console.log(
1828
+ 'Package'.padEnd(30) +
1829
+ 'Version'.padEnd(12) +
1830
+ 'Tokens'.padStart(12) +
1831
+ 'Status'.padStart(10) +
1832
+ 'Security'.padStart(10)
1833
+ );
1834
+ console.log('─'.repeat(80));
1835
+
1836
+ for (const dep of analysis.dependencies.slice(0, 20)) {
1837
+ const active = dep.active ? 'āœ“' : ' ';
1838
+ const security = dep.security?.status || 'unknown';
1839
+ const securityIcon = security === 'ok' ? 'āœ…' : security === 'review' ? 'āš ļø' : 'ā“';
1840
+
1841
+ console.log(
1842
+ `${active} ${dep.name}`.substring(0, 30).padEnd(30) +
1843
+ dep.version.substring(0, 10).padEnd(12) +
1844
+ dep.tokens.toLocaleString().padStart(12) +
1845
+ dep.status.padStart(10) +
1846
+ securityIcon.padStart(8)
1847
+ );
1848
+ }
1849
+
1850
+ if (analysis.dependencies.length > 20) {
1851
+ console.log(`... and ${analysis.dependencies.length - 20} more dependencies`);
609
1852
  }
1853
+ console.log();
610
1854
 
611
- const inputFile = args[inputIndex];
612
- const fromFormat = args[fromIndex + 1];
613
- const toFormat = args[toIndex + 1];
1855
+ // Display security alerts
1856
+ const securityIssues = analysis.dependencies.filter(
1857
+ (d) => d.security?.vulnerabilities?.length > 0
1858
+ );
614
1859
 
615
- if (!inputFile) {
616
- console.error('āŒ No input file specified');
617
- process.exit(1);
1860
+ if (securityIssues.length > 0) {
1861
+ console.log('āš ļø SECURITY ALERTS:');
1862
+ console.log('─'.repeat(60));
1863
+
1864
+ for (const dep of securityIssues) {
1865
+ for (const vuln of dep.security.vulnerabilities) {
1866
+ console.log(
1867
+ ` - ${dep.name}@${dep.version}: ${vuln.severity.toUpperCase()} - ${vuln.title}`
1868
+ );
1869
+ if (vuln.fixAvailable) {
1870
+ console.log(` šŸ’” Fix available - consider upgrading`);
1871
+ }
1872
+ }
1873
+ }
1874
+ console.log();
618
1875
  }
619
1876
 
620
- // Generate output filename
621
- const outputFile = inputFile.replace(/\.[^.]+$/, `.${toFormat}`);
1877
+ // Generate dependency context
1878
+ console.log('šŸ“„ Generating dependency context...');
1879
+ const context = scanner.generateContext(analysis, activeImports);
1880
+
1881
+ // Write to file
1882
+ const outputPath = resolve(process.cwd(), 'dependency-context.json');
1883
+ const fs = await import('fs');
1884
+ fs.writeFileSync(outputPath, JSON.stringify(context, null, 2));
622
1885
 
623
- console.log('šŸ”„ Converting formats...');
624
- console.log(` Input: ${inputFile} (${fromFormat})`);
625
- console.log(` Output: ${outputFile} (${toFormat})`);
1886
+ const contextTokens = JSON.stringify(context).length / 4; // Rough estimate
1887
+ console.log(
1888
+ `āœ… dependency-context.json created (~${Math.round(contextTokens).toLocaleString()} tokens)`
1889
+ );
1890
+ console.log(` Active dependencies: ${analysis.summary.activeDependencies}`);
1891
+ console.log(
1892
+ ` Total tokens in dependencies: ${analysis.summary.totalTokens.toLocaleString()}`
1893
+ );
626
1894
  console.log();
627
1895
 
628
- try {
629
- const result = converter.convertFile(inputFile, outputFile, fromFormat, toFormat);
1896
+ // Display type definitions if extracted
1897
+ if (includeTypes && analysis.summary.typesExtracted > 0) {
1898
+ console.log(`šŸ“ TypeScript definitions extracted: ${analysis.summary.typesExtracted} files`);
1899
+ console.log();
1900
+ }
630
1901
 
631
- console.log('āœ… Conversion successful!');
632
- console.log(` Input size: ${result.inputSize.toLocaleString()} chars`);
633
- console.log(` Output size: ${result.outputSize.toLocaleString()} chars`);
634
- console.log(` Savings: ${result.savingsPercent} (${result.savings} chars)`);
635
- console.log(` Output file: ${result.outputFile}`);
636
- } catch (error) {
637
- console.error('āŒ Conversion failed:', error.message);
638
- process.exit(1);
1902
+ // Display largest dependencies
1903
+ if (analysis.summary.largestDependencies.length > 0) {
1904
+ console.log('šŸ† LARGEST DEPENDENCIES BY TOKEN COUNT:');
1905
+ console.log('─'.repeat(40));
1906
+ for (const dep of analysis.summary.largestDependencies.slice(0, 5)) {
1907
+ console.log(` ${dep.name.padEnd(25)} ${dep.tokens.toLocaleString().padStart(10)} tokens`);
1908
+ }
1909
+ console.log();
639
1910
  }
1911
+ } catch (error) {
1912
+ console.error('āŒ Dependency scan failed:', error.message);
1913
+ console.error();
1914
+ console.error('Make sure you are in a Node.js project with package.json and node_modules.');
1915
+ process.exit(1);
1916
+ }
1917
+ }
1918
+
1919
+ /**
1920
+ * Run performance dashboard (FEAT-001)
1921
+ * @param {string[]} args - Command line arguments
1922
+ */
1923
+ async function runPerfDashboard(args) {
1924
+ console.log('šŸ“Š Ctxman Performance Dashboard');
1925
+ console.log('═'.repeat(60));
1926
+ console.log();
1927
+
1928
+ // Parse period argument
1929
+ const periodIndex = args.findIndex((arg) => arg === '--period');
1930
+ const period = periodIndex !== -1 && args[periodIndex + 1] ? args[periodIndex + 1] : '7d';
1931
+
1932
+ // Dynamic import for PerformanceDashboard
1933
+ const { PerformanceDashboard } = await import('../lib/analyzers/performance-dashboard.js');
1934
+
1935
+ const dashboard = new PerformanceDashboard({
1936
+ projectRoot: process.cwd(),
1937
+ });
1938
+
1939
+ // Run current analysis first to collect fresh metrics
1940
+ console.log('šŸ”„ Running analysis to collect current metrics...');
1941
+ console.log();
1942
+
1943
+ const originalLog = console.log;
1944
+ const logs = [];
1945
+ console.log = (...args) => logs.push(args);
1946
+
1947
+ try {
1948
+ await dashboard.runAnalysis();
1949
+ } catch (_error) {
1950
+ // Continue even if analysis fails - we may have historical data
1951
+ }
1952
+
1953
+ console.log = originalLog;
1954
+
1955
+ // Display the dashboard
1956
+ dashboard.display({ period });
1957
+
1958
+ // Save report if requested
1959
+ if (args.includes('--save-report')) {
1960
+ const reportPath = resolve(process.cwd(), 'performance-report.txt');
1961
+ const fs = await import('fs');
1962
+ fs.writeFileSync(reportPath, dashboard.render({ period }));
1963
+ console.log(`šŸ’¾ Report saved to: performance-report.txt`);
1964
+ console.log();
1965
+ }
1966
+ }
1967
+
1968
+ /**
1969
+ * Create a context version automatically after CLI analysis (FEAT-012)
1970
+ * @param {object} stats - Analysis stats
1971
+ * @param {object} options - CLI options
1972
+ */
1973
+ async function autoCreateVersion(stats, options) {
1974
+ const manager = new ContextVersioning(process.cwd());
1975
+
1976
+ const config = {
1977
+ exclude: options.exclude || [],
1978
+ include: options.include || [],
1979
+ targetModel: options.targetModel || null,
1980
+ };
1981
+
1982
+ const { id, version } = await manager.createVersion(stats, config, '');
1983
+
1984
+ console.log(`\nšŸ“œ Context version created: ${id}`);
1985
+ console.log(` Files: ${version.summary.totalFiles.toLocaleString()}`);
1986
+ console.log(` Tokens: ${version.summary.totalTokens.toLocaleString()}`);
1987
+ if (version.gitCommit) {
1988
+ console.log(` Git: ${version.gitCommit}`);
1989
+ }
1990
+ console.log();
640
1991
  }
641
1992
 
642
1993
  // ESM entry point
643
- main().catch(error => {
644
- console.error('āŒ Error:', error.message);
645
- // Exit codes:
646
- // 0: Success
647
- // 1: General error
648
- // 2: Validation/argument error
649
- process.exit(1);
1994
+ main().catch((error) => {
1995
+ console.error('āŒ Error:', error.message);
1996
+ // Exit codes:
1997
+ // 0: Success
1998
+ // 1: General error
1999
+ // 2: Validation/argument error
2000
+ process.exit(1);
650
2001
  });