erosolar-cli 1.7.14 → 1.7.15

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 (61) hide show
  1. package/dist/core/responseVerifier.d.ts +79 -0
  2. package/dist/core/responseVerifier.d.ts.map +1 -0
  3. package/dist/core/responseVerifier.js +443 -0
  4. package/dist/core/responseVerifier.js.map +1 -0
  5. package/dist/shell/interactiveShell.d.ts +5 -0
  6. package/dist/shell/interactiveShell.d.ts.map +1 -1
  7. package/dist/shell/interactiveShell.js +38 -0
  8. package/dist/shell/interactiveShell.js.map +1 -1
  9. package/dist/ui/ShellUIAdapter.d.ts +3 -0
  10. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  11. package/dist/ui/ShellUIAdapter.js +4 -10
  12. package/dist/ui/ShellUIAdapter.js.map +1 -1
  13. package/dist/ui/persistentPrompt.d.ts +4 -0
  14. package/dist/ui/persistentPrompt.d.ts.map +1 -1
  15. package/dist/ui/persistentPrompt.js +10 -11
  16. package/dist/ui/persistentPrompt.js.map +1 -1
  17. package/package.json +1 -1
  18. package/dist/bin/core/agent.js +0 -362
  19. package/dist/bin/core/agentProfileManifest.js +0 -187
  20. package/dist/bin/core/agentProfiles.js +0 -34
  21. package/dist/bin/core/agentRulebook.js +0 -135
  22. package/dist/bin/core/agentSchemaLoader.js +0 -233
  23. package/dist/bin/core/contextManager.js +0 -412
  24. package/dist/bin/core/contextWindow.js +0 -122
  25. package/dist/bin/core/customCommands.js +0 -80
  26. package/dist/bin/core/errors/apiKeyErrors.js +0 -114
  27. package/dist/bin/core/errors/errorTypes.js +0 -340
  28. package/dist/bin/core/errors/safetyValidator.js +0 -304
  29. package/dist/bin/core/errors.js +0 -32
  30. package/dist/bin/core/modelDiscovery.js +0 -755
  31. package/dist/bin/core/preferences.js +0 -224
  32. package/dist/bin/core/schemaValidator.js +0 -92
  33. package/dist/bin/core/secretStore.js +0 -199
  34. package/dist/bin/core/sessionStore.js +0 -187
  35. package/dist/bin/core/toolRuntime.js +0 -290
  36. package/dist/bin/core/types.js +0 -1
  37. package/dist/bin/shell/bracketedPasteManager.js +0 -350
  38. package/dist/bin/shell/fileChangeTracker.js +0 -65
  39. package/dist/bin/shell/interactiveShell.js +0 -2908
  40. package/dist/bin/shell/liveStatus.js +0 -78
  41. package/dist/bin/shell/shellApp.js +0 -290
  42. package/dist/bin/shell/systemPrompt.js +0 -60
  43. package/dist/bin/shell/updateManager.js +0 -108
  44. package/dist/bin/ui/ShellUIAdapter.js +0 -459
  45. package/dist/bin/ui/UnifiedUIController.js +0 -183
  46. package/dist/bin/ui/animation/AnimationScheduler.js +0 -430
  47. package/dist/bin/ui/codeHighlighter.js +0 -854
  48. package/dist/bin/ui/designSystem.js +0 -121
  49. package/dist/bin/ui/display.js +0 -1222
  50. package/dist/bin/ui/interrupts/InterruptManager.js +0 -437
  51. package/dist/bin/ui/layout.js +0 -139
  52. package/dist/bin/ui/orchestration/StatusOrchestrator.js +0 -403
  53. package/dist/bin/ui/outputMode.js +0 -38
  54. package/dist/bin/ui/persistentPrompt.js +0 -183
  55. package/dist/bin/ui/richText.js +0 -338
  56. package/dist/bin/ui/shortcutsHelp.js +0 -87
  57. package/dist/bin/ui/telemetry/UITelemetry.js +0 -443
  58. package/dist/bin/ui/textHighlighter.js +0 -210
  59. package/dist/bin/ui/theme.js +0 -116
  60. package/dist/bin/ui/toolDisplay.js +0 -423
  61. package/dist/bin/ui/toolDisplayAdapter.js +0 -357
@@ -1,412 +0,0 @@
1
- /**
2
- * ContextManager - Manages conversation context to prevent token limit leaks
3
- *
4
- * Responsibilities:
5
- * - Truncate tool outputs intelligently
6
- * - Prune old conversation history with LLM summarization
7
- * - Track and estimate token usage
8
- * - Keep conversation within budget based on model context windows
9
- * - Proactively shrink context before hitting limits
10
- */
11
- import { calculateContextThresholds } from './contextWindow.js';
12
- /**
13
- * Summarization prompt template
14
- */
15
- export const SUMMARIZATION_PROMPT = `You are a context summarization assistant. Your task is to create a concise but comprehensive summary of the earlier conversation history.
16
-
17
- IMPORTANT GUIDELINES:
18
- 1. Preserve KEY INFORMATION that may be needed for future reference:
19
- - File paths and locations mentioned
20
- - Variable names, function names, class names
21
- - Error messages and their solutions
22
- - User preferences and decisions
23
- - Important code snippets (keep brief)
24
- - Task progress and what was accomplished
25
-
26
- 2. Keep the summary CONCISE but COMPLETE:
27
- - Focus on facts and outcomes, not back-and-forth dialogue
28
- - Use bullet points for clarity
29
- - Group related information together
30
- - Skip pleasantries and redundant confirmations
31
-
32
- 3. Format the summary as:
33
- ## Key Context
34
- - [Important decisions and preferences]
35
-
36
- ## Work Completed
37
- - [What was done, files modified, etc.]
38
-
39
- ## Important Details
40
- - [File paths, function names, error solutions, etc.]
41
-
42
- Summarize the following conversation history:
43
-
44
- {conversation}`;
45
- export class ContextManager {
46
- constructor(config = {}) {
47
- this.config = {
48
- maxTokens: 130000, // Leave room below 131072 limit
49
- targetTokens: 100000, // Target to trigger pruning
50
- maxToolOutputLength: 10000, // 10k chars max per tool output
51
- preserveRecentMessages: 10, // Keep last 10 user/assistant exchanges
52
- estimatedCharsPerToken: 4,
53
- ...config,
54
- };
55
- }
56
- /**
57
- * Truncate tool output intelligently
58
- */
59
- truncateToolOutput(output, toolName) {
60
- const originalLength = output.length;
61
- if (originalLength <= this.config.maxToolOutputLength) {
62
- return {
63
- content: output,
64
- wasTruncated: false,
65
- originalLength,
66
- truncatedLength: originalLength,
67
- };
68
- }
69
- // Intelligent truncation based on tool type
70
- let truncated = this.intelligentTruncate(output, toolName);
71
- const truncatedLength = truncated.length;
72
- return {
73
- content: truncated,
74
- wasTruncated: true,
75
- originalLength,
76
- truncatedLength,
77
- };
78
- }
79
- /**
80
- * Intelligent truncation based on tool type
81
- */
82
- intelligentTruncate(output, toolName) {
83
- const maxLength = this.config.maxToolOutputLength;
84
- // For file reads, show beginning and end
85
- if (toolName === 'Read' || toolName === 'read_file') {
86
- return this.truncateFileOutput(output, maxLength);
87
- }
88
- // For search results, keep first N results
89
- if (toolName === 'Grep' || toolName === 'grep_search' || toolName === 'Glob') {
90
- return this.truncateSearchOutput(output, maxLength);
91
- }
92
- // For bash/command output, keep end (usually most relevant)
93
- if (toolName === 'Bash' || toolName === 'bash' || toolName === 'execute_bash') {
94
- return this.truncateBashOutput(output, maxLength);
95
- }
96
- // Default: show beginning with truncation notice
97
- return this.truncateDefault(output, maxLength);
98
- }
99
- truncateFileOutput(output, maxLength) {
100
- const lines = output.split('\n');
101
- if (lines.length <= 100) {
102
- // For small files, just truncate text
103
- return this.truncateDefault(output, maxLength);
104
- }
105
- // Show first 50 and last 50 lines
106
- const keepLines = Math.floor(maxLength / 100); // Rough estimate
107
- const headLines = lines.slice(0, keepLines);
108
- const tailLines = lines.slice(-keepLines);
109
- const truncatedCount = lines.length - (keepLines * 2);
110
- return [
111
- ...headLines,
112
- `\n... [${truncatedCount} lines truncated for context management] ...\n`,
113
- ...tailLines,
114
- ].join('\n');
115
- }
116
- truncateSearchOutput(output, maxLength) {
117
- const lines = output.split('\n');
118
- const keepLines = Math.floor(maxLength / 80); // Rough average line length
119
- if (lines.length <= keepLines) {
120
- return output;
121
- }
122
- const truncatedCount = lines.length - keepLines;
123
- return [
124
- ...lines.slice(0, keepLines),
125
- `\n... [${truncatedCount} more results truncated for context management] ...`,
126
- ].join('\n');
127
- }
128
- truncateBashOutput(output, maxLength) {
129
- if (output.length <= maxLength) {
130
- return output;
131
- }
132
- // For command output, the end is usually most important (errors, final status)
133
- const keepChars = Math.floor(maxLength * 0.8); // 80% at end
134
- const prefixChars = maxLength - keepChars - 100; // Small prefix
135
- const prefix = output.slice(0, prefixChars);
136
- const suffix = output.slice(-keepChars);
137
- const truncatedChars = output.length - prefixChars - keepChars;
138
- return `${prefix}\n\n... [${truncatedChars} characters truncated for context management] ...\n\n${suffix}`;
139
- }
140
- truncateDefault(output, maxLength) {
141
- if (output.length <= maxLength) {
142
- return output;
143
- }
144
- const truncatedChars = output.length - maxLength + 100; // Account for notice
145
- return `${output.slice(0, maxLength - 100)}\n\n... [${truncatedChars} characters truncated for context management] ...`;
146
- }
147
- /**
148
- * Estimate tokens in a message
149
- */
150
- estimateTokens(message) {
151
- let charCount = 0;
152
- if (message.content) {
153
- charCount += message.content.length;
154
- }
155
- if (message.role === 'assistant' && message.toolCalls) {
156
- // Tool calls add overhead
157
- for (const call of message.toolCalls) {
158
- charCount += call.name.length;
159
- charCount += JSON.stringify(call.arguments).length;
160
- }
161
- }
162
- return Math.ceil(charCount / this.config.estimatedCharsPerToken);
163
- }
164
- /**
165
- * Estimate total tokens in conversation
166
- */
167
- estimateTotalTokens(messages) {
168
- return messages.reduce((sum, msg) => sum + this.estimateTokens(msg), 0);
169
- }
170
- /**
171
- * Prune old messages when approaching limit
172
- *
173
- * Synchronously removes old messages to stay within budget.
174
- * If LLM summarization is available and enabled, this method will be async.
175
- */
176
- pruneMessages(messages) {
177
- const totalTokens = this.estimateTotalTokens(messages);
178
- // Only prune if we're above target
179
- if (totalTokens < this.config.targetTokens) {
180
- return { pruned: messages, removed: 0 };
181
- }
182
- // Always keep system message (first)
183
- const firstMessage = messages[0];
184
- const systemMessage = firstMessage?.role === 'system' ? firstMessage : null;
185
- const conversationMessages = systemMessage ? messages.slice(1) : messages;
186
- // Keep recent messages based on preserveRecentMessages
187
- // Count user/assistant pairs
188
- const recentMessages = [];
189
- let exchangeCount = 0;
190
- for (let i = conversationMessages.length - 1; i >= 0; i--) {
191
- const msg = conversationMessages[i];
192
- if (!msg)
193
- continue;
194
- recentMessages.unshift(msg);
195
- if (msg.role === 'user') {
196
- exchangeCount++;
197
- if (exchangeCount >= this.config.preserveRecentMessages) {
198
- break;
199
- }
200
- }
201
- }
202
- // Build pruned message list
203
- const pruned = [];
204
- if (systemMessage) {
205
- pruned.push(systemMessage);
206
- }
207
- // Add a context summary message if we removed messages
208
- const removedCount = conversationMessages.length - recentMessages.length;
209
- if (removedCount > 0) {
210
- pruned.push({
211
- role: 'system',
212
- content: `[Context Manager: Removed ${removedCount} old messages to stay within token budget. Recent conversation history preserved.]`,
213
- });
214
- }
215
- pruned.push(...recentMessages);
216
- return {
217
- pruned,
218
- removed: removedCount,
219
- };
220
- }
221
- /**
222
- * Prune messages with LLM-based summarization
223
- *
224
- * This is an async version that uses the LLM to create intelligent summaries
225
- * instead of just removing old messages. Should be called BEFORE generation.
226
- */
227
- async pruneMessagesWithSummary(messages) {
228
- const totalTokens = this.estimateTotalTokens(messages);
229
- // Only prune if we're above target
230
- if (totalTokens < this.config.targetTokens) {
231
- return { pruned: messages, removed: 0, summarized: false };
232
- }
233
- // If no summarization callback or disabled, fall back to simple pruning
234
- if (!this.config.summarizationCallback || !this.config.useLLMSummarization) {
235
- const result = this.pruneMessages(messages);
236
- return { ...result, summarized: false };
237
- }
238
- // Partition messages
239
- const firstMessage = messages[0];
240
- const systemMessage = firstMessage?.role === 'system' ? firstMessage : null;
241
- const conversationMessages = systemMessage ? messages.slice(1) : messages;
242
- // Keep recent messages
243
- const recentMessages = [];
244
- let exchangeCount = 0;
245
- for (let i = conversationMessages.length - 1; i >= 0; i--) {
246
- const msg = conversationMessages[i];
247
- if (!msg)
248
- continue;
249
- recentMessages.unshift(msg);
250
- if (msg.role === 'user') {
251
- exchangeCount++;
252
- if (exchangeCount >= this.config.preserveRecentMessages) {
253
- break;
254
- }
255
- }
256
- }
257
- const toSummarize = conversationMessages.slice(0, conversationMessages.length - recentMessages.length);
258
- // If nothing to summarize, return as-is
259
- if (toSummarize.length === 0) {
260
- return { pruned: messages, removed: 0, summarized: false };
261
- }
262
- try {
263
- // Call the LLM to summarize old messages
264
- const summary = await this.config.summarizationCallback(toSummarize);
265
- // Build pruned message list with summary
266
- const pruned = [];
267
- if (systemMessage) {
268
- pruned.push(systemMessage);
269
- }
270
- // Add intelligent summary
271
- pruned.push({
272
- role: 'system',
273
- content: [
274
- '=== Context Summary (Auto-generated) ===',
275
- summary.trim(),
276
- '',
277
- `[Summarized ${toSummarize.length} earlier messages. Recent ${recentMessages.length} messages preserved below.]`,
278
- ].join('\n'),
279
- });
280
- pruned.push(...recentMessages);
281
- return {
282
- pruned,
283
- removed: toSummarize.length,
284
- summarized: true,
285
- };
286
- }
287
- catch (error) {
288
- // If summarization fails, fall back to simple pruning
289
- const result = this.pruneMessages(messages);
290
- return { ...result, summarized: false };
291
- }
292
- }
293
- /**
294
- * Check if we're approaching the limit
295
- */
296
- isApproachingLimit(messages) {
297
- const totalTokens = this.estimateTotalTokens(messages);
298
- return totalTokens >= this.config.targetTokens;
299
- }
300
- /**
301
- * Get warning level for current context usage
302
- * Returns: null (no warning), 'info' (<70%), 'warning' (70-90%), 'danger' (>90%)
303
- */
304
- getWarningLevel(messages) {
305
- const totalTokens = this.estimateTotalTokens(messages);
306
- const percentage = (totalTokens / this.config.maxTokens) * 100;
307
- if (percentage > 90) {
308
- return 'danger';
309
- }
310
- else if (percentage > 70) {
311
- return 'warning';
312
- }
313
- else if (percentage > 50) {
314
- return 'info';
315
- }
316
- return null;
317
- }
318
- /**
319
- * Get a human-readable warning message
320
- */
321
- getWarningMessage(messages) {
322
- const stats = this.getStats(messages);
323
- const warningLevel = this.getWarningLevel(messages);
324
- if (warningLevel === 'danger') {
325
- return `⚠️ Context usage critical (${stats.percentage}%). Consider starting a new session or the next request may fail.`;
326
- }
327
- else if (warningLevel === 'warning') {
328
- return `Context usage high (${stats.percentage}%). Automatic cleanup will occur soon.`;
329
- }
330
- return null;
331
- }
332
- /**
333
- * Get context stats
334
- */
335
- getStats(messages) {
336
- const totalTokens = this.estimateTotalTokens(messages);
337
- const percentage = Math.round((totalTokens / this.config.maxTokens) * 100);
338
- return {
339
- totalTokens,
340
- percentage,
341
- isOverLimit: totalTokens >= this.config.maxTokens,
342
- isApproachingLimit: totalTokens >= this.config.targetTokens,
343
- };
344
- }
345
- /**
346
- * Update configuration
347
- */
348
- updateConfig(config) {
349
- this.config = { ...this.config, ...config };
350
- }
351
- }
352
- /**
353
- * Create a default context manager instance with model-aware limits
354
- */
355
- export function createDefaultContextManager(overrides, model) {
356
- // Get model-specific thresholds
357
- const thresholds = calculateContextThresholds(model);
358
- return new ContextManager({
359
- maxTokens: thresholds.maxTokens,
360
- targetTokens: thresholds.targetTokens, // Start pruning at 60%
361
- warningTokens: thresholds.warningTokens, // Warn at 50%
362
- criticalTokens: thresholds.criticalTokens, // Critical at 75%
363
- maxToolOutputLength: 5000, // 5k chars max per tool (reduced for safety)
364
- preserveRecentMessages: 5, // Keep last 5 exchanges
365
- estimatedCharsPerToken: 3.5, // More aggressive estimate (accounts for special tokens, JSON overhead)
366
- useLLMSummarization: true, // Enable LLM summarization by default
367
- model,
368
- ...overrides,
369
- });
370
- }
371
- /**
372
- * Format conversation messages into readable text for summarization
373
- */
374
- export function formatMessagesForSummary(messages) {
375
- const lines = [];
376
- for (const msg of messages) {
377
- if (msg.role === 'user') {
378
- lines.push(`USER: ${msg.content}`);
379
- }
380
- else if (msg.role === 'assistant') {
381
- let content = msg.content || '';
382
- if (msg.toolCalls && msg.toolCalls.length > 0) {
383
- const toolNames = msg.toolCalls.map(tc => tc.name);
384
- content += ` [Called tools: ${toolNames.join(', ')}]`;
385
- }
386
- lines.push(`ASSISTANT: ${content}`);
387
- }
388
- else if (msg.role === 'tool') {
389
- // Truncate long tool outputs for summarization
390
- const output = msg.content.length > 500
391
- ? msg.content.slice(0, 500) + '...'
392
- : msg.content;
393
- lines.push(`TOOL (${msg.name}): ${output}`);
394
- }
395
- // Skip system messages in summary input
396
- }
397
- return lines.join('\n\n');
398
- }
399
- /**
400
- * Create a summarization callback using the given provider
401
- */
402
- export function createSummarizationCallback(provider) {
403
- return async (messages) => {
404
- // Format messages into readable conversation
405
- const conversationText = formatMessagesForSummary(messages);
406
- // Create summarization prompt
407
- const prompt = SUMMARIZATION_PROMPT.replace('{conversation}', conversationText);
408
- // Call provider to generate summary (no tools needed)
409
- const response = await provider.generate([{ role: 'user', content: prompt }], []);
410
- return response.content || '';
411
- };
412
- }
@@ -1,122 +0,0 @@
1
- /**
2
- * Model Context Window Management
3
- *
4
- * Maps models to their context window sizes and provides utilities
5
- * for dynamic context limit configuration.
6
- */
7
- const MODEL_CONTEXT_WINDOWS = [
8
- // OpenAI GPT-5.x series (200K context)
9
- { pattern: /^gpt-5\.1-?codex/i, contextWindow: 200000, targetTokens: 140000 },
10
- { pattern: /^gpt-5(?:\.1|-?pro|-?mini|-?nano|-?max)/i, contextWindow: 200000, targetTokens: 140000 },
11
- { pattern: /^gpt-5$/i, contextWindow: 200000, targetTokens: 140000 },
12
- // OpenAI GPT-4o series (128K context)
13
- { pattern: /^gpt-4o/i, contextWindow: 128000, targetTokens: 90000 },
14
- { pattern: /^gpt-4-turbo/i, contextWindow: 128000, targetTokens: 90000 },
15
- { pattern: /^gpt-4-(?:0125|1106)/i, contextWindow: 128000, targetTokens: 90000 },
16
- // OpenAI GPT-4 (8K-32K context)
17
- { pattern: /^gpt-4-32k/i, contextWindow: 32000, targetTokens: 24000 },
18
- { pattern: /^gpt-4(?![o-])/i, contextWindow: 8192, targetTokens: 6000 },
19
- // OpenAI o1/o3 reasoning models (200K context)
20
- { pattern: /^o1(?:-pro|-mini)?/i, contextWindow: 200000, targetTokens: 140000 },
21
- { pattern: /^o3(?:-pro|-mini)?/i, contextWindow: 200000, targetTokens: 140000 },
22
- // OpenAI GPT-3.5 (16K context)
23
- { pattern: /^gpt-3\.5-turbo-16k/i, contextWindow: 16000, targetTokens: 12000 },
24
- { pattern: /^gpt-3\.5/i, contextWindow: 4096, targetTokens: 3000 },
25
- // Anthropic Claude 4.x series (200K context)
26
- { pattern: /^claude-(?:sonnet|opus|haiku)-4/i, contextWindow: 200000, targetTokens: 140000 },
27
- { pattern: /^(?:sonnet|opus|haiku)-4/i, contextWindow: 200000, targetTokens: 140000 },
28
- // Anthropic Claude 3.x series (200K context)
29
- { pattern: /^claude-3/i, contextWindow: 200000, targetTokens: 140000 },
30
- // Anthropic Claude 2.x (100K context)
31
- { pattern: /^claude-2/i, contextWindow: 100000, targetTokens: 70000 },
32
- // Google Gemini Pro (1M+ context, capped at 200K for safety)
33
- { pattern: /^gemini-(?:1\.5|2\.0)-(?:pro|flash)/i, contextWindow: 1000000, targetTokens: 200000 },
34
- { pattern: /^gemini-pro/i, contextWindow: 32000, targetTokens: 24000 },
35
- // DeepSeek (64K-128K context)
36
- { pattern: /^deepseek-(?:chat|coder|reasoner)/i, contextWindow: 128000, targetTokens: 90000 },
37
- { pattern: /^deepseek/i, contextWindow: 64000, targetTokens: 45000 },
38
- // xAI Grok (128K context)
39
- { pattern: /^grok/i, contextWindow: 128000, targetTokens: 90000 },
40
- // Ollama/Local models
41
- { pattern: /^llama-?3\.?[12]?:?(?:70b|405b)/i, contextWindow: 128000, targetTokens: 90000 },
42
- { pattern: /^llama-?3/i, contextWindow: 8192, targetTokens: 6000 },
43
- { pattern: /^llama-?2/i, contextWindow: 4096, targetTokens: 3000 },
44
- { pattern: /^mistral/i, contextWindow: 32000, targetTokens: 24000 },
45
- { pattern: /^mixtral/i, contextWindow: 32000, targetTokens: 24000 },
46
- { pattern: /^codellama/i, contextWindow: 16000, targetTokens: 12000 },
47
- { pattern: /^qwen/i, contextWindow: 32000, targetTokens: 24000 },
48
- { pattern: /^phi/i, contextWindow: 4096, targetTokens: 3000 },
49
- ];
50
- // Default fallback values
51
- const DEFAULT_CONTEXT_WINDOW = 128000;
52
- const DEFAULT_TARGET_TOKENS = 90000;
53
- /**
54
- * Get context window information for a model.
55
- */
56
- export function getModelContextInfo(model) {
57
- if (!model) {
58
- return {
59
- model: 'unknown',
60
- contextWindow: DEFAULT_CONTEXT_WINDOW,
61
- targetTokens: DEFAULT_TARGET_TOKENS,
62
- isDefault: true,
63
- };
64
- }
65
- const normalized = model.trim();
66
- for (const entry of MODEL_CONTEXT_WINDOWS) {
67
- if (entry.pattern.test(normalized)) {
68
- return {
69
- model,
70
- contextWindow: entry.contextWindow,
71
- targetTokens: entry.targetTokens,
72
- isDefault: false,
73
- };
74
- }
75
- }
76
- return {
77
- model,
78
- contextWindow: DEFAULT_CONTEXT_WINDOW,
79
- targetTokens: DEFAULT_TARGET_TOKENS,
80
- isDefault: true,
81
- };
82
- }
83
- /**
84
- * Returns the approximate context window (in tokens) for the provided model id.
85
- * Falls back to null when the model is unknown so callers can handle gracefully.
86
- */
87
- export function getContextWindowTokens(model) {
88
- if (!model) {
89
- return null;
90
- }
91
- const info = getModelContextInfo(model);
92
- return info.isDefault ? null : info.contextWindow;
93
- }
94
- /**
95
- * Get safe target token count for a model.
96
- * This is the threshold at which context pruning should begin.
97
- */
98
- export function getSafeTargetTokens(model) {
99
- const info = getModelContextInfo(model);
100
- return info.targetTokens;
101
- }
102
- /**
103
- * Calculate all context management thresholds for a model.
104
- *
105
- * Thresholds are set conservatively to prevent context overflow errors:
106
- * - targetTokens: Start proactive pruning at 60% to leave ample room
107
- * - warningTokens: Show warning at 50% so user is aware
108
- * - criticalTokens: Aggressive pruning at 75%
109
- * - safetyBuffer: Reserve 5% for API overhead and response tokens
110
- */
111
- export function calculateContextThresholds(model) {
112
- const info = getModelContextInfo(model);
113
- const contextWindow = info.contextWindow;
114
- // Apply 5% safety buffer to account for API overhead
115
- const effectiveMax = Math.floor(contextWindow * 0.95);
116
- return {
117
- maxTokens: effectiveMax,
118
- targetTokens: Math.floor(contextWindow * 0.60), // Start pruning at 60% (more aggressive)
119
- warningTokens: Math.floor(contextWindow * 0.50), // Warn at 50%
120
- criticalTokens: Math.floor(contextWindow * 0.75), // Critical at 75%
121
- };
122
- }
@@ -1,80 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { homedir } from 'node:os';
4
- const dataRoot = process.env['EROSOLAR_DATA_DIR']?.trim() || join(homedir(), '.erosolar');
5
- const defaultCommandsDir = process.env['EROSOLAR_COMMANDS_DIR']?.trim() || join(dataRoot, 'commands');
6
- export function loadCustomSlashCommands(dir = defaultCommandsDir) {
7
- if (!existsSync(dir)) {
8
- return [];
9
- }
10
- const files = readdirSync(dir).filter((file) => file.toLowerCase().endsWith('.json'));
11
- const commands = [];
12
- const seen = new Set();
13
- for (const file of files) {
14
- const filePath = join(dir, file);
15
- try {
16
- const raw = readFileSync(filePath, 'utf8');
17
- const parsed = JSON.parse(raw);
18
- if (Array.isArray(parsed)) {
19
- for (const entry of parsed) {
20
- const cmd = normalizeCommand(entry, filePath);
21
- if (cmd && !seen.has(cmd.command)) {
22
- seen.add(cmd.command);
23
- commands.push(cmd);
24
- }
25
- }
26
- }
27
- else {
28
- const cmd = normalizeCommand(parsed, filePath);
29
- if (cmd && !seen.has(cmd.command)) {
30
- seen.add(cmd.command);
31
- commands.push(cmd);
32
- }
33
- }
34
- }
35
- catch (error) {
36
- console.warn(`[custom commands] Failed to load ${filePath}:`, error);
37
- }
38
- }
39
- return commands;
40
- }
41
- export function buildCustomCommandPrompt(command, input, context) {
42
- const replacements = {
43
- input,
44
- workspace: context.workspace,
45
- profile: context.profile,
46
- provider: context.provider,
47
- model: context.model,
48
- };
49
- return command.template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => {
50
- const normalized = key.toLowerCase();
51
- return replacements[normalized] ?? replacements[key] ?? '';
52
- });
53
- }
54
- function normalizeCommand(entry, source) {
55
- if (!entry || typeof entry !== 'object') {
56
- return null;
57
- }
58
- const record = entry;
59
- const rawCommandValue = record['command'];
60
- const rawCommand = typeof rawCommandValue === 'string' ? rawCommandValue.trim() : '';
61
- const command = rawCommand ? (rawCommand.startsWith('/') ? rawCommand : `/${rawCommand}`) : '';
62
- const descriptionValue = record['description'];
63
- const description = typeof descriptionValue === 'string' && descriptionValue.trim()
64
- ? descriptionValue.trim()
65
- : 'Custom command';
66
- const templateEntry = record['template'] ?? record['prompt'];
67
- const templateValue = typeof templateEntry === 'string' ? templateEntry : '';
68
- if (!command || !templateValue.trim()) {
69
- return null;
70
- }
71
- const requireInputEntry = record['requireInput'] ?? record['inputRequired'];
72
- const requireInput = typeof requireInputEntry === 'boolean' ? requireInputEntry : false;
73
- return {
74
- command,
75
- description,
76
- template: templateValue,
77
- requireInput,
78
- source,
79
- };
80
- }