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
@@ -7,122 +7,161 @@
7
7
  * Base adapter interface for all tokenizers
8
8
  */
9
9
  export class TokenizerAdapter {
10
- constructor() {
11
- this.available = false;
12
- this.name = 'Base';
13
- }
10
+ constructor() {
11
+ this.available = false;
12
+ this.name = 'Base';
13
+ // Whether counts match the model's own tokenizer, or only approximate it
14
+ this.exact = false;
15
+ }
16
+
17
+ /**
18
+ * Initialize the tokenizer (load libraries, etc.)
19
+ * @returns {Promise<boolean>} True if initialized successfully
20
+ */
21
+ async initialize() {
22
+ return false;
23
+ }
24
+
25
+ /**
26
+ * Count tokens in content
27
+ * @param {string} content - Text to count tokens for
28
+ * @returns {number} Token count
29
+ */
30
+ count(_content) {
31
+ throw new Error('count() must be implemented by subclass');
32
+ }
33
+
34
+ /**
35
+ * Check if tokenizer is available
36
+ * @returns {boolean}
37
+ */
38
+ isAvailable() {
39
+ return this.available;
40
+ }
41
+
42
+ /**
43
+ * Get tokenizer name
44
+ * @returns {string}
45
+ */
46
+ getName() {
47
+ return this.name;
48
+ }
49
+ }
14
50
 
15
- /**
16
- * Initialize the tokenizer (load libraries, etc.)
17
- * @returns {Promise<boolean>} True if initialized successfully
18
- */
19
- async initialize() {
20
- return false;
21
- }
51
+ /**
52
+ * OpenAI models since GPT-4o use o200k_base; GPT-4, GPT-3.5 and embeddings use cl100k_base.
53
+ * OpenAI has not published GPT-6's encoding, so o200k_base only approximates it.
54
+ */
55
+ const O200K_MODEL = /^(gpt-4o|gpt-4\.1|gpt-4\.5|gpt-5|gpt-6|gpt-oss|o\d|chatgpt|codex)/;
56
+ const UNPUBLISHED_ENCODING = /^gpt-6/;
22
57
 
23
- /**
24
- * Count tokens in content
25
- * @param {string} content - Text to count tokens for
26
- * @returns {number} Token count
27
- */
28
- count(content) {
29
- throw new Error('count() must be implemented by subclass');
30
- }
58
+ /**
59
+ * tiktoken encoding for an OpenAI model name
60
+ * @param {string} modelName - Model name, e.g. `gpt-5`, `gpt-4o-mini`, `gpt-4`
61
+ * @returns {'o200k_base'|'cl100k_base'}
62
+ */
63
+ export function encodingForModel(modelName = '') {
64
+ return O200K_MODEL.test(String(modelName).toLowerCase()) ? 'o200k_base' : 'cl100k_base';
65
+ }
31
66
 
32
- /**
33
- * Check if tokenizer is available
34
- * @returns {boolean}
35
- */
36
- isAvailable() {
37
- return this.available;
38
- }
67
+ /**
68
+ * Whether tiktoken's count is the model's own count
69
+ * @param {string} modelName - OpenAI model name
70
+ * @returns {boolean}
71
+ */
72
+ export function isExactEncoding(modelName = '') {
73
+ return !UNPUBLISHED_ENCODING.test(String(modelName).toLowerCase());
74
+ }
39
75
 
40
- /**
41
- * Get tokenizer name
42
- * @returns {string}
43
- */
44
- getName() {
45
- return this.name;
46
- }
76
+ /** OpenAI model names: gpt-*, o-series reasoning models, codex */
77
+ function isOpenAIModel(lowerModel) {
78
+ return /^(gpt|chatgpt|codex|o\d)/.test(lowerModel);
47
79
  }
48
80
 
49
81
  /**
50
82
  * OpenAI Tokenizer (tiktoken)
51
- * For GPT-4, GPT-5, o1 models
83
+ * For GPT and o-series models; picks o200k_base or cl100k_base per model
52
84
  */
53
85
  export class TiktokenAdapter extends TokenizerAdapter {
54
- constructor() {
55
- super();
56
- this.name = 'Tiktoken (OpenAI)';
57
- this.tiktoken = null;
58
- this.encoding = null;
59
- }
60
-
61
- async initialize() {
62
- try {
63
- const tiktokenModule = await import('tiktoken');
64
- this.tiktoken = tiktokenModule.default || tiktokenModule;
65
- this.available = true;
66
- return true;
67
- } catch (err) {
68
- this.available = false;
69
- return false;
70
- }
71
- }
72
-
73
- count(content) {
74
- if (!this.available || !this.tiktoken) {
75
- throw new Error('Tiktoken not available');
76
- }
77
-
78
- try {
79
- // Use cl100k_base for GPT-4, GPT-4o, GPT-5
80
- // Use o200k_base for newer models if needed
81
- const encoding = this.tiktoken.get_encoding('cl100k_base');
82
- const tokens = encoding.encode(content);
83
- encoding.free();
84
- return tokens.length;
85
- } catch (error) {
86
- throw new Error(`Tiktoken encoding failed: ${error.message}`);
87
- }
88
- }
86
+ constructor() {
87
+ super();
88
+ this.name = 'Tiktoken (OpenAI)';
89
+ this.exact = true;
90
+ this.tiktoken = null;
91
+ this.encodings = new Map();
92
+ }
93
+
94
+ async initialize() {
95
+ try {
96
+ const tiktokenModule = await import('tiktoken');
97
+ this.tiktoken = tiktokenModule.default || tiktokenModule;
98
+ this.available = true;
99
+ return true;
100
+ } catch (_err) {
101
+ this.available = false;
102
+ return false;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @param {string} content - Text to count
108
+ * @param {string} [encodingName='cl100k_base'] - tiktoken encoding
109
+ * @returns {number} Token count
110
+ */
111
+ count(content, encodingName = 'cl100k_base') {
112
+ if (!this.available || !this.tiktoken) {
113
+ throw new Error('Tiktoken not available');
114
+ }
115
+
116
+ try {
117
+ // Built once per encoding: get_encoding() costs ~150 ms, encode() a few ms
118
+ if (!this.encodings.has(encodingName)) {
119
+ this.encodings.set(encodingName, this.tiktoken.get_encoding(encodingName));
120
+ }
121
+ return this.encodings.get(encodingName).encode(content).length;
122
+ } catch (error) {
123
+ throw new Error(`Tiktoken encoding failed: ${error.message}`);
124
+ }
125
+ }
89
126
  }
90
127
 
91
128
  /**
92
- * Anthropic Tokenizer
93
- * For Claude models (Claude 3, Claude Sonnet, etc.)
129
+ * Anthropic's legacy tokenizer package (@anthropic-ai/tokenizer)
130
+ * It implements the Claude 2 tokenizer; Anthropic documents that it is not accurate
131
+ * for Claude 3 and later, whose exact counts come only from the count_tokens API.
132
+ * Used as an approximation for Claude models when installed.
94
133
  */
95
134
  export class AnthropicAdapter extends TokenizerAdapter {
96
- constructor() {
97
- super();
98
- this.name = 'Anthropic (Claude)';
99
- this.tokenizer = null;
100
- }
101
-
102
- async initialize() {
103
- try {
104
- const anthropicModule = await import('@anthropic-ai/tokenizer');
105
- this.tokenizer = anthropicModule.default || anthropicModule;
106
- this.available = true;
107
- return true;
108
- } catch (err) {
109
- this.available = false;
110
- return false;
111
- }
112
- }
113
-
114
- count(content) {
115
- if (!this.available || !this.tokenizer) {
116
- throw new Error('Anthropic tokenizer not available');
117
- }
118
-
119
- try {
120
- // Anthropic tokenizer API
121
- return this.tokenizer.countTokens(content);
122
- } catch (error) {
123
- throw new Error(`Anthropic tokenizer failed: ${error.message}`);
124
- }
125
- }
135
+ constructor() {
136
+ super();
137
+ this.name = 'Anthropic legacy tokenizer (approximate)';
138
+ this.tokenizer = null;
139
+ }
140
+
141
+ async initialize() {
142
+ try {
143
+ const anthropicModule = await import('@anthropic-ai/tokenizer');
144
+ this.tokenizer = anthropicModule.default || anthropicModule;
145
+ this.available = true;
146
+ return true;
147
+ } catch (_err) {
148
+ this.available = false;
149
+ return false;
150
+ }
151
+ }
152
+
153
+ count(content) {
154
+ if (!this.available || !this.tokenizer) {
155
+ throw new Error('Anthropic tokenizer not available');
156
+ }
157
+
158
+ try {
159
+ // Anthropic tokenizer API
160
+ return this.tokenizer.countTokens(content);
161
+ } catch (error) {
162
+ throw new Error(`Anthropic tokenizer failed: ${error.message}`);
163
+ }
164
+ }
126
165
  }
127
166
 
128
167
  /**
@@ -130,22 +169,22 @@ export class AnthropicAdapter extends TokenizerAdapter {
130
169
  * Currently uses estimation (no official tokenizer package)
131
170
  */
132
171
  export class GeminiAdapter extends TokenizerAdapter {
133
- constructor() {
134
- super();
135
- this.name = 'Gemini (Estimation)';
136
- this.available = true; // Always available (estimation)
137
- }
138
-
139
- async initialize() {
140
- this.available = true;
141
- return true;
142
- }
143
-
144
- count(content) {
145
- // Gemini uses similar tokenization to GPT models
146
- // Estimation: ~3.5 chars per token for English text
147
- return Math.round(content.length / 3.5);
148
- }
172
+ constructor() {
173
+ super();
174
+ this.name = 'Gemini (Estimation)';
175
+ this.available = true; // Always available (estimation)
176
+ }
177
+
178
+ async initialize() {
179
+ this.available = true;
180
+ return true;
181
+ }
182
+
183
+ count(content) {
184
+ // Gemini uses similar tokenization to GPT models
185
+ // Estimation: ~3.5 chars per token for English text
186
+ return Math.round(content.length / 3.5);
187
+ }
149
188
  }
150
189
 
151
190
  /**
@@ -153,31 +192,32 @@ export class GeminiAdapter extends TokenizerAdapter {
153
192
  * Uses tiktoken-compatible encoding (OpenAI-compatible API)
154
193
  */
155
194
  export class DeepSeekAdapter extends TokenizerAdapter {
156
- constructor() {
157
- super();
158
- this.name = 'DeepSeek (tiktoken)';
159
- this.tiktokenAdapter = null;
160
- }
161
-
162
- async initialize() {
163
- try {
164
- this.tiktokenAdapter = new TiktokenAdapter();
165
- await this.tiktokenAdapter.initialize();
166
- this.available = this.tiktokenAdapter.isAvailable();
167
- return this.available;
168
- } catch (err) {
169
- this.available = false;
170
- return false;
171
- }
172
- }
173
-
174
- count(content) {
175
- if (!this.available || !this.tiktokenAdapter) {
176
- // Fallback to estimation
177
- return Math.round(content.length / 3.5);
178
- }
179
- return this.tiktokenAdapter.count(content);
180
- }
195
+ constructor() {
196
+ super();
197
+ // DeepSeek has its own tokenizer; cl100k_base only approximates it
198
+ this.name = 'DeepSeek (tiktoken approximation)';
199
+ this.tiktokenAdapter = null;
200
+ }
201
+
202
+ async initialize() {
203
+ try {
204
+ this.tiktokenAdapter = new TiktokenAdapter();
205
+ await this.tiktokenAdapter.initialize();
206
+ this.available = this.tiktokenAdapter.isAvailable();
207
+ return this.available;
208
+ } catch (_err) {
209
+ this.available = false;
210
+ return false;
211
+ }
212
+ }
213
+
214
+ count(content) {
215
+ if (!this.available || !this.tiktokenAdapter) {
216
+ // Fallback to estimation
217
+ return Math.round(content.length / 3.5);
218
+ }
219
+ return this.tiktokenAdapter.count(content);
220
+ }
181
221
  }
182
222
 
183
223
  /**
@@ -185,37 +225,38 @@ export class DeepSeekAdapter extends TokenizerAdapter {
185
225
  * For Llama 3.x models
186
226
  */
187
227
  export class LlamaAdapter extends TokenizerAdapter {
188
- constructor() {
189
- super();
190
- this.name = 'Llama 3 (llama3-tokenizer-js)';
191
- this.tokenizer = null;
192
- }
193
-
194
- async initialize() {
195
- try {
196
- const llamaModule = await import('llama3-tokenizer-js');
197
- this.tokenizer = llamaModule.default || llamaModule;
198
- this.available = true;
199
- return true;
200
- } catch (err) {
201
- this.available = false;
202
- return false;
203
- }
204
- }
205
-
206
- count(content) {
207
- if (!this.available || !this.tokenizer) {
208
- throw new Error('Llama tokenizer not available');
209
- }
210
-
211
- try {
212
- // llama3-tokenizer-js API
213
- const tokens = this.tokenizer.encode(content);
214
- return tokens.length;
215
- } catch (error) {
216
- throw new Error(`Llama tokenizer failed: ${error.message}`);
217
- }
218
- }
228
+ constructor() {
229
+ super();
230
+ this.name = 'Llama 3 (llama3-tokenizer-js)';
231
+ this.exact = true;
232
+ this.tokenizer = null;
233
+ }
234
+
235
+ async initialize() {
236
+ try {
237
+ const llamaModule = await import('llama3-tokenizer-js');
238
+ this.tokenizer = llamaModule.default || llamaModule;
239
+ this.available = true;
240
+ return true;
241
+ } catch (_err) {
242
+ this.available = false;
243
+ return false;
244
+ }
245
+ }
246
+
247
+ count(content) {
248
+ if (!this.available || !this.tokenizer) {
249
+ throw new Error('Llama tokenizer not available');
250
+ }
251
+
252
+ try {
253
+ // llama3-tokenizer-js API
254
+ const tokens = this.tokenizer.encode(content);
255
+ return tokens.length;
256
+ } catch (error) {
257
+ throw new Error(`Llama tokenizer failed: ${error.message}`);
258
+ }
259
+ }
219
260
  }
220
261
 
221
262
  /**
@@ -223,219 +264,222 @@ export class LlamaAdapter extends TokenizerAdapter {
223
264
  * Uses character count heuristics
224
265
  */
225
266
  export class EstimationAdapter extends TokenizerAdapter {
226
- constructor() {
227
- super();
228
- this.name = 'Estimation (Fallback)';
229
- this.available = true; // Always available
230
- }
231
-
232
- async initialize() {
233
- this.available = true;
234
- return true;
235
- }
236
-
237
- count(content) {
238
- // Conservative estimation: ~3.5 chars per token
239
- // Works reasonably well for most languages
240
- return Math.round(content.length / 3.5);
241
- }
267
+ constructor() {
268
+ super();
269
+ this.name = 'Estimation (Fallback)';
270
+ this.available = true; // Always available
271
+ }
272
+
273
+ async initialize() {
274
+ this.available = true;
275
+ return true;
276
+ }
277
+
278
+ count(content) {
279
+ // Conservative estimation: ~3.5 chars per token
280
+ // Works reasonably well for most languages
281
+ return Math.round(content.length / 3.5);
282
+ }
242
283
  }
243
284
 
244
285
  /**
245
286
  * Telemetry tracker for tokenizer usage
246
287
  */
247
288
  class TokenizerTelemetry {
248
- constructor() {
249
- this.stats = {
250
- totalCalls: 0,
251
- byTokenizer: {},
252
- byModel: {},
253
- totalTokens: 0,
254
- errors: 0,
289
+ constructor() {
290
+ this.stats = {
291
+ totalCalls: 0,
292
+ byTokenizer: {},
293
+ byModel: {},
294
+ totalTokens: 0,
295
+ errors: 0,
296
+ };
297
+ }
298
+
299
+ track(tokenizerName, modelName, tokens, error = null) {
300
+ this.stats.totalCalls++;
301
+
302
+ if (error) {
303
+ this.stats.errors++;
304
+ } else {
305
+ // Track by tokenizer
306
+ if (!this.stats.byTokenizer[tokenizerName]) {
307
+ this.stats.byTokenizer[tokenizerName] = {
308
+ calls: 0,
309
+ tokens: 0,
255
310
  };
256
- }
257
-
258
- track(tokenizerName, modelName, tokens, error = null) {
259
- this.stats.totalCalls++;
260
-
261
- if (error) {
262
- this.stats.errors++;
263
- } else {
264
- // Track by tokenizer
265
- if (!this.stats.byTokenizer[tokenizerName]) {
266
- this.stats.byTokenizer[tokenizerName] = {
267
- calls: 0,
268
- tokens: 0,
269
- };
270
- }
271
- this.stats.byTokenizer[tokenizerName].calls++;
272
- this.stats.byTokenizer[tokenizerName].tokens += tokens;
273
-
274
- // Track by model
275
- if (!this.stats.byModel[modelName]) {
276
- this.stats.byModel[modelName] = {
277
- calls: 0,
278
- tokens: 0,
279
- };
280
- }
281
- this.stats.byModel[modelName].calls++;
282
- this.stats.byModel[modelName].tokens += tokens;
283
-
284
- this.stats.totalTokens += tokens;
285
- }
286
- }
287
-
288
- getStats() {
289
- return { ...this.stats };
290
- }
291
-
292
- reset() {
293
- this.stats = {
294
- totalCalls: 0,
295
- byTokenizer: {},
296
- byModel: {},
297
- totalTokens: 0,
298
- errors: 0,
311
+ }
312
+ this.stats.byTokenizer[tokenizerName].calls++;
313
+ this.stats.byTokenizer[tokenizerName].tokens += tokens;
314
+
315
+ // Track by model
316
+ if (!this.stats.byModel[modelName]) {
317
+ this.stats.byModel[modelName] = {
318
+ calls: 0,
319
+ tokens: 0,
299
320
  };
300
- }
321
+ }
322
+ this.stats.byModel[modelName].calls++;
323
+ this.stats.byModel[modelName].tokens += tokens;
324
+
325
+ this.stats.totalTokens += tokens;
326
+ }
327
+ }
328
+
329
+ getStats() {
330
+ return { ...this.stats };
331
+ }
332
+
333
+ reset() {
334
+ this.stats = {
335
+ totalCalls: 0,
336
+ byTokenizer: {},
337
+ byModel: {},
338
+ totalTokens: 0,
339
+ errors: 0,
340
+ };
341
+ }
301
342
  }
302
343
 
303
344
  /**
304
345
  * Tokenizer factory and manager with telemetry
305
346
  */
306
347
  export class TokenizerManager {
307
- constructor(options = {}) {
308
- this.adapters = new Map();
309
- this.initialized = false;
310
- this.telemetry = new TokenizerTelemetry();
311
- this.enableTelemetry = options.enableTelemetry !== false; // Default: enabled
312
- }
313
-
314
- /**
315
- * Initialize all available tokenizers
316
- */
317
- async initialize() {
318
- if (this.initialized) return;
319
-
320
- const adapters = [
321
- { key: 'tiktoken', adapter: new TiktokenAdapter() },
322
- { key: 'anthropic', adapter: new AnthropicAdapter() },
323
- { key: 'gemini', adapter: new GeminiAdapter() },
324
- { key: 'deepseek', adapter: new DeepSeekAdapter() },
325
- { key: 'llama', adapter: new LlamaAdapter() },
326
- { key: 'estimation', adapter: new EstimationAdapter() },
327
- ];
328
-
329
- for (const { key, adapter } of adapters) {
330
- await adapter.initialize();
331
- this.adapters.set(key, adapter);
332
- }
333
-
334
- this.initialized = true;
335
- }
336
-
337
- /**
338
- * Get tokenizer for specific model with telemetry
339
- * @param {string} modelName - Model name
340
- * @returns {TokenizerAdapter}
341
- */
342
- getTokenizerForModel(modelName) {
343
- if (!this.initialized) {
344
- throw new Error('TokenizerManager not initialized. Call initialize() first.');
345
- }
346
-
347
- const lowerModel = modelName.toLowerCase();
348
-
349
- // OpenAI models
350
- if (lowerModel.includes('gpt') || lowerModel.includes('o1') || lowerModel.includes('o3')) {
351
- const tiktoken = this.adapters.get('tiktoken');
352
- if (tiktoken.isAvailable()) return tiktoken;
353
- }
354
-
355
- // Anthropic models
356
- if (lowerModel.includes('claude')) {
357
- const anthropic = this.adapters.get('anthropic');
358
- if (anthropic.isAvailable()) return anthropic;
359
- }
360
-
361
- // Google models
362
- if (lowerModel.includes('gemini')) {
363
- return this.adapters.get('gemini');
364
- }
365
-
366
- // DeepSeek models
367
- if (lowerModel.includes('deepseek')) {
368
- const deepseek = this.adapters.get('deepseek');
369
- if (deepseek.isAvailable()) return deepseek;
370
- }
371
-
372
- // Llama models
373
- if (lowerModel.includes('llama')) {
374
- const llama = this.adapters.get('llama');
375
- if (llama.isAvailable()) return llama;
376
- }
377
-
378
- // Fallback to estimation
379
- return this.adapters.get('estimation');
380
- }
381
-
382
- /**
383
- * Count tokens with telemetry tracking
384
- * @param {string} content - Content to tokenize
385
- * @param {string} modelName - Model name
386
- * @returns {number} Token count
387
- */
388
- countWithTelemetry(content, modelName) {
389
- const tokenizer = this.getTokenizerForModel(modelName);
390
- let tokens = 0;
391
- let error = null;
392
-
393
- try {
394
- tokens = tokenizer.count(content);
395
- if (this.enableTelemetry) {
396
- this.telemetry.track(tokenizer.getName(), modelName, tokens);
397
- }
398
- } catch (err) {
399
- error = err;
400
- if (this.enableTelemetry) {
401
- this.telemetry.track(tokenizer.getName(), modelName, 0, err);
402
- }
403
- throw err;
404
- }
405
-
406
- return tokens;
407
- }
408
-
409
- /**
410
- * Get telemetry stats
411
- * @returns {Object} Telemetry statistics
412
- */
413
- getTelemetry() {
414
- return this.telemetry.getStats();
415
- }
416
-
417
- /**
418
- * Reset telemetry
419
- */
420
- resetTelemetry() {
421
- this.telemetry.reset();
422
- }
423
-
424
- /**
425
- * Get all available tokenizers
426
- * @returns {Array<{key: string, name: string, available: boolean}>}
427
- */
428
- getAvailableTokenizers() {
429
- const result = [];
430
- for (const [key, adapter] of this.adapters.entries()) {
431
- result.push({
432
- key,
433
- name: adapter.getName(),
434
- available: adapter.isAvailable(),
435
- });
436
- }
437
- return result;
438
- }
348
+ constructor(options = {}) {
349
+ this.adapters = new Map();
350
+ this.initialized = false;
351
+ this.telemetry = new TokenizerTelemetry();
352
+ this.enableTelemetry = options.enableTelemetry !== false; // Default: enabled
353
+ }
354
+
355
+ /**
356
+ * Initialize all available tokenizers
357
+ */
358
+ async initialize() {
359
+ if (this.initialized) return;
360
+
361
+ const adapters = [
362
+ { key: 'tiktoken', adapter: new TiktokenAdapter() },
363
+ { key: 'anthropic', adapter: new AnthropicAdapter() },
364
+ { key: 'gemini', adapter: new GeminiAdapter() },
365
+ { key: 'deepseek', adapter: new DeepSeekAdapter() },
366
+ { key: 'llama', adapter: new LlamaAdapter() },
367
+ { key: 'estimation', adapter: new EstimationAdapter() },
368
+ ];
369
+
370
+ for (const { key, adapter } of adapters) {
371
+ await adapter.initialize();
372
+ this.adapters.set(key, adapter);
373
+ }
374
+
375
+ this.initialized = true;
376
+ }
377
+
378
+ /**
379
+ * Get tokenizer for specific model with telemetry
380
+ * @param {string} modelName - Model name
381
+ * @returns {TokenizerAdapter}
382
+ */
383
+ getTokenizerForModel(modelName) {
384
+ if (!this.initialized) {
385
+ throw new Error('TokenizerManager not initialized. Call initialize() first.');
386
+ }
387
+
388
+ const lowerModel = modelName.toLowerCase();
389
+
390
+ // OpenAI models
391
+ if (isOpenAIModel(lowerModel)) {
392
+ const tiktoken = this.adapters.get('tiktoken');
393
+ if (tiktoken.isAvailable()) return tiktoken;
394
+ }
395
+
396
+ // Anthropic models
397
+ if (lowerModel.includes('claude')) {
398
+ const anthropic = this.adapters.get('anthropic');
399
+ if (anthropic.isAvailable()) return anthropic;
400
+ }
401
+
402
+ // Google models
403
+ if (lowerModel.includes('gemini')) {
404
+ return this.adapters.get('gemini');
405
+ }
406
+
407
+ // DeepSeek models
408
+ if (lowerModel.includes('deepseek')) {
409
+ const deepseek = this.adapters.get('deepseek');
410
+ if (deepseek.isAvailable()) return deepseek;
411
+ }
412
+
413
+ // Llama models
414
+ if (lowerModel.includes('llama')) {
415
+ const llama = this.adapters.get('llama');
416
+ if (llama.isAvailable()) return llama;
417
+ }
418
+
419
+ // Fallback to estimation
420
+ return this.adapters.get('estimation');
421
+ }
422
+
423
+ /**
424
+ * Count tokens with telemetry tracking
425
+ * @param {string} content - Content to tokenize
426
+ * @param {string} modelName - Model name
427
+ * @returns {number} Token count
428
+ */
429
+ countWithTelemetry(content, modelName) {
430
+ const tokenizer = this.getTokenizerForModel(modelName);
431
+ let tokens = 0;
432
+ let _error = null;
433
+
434
+ try {
435
+ tokens =
436
+ tokenizer instanceof TiktokenAdapter
437
+ ? tokenizer.count(content, encodingForModel(modelName))
438
+ : tokenizer.count(content);
439
+ if (this.enableTelemetry) {
440
+ this.telemetry.track(tokenizer.getName(), modelName, tokens);
441
+ }
442
+ } catch (err) {
443
+ if (this.enableTelemetry) {
444
+ this.telemetry.track(tokenizer.getName(), modelName, 0, err);
445
+ }
446
+ throw err;
447
+ }
448
+
449
+ return tokens;
450
+ }
451
+
452
+ /**
453
+ * Get telemetry stats
454
+ * @returns {Object} Telemetry statistics
455
+ */
456
+ getTelemetry() {
457
+ return this.telemetry.getStats();
458
+ }
459
+
460
+ /**
461
+ * Reset telemetry
462
+ */
463
+ resetTelemetry() {
464
+ this.telemetry.reset();
465
+ }
466
+
467
+ /**
468
+ * Get all available tokenizers
469
+ * @returns {Array<{key: string, name: string, available: boolean}>}
470
+ */
471
+ getAvailableTokenizers() {
472
+ const result = [];
473
+ for (const [key, adapter] of this.adapters.entries()) {
474
+ result.push({
475
+ key,
476
+ name: adapter.getName(),
477
+ available: adapter.isAvailable(),
478
+ exact: adapter.exact,
479
+ });
480
+ }
481
+ return result;
482
+ }
439
483
  }
440
484
 
441
485
  // Singleton instance
@@ -446,9 +490,9 @@ let managerInstance = null;
446
490
  * @returns {Promise<TokenizerManager>}
447
491
  */
448
492
  export async function getTokenizerManager() {
449
- if (!managerInstance) {
450
- managerInstance = new TokenizerManager();
451
- await managerInstance.initialize();
452
- }
453
- return managerInstance;
493
+ if (!managerInstance) {
494
+ managerInstance = new TokenizerManager();
495
+ await managerInstance.initialize();
496
+ }
497
+ return managerInstance;
454
498
  }