vg-coder-cli 2.0.24 → 2.0.26
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.
- package/.vg/tree-state.json +1 -7
- package/package.json +1 -1
- package/src/index.js +36 -0
- package/src/server/api-server.js +241 -29
- package/src/server/project-manager.js +353 -0
- package/src/server/terminal-manager.js +130 -3
- package/src/server/views/css/iframe.css +1 -0
- package/src/server/views/css/terminal.css +88 -0
- package/src/server/views/dashboard.css +391 -16
- package/src/server/views/dashboard.html +150 -19
- package/src/server/views/js/features/commands.js +230 -0
- package/src/server/views/js/features/iframe-manager.js +6 -2
- package/src/server/views/js/features/project-switcher.js +153 -0
- package/src/server/views/js/features/terminal.js +280 -8
- package/src/server/views/js/main.js +65 -0
- package/src/server/views/js/utils/log-utils.js +164 -0
- package/src/server/views/js/utils/smart-copy-engine.js +283 -0
- package/src/server/views/vg-coder/controller.js +376 -2
- package/vetgo-auto/chrome/src/controller.ts +75 -1
- package/vetgo-auto/chrome/src/utils/ai-domains.ts +33 -0
- package/vetgo-auto/chrome/src/utils/injector-script.ts +251 -0
- package/vetgo-auto/vg-coder.zip +0 -0
- package/vg-coder-cli-2.0.26.tgz +0 -0
- package/vg-coder-cli-2.0.23.tgz +0 -0
- package/vg-coder-cli-2.0.24.tgz +0 -0
|
@@ -46,12 +46,34 @@ export function createNewTerminal() {
|
|
|
46
46
|
wrapper.style.left = `${400 + offset}px`;
|
|
47
47
|
wrapper.style.zIndex = ++maxZIndex;
|
|
48
48
|
|
|
49
|
-
// HTML Template - Đã thêm
|
|
49
|
+
// HTML Template - Đã thêm Copy Buttons
|
|
50
50
|
wrapper.innerHTML = `
|
|
51
51
|
<div class="terminal-header" id="header-${termId}" ondblclick="window.toggleMinimize('${termId}')">
|
|
52
52
|
<div class="terminal-title-group">
|
|
53
53
|
<span>>_</span> Terminal (${activeTerminals.size + 1})
|
|
54
54
|
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Copy Button Group -->
|
|
57
|
+
<div class="terminal-copy-group" id="copy-group-${termId}">
|
|
58
|
+
<button class="copy-btn copy-smart" onclick="window.copyTerminalLog('${termId}', 'smart')" title="Smart Copy (optimized for 3000 tokens)">
|
|
59
|
+
🧠 <span class="token-badge" id="badge-smart-${termId}">0</span>
|
|
60
|
+
</button>
|
|
61
|
+
<button class="copy-btn copy-errors" onclick="window.copyTerminalLog('${termId}', 'errors')" title="Errors Only (with context)">
|
|
62
|
+
⚠️ <span class="token-badge" id="badge-errors-${termId}">0</span>
|
|
63
|
+
</button>
|
|
64
|
+
<button class="copy-btn copy-recent" onclick="window.copyTerminalLog('${termId}', 'recent')" title="Recent 200 lines">
|
|
65
|
+
📄 <span class="token-badge" id="badge-recent-${termId}">0</span>
|
|
66
|
+
</button>
|
|
67
|
+
<button class="copy-btn copy-all" onclick="window.copyTerminalLog('${termId}', 'all')" title="Copy All">
|
|
68
|
+
📦 <span class="token-badge" id="badge-all-${termId}">0</span>
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<!-- Clear Button -->
|
|
73
|
+
<button class="term-btn-clear" onclick="window.clearTerminal('${termId}')" title="Clear Terminal">
|
|
74
|
+
🗑️
|
|
75
|
+
</button>
|
|
76
|
+
|
|
55
77
|
<div class="terminal-controls">
|
|
56
78
|
<button class="term-btn minimize" onclick="window.toggleMinimize('${termId}')" title="Minimize/Restore">-</button>
|
|
57
79
|
<button class="term-btn maximize" onclick="window.toggleMaximize('${termId}')" title="Maximize">+</button>
|
|
@@ -111,15 +133,90 @@ export function createNewTerminal() {
|
|
|
111
133
|
// 4. Make Draggable
|
|
112
134
|
makeDraggable(wrapper, document.getElementById(`header-${termId}`));
|
|
113
135
|
|
|
114
|
-
// 5. Store Session
|
|
115
|
-
activeTerminals.set(termId, {
|
|
136
|
+
// 5. Store Session with log buffer
|
|
137
|
+
activeTerminals.set(termId, {
|
|
138
|
+
term,
|
|
139
|
+
fitAddon,
|
|
140
|
+
element: wrapper,
|
|
141
|
+
logBuffer: [], // Local buffer for quick access
|
|
142
|
+
partialLine: '' // Buffer for incomplete lines
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// 6. Setup log buffer capture and token count updates
|
|
146
|
+
// Listen to terminal's onData event (this fires for user input)
|
|
147
|
+
// We need to listen to the actual output from the backend
|
|
148
|
+
socket.on('terminal:data', ({ termId: dataTermId, data }) => {
|
|
149
|
+
if (dataTermId !== termId) return;
|
|
150
|
+
|
|
151
|
+
const session = activeTerminals.get(termId);
|
|
152
|
+
if (!session) return;
|
|
116
153
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
154
|
+
// Strip ANSI codes
|
|
155
|
+
const cleanData = data.replace(
|
|
156
|
+
// eslint-disable-next-line no-control-regex
|
|
157
|
+
/\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b\][^\x1b]*\x1b\\/g,
|
|
158
|
+
''
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Accumulate partial line
|
|
162
|
+
session.partialLine += cleanData;
|
|
163
|
+
|
|
164
|
+
// Split by newlines and process complete lines
|
|
165
|
+
const lines = session.partialLine.split(/\r?\n/);
|
|
166
|
+
|
|
167
|
+
// Keep the last incomplete line in partialLine
|
|
168
|
+
session.partialLine = lines.pop() || '';
|
|
169
|
+
|
|
170
|
+
// Add complete lines to buffer
|
|
171
|
+
lines.forEach(line => {
|
|
172
|
+
if (line.trim().length > 0) {
|
|
173
|
+
session.logBuffer.push(line);
|
|
174
|
+
// Maintain max 10000 lines
|
|
175
|
+
if (session.logBuffer.length > 10000) {
|
|
176
|
+
session.logBuffer.shift();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Update token counts (debounced)
|
|
182
|
+
if (lines.length > 0) {
|
|
183
|
+
updateTokenCounts(termId);
|
|
184
|
+
}
|
|
122
185
|
});
|
|
186
|
+
|
|
187
|
+
// 7. Get current project ID from API
|
|
188
|
+
let currentProjectId = null;
|
|
189
|
+
fetch('/api/projects')
|
|
190
|
+
.then(res => res.json())
|
|
191
|
+
.then(data => {
|
|
192
|
+
currentProjectId = data.activeProjectId;
|
|
193
|
+
|
|
194
|
+
// Store in session
|
|
195
|
+
activeTerminals.get(termId).projectId = currentProjectId;
|
|
196
|
+
|
|
197
|
+
// Init Backend Process with projectId
|
|
198
|
+
socket.emit('terminal:init', {
|
|
199
|
+
termId,
|
|
200
|
+
cols: term.cols,
|
|
201
|
+
rows: term.rows,
|
|
202
|
+
projectId: currentProjectId
|
|
203
|
+
});
|
|
204
|
+
})
|
|
205
|
+
.catch(err => {
|
|
206
|
+
console.error('Failed to get project info:', err);
|
|
207
|
+
// Fallback without projectId
|
|
208
|
+
socket.emit('terminal:init', {
|
|
209
|
+
termId,
|
|
210
|
+
cols: term.cols,
|
|
211
|
+
rows: term.rows
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// 8. Initial token count update
|
|
216
|
+
setTimeout(() => updateTokenCounts(termId), 100);
|
|
217
|
+
|
|
218
|
+
// Return termId so caller can use it
|
|
219
|
+
return termId;
|
|
123
220
|
}
|
|
124
221
|
|
|
125
222
|
/**
|
|
@@ -245,8 +342,183 @@ function makeDraggable(element, handle) {
|
|
|
245
342
|
}
|
|
246
343
|
}
|
|
247
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Copy terminal log with specified strategy
|
|
347
|
+
* @param {string} termId - Terminal ID
|
|
348
|
+
* @param {string} strategy - 'smart' | 'errors' | 'recent' | 'all'
|
|
349
|
+
*/
|
|
350
|
+
async function copyTerminalLog(termId, strategy) {
|
|
351
|
+
const session = activeTerminals.get(termId);
|
|
352
|
+
if (!session || !session.logBuffer || session.logBuffer.length === 0) {
|
|
353
|
+
showToast('⚠️ No logs to copy', 'warning');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
// Use SmartCopyEngine to generate content
|
|
359
|
+
let result;
|
|
360
|
+
const lines = session.logBuffer;
|
|
361
|
+
|
|
362
|
+
switch (strategy) {
|
|
363
|
+
case 'smart':
|
|
364
|
+
result = window.SmartCopyEngine.generateSmartCopy(lines, 3000);
|
|
365
|
+
break;
|
|
366
|
+
case 'errors':
|
|
367
|
+
result = window.SmartCopyEngine.generateErrorsOnly(lines);
|
|
368
|
+
break;
|
|
369
|
+
case 'recent':
|
|
370
|
+
result = window.SmartCopyEngine.generateRecent(lines, 200);
|
|
371
|
+
break;
|
|
372
|
+
case 'all':
|
|
373
|
+
result = window.SmartCopyEngine.generateCopyAll(lines);
|
|
374
|
+
break;
|
|
375
|
+
default:
|
|
376
|
+
result = window.SmartCopyEngine.generateSmartCopy(lines, 3000);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Copy to clipboard
|
|
380
|
+
await navigator.clipboard.writeText(result.content);
|
|
381
|
+
|
|
382
|
+
// Show success toast with details
|
|
383
|
+
const strategyNames = {
|
|
384
|
+
smart: '🧠 Smart',
|
|
385
|
+
errors: '⚠️ Errors',
|
|
386
|
+
recent: '📄 Recent',
|
|
387
|
+
all: '📦 All'
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
let message = `✅ Copied ${result.tokens} tokens (${strategyNames[strategy]})`;
|
|
391
|
+
|
|
392
|
+
if (result.stats && result.stats.message) {
|
|
393
|
+
message += `\n${result.stats.message}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (result.warning) {
|
|
397
|
+
message += `\n${result.warning}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
showToast(message, 'success');
|
|
401
|
+
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error('Copy failed:', error);
|
|
404
|
+
showToast('❌ Failed to copy logs', 'error');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Update token count badges for a terminal
|
|
410
|
+
* Debounced to avoid excessive updates
|
|
411
|
+
*/
|
|
412
|
+
let updateTokenCountsTimeout = null;
|
|
413
|
+
function updateTokenCounts(termId) {
|
|
414
|
+
// Debounce updates
|
|
415
|
+
clearTimeout(updateTokenCountsTimeout);
|
|
416
|
+
updateTokenCountsTimeout = setTimeout(() => {
|
|
417
|
+
const session = activeTerminals.get(termId);
|
|
418
|
+
if (!session || !session.logBuffer) return;
|
|
419
|
+
|
|
420
|
+
const analysis = window.SmartCopyEngine.analyzeLogBuffer(session.logBuffer);
|
|
421
|
+
|
|
422
|
+
// Update badges
|
|
423
|
+
updateBadge(`badge-smart-${termId}`, analysis.smart.tokens);
|
|
424
|
+
updateBadge(`badge-errors-${termId}`, analysis.errors.tokens);
|
|
425
|
+
updateBadge(`badge-recent-${termId}`, analysis.recent.tokens);
|
|
426
|
+
updateBadge(`badge-all-${termId}`, analysis.all.tokens);
|
|
427
|
+
}, 500); // 500ms debounce
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Update a single badge element
|
|
432
|
+
*/
|
|
433
|
+
function updateBadge(badgeId, tokens) {
|
|
434
|
+
const badge = document.getElementById(badgeId);
|
|
435
|
+
if (badge) {
|
|
436
|
+
badge.textContent = formatTokenCount(tokens);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Format token count for display
|
|
442
|
+
*/
|
|
443
|
+
function formatTokenCount(tokens) {
|
|
444
|
+
if (tokens === 0) return '0';
|
|
445
|
+
if (tokens < 1000) return tokens.toString();
|
|
446
|
+
if (tokens < 10000) return (tokens / 1000).toFixed(1) + 'k';
|
|
447
|
+
return Math.floor(tokens / 1000) + 'k';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Show toast notification
|
|
452
|
+
*/
|
|
453
|
+
function showToast(message, type = 'info') {
|
|
454
|
+
const toast = document.getElementById('toast');
|
|
455
|
+
if (!toast) return;
|
|
456
|
+
|
|
457
|
+
toast.textContent = message;
|
|
458
|
+
toast.className = 'toast show';
|
|
459
|
+
|
|
460
|
+
if (type === 'success') {
|
|
461
|
+
toast.style.background = '#28a745';
|
|
462
|
+
} else if (type === 'error') {
|
|
463
|
+
toast.style.background = '#dc3545';
|
|
464
|
+
} else if (type === 'warning') {
|
|
465
|
+
toast.style.background = '#ffc107';
|
|
466
|
+
toast.style.color = '#000';
|
|
467
|
+
} else {
|
|
468
|
+
toast.style.background = '#007bff';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
setTimeout(() => {
|
|
472
|
+
toast.classList.remove('show');
|
|
473
|
+
}, 3000);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Clear terminal display and log buffer
|
|
478
|
+
* @param {string} termId - Terminal ID
|
|
479
|
+
*/
|
|
480
|
+
function clearTerminal(termId) {
|
|
481
|
+
const session = activeTerminals.get(termId);
|
|
482
|
+
if (!session) return;
|
|
483
|
+
|
|
484
|
+
// Clear the xterm display
|
|
485
|
+
session.term.clear();
|
|
486
|
+
|
|
487
|
+
// Clear the log buffer
|
|
488
|
+
session.logBuffer = [];
|
|
489
|
+
session.partialLine = '';
|
|
490
|
+
|
|
491
|
+
// Reset token counts to 0
|
|
492
|
+
updateBadge(`badge-smart-${termId}`, 0);
|
|
493
|
+
updateBadge(`badge-errors-${termId}`, 0);
|
|
494
|
+
updateBadge(`badge-recent-${termId}`, 0);
|
|
495
|
+
updateBadge(`badge-all-${termId}`, 0);
|
|
496
|
+
|
|
497
|
+
// Show toast notification
|
|
498
|
+
showToast('🗑️ Terminal cleared', 'info');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Update terminal visibility based on active project
|
|
503
|
+
* @param {string} activeProjectId - Active project ID
|
|
504
|
+
*/
|
|
505
|
+
function updateTerminalVisibility(activeProjectId) {
|
|
506
|
+
activeTerminals.forEach((session, termId) => {
|
|
507
|
+
const shouldShow = !session.projectId || session.projectId === activeProjectId;
|
|
508
|
+
session.element.style.display = shouldShow ? 'block' : 'none';
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const visibleCount = Array.from(activeTerminals.values())
|
|
512
|
+
.filter(s => s.element.style.display !== 'none').length;
|
|
513
|
+
|
|
514
|
+
console.log(`Updated terminal visibility: ${visibleCount} visible for project ${activeProjectId}`);
|
|
515
|
+
}
|
|
516
|
+
|
|
248
517
|
// Global Exports for HTML onclick
|
|
249
518
|
window.createNewTerminal = createNewTerminal;
|
|
250
519
|
window.closeTerminal = closeTerminalUI;
|
|
251
520
|
window.toggleMinimize = toggleMinimize;
|
|
252
521
|
window.toggleMaximize = toggleMaximize;
|
|
522
|
+
window.copyTerminalLog = copyTerminalLog;
|
|
523
|
+
window.clearTerminal = clearTerminal;
|
|
524
|
+
window.updateTerminalVisibility = updateTerminalVisibility;
|
|
@@ -9,6 +9,8 @@ import { initTerminal, createNewTerminal } from './features/terminal.js';
|
|
|
9
9
|
import { initEditorTabs } from './features/editor-tabs.js';
|
|
10
10
|
import { initMonaco, updateMonacoTheme } from './features/monaco-manager.js';
|
|
11
11
|
import { initResizeHandler } from './features/resize.js';
|
|
12
|
+
import { initSavedCommands } from './features/commands.js';
|
|
13
|
+
import { initProjectSwitcher } from './features/project-switcher.js';
|
|
12
14
|
|
|
13
15
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
14
16
|
// Load system prompt text
|
|
@@ -43,12 +45,52 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
43
45
|
// Initialize Resize Handler
|
|
44
46
|
initResizeHandler();
|
|
45
47
|
|
|
48
|
+
// Initialize Saved Commands
|
|
49
|
+
initSavedCommands();
|
|
50
|
+
|
|
51
|
+
// Initialize Project Switcher
|
|
52
|
+
await initProjectSwitcher();
|
|
53
|
+
|
|
46
54
|
// Set default tab to AI Assistant
|
|
47
55
|
if (window.switchTab) {
|
|
48
56
|
window.switchTab('ai-assistant');
|
|
49
57
|
}
|
|
50
58
|
});
|
|
51
59
|
|
|
60
|
+
// Global event handler for project switches
|
|
61
|
+
window.addEventListener('project-switched', async (event) => {
|
|
62
|
+
const { projectId, projectName, project } = event.detail;
|
|
63
|
+
console.log(`Project switched to: ${projectName}`);
|
|
64
|
+
|
|
65
|
+
// Reload project info
|
|
66
|
+
await loadProjectInfo();
|
|
67
|
+
|
|
68
|
+
// Filter terminal visibility (show only terminals for active project)
|
|
69
|
+
if (window.updateTerminalVisibility) {
|
|
70
|
+
window.updateTerminalVisibility(projectId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Reload saved commands for new project
|
|
74
|
+
if (window.loadSavedCommands) {
|
|
75
|
+
await window.loadSavedCommands();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Reset tree view (hide it)
|
|
79
|
+
const treeContainer = document.getElementById('structure-tree');
|
|
80
|
+
if (treeContainer) {
|
|
81
|
+
treeContainer.style.display = 'none';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Clear tree content
|
|
85
|
+
const treeContent = document.getElementById('tree-content');
|
|
86
|
+
if (treeContent) {
|
|
87
|
+
treeContent.innerHTML = '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// TODO: Refresh other context-dependent components
|
|
91
|
+
// - Git view refresh
|
|
92
|
+
});
|
|
93
|
+
|
|
52
94
|
async function checkServerStatus() {
|
|
53
95
|
const statusEl = document.getElementById('status');
|
|
54
96
|
const isHealthy = await checkHealth();
|
|
@@ -169,3 +211,26 @@ window.copyChromeUrl = function(event) {
|
|
|
169
211
|
}, 2000);
|
|
170
212
|
});
|
|
171
213
|
}
|
|
214
|
+
|
|
215
|
+
window.stopServer = async function() {
|
|
216
|
+
if (!confirm('Are you sure you want to stop the server?')) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
await fetch('/api/shutdown', { method: 'POST' });
|
|
222
|
+
showToast('Server stopped successfully', 'success');
|
|
223
|
+
|
|
224
|
+
// Show a message that server is stopped
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
document.body.innerHTML = `
|
|
227
|
+
<div style="display: flex; align-items: center; justify-content: center; height: 100vh; flex-direction: column; gap: 20px;">
|
|
228
|
+
<h2>🛑 Server Stopped</h2>
|
|
229
|
+
<p>You can close this tab now.</p>
|
|
230
|
+
</div>
|
|
231
|
+
`;
|
|
232
|
+
}, 1000);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('Failed to stop server:', error);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Utilities for Smart Log Copy Feature
|
|
3
|
+
* Provides ANSI stripping, token estimation, and log classification
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Strip ANSI escape codes from text
|
|
8
|
+
* Removes color codes, cursor movements, and other terminal control sequences
|
|
9
|
+
* @param {string} text - Text with potential ANSI codes
|
|
10
|
+
* @returns {string} Clean text without ANSI codes
|
|
11
|
+
*/
|
|
12
|
+
function stripAnsiCodes(text) {
|
|
13
|
+
if (!text) return '';
|
|
14
|
+
|
|
15
|
+
// Remove ANSI escape sequences
|
|
16
|
+
// Pattern matches: ESC [ ... m (colors, styles)
|
|
17
|
+
// ESC [ ... H/J/K (cursor movements, clear)
|
|
18
|
+
// ESC ] ... BEL/ST (operating system commands)
|
|
19
|
+
return text.replace(
|
|
20
|
+
// eslint-disable-next-line no-control-regex
|
|
21
|
+
/\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b\][^\x1b]*\x1b\\/g,
|
|
22
|
+
''
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Estimate token count from text
|
|
28
|
+
* Uses approximation: 1 token ≈ 4 characters (for English/code)
|
|
29
|
+
* This is a simplified estimation and may vary ±20% from actual tokenization
|
|
30
|
+
* @param {string} text - Text to estimate
|
|
31
|
+
* @returns {number} Estimated token count
|
|
32
|
+
*/
|
|
33
|
+
function estimateTokens(text) {
|
|
34
|
+
if (!text) return 0;
|
|
35
|
+
|
|
36
|
+
const chars = text.length;
|
|
37
|
+
const words = text.split(/\s+/).filter(w => w.length > 0).length;
|
|
38
|
+
|
|
39
|
+
// Use the maximum of two methods for better accuracy:
|
|
40
|
+
// Method 1: 1 token ≈ 0.75 words
|
|
41
|
+
// Method 2: 1 token ≈ 4 characters
|
|
42
|
+
const tokensFromWords = Math.ceil(words / 0.75);
|
|
43
|
+
const tokensFromChars = Math.ceil(chars / 4);
|
|
44
|
+
|
|
45
|
+
return Math.max(tokensFromWords, tokensFromChars);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Classify a log line by severity
|
|
50
|
+
* @param {string} line - Single log line
|
|
51
|
+
* @returns {'ERROR'|'WARNING'|'NORMAL'} Classification
|
|
52
|
+
*/
|
|
53
|
+
function classifyLogLine(line) {
|
|
54
|
+
if (!line) return 'NORMAL';
|
|
55
|
+
|
|
56
|
+
const lowerLine = line.toLowerCase();
|
|
57
|
+
|
|
58
|
+
// Error patterns
|
|
59
|
+
const errorPatterns = [
|
|
60
|
+
'error', 'err:', 'exception', 'fatal', 'failed', 'failure',
|
|
61
|
+
'cannot', 'could not', 'unable to', 'not found', '404',
|
|
62
|
+
'stack trace', 'traceback', 'segmentation fault', 'core dumped'
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Warning patterns
|
|
66
|
+
const warningPatterns = [
|
|
67
|
+
'warn', 'warning', 'deprecated', 'deprecation',
|
|
68
|
+
'caution', 'attention', 'notice'
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Check for errors first (higher priority)
|
|
72
|
+
if (errorPatterns.some(pattern => lowerLine.includes(pattern))) {
|
|
73
|
+
return 'ERROR';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Then check for warnings
|
|
77
|
+
if (warningPatterns.some(pattern => lowerLine.includes(pattern))) {
|
|
78
|
+
return 'WARNING';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return 'NORMAL';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Extract error and warning lines with context
|
|
86
|
+
* @param {string[]} lines - Array of log lines
|
|
87
|
+
* @param {number} contextLines - Number of lines before/after to include (default: 2)
|
|
88
|
+
* @returns {Object[]} Array of {lineIndex, line, type, context}
|
|
89
|
+
*/
|
|
90
|
+
function extractErrors(lines, contextLines = 2) {
|
|
91
|
+
const errors = [];
|
|
92
|
+
const includedIndices = new Set();
|
|
93
|
+
|
|
94
|
+
// First pass: identify error/warning lines
|
|
95
|
+
lines.forEach((line, index) => {
|
|
96
|
+
const type = classifyLogLine(line);
|
|
97
|
+
if (type === 'ERROR' || type === 'WARNING') {
|
|
98
|
+
// Include the error line and context
|
|
99
|
+
const startIndex = Math.max(0, index - contextLines);
|
|
100
|
+
const endIndex = Math.min(lines.length - 1, index + contextLines);
|
|
101
|
+
|
|
102
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
103
|
+
includedIndices.add(i);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
errors.push({
|
|
107
|
+
lineIndex: index,
|
|
108
|
+
line,
|
|
109
|
+
type,
|
|
110
|
+
contextStart: startIndex,
|
|
111
|
+
contextEnd: endIndex
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Second pass: build result with context
|
|
117
|
+
const result = [];
|
|
118
|
+
const sortedIndices = Array.from(includedIndices).sort((a, b) => a - b);
|
|
119
|
+
|
|
120
|
+
sortedIndices.forEach(index => {
|
|
121
|
+
result.push({
|
|
122
|
+
lineIndex: index,
|
|
123
|
+
line: lines[index],
|
|
124
|
+
type: classifyLogLine(lines[index])
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Format file size in human-readable format
|
|
133
|
+
* @param {number} bytes - Size in bytes
|
|
134
|
+
* @returns {string} Formatted size
|
|
135
|
+
*/
|
|
136
|
+
function formatBytes(bytes) {
|
|
137
|
+
if (bytes === 0) return '0 B';
|
|
138
|
+
const k = 1024;
|
|
139
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
140
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
141
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Export for Node.js
|
|
145
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
146
|
+
module.exports = {
|
|
147
|
+
stripAnsiCodes,
|
|
148
|
+
estimateTokens,
|
|
149
|
+
classifyLogLine,
|
|
150
|
+
extractErrors,
|
|
151
|
+
formatBytes
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Export for browser
|
|
156
|
+
if (typeof window !== 'undefined') {
|
|
157
|
+
window.LogUtils = {
|
|
158
|
+
stripAnsiCodes,
|
|
159
|
+
estimateTokens,
|
|
160
|
+
classifyLogLine,
|
|
161
|
+
extractErrors,
|
|
162
|
+
formatBytes
|
|
163
|
+
};
|
|
164
|
+
}
|