nova-terminal-assistant 0.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.

Potentially problematic release.


This version of nova-terminal-assistant might be problematic. Click here for more details.

Files changed (192) hide show
  1. package/README.md +358 -0
  2. package/bin/nova +38 -0
  3. package/bin/nova.js +12 -0
  4. package/package.json +67 -0
  5. package/src/cli/commands/SmartCompletion.ts +458 -0
  6. package/src/cli/index.ts +5 -0
  7. package/src/cli/startup/IFlowRepl.ts +212 -0
  8. package/src/cli/startup/InkBasedRepl.ts +1056 -0
  9. package/src/cli/startup/InteractiveRepl.ts +2833 -0
  10. package/src/cli/startup/NovaApp.ts +1861 -0
  11. package/src/cli/startup/index.ts +4 -0
  12. package/src/cli/startup/parseArgs.ts +293 -0
  13. package/src/cli/test-modules.ts +27 -0
  14. package/src/cli/ui/IFlowDropdown.ts +425 -0
  15. package/src/cli/ui/ModernReplUI.ts +276 -0
  16. package/src/cli/ui/SimpleSelector2.ts +215 -0
  17. package/src/cli/ui/components/ConfirmDialog.ts +176 -0
  18. package/src/cli/ui/components/ErrorPanel.ts +364 -0
  19. package/src/cli/ui/components/InkAppRunner.tsx +67 -0
  20. package/src/cli/ui/components/InkComponents.tsx +613 -0
  21. package/src/cli/ui/components/NovaInkApp.tsx +312 -0
  22. package/src/cli/ui/components/ProgressBar.ts +177 -0
  23. package/src/cli/ui/components/ProgressIndicator.ts +298 -0
  24. package/src/cli/ui/components/QuickActions.ts +396 -0
  25. package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
  26. package/src/cli/ui/components/StatusBar.ts +194 -0
  27. package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
  28. package/src/cli/ui/components/index.ts +27 -0
  29. package/src/cli/ui/ink-prototype.tsx +347 -0
  30. package/src/cli/utils/CliUI.ts +336 -0
  31. package/src/cli/utils/CompletionHelper.ts +388 -0
  32. package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
  33. package/src/cli/utils/EnhancedCompleter.ts +513 -0
  34. package/src/cli/utils/ErrorEnhancer.ts +429 -0
  35. package/src/cli/utils/OutputFormatter.ts +193 -0
  36. package/src/cli/utils/index.ts +9 -0
  37. package/src/core/agents/AgentOrchestrator.ts +515 -0
  38. package/src/core/agents/index.ts +17 -0
  39. package/src/core/audit/AuditLogger.ts +509 -0
  40. package/src/core/audit/index.ts +11 -0
  41. package/src/core/auth/AuthManager.d.ts.map +1 -0
  42. package/src/core/auth/AuthManager.ts +138 -0
  43. package/src/core/auth/index.d.ts.map +1 -0
  44. package/src/core/auth/index.ts +2 -0
  45. package/src/core/config/ConfigManager.d.ts.map +1 -0
  46. package/src/core/config/ConfigManager.test.ts +183 -0
  47. package/src/core/config/ConfigManager.ts +1219 -0
  48. package/src/core/config/index.d.ts.map +1 -0
  49. package/src/core/config/index.ts +1 -0
  50. package/src/core/context/ContextBuilder.d.ts.map +1 -0
  51. package/src/core/context/ContextBuilder.ts +171 -0
  52. package/src/core/context/ContextCompressor.d.ts.map +1 -0
  53. package/src/core/context/ContextCompressor.ts +642 -0
  54. package/src/core/context/LayeredMemoryManager.ts +657 -0
  55. package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
  56. package/src/core/context/MemoryDiscovery.ts +175 -0
  57. package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
  58. package/src/core/context/defaultSystemPrompt.ts +35 -0
  59. package/src/core/context/index.d.ts.map +1 -0
  60. package/src/core/context/index.ts +22 -0
  61. package/src/core/extensions/SkillGenerator.ts +421 -0
  62. package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
  63. package/src/core/extensions/SkillInstaller.ts +257 -0
  64. package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
  65. package/src/core/extensions/SkillRegistry.ts +361 -0
  66. package/src/core/extensions/SkillValidator.ts +525 -0
  67. package/src/core/extensions/index.ts +15 -0
  68. package/src/core/index.d.ts.map +1 -0
  69. package/src/core/index.ts +42 -0
  70. package/src/core/mcp/McpManager.d.ts.map +1 -0
  71. package/src/core/mcp/McpManager.ts +632 -0
  72. package/src/core/mcp/index.d.ts.map +1 -0
  73. package/src/core/mcp/index.ts +2 -0
  74. package/src/core/model/ModelClient.d.ts.map +1 -0
  75. package/src/core/model/ModelClient.ts +217 -0
  76. package/src/core/model/ModelConnectionTester.ts +363 -0
  77. package/src/core/model/ModelValidator.ts +348 -0
  78. package/src/core/model/index.d.ts.map +1 -0
  79. package/src/core/model/index.ts +6 -0
  80. package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
  81. package/src/core/model/providers/AnthropicProvider.ts +279 -0
  82. package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
  83. package/src/core/model/providers/CodingPlanProvider.ts +210 -0
  84. package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
  85. package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
  86. package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
  87. package/src/core/model/providers/OllamaManager.ts +201 -0
  88. package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
  89. package/src/core/model/providers/OllamaProvider.ts +73 -0
  90. package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  91. package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
  92. package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
  93. package/src/core/model/providers/OpenAIProvider.ts +29 -0
  94. package/src/core/model/providers/index.d.ts.map +1 -0
  95. package/src/core/model/providers/index.ts +12 -0
  96. package/src/core/model/types.d.ts.map +1 -0
  97. package/src/core/model/types.ts +77 -0
  98. package/src/core/security/ApprovalManager.d.ts.map +1 -0
  99. package/src/core/security/ApprovalManager.ts +174 -0
  100. package/src/core/security/FileFilter.d.ts.map +1 -0
  101. package/src/core/security/FileFilter.ts +141 -0
  102. package/src/core/security/HookExecutor.d.ts.map +1 -0
  103. package/src/core/security/HookExecutor.ts +178 -0
  104. package/src/core/security/SandboxExecutor.ts +447 -0
  105. package/src/core/security/index.d.ts.map +1 -0
  106. package/src/core/security/index.ts +8 -0
  107. package/src/core/session/AgentLoop.d.ts.map +1 -0
  108. package/src/core/session/AgentLoop.ts +501 -0
  109. package/src/core/session/SessionManager.d.ts.map +1 -0
  110. package/src/core/session/SessionManager.test.ts +183 -0
  111. package/src/core/session/SessionManager.ts +460 -0
  112. package/src/core/session/index.d.ts.map +1 -0
  113. package/src/core/session/index.ts +3 -0
  114. package/src/core/telemetry/Telemetry.d.ts.map +1 -0
  115. package/src/core/telemetry/Telemetry.ts +90 -0
  116. package/src/core/telemetry/TelemetryService.ts +531 -0
  117. package/src/core/telemetry/index.d.ts.map +1 -0
  118. package/src/core/telemetry/index.ts +12 -0
  119. package/src/core/testing/AutoFixer.ts +385 -0
  120. package/src/core/testing/ErrorAnalyzer.ts +499 -0
  121. package/src/core/testing/TestRunner.ts +265 -0
  122. package/src/core/testing/agent-cli-tests.ts +538 -0
  123. package/src/core/testing/index.ts +11 -0
  124. package/src/core/tools/ToolRegistry.d.ts.map +1 -0
  125. package/src/core/tools/ToolRegistry.test.ts +206 -0
  126. package/src/core/tools/ToolRegistry.ts +260 -0
  127. package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
  128. package/src/core/tools/impl/EditFileTool.ts +97 -0
  129. package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
  130. package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
  131. package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
  132. package/src/core/tools/impl/MemoryTool.ts +102 -0
  133. package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
  134. package/src/core/tools/impl/ReadFileTool.ts +58 -0
  135. package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
  136. package/src/core/tools/impl/SearchContentTool.ts +94 -0
  137. package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
  138. package/src/core/tools/impl/SearchFileTool.ts +61 -0
  139. package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
  140. package/src/core/tools/impl/ShellTool.ts +118 -0
  141. package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
  142. package/src/core/tools/impl/TaskTool.ts +207 -0
  143. package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
  144. package/src/core/tools/impl/TodoTool.ts +122 -0
  145. package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
  146. package/src/core/tools/impl/WebFetchTool.ts +103 -0
  147. package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
  148. package/src/core/tools/impl/WebSearchTool.ts +89 -0
  149. package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
  150. package/src/core/tools/impl/WriteFileTool.ts +49 -0
  151. package/src/core/tools/impl/index.d.ts.map +1 -0
  152. package/src/core/tools/impl/index.ts +16 -0
  153. package/src/core/tools/index.d.ts.map +1 -0
  154. package/src/core/tools/index.ts +7 -0
  155. package/src/core/tools/schemas/execution.d.ts.map +1 -0
  156. package/src/core/tools/schemas/execution.ts +42 -0
  157. package/src/core/tools/schemas/file.d.ts.map +1 -0
  158. package/src/core/tools/schemas/file.ts +119 -0
  159. package/src/core/tools/schemas/index.d.ts.map +1 -0
  160. package/src/core/tools/schemas/index.ts +11 -0
  161. package/src/core/tools/schemas/memory.d.ts.map +1 -0
  162. package/src/core/tools/schemas/memory.ts +52 -0
  163. package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
  164. package/src/core/tools/schemas/orchestration.ts +44 -0
  165. package/src/core/tools/schemas/search.d.ts.map +1 -0
  166. package/src/core/tools/schemas/search.ts +112 -0
  167. package/src/core/tools/schemas/todo.d.ts.map +1 -0
  168. package/src/core/tools/schemas/todo.ts +32 -0
  169. package/src/core/tools/schemas/web.d.ts.map +1 -0
  170. package/src/core/tools/schemas/web.ts +86 -0
  171. package/src/core/types/config.d.ts.map +1 -0
  172. package/src/core/types/config.ts +200 -0
  173. package/src/core/types/errors.d.ts.map +1 -0
  174. package/src/core/types/errors.ts +204 -0
  175. package/src/core/types/index.d.ts.map +1 -0
  176. package/src/core/types/index.ts +8 -0
  177. package/src/core/types/session.d.ts.map +1 -0
  178. package/src/core/types/session.ts +216 -0
  179. package/src/core/types/tools.d.ts.map +1 -0
  180. package/src/core/types/tools.ts +157 -0
  181. package/src/core/utils/CheckpointManager.d.ts.map +1 -0
  182. package/src/core/utils/CheckpointManager.ts +327 -0
  183. package/src/core/utils/Logger.d.ts.map +1 -0
  184. package/src/core/utils/Logger.ts +98 -0
  185. package/src/core/utils/RetryManager.ts +471 -0
  186. package/src/core/utils/TokenCounter.d.ts.map +1 -0
  187. package/src/core/utils/TokenCounter.ts +414 -0
  188. package/src/core/utils/VectorMemoryStore.ts +440 -0
  189. package/src/core/utils/helpers.d.ts.map +1 -0
  190. package/src/core/utils/helpers.ts +89 -0
  191. package/src/core/utils/index.d.ts.map +1 -0
  192. package/src/core/utils/index.ts +19 -0
@@ -0,0 +1,414 @@
1
+ // ============================================================================
2
+ // TokenCounter - Precise token counting using tiktoken
3
+ // ============================================================================
4
+
5
+ import type { Message, ContentBlock } from '../types/session.js';
6
+
7
+ /**
8
+ * Model encoding mappings for tiktoken
9
+ * Maps model names to their corresponding tiktoken encoding
10
+ */
11
+ const MODEL_ENCODING_MAP: Record<string, string> = {
12
+ // Anthropic Claude models (use cl100k_base, similar to GPT-4)
13
+ 'claude-3': 'cl100k_base',
14
+ 'claude-sonnet': 'cl100k_base',
15
+ 'claude-opus': 'cl100k_base',
16
+ 'claude-haiku': 'cl100k_base',
17
+
18
+ // OpenAI models
19
+ 'gpt-4': 'cl100k_base',
20
+ 'gpt-4o': 'o200k_base',
21
+ 'gpt-4-turbo': 'cl100k_base',
22
+ 'gpt-3.5-turbo': 'cl100k_base',
23
+ 'o1': 'o200k_base',
24
+ 'o1-mini': 'o200k_base',
25
+ 'o3-mini': 'o200k_base',
26
+
27
+ // Google models (approximate with cl100k_base)
28
+ 'gemini': 'cl100k_base',
29
+
30
+ // DeepSeek (use cl100k_base)
31
+ 'deepseek': 'cl100k_base',
32
+
33
+ // Qwen (use cl100k_base)
34
+ 'qwen': 'cl100k_base',
35
+
36
+ // GLM (use cl100k_base)
37
+ 'glm': 'cl100k_base',
38
+
39
+ // Llama models
40
+ 'llama': 'cl100k_base',
41
+
42
+ // Default fallback
43
+ 'default': 'cl100k_base',
44
+ };
45
+
46
+ /**
47
+ * Special tokens added per message for different providers
48
+ * Based on official documentation
49
+ */
50
+ const MESSAGE_TOKEN_OVERHEAD: Record<string, { perMessage: number; perName: number }> = {
51
+ anthropic: { perMessage: 4, perName: 0 }, // Approximate
52
+ openai: { perMessage: 3, perName: 1 }, // Official: every message follows <im_start>{role/name}\n{content}<im_end>\n
53
+ default: { perMessage: 4, perName: 0 },
54
+ };
55
+
56
+ /**
57
+ * Simple tokenizer implementation without external dependencies
58
+ * Uses character-based estimation with model-specific adjustments
59
+ */
60
+ class SimpleTokenizer {
61
+ private encoding: string;
62
+ private avgCharsPerToken: number;
63
+
64
+ constructor(encoding: string) {
65
+ this.encoding = encoding;
66
+ // Different encodings have different average chars per token
67
+ this.avgCharsPerToken = encoding === 'o200k_base' ? 3.5 : 4;
68
+ }
69
+
70
+ /**
71
+ * Encode text to tokens (simplified implementation)
72
+ * Returns array of token strings for counting
73
+ */
74
+ encode(text: string): string[] {
75
+ // Simple word-based tokenization with punctuation handling
76
+ const tokens: string[] = [];
77
+ let current = '';
78
+ let inWord = false;
79
+
80
+ for (const char of text) {
81
+ const isWordChar = /[a-zA-Z0-9]/.test(char);
82
+
83
+ if (isWordChar !== inWord && current) {
84
+ tokens.push(current);
85
+ current = '';
86
+ }
87
+
88
+ current += char;
89
+ inWord = isWordChar;
90
+
91
+ // Break long sequences
92
+ if (current.length >= Math.ceil(this.avgCharsPerToken * 2)) {
93
+ tokens.push(current);
94
+ current = '';
95
+ }
96
+ }
97
+
98
+ if (current) {
99
+ tokens.push(current);
100
+ }
101
+
102
+ return tokens;
103
+ }
104
+
105
+ /**
106
+ * Count tokens in text
107
+ */
108
+ count(text: string): number {
109
+ if (!text) return 0;
110
+
111
+ // For more accurate counting, use word-based estimation
112
+ // English: ~1.3 tokens per word
113
+ // Chinese: ~0.5-0.8 tokens per character (due to subword tokenization)
114
+ // Code: ~0.5-0.7 tokens per word
115
+
116
+ const words = text.split(/\s+/).filter(w => w.length > 0);
117
+ const wordCount = words.length;
118
+
119
+ // Count CJK characters separately (they typically tokenize as 1-2 chars per token)
120
+ const cjkChars = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
121
+ const nonCjkLength = text.length - cjkChars;
122
+
123
+ // Estimate: CJK ~0.6 tokens per char, non-CJK ~4 chars per token
124
+ const cjkTokens = Math.ceil(cjkChars * 0.6);
125
+ const nonCjkTokens = Math.ceil(nonCjkLength / this.avgCharsPerToken);
126
+
127
+ // Add overhead for special tokens and structure
128
+ const overhead = Math.ceil(wordCount * 0.1);
129
+
130
+ return cjkTokens + nonCjkTokens + overhead;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Token counter with caching for performance
136
+ */
137
+ export class TokenCounter {
138
+ private static instance: TokenCounter;
139
+ private tokenizers: Map<string, SimpleTokenizer> = new Map();
140
+ private cache: Map<string, number> = new Map();
141
+ private cacheMaxSize = 1000;
142
+
143
+ private constructor() {}
144
+
145
+ static getInstance(): TokenCounter {
146
+ if (!TokenCounter.instance) {
147
+ TokenCounter.instance = new TokenCounter();
148
+ }
149
+ return TokenCounter.instance;
150
+ }
151
+
152
+ /**
153
+ * Get tokenizer for a model
154
+ */
155
+ private getTokenizer(model: string): SimpleTokenizer {
156
+ // Normalize model name
157
+ const normalizedModel = this.normalizeModelName(model);
158
+ const encoding = MODEL_ENCODING_MAP[normalizedModel] ?? 'cl100k_base';
159
+
160
+ if (!this.tokenizers.has(encoding)) {
161
+ this.tokenizers.set(encoding, new SimpleTokenizer(encoding));
162
+ }
163
+
164
+ return this.tokenizers.get(encoding)!;
165
+ }
166
+
167
+ /**
168
+ * Normalize model name to match encoding map
169
+ */
170
+ private normalizeModelName(model: string): string {
171
+ const lower = model.toLowerCase();
172
+
173
+ // Check direct matches first
174
+ for (const key of Object.keys(MODEL_ENCODING_MAP)) {
175
+ if (lower.includes(key)) {
176
+ return key;
177
+ }
178
+ }
179
+
180
+ return 'default';
181
+ }
182
+
183
+ /**
184
+ * Get provider type from model name
185
+ */
186
+ private getProviderType(model: string): string {
187
+ const lower = model.toLowerCase();
188
+ if (lower.includes('claude') || lower.includes('anthropic')) return 'anthropic';
189
+ if (lower.includes('gpt') || lower.includes('o1') || lower.includes('o3')) return 'openai';
190
+ return 'default';
191
+ }
192
+
193
+ /**
194
+ * Count tokens in a text string
195
+ */
196
+ countTokens(text: string, model: string = 'default'): number {
197
+ if (!text) return 0;
198
+
199
+ // Check cache
200
+ const cacheKey = `${model}:${text.slice(0, 100)}:${text.length}`;
201
+ if (this.cache.has(cacheKey)) {
202
+ return this.cache.get(cacheKey)!;
203
+ }
204
+
205
+ const tokenizer = this.getTokenizer(model);
206
+ const count = tokenizer.count(text);
207
+
208
+ // Cache result
209
+ if (this.cache.size >= this.cacheMaxSize) {
210
+ // Remove oldest entries
211
+ const keys = Array.from(this.cache.keys()).slice(0, Math.floor(this.cacheMaxSize / 2));
212
+ for (const key of keys) {
213
+ this.cache.delete(key);
214
+ }
215
+ }
216
+ this.cache.set(cacheKey, count);
217
+
218
+ return count;
219
+ }
220
+
221
+ /**
222
+ * Count tokens in a message
223
+ */
224
+ countMessageTokens(message: Message, model: string = 'default'): number {
225
+ const provider = this.getProviderType(model);
226
+ const overhead = MESSAGE_TOKEN_OVERHEAD[provider] ?? { perMessage: 4, perName: 0 };
227
+
228
+ let count = overhead.perMessage;
229
+
230
+ // Count role tokens
231
+ count += this.countTokens(message.role, model);
232
+
233
+ // Count content tokens
234
+ for (const block of message.content) {
235
+ count += this.countContentBlockTokens(block, model);
236
+ }
237
+
238
+ // Add name overhead if present
239
+ if ((message as any).name) {
240
+ count += overhead.perName + this.countTokens((message as any).name, model);
241
+ }
242
+
243
+ return count;
244
+ }
245
+
246
+ /**
247
+ * Count tokens in a content block
248
+ */
249
+ countContentBlockTokens(block: ContentBlock, model: string = 'default'): number {
250
+ switch (block.type) {
251
+ case 'text':
252
+ return this.countTokens(block.text, model);
253
+
254
+ case 'tool_use':
255
+ // Tool name + input JSON
256
+ return this.countTokens(block.name, model) +
257
+ this.countTokens(JSON.stringify(block.input), model) + 4; // overhead
258
+
259
+ case 'tool_result':
260
+ // Tool ID + content
261
+ const contentTokens = typeof block.content === 'string'
262
+ ? this.countTokens(block.content, model)
263
+ : block.content.reduce((sum, c) => {
264
+ const text = 'text' in c ? c.text : '';
265
+ return sum + this.countTokens(text, model);
266
+ }, 0);
267
+ return this.countTokens(block.tool_use_id, model) + contentTokens + 4;
268
+
269
+ case 'image':
270
+ // Images are counted differently by different providers
271
+ // Approximate based on image size if available
272
+ if (block.source.type === 'base64' && block.source.data) {
273
+ // Rough estimate: 85 tokens for a 512x512 image, scales with pixels
274
+ const base64Length = block.source.data.length;
275
+ return Math.ceil(base64Length / 1000) * 85; // Very rough estimate
276
+ }
277
+ return 85; // Default image token cost
278
+
279
+ case 'thinking':
280
+ return this.countTokens(block.thinking, model);
281
+
282
+ default:
283
+ return 0;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Count tokens in an array of messages
289
+ */
290
+ countMessagesTokens(messages: Message[], model: string = 'default'): number {
291
+ let total = 0;
292
+
293
+ for (const message of messages) {
294
+ total += this.countMessageTokens(message, model);
295
+ }
296
+
297
+ // Add message overhead (every reply is primed with <im_start>assistant)
298
+ const provider = this.getProviderType(model);
299
+ total += MESSAGE_TOKEN_OVERHEAD[provider]?.perMessage || 4;
300
+
301
+ return total;
302
+ }
303
+
304
+ /**
305
+ * Estimate tokens for a system prompt
306
+ */
307
+ countSystemPromptTokens(systemPrompt: string, model: string = 'default'): number {
308
+ // System prompts have slightly higher overhead
309
+ return this.countTokens(systemPrompt, model) + 4;
310
+ }
311
+
312
+ /**
313
+ * Fit content within a token budget
314
+ * Intelligently truncates while preserving structure
315
+ */
316
+ fitWithinBudget(
317
+ content: string,
318
+ budget: number,
319
+ model: string = 'default',
320
+ options: {
321
+ preserveHead?: number; // Percentage of budget for head (default 60%)
322
+ preserveTail?: number; // Percentage of budget for tail (default 30%)
323
+ ellipsis?: string; // Ellipsis string (default "...")
324
+ } = {}
325
+ ): { content: string; tokens: number; truncated: boolean } {
326
+ const currentTokens = this.countTokens(content, model);
327
+
328
+ if (currentTokens <= budget) {
329
+ return { content, tokens: currentTokens, truncated: false };
330
+ }
331
+
332
+ const {
333
+ preserveHead = 0.6,
334
+ preserveTail = 0.3,
335
+ ellipsis = '\n... [truncated] ...\n'
336
+ } = options;
337
+
338
+ const headBudget = Math.floor(budget * preserveHead);
339
+ const tailBudget = Math.floor(budget * preserveTail);
340
+ const ellipsisTokens = this.countTokens(ellipsis, model);
341
+
342
+ // Binary search for head cutoff
343
+ let headEnd = 0;
344
+ let low = 0, high = content.length;
345
+ while (low < high) {
346
+ const mid = Math.floor((low + high) / 2);
347
+ if (this.countTokens(content.slice(0, mid), model) <= headBudget) {
348
+ headEnd = mid;
349
+ low = mid + 1;
350
+ } else {
351
+ high = mid;
352
+ }
353
+ }
354
+
355
+ // Binary search for tail start
356
+ let tailStart = content.length;
357
+ low = 0, high = content.length;
358
+ while (low < high) {
359
+ const mid = Math.floor((low + high) / 2);
360
+ if (this.countTokens(content.slice(mid), model) <= tailBudget) {
361
+ tailStart = mid;
362
+ high = mid;
363
+ } else {
364
+ low = mid + 1;
365
+ }
366
+ }
367
+
368
+ // Construct truncated content
369
+ const truncatedContent = content.slice(0, headEnd) + ellipsis + content.slice(tailStart);
370
+ const finalTokens = this.countTokens(truncatedContent, model);
371
+
372
+ return {
373
+ content: truncatedContent,
374
+ tokens: finalTokens,
375
+ truncated: true
376
+ };
377
+ }
378
+
379
+ /**
380
+ * Calculate context window usage percentage
381
+ */
382
+ calculateUsage(current: number, max: number): number {
383
+ return Math.min(100, Math.round((current / max) * 100));
384
+ }
385
+
386
+ /**
387
+ * Get usage level indicator
388
+ */
389
+ getUsageLevel(percentage: number): 'low' | 'medium' | 'high' | 'critical' {
390
+ if (percentage < 50) return 'low';
391
+ if (percentage < 70) return 'medium';
392
+ if (percentage < 90) return 'high';
393
+ return 'critical';
394
+ }
395
+
396
+ /**
397
+ * Clear cache
398
+ */
399
+ clearCache(): void {
400
+ this.cache.clear();
401
+ }
402
+ }
403
+
404
+ // Export singleton instance
405
+ export const tokenCounter = TokenCounter.getInstance();
406
+
407
+ // Export convenience functions
408
+ export function countTokens(text: string, model?: string): number {
409
+ return tokenCounter.countTokens(text, model);
410
+ }
411
+
412
+ export function countMessagesTokens(messages: Message[], model?: string): number {
413
+ return tokenCounter.countMessagesTokens(messages, model);
414
+ }