ctxman 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/.ctxman/llm-profiles.json +1067 -39
  2. package/.ctxman/profiles/backend-team.json +13 -0
  3. package/.ctxman/profiles/default.json +12 -0
  4. package/.ctxman/profiles/devops-team.json +21 -0
  5. package/.ctxman/profiles/frontend-team.json +20 -0
  6. package/.ctxman/templates/bug-fix.json +17 -0
  7. package/.ctxman/templates/code-review.json +9 -0
  8. package/.ctxman/templates/documentation.json +9 -0
  9. package/.ctxman/templates/feature.json +9 -0
  10. package/.ctxman/templates/full-context.json +9 -0
  11. package/.ctxman/templates/refactoring.json +18 -0
  12. package/.ctxman/templates/testing.json +9 -0
  13. package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
  14. package/LICENSE +21 -0
  15. package/README.md +380 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -13
  18. package/bin/cm-audit.js +124 -0
  19. package/bin/cm-count.js +151 -0
  20. package/bin/cm-gitingest.js +128 -115
  21. package/bin/cm-map.js +242 -0
  22. package/bin/cm-mcp.js +293 -0
  23. package/bin/cm-pack.js +405 -0
  24. package/bin/cm-policy.js +170 -0
  25. package/bin/cm-pr.js +248 -0
  26. package/bin/cm-search.js +178 -0
  27. package/bin/cm-update.js +128 -128
  28. package/bin/cm-why.js +197 -0
  29. package/bin/mcp-server.js +8 -24
  30. package/ctxman.js +237 -183
  31. package/examples/README.md +19 -4
  32. package/examples/github-actions/ctxman-check.yml +49 -0
  33. package/examples/wizard-profiles/security-audit/profile.json +7 -1
  34. package/index.d.ts +1422 -0
  35. package/index.js +55 -31
  36. package/lib/agents/pr-failure-agent.js +48 -45
  37. package/lib/analyzers/ai-suggester.js +451 -0
  38. package/lib/analyzers/dependency-scanner.js +505 -0
  39. package/lib/analyzers/duplicate-detector.js +264 -0
  40. package/lib/analyzers/go-method-analyzer.js +151 -110
  41. package/lib/analyzers/import-tracker.js +431 -0
  42. package/lib/analyzers/method-analyzer.js +1055 -434
  43. package/lib/analyzers/performance-dashboard.js +683 -0
  44. package/lib/analyzers/token-calculator.js +895 -504
  45. package/lib/analyzers/unused-exports-detector.js +336 -0
  46. package/lib/api/mcp/install.js +424 -0
  47. package/lib/api/mcp/prompts.js +281 -281
  48. package/lib/api/mcp/resources.js +300 -241
  49. package/lib/api/mcp/sandbox.js +89 -0
  50. package/lib/api/mcp/server.js +675 -312
  51. package/lib/api/mcp/start.js +125 -0
  52. package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
  53. package/lib/api/rest/server.js +105 -28
  54. package/lib/cache/CacheManager.js +9 -17
  55. package/lib/core/Analyzer.js +16 -10
  56. package/lib/core/ContextBuilder.js +19 -28
  57. package/lib/core/Reporter.js +15 -14
  58. package/lib/core/Scanner.js +81 -12
  59. package/lib/count/index.js +100 -0
  60. package/lib/formatters/format-registry.js +394 -372
  61. package/lib/formatters/gitingest-formatter.js +656 -621
  62. package/lib/formatters/toon-formatter-v1.3.js +637 -632
  63. package/lib/formatters/toon-formatter.js +239 -240
  64. package/lib/integrations/git/BlameTracker.js +4 -4
  65. package/lib/integrations/git/DiffAnalyzer.js +38 -31
  66. package/lib/integrations/git/GitClient.js +71 -53
  67. package/lib/lsp/server.js +311 -0
  68. package/lib/map/graph.js +299 -0
  69. package/lib/map/imports.js +233 -0
  70. package/lib/map/index.js +33 -0
  71. package/lib/map/rank.js +253 -0
  72. package/lib/map/render.js +346 -0
  73. package/lib/map/resolver.js +574 -0
  74. package/lib/pack/budget.js +111 -0
  75. package/lib/pack/code-lexer.js +360 -0
  76. package/lib/pack/compress.js +414 -0
  77. package/lib/pack/index.js +354 -0
  78. package/lib/pack/renderers/common.js +134 -0
  79. package/lib/pack/renderers/index.js +24 -0
  80. package/lib/pack/renderers/json.js +47 -0
  81. package/lib/pack/renderers/markdown.js +54 -0
  82. package/lib/pack/renderers/plain.js +38 -0
  83. package/lib/pack/renderers/xml.js +85 -0
  84. package/lib/pack/select.js +177 -0
  85. package/lib/pack/split.js +182 -0
  86. package/lib/pack/tree.js +35 -0
  87. package/lib/parsers/gitignore-parser.js +113 -107
  88. package/lib/parsers/method-filter-parser.js +50 -33
  89. package/lib/plugins/ExporterPlugin.js +3 -3
  90. package/lib/plugins/LanguagePlugin.js +3 -3
  91. package/lib/plugins/PluginManager.js +7 -13
  92. package/lib/policy/audit.js +294 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +52 -0
  96. package/lib/policy/policy.js +489 -0
  97. package/lib/pr/diff-parser.js +267 -0
  98. package/lib/pr/method-mapper.js +148 -0
  99. package/lib/pr/pr-context.js +829 -0
  100. package/lib/pr/pr-git.js +149 -0
  101. package/lib/pr/pr-renderer.js +644 -0
  102. package/lib/pr/related-tests.js +296 -0
  103. package/lib/rag/DataSourcePlugin.js +5 -5
  104. package/lib/rag/EmbeddingProvider.js +19 -8
  105. package/lib/rag/Indexer.js +14 -5
  106. package/lib/rag/VectorStoreFactory.js +3 -3
  107. package/lib/rag/sources/FileDataSource.js +6 -6
  108. package/lib/rag/stores/BaseVectorStore.js +5 -5
  109. package/lib/rag/stores/ChromaDBStore.js +10 -10
  110. package/lib/rag/stores/LanceDBStore.js +29 -19
  111. package/lib/rag/stores/PineconeStore.js +11 -9
  112. package/lib/search/bm25.js +433 -0
  113. package/lib/search/code-units.js +201 -0
  114. package/lib/search/symbols.js +189 -0
  115. package/lib/ui/dashboard.js +120 -89
  116. package/lib/ui/index.js +1 -6
  117. package/lib/ui/init-wizard-ui.js +550 -0
  118. package/lib/ui/progress-bar.js +94 -88
  119. package/lib/ui/select-input.js +40 -32
  120. package/lib/ui/template-selector.js +99 -0
  121. package/lib/ui/wizard.js +374 -329
  122. package/lib/utils/claude-token-counter.js +151 -0
  123. package/lib/utils/cli-output.js +17 -0
  124. package/lib/utils/clipboard-utils.js +49 -50
  125. package/lib/utils/config-utils.js +81 -66
  126. package/lib/utils/context-versioning.js +532 -0
  127. package/lib/utils/error-handler.js +112 -112
  128. package/lib/utils/file-utils.js +153 -69
  129. package/lib/utils/format-converter.js +356 -283
  130. package/lib/utils/git-utils.js +366 -294
  131. package/lib/utils/llm-cost-estimator.js +281 -0
  132. package/lib/utils/llm-detector.js +242 -78
  133. package/lib/utils/logger.js +243 -237
  134. package/lib/utils/messages.js +355 -0
  135. package/lib/utils/multi-repo-manager.js +364 -0
  136. package/lib/utils/profile-manager.js +380 -0
  137. package/lib/utils/safe-read.js +61 -0
  138. package/lib/utils/secret-redactor.js +61 -0
  139. package/lib/utils/snapshot-manager.js +679 -0
  140. package/lib/utils/template-manager.js +378 -0
  141. package/lib/utils/token-utils.js +201 -170
  142. package/lib/utils/tokenizer-adapter.js +421 -377
  143. package/lib/utils/updater.js +539 -506
  144. package/lib/watch/FileWatcher.js +12 -8
  145. package/lib/watch/IncrementalAnalyzer.js +7 -7
  146. package/lib/why/coupling.js +81 -0
  147. package/lib/why/errors.js +23 -0
  148. package/lib/why/git.js +169 -0
  149. package/lib/why/history.js +108 -0
  150. package/lib/why/index.js +720 -0
  151. package/lib/why/ownership.js +79 -0
  152. package/lib/why/paths.js +207 -0
  153. package/lib/why/references.js +259 -0
  154. package/lib/why/render.js +722 -0
  155. package/lib/why/search.js +358 -0
  156. package/lib/why/target.js +389 -0
  157. package/lib/wizards/config-generator.js +255 -0
  158. package/lib/wizards/project-detector.js +571 -0
  159. package/package.json +44 -10
  160. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  161. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -4,416 +4,438 @@
4
4
  * Supports: TOON, JSON, YAML, CSV, XML, Markdown, Protobuf, GitIngest
5
5
  */
6
6
 
7
- import ToonFormatter from './toon-formatter.js';
8
- import GitIngestFormatter from './gitingest-formatter.js';
7
+ import { encode as encodeToon } from '@toon-format/toon';
9
8
 
10
9
  class FormatRegistry {
11
- constructor() {
12
- this.formatters = new Map();
13
- this.registerDefaultFormatters();
14
- }
15
-
16
- /**
17
- * Register all default formatters
18
- */
19
- registerDefaultFormatters() {
20
- // TOON format (40-50% token reduction)
21
- this.register('toon', {
22
- name: 'TOON',
23
- description: 'Tabular Object Oriented Notation (40-50% token reduction)',
24
- extension: '.toon',
25
- mimeType: 'text/plain',
26
- encoder: (data) => {
27
- const formatter = new ToonFormatter();
28
- return formatter.encode(data);
29
- }
30
- });
31
-
32
- // JSON format (standard)
33
- this.register('json', {
34
- name: 'JSON',
35
- description: 'JavaScript Object Notation (standard format)',
36
- extension: '.json',
37
- mimeType: 'application/json',
38
- encoder: (data) => JSON.stringify(data, null, 2)
39
- });
40
-
41
- // JSON Compact (no whitespace)
42
- this.register('json-compact', {
43
- name: 'JSON Compact',
44
- description: 'Minified JSON without whitespace',
45
- extension: '.json',
46
- mimeType: 'application/json',
47
- encoder: (data) => JSON.stringify(data)
48
- });
49
-
50
- // YAML format
51
- this.register('yaml', {
52
- name: 'YAML',
53
- description: 'YAML Ain\'t Markup Language (human-readable)',
54
- extension: '.yaml',
55
- mimeType: 'text/yaml',
56
- encoder: (data) => this.encodeYAML(data)
57
- });
58
-
59
- // Markdown format
60
- this.register('markdown', {
61
- name: 'Markdown',
62
- description: 'Markdown documentation format',
63
- extension: '.md',
64
- mimeType: 'text/markdown',
65
- encoder: (data) => this.encodeMarkdown(data)
66
- });
67
-
68
- // CSV format
69
- this.register('csv', {
70
- name: 'CSV',
71
- description: 'Comma-Separated Values (spreadsheet compatible)',
72
- extension: '.csv',
73
- mimeType: 'text/csv',
74
- encoder: (data) => this.encodeCSV(data)
75
- });
76
-
77
- // XML format
78
- this.register('xml', {
79
- name: 'XML',
80
- description: 'Extensible Markup Language (enterprise compatible)',
81
- extension: '.xml',
82
- mimeType: 'application/xml',
83
- encoder: (data) => this.encodeXML(data)
84
- });
85
-
86
- // GitIngest format
87
- this.register('gitingest', {
88
- name: 'GitIngest',
89
- description: 'Single-file digest for LLM consumption',
90
- extension: '.txt',
91
- mimeType: 'text/plain',
92
- encoder: (data, options) => {
93
- // GitIngest uses a different interface
94
- if (options && options.formatter) {
95
- return options.formatter.generateDigest();
96
- }
97
- throw new Error('GitIngest format requires a GitIngestFormatter instance');
98
- }
99
- });
100
- }
101
-
102
- /**
103
- * Register a custom formatter
104
- */
105
- register(name, formatter) {
106
- if (!formatter.encoder || typeof formatter.encoder !== 'function') {
107
- throw new Error(`Formatter '${name}' must have an encoder function`);
10
+ constructor() {
11
+ this.formatters = new Map();
12
+ this.registerDefaultFormatters();
13
+ }
14
+
15
+ /**
16
+ * Register all default formatters
17
+ */
18
+ registerDefaultFormatters() {
19
+ // TOON format: measured 25% (file lists) to 66% (method lists) fewer tokens than JSON
20
+ this.register('toon', {
21
+ name: 'TOON',
22
+ description: 'Token-Oriented Object Notation (fewer tokens than JSON)',
23
+ extension: '.toon',
24
+ mimeType: 'text/plain',
25
+ // Official encoder: ToonFormatter.encode()'s legacy sync output is not valid TOON
26
+ encoder: (data) => encodeToon(data),
27
+ });
28
+
29
+ // JSON format (standard)
30
+ this.register('json', {
31
+ name: 'JSON',
32
+ description: 'JavaScript Object Notation (standard format)',
33
+ extension: '.json',
34
+ mimeType: 'application/json',
35
+ encoder: (data) => JSON.stringify(data, null, 2),
36
+ });
37
+
38
+ // JSON Compact (no whitespace)
39
+ this.register('json-compact', {
40
+ name: 'JSON Compact',
41
+ description: 'Minified JSON without whitespace',
42
+ extension: '.json',
43
+ mimeType: 'application/json',
44
+ encoder: (data) => JSON.stringify(data),
45
+ });
46
+
47
+ // YAML format
48
+ this.register('yaml', {
49
+ name: 'YAML',
50
+ description: "YAML Ain't Markup Language (human-readable)",
51
+ extension: '.yaml',
52
+ mimeType: 'text/yaml',
53
+ encoder: (data) => this.encodeYAML(data),
54
+ });
55
+
56
+ // Markdown format
57
+ this.register('markdown', {
58
+ name: 'Markdown',
59
+ description: 'Markdown documentation format',
60
+ extension: '.md',
61
+ mimeType: 'text/markdown',
62
+ encoder: (data) => this.encodeMarkdown(data),
63
+ });
64
+
65
+ // CSV format
66
+ this.register('csv', {
67
+ name: 'CSV',
68
+ description: 'Comma-Separated Values (spreadsheet compatible)',
69
+ extension: '.csv',
70
+ mimeType: 'text/csv',
71
+ encoder: (data) => this.encodeCSV(data),
72
+ });
73
+
74
+ // XML format
75
+ this.register('xml', {
76
+ name: 'XML',
77
+ description: 'Extensible Markup Language (enterprise compatible)',
78
+ extension: '.xml',
79
+ mimeType: 'application/xml',
80
+ encoder: (data) => this.encodeXML(data),
81
+ });
82
+
83
+ // GitIngest format
84
+ this.register('gitingest', {
85
+ name: 'GitIngest',
86
+ description: 'Single-file digest for LLM consumption',
87
+ extension: '.txt',
88
+ mimeType: 'text/plain',
89
+ encoder: (data, options) => {
90
+ // GitIngest uses a different interface
91
+ if (options && options.formatter) {
92
+ return options.formatter.generateDigest();
108
93
  }
109
- this.formatters.set(name, formatter);
94
+ throw new Error('GitIngest format requires a GitIngestFormatter instance');
95
+ },
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Register a custom formatter
101
+ */
102
+ register(name, formatter) {
103
+ if (!formatter.encoder || typeof formatter.encoder !== 'function') {
104
+ throw new Error(`Formatter '${name}' must have an encoder function`);
110
105
  }
111
-
112
- /**
113
- * Get formatter by name
114
- */
115
- get(name) {
116
- const formatter = this.formatters.get(name);
117
- if (!formatter) {
118
- throw new Error(`Unknown format: ${name}. Available formats: ${this.listFormats().join(', ')}`);
119
- }
120
- return formatter;
106
+ this.formatters.set(name, formatter);
107
+ }
108
+
109
+ /**
110
+ * Get formatter by name
111
+ */
112
+ get(name) {
113
+ const formatter = this.formatters.get(name);
114
+ if (!formatter) {
115
+ throw new Error(
116
+ `Unknown format: ${name}. Available formats: ${this.listFormats().join(', ')}`
117
+ );
121
118
  }
122
-
123
- /**
124
- * Check if format exists
125
- */
126
- has(name) {
127
- return this.formatters.has(name);
119
+ return formatter;
120
+ }
121
+
122
+ /**
123
+ * Check if format exists
124
+ */
125
+ has(name) {
126
+ return this.formatters.has(name);
127
+ }
128
+
129
+ /**
130
+ * List all available formats
131
+ */
132
+ listFormats() {
133
+ return Array.from(this.formatters.keys());
134
+ }
135
+
136
+ /**
137
+ * Get format info
138
+ */
139
+ getInfo(name) {
140
+ const formatter = this.get(name);
141
+ return {
142
+ name: formatter.name,
143
+ description: formatter.description,
144
+ extension: formatter.extension,
145
+ mimeType: formatter.mimeType,
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Get all formats info
151
+ */
152
+ getAllInfo() {
153
+ const info = {};
154
+ for (const [name, formatter] of this.formatters) {
155
+ info[name] = {
156
+ name: formatter.name,
157
+ description: formatter.description,
158
+ extension: formatter.extension,
159
+ mimeType: formatter.mimeType,
160
+ };
128
161
  }
162
+ return info;
163
+ }
164
+
165
+ /**
166
+ * Encode data to specified format
167
+ */
168
+ encode(name, data, options = {}) {
169
+ const formatter = this.get(name);
170
+ return formatter.encoder(data, options);
171
+ }
172
+
173
+ /**
174
+ * YAML encoder (simple implementation)
175
+ */
176
+ encodeYAML(data, indent = 0) {
177
+ const prefix = ' '.repeat(indent);
178
+ const type = this.getType(data);
179
+
180
+ switch (type) {
181
+ case 'null':
182
+ case 'undefined':
183
+ return 'null';
184
+
185
+ case 'boolean':
186
+ case 'number':
187
+ return String(data);
188
+
189
+ case 'string':
190
+ return this.encodeYAMLString(data);
191
+
192
+ case 'array':
193
+ if (data.length === 0) return '[]';
194
+ let arrResult = '\n';
195
+ for (const item of data) {
196
+ arrResult += prefix + '- ' + this.encodeYAML(item, indent + 1) + '\n';
197
+ }
198
+ return arrResult.trimEnd();
199
+
200
+ case 'object':
201
+ const keys = Object.keys(data);
202
+ if (keys.length === 0) return '{}';
203
+ let objResult = '\n';
204
+ for (const key of keys) {
205
+ const yamlKey = this.encodeYAMLString(key);
206
+ objResult += prefix + yamlKey + ': ' + this.encodeYAML(data[key], indent + 1) + '\n';
207
+ }
208
+ return objResult.trimEnd();
129
209
 
130
- /**
131
- * List all available formats
132
- */
133
- listFormats() {
134
- return Array.from(this.formatters.keys());
210
+ default:
211
+ return 'null';
135
212
  }
136
-
137
- /**
138
- * Get format info
139
- */
140
- getInfo(name) {
141
- const formatter = this.get(name);
142
- return {
143
- name: formatter.name,
144
- description: formatter.description,
145
- extension: formatter.extension,
146
- mimeType: formatter.mimeType
147
- };
213
+ }
214
+
215
+ /**
216
+ * Encode a string scalar or mapping key. Anything a YAML reader would take as
217
+ * another type, as syntax, or would trim is written as a JSON string, which is
218
+ * a valid YAML double-quoted scalar with proper escaping.
219
+ */
220
+ encodeYAMLString(str) {
221
+ const needsQuotes =
222
+ str === '' ||
223
+ str !== str.trim() ||
224
+ /[:\n\r{}[\],&*#?|<>=!%@`]/.test(str) ||
225
+ /^[-'"]/.test(str) ||
226
+ /^[-+.]?\d/.test(str) ||
227
+ /^(true|false|null|~|[-+]?\.inf|\.nan)$/i.test(str);
228
+
229
+ return needsQuotes ? JSON.stringify(str) : str;
230
+ }
231
+
232
+ /**
233
+ * Markdown encoder
234
+ */
235
+ encodeMarkdown(data) {
236
+ const { project, paths, methods, methodStats } = data;
237
+
238
+ let md = `# ${project?.root || 'Project'} - Context Analysis\n\n`;
239
+
240
+ // Project summary
241
+ if (project) {
242
+ md += '## Project Summary\n\n';
243
+ md += `- **Total Files**: ${project.totalFiles}\n`;
244
+ md += `- **Total Tokens**: ${project.totalTokens?.toLocaleString()}\n\n`;
148
245
  }
149
246
 
150
- /**
151
- * Get all formats info
152
- */
153
- getAllInfo() {
154
- const info = {};
155
- for (const [name, formatter] of this.formatters) {
156
- info[name] = {
157
- name: formatter.name,
158
- description: formatter.description,
159
- extension: formatter.extension,
160
- mimeType: formatter.mimeType
161
- };
247
+ // Paths
248
+ if (paths) {
249
+ md += '## File Structure\n\n';
250
+ for (const [dir, files] of Object.entries(paths)) {
251
+ md += `### ${dir}\n\n`;
252
+ for (const file of files) {
253
+ md += `- ${file}\n`;
162
254
  }
163
- return info;
255
+ md += '\n';
256
+ }
164
257
  }
165
258
 
166
- /**
167
- * Encode data to specified format
168
- */
169
- encode(name, data, options = {}) {
170
- const formatter = this.get(name);
171
- return formatter.encoder(data, options);
172
- }
173
-
174
- /**
175
- * YAML encoder (simple implementation)
176
- */
177
- encodeYAML(data, indent = 0) {
178
- const prefix = ' '.repeat(indent);
179
- const type = this.getType(data);
180
-
181
- switch (type) {
182
- case 'null':
183
- case 'undefined':
184
- return 'null';
185
-
186
- case 'boolean':
187
- case 'number':
188
- return String(data);
189
-
190
- case 'string':
191
- // Quote if contains special characters
192
- if (/[:\n\r{}[\],&*#?|<>=!%@`]/.test(data)) {
193
- return `"${data.replace(/"/g, '\\"')}"`;
194
- }
195
- return data;
196
-
197
- case 'array':
198
- if (data.length === 0) return '[]';
199
- let arrResult = '\n';
200
- for (const item of data) {
201
- arrResult += prefix + '- ' + this.encodeYAML(item, indent + 1) + '\n';
202
- }
203
- return arrResult.trimEnd();
204
-
205
- case 'object':
206
- const keys = Object.keys(data);
207
- if (keys.length === 0) return '{}';
208
- let objResult = '\n';
209
- for (const key of keys) {
210
- objResult += prefix + key + ': ' + this.encodeYAML(data[key], indent + 1) + '\n';
211
- }
212
- return objResult.trimEnd();
213
-
214
- default:
215
- return 'null';
259
+ // Methods
260
+ if (methods) {
261
+ md += '## Methods\n\n';
262
+ for (const [file, methodList] of Object.entries(methods)) {
263
+ md += `### ${file}\n\n`;
264
+ md += '| Method | Line | Tokens |\n';
265
+ md += '|--------|------|--------|\n';
266
+ for (const method of methodList) {
267
+ md += `| ${method.name} | ${method.line} | ${method.tokens} |\n`;
216
268
  }
269
+ md += '\n';
270
+ }
217
271
  }
218
272
 
219
- /**
220
- * Markdown encoder
221
- */
222
- encodeMarkdown(data) {
223
- const { project, paths, methods, methodStats } = data;
224
-
225
- let md = `# ${project?.root || 'Project'} - Context Analysis\n\n`;
226
-
227
- // Project summary
228
- if (project) {
229
- md += '## Project Summary\n\n';
230
- md += `- **Total Files**: ${project.totalFiles}\n`;
231
- md += `- **Total Tokens**: ${project.totalTokens?.toLocaleString()}\n\n`;
232
- }
273
+ // Method stats
274
+ if (methodStats) {
275
+ md += '## Statistics\n\n';
276
+ md += `- **Total Methods**: ${methodStats.totalMethods}\n`;
277
+ md += `- **Included Methods**: ${methodStats.includedMethods}\n`;
278
+ md += `- **Total Method Tokens**: ${methodStats.totalMethodTokens || 0}\n`;
279
+ }
233
280
 
234
- // Paths
235
- if (paths) {
236
- md += '## File Structure\n\n';
237
- for (const [dir, files] of Object.entries(paths)) {
238
- md += `### ${dir}\n\n`;
239
- for (const file of files) {
240
- md += `- ${file}\n`;
241
- }
242
- md += '\n';
243
- }
244
- }
281
+ return md;
282
+ }
245
283
 
246
- // Methods
247
- if (methods) {
248
- md += '## Methods\n\n';
249
- for (const [file, methodList] of Object.entries(methods)) {
250
- md += `### ${file}\n\n`;
251
- md += '| Method | Line | Tokens |\n';
252
- md += '|--------|------|--------|\n';
253
- for (const method of methodList) {
254
- md += `| ${method.name} | ${method.line} | ${method.tokens} |\n`;
255
- }
256
- md += '\n';
257
- }
258
- }
284
+ /**
285
+ * CSV encoder
286
+ */
287
+ encodeCSV(data) {
288
+ const { project: _project, methods } = data;
259
289
 
260
- // Method stats
261
- if (methodStats) {
262
- md += '## Statistics\n\n';
263
- md += `- **Total Methods**: ${methodStats.totalMethods}\n`;
264
- md += `- **Included Methods**: ${methodStats.includedMethods}\n`;
265
- md += `- **Total Method Tokens**: ${methodStats.totalMethodTokens || 0}\n`;
266
- }
267
-
268
- return md;
290
+ if (!methods) {
291
+ // File-level CSV
292
+ return this.encodeFilesCSV(data);
269
293
  }
270
294
 
271
- /**
272
- * CSV encoder
273
- */
274
- encodeCSV(data) {
275
- const { project, methods } = data;
295
+ // Method-level CSV
296
+ let csv = 'File,Method,Line,Tokens\n';
276
297
 
277
- if (!methods) {
278
- // File-level CSV
279
- return this.encodeFilesCSV(data);
280
- }
298
+ for (const [file, methodList] of Object.entries(methods)) {
299
+ for (const method of methodList) {
300
+ csv += `"${file}","${method.name}",${method.line},${method.tokens}\n`;
301
+ }
302
+ }
281
303
 
282
- // Method-level CSV
283
- let csv = 'File,Method,Line,Tokens\n';
304
+ return csv;
305
+ }
284
306
 
285
- for (const [file, methodList] of Object.entries(methods)) {
286
- for (const method of methodList) {
287
- csv += `"${file}","${method.name}",${method.line},${method.tokens}\n`;
288
- }
289
- }
307
+ /**
308
+ * CSV encoder for file-level data
309
+ */
310
+ encodeFilesCSV(data) {
311
+ const { paths } = data;
290
312
 
291
- return csv;
313
+ if (!paths) {
314
+ return 'Directory,File\n';
292
315
  }
293
316
 
294
- /**
295
- * CSV encoder for file-level data
296
- */
297
- encodeFilesCSV(data) {
298
- const { paths } = data;
299
-
300
- if (!paths) {
301
- return 'Directory,File\n';
302
- }
303
-
304
- let csv = 'Directory,File\n';
305
- for (const [dir, files] of Object.entries(paths)) {
306
- for (const file of files) {
307
- csv += `"${dir}","${file}"\n`;
308
- }
309
- }
317
+ let csv = 'Directory,File\n';
318
+ for (const [dir, files] of Object.entries(paths)) {
319
+ for (const file of files) {
320
+ csv += `"${dir}","${file}"\n`;
321
+ }
322
+ }
310
323
 
311
- return csv;
324
+ return csv;
325
+ }
326
+
327
+ /**
328
+ * XML encoder
329
+ */
330
+ encodeXML(data, indent = 0) {
331
+ const prefix = ' '.repeat(indent);
332
+ const type = this.getType(data);
333
+
334
+ if (indent === 0) {
335
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
336
+ xml += '<context>\n';
337
+ xml += this.encodeXML(data, 1);
338
+ xml += '</context>';
339
+ return xml;
312
340
  }
313
341
 
314
- /**
315
- * XML encoder
316
- */
317
- encodeXML(data, indent = 0) {
318
- const prefix = ' '.repeat(indent);
319
- const type = this.getType(data);
320
-
321
- if (indent === 0) {
322
- let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
323
- xml += '<context>\n';
324
- xml += this.encodeXML(data, 1);
325
- xml += '</context>';
326
- return xml;
342
+ switch (type) {
343
+ case 'null':
344
+ case 'undefined':
345
+ return '';
346
+
347
+ case 'boolean':
348
+ case 'number':
349
+ case 'string':
350
+ return this.escapeXML(String(data));
351
+
352
+ // Recurse with indent + 1, never 0: indent 0 is what emits the document wrapper.
353
+ case 'array':
354
+ let arrXML = '';
355
+ for (const item of data) {
356
+ const itemType = this.getType(item);
357
+
358
+ if (itemType === 'object' || itemType === 'array') {
359
+ arrXML += prefix + '<item>\n';
360
+ arrXML += this.encodeXML(item, indent + 1);
361
+ arrXML += prefix + '</item>\n';
362
+ } else {
363
+ arrXML += prefix + '<item>' + this.encodeXML(item, indent + 1) + '</item>\n';
364
+ }
327
365
  }
328
-
329
- switch (type) {
330
- case 'null':
331
- case 'undefined':
332
- return '';
333
-
334
- case 'boolean':
335
- case 'number':
336
- case 'string':
337
- return this.escapeXML(String(data));
338
-
339
- case 'array':
340
- let arrXML = '';
341
- for (const item of data) {
342
- arrXML += prefix + '<item>' + this.encodeXML(item, 0) + '</item>\n';
343
- }
344
- return arrXML;
345
-
346
- case 'object':
347
- let objXML = '';
348
- for (const [key, value] of Object.entries(data)) {
349
- const safeKey = this.sanitizeXMLTag(key);
350
- const valueType = this.getType(value);
351
-
352
- if (valueType === 'object' || valueType === 'array') {
353
- objXML += prefix + `<${safeKey}>\n`;
354
- objXML += this.encodeXML(value, indent + 1);
355
- objXML += prefix + `</${safeKey}>\n`;
356
- } else {
357
- objXML += prefix + `<${safeKey}>`;
358
- objXML += this.encodeXML(value, 0);
359
- objXML += `</${safeKey}>\n`;
360
- }
361
- }
362
- return objXML;
363
-
364
- default:
365
- return '';
366
+ return arrXML;
367
+
368
+ case 'object':
369
+ let objXML = '';
370
+ for (const [key, value] of Object.entries(data)) {
371
+ const safeKey = this.sanitizeXMLTag(key);
372
+ const valueType = this.getType(value);
373
+
374
+ if (valueType === 'object' || valueType === 'array') {
375
+ objXML += prefix + `<${safeKey}>\n`;
376
+ objXML += this.encodeXML(value, indent + 1);
377
+ objXML += prefix + `</${safeKey}>\n`;
378
+ } else {
379
+ objXML += prefix + `<${safeKey}>`;
380
+ objXML += this.encodeXML(value, indent + 1);
381
+ objXML += `</${safeKey}>\n`;
382
+ }
366
383
  }
367
- }
384
+ return objXML;
368
385
 
369
- /**
370
- * Escape XML special characters
371
- */
372
- escapeXML(str) {
373
- return str
374
- .replace(/&/g, '&amp;')
375
- .replace(/</g, '&lt;')
376
- .replace(/>/g, '&gt;')
377
- .replace(/"/g, '&quot;')
378
- .replace(/'/g, '&apos;');
386
+ default:
387
+ return '';
379
388
  }
380
-
381
- /**
382
- * Sanitize string for use as XML tag
383
- */
384
- sanitizeXMLTag(str) {
385
- return str.replace(/[^a-zA-Z0-9_-]/g, '_');
389
+ }
390
+
391
+ /**
392
+ * Escape XML special characters
393
+ */
394
+ escapeXML(str) {
395
+ return str
396
+ .replace(/&/g, '&amp;')
397
+ .replace(/</g, '&lt;')
398
+ .replace(/>/g, '&gt;')
399
+ .replace(/"/g, '&quot;')
400
+ .replace(/'/g, '&apos;');
401
+ }
402
+
403
+ /**
404
+ * Sanitize string for use as XML tag
405
+ */
406
+ sanitizeXMLTag(str) {
407
+ return str.replace(/[^a-zA-Z0-9_-]/g, '_');
408
+ }
409
+
410
+ /**
411
+ * Get value type
412
+ */
413
+ getType(value) {
414
+ if (value === null) return 'null';
415
+ if (value === undefined) return 'undefined';
416
+ if (Array.isArray(value)) return 'array';
417
+ return typeof value;
418
+ }
419
+
420
+ /**
421
+ * Auto-detect best format for data
422
+ */
423
+ suggestFormat(data) {
424
+ const { methods, paths } = data;
425
+
426
+ // Method-level data benefits most from TOON
427
+ if (methods) {
428
+ return 'toon';
386
429
  }
387
430
 
388
- /**
389
- * Get value type
390
- */
391
- getType(value) {
392
- if (value === null) return 'null';
393
- if (value === undefined) return 'undefined';
394
- if (Array.isArray(value)) return 'array';
395
- return typeof value;
431
+ // File lists work well with TOON too
432
+ if (paths) {
433
+ return 'toon';
396
434
  }
397
435
 
398
- /**
399
- * Auto-detect best format for data
400
- */
401
- suggestFormat(data) {
402
- const { methods, paths } = data;
403
-
404
- // Method-level data benefits most from TOON
405
- if (methods) {
406
- return 'toon';
407
- }
408
-
409
- // File lists work well with TOON too
410
- if (paths) {
411
- return 'toon';
412
- }
413
-
414
- // Default to JSON
415
- return 'json';
416
- }
436
+ // Default to JSON
437
+ return 'json';
438
+ }
417
439
  }
418
440
 
419
441
  export default FormatRegistry;