termux-dev 1.2.2 → 1.4.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.
- package/README.md +48 -0
- package/assets/banner.svg +1 -1
- package/assets/preview.png +0 -0
- package/dist/cli/doctor.js +8 -2
- package/dist/cli/export.js +62 -0
- package/dist/cli/headless.js +110 -0
- package/dist/cli/index.js +411 -224
- package/dist/cli/prompt.js +16 -2
- package/dist/cli/server.js +1 -1
- package/dist/cli/updater.js +0 -6
- package/dist/core/commands.js +100 -0
- package/dist/core/history.js +10 -0
- package/dist/core/loop.js +5 -1
- package/dist/core/models.js +1 -1
- package/dist/core/pricing.js +1 -1
- package/dist/core/repomap.js +135 -0
- package/dist/core/usage.js +119 -0
- package/dist/mcp/client.js +218 -0
- package/dist/mcp/manager.js +196 -0
- package/dist/mcp/types.js +1 -0
- package/dist/permissions/guard.js +69 -15
- package/dist/prompts/builder.js +9 -0
- package/dist/providers/openai.js +37 -8
- package/dist/tools/bash.js +7 -0
- package/dist/tools/fs.js +1 -1
- package/dist/tools/index.js +4 -2
- package/dist/tools/web.js +3 -3
- package/package.json +1 -1
package/dist/cli/prompt.js
CHANGED
|
@@ -2,11 +2,15 @@ import pc from 'picocolors';
|
|
|
2
2
|
import { scanProjectFiles } from './files.js';
|
|
3
3
|
import { saveClipboardImage, processPastedFilePath } from './clipboard.js';
|
|
4
4
|
import { getCurrentTheme, listThemes } from './theme.js';
|
|
5
|
+
import { CustomCommandManager } from '../core/commands.js';
|
|
5
6
|
export const SLASH_COMMANDS = [
|
|
6
7
|
{ cmd: '/new', desc: 'Start a new clean chat session' },
|
|
7
8
|
{ cmd: '/resume', desc: 'Resume a previous chat session' },
|
|
8
9
|
{ cmd: '/session', desc: 'Show active session ID, stats, and info' },
|
|
9
10
|
{ cmd: '/session del', desc: 'Select and delete saved sessions' },
|
|
11
|
+
{ cmd: '/usage', desc: 'Show network bandwidth, data saver & token cost' },
|
|
12
|
+
{ cmd: '/export', desc: 'Export session conversation to Markdown' },
|
|
13
|
+
{ cmd: '/mcp', desc: 'Manage Model Context Protocol (MCP) servers & tools' },
|
|
10
14
|
{ cmd: '/theme', desc: 'Switch UI theme (Cyan, Purple, Matrix, Amber, etc.)' },
|
|
11
15
|
{ cmd: '/doctor', desc: 'Run system & environment health diagnostics' },
|
|
12
16
|
{ cmd: '/settings', desc: 'Configure permissions & auto-approval' },
|
|
@@ -43,10 +47,14 @@ export function askPrompt(opts = {}) {
|
|
|
43
47
|
let historyIndex = GLOBAL_PROMPT_HISTORY.length;
|
|
44
48
|
let tempDraft = '';
|
|
45
49
|
let availableFiles = [];
|
|
46
|
-
|
|
50
|
+
let customCommandsList = [];
|
|
51
|
+
// Preload project files and custom slash commands
|
|
47
52
|
scanProjectFiles().then(files => {
|
|
48
53
|
availableFiles = files;
|
|
49
54
|
}).catch(() => { });
|
|
55
|
+
CustomCommandManager.listCommands().then(cmds => {
|
|
56
|
+
customCommandsList = cmds.map(c => ({ cmd: c.cmd, desc: c.desc }));
|
|
57
|
+
}).catch(() => { });
|
|
50
58
|
const pastes = [];
|
|
51
59
|
const imageAttachments = [];
|
|
52
60
|
const usedImageNames = new Set();
|
|
@@ -89,7 +97,13 @@ export function askPrompt(opts = {}) {
|
|
|
89
97
|
}
|
|
90
98
|
if (input.startsWith('/')) {
|
|
91
99
|
const q = input.trim().toLowerCase();
|
|
92
|
-
const
|
|
100
|
+
const baseList = [...SLASH_COMMANDS];
|
|
101
|
+
for (const cc of customCommandsList) {
|
|
102
|
+
if (!baseList.some(b => b.cmd === cc.cmd)) {
|
|
103
|
+
baseList.push({ cmd: cc.cmd, desc: cc.desc });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const filtered = baseList.filter(c => c.cmd.toLowerCase().startsWith(q) || q === '/');
|
|
93
107
|
return filtered.map(c => ({
|
|
94
108
|
label: c.cmd,
|
|
95
109
|
desc: c.desc,
|
package/dist/cli/server.js
CHANGED
|
@@ -139,7 +139,7 @@ function renderDirectoryHtml(dirPath, relPath, files, port) {
|
|
|
139
139
|
${parentLink}
|
|
140
140
|
${items || '<li style="padding: 20px; text-align: center; color: #6e7681;">No visible files in this directory</li>'}
|
|
141
141
|
</ul>
|
|
142
|
-
<div class="footer">devx v1.
|
|
142
|
+
<div class="footer">devx v1.4.0 • Terminal-Native AI Assistant</div>
|
|
143
143
|
</div>
|
|
144
144
|
</body>
|
|
145
145
|
</html>`;
|
package/dist/cli/updater.js
CHANGED
|
@@ -85,12 +85,6 @@ export async function checkForUpdates(timeoutMs = 10000) {
|
|
|
85
85
|
currentVersion,
|
|
86
86
|
latestVersion: latestVersion || currentVersion
|
|
87
87
|
};
|
|
88
|
-
return {
|
|
89
|
-
updateAvailable: false,
|
|
90
|
-
currentVersion,
|
|
91
|
-
latestVersion: currentVersion,
|
|
92
|
-
error: `HTTP ${res.status}`
|
|
93
|
-
};
|
|
94
88
|
}
|
|
95
89
|
catch (err) {
|
|
96
90
|
clearTimeout(timer);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import fsSync from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
export class CustomCommandManager {
|
|
6
|
+
static cachedCommands = null;
|
|
7
|
+
static lastScanTime = 0;
|
|
8
|
+
/**
|
|
9
|
+
* Scans for custom commands in project and global directories
|
|
10
|
+
*/
|
|
11
|
+
static async listCommands(forceRefresh = false) {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
if (!forceRefresh && this.cachedCommands && now - this.lastScanTime < 5000) {
|
|
14
|
+
return this.cachedCommands;
|
|
15
|
+
}
|
|
16
|
+
const commandMap = new Map();
|
|
17
|
+
// 1. Search directories (higher priority overrides lower priority)
|
|
18
|
+
const searchDirs = [
|
|
19
|
+
path.join(os.homedir(), '.devx', 'commands'),
|
|
20
|
+
path.join(process.cwd(), '.claude', 'commands'),
|
|
21
|
+
path.join(process.cwd(), '.devx', 'commands')
|
|
22
|
+
];
|
|
23
|
+
for (const dir of searchDirs) {
|
|
24
|
+
try {
|
|
25
|
+
if (!fsSync.existsSync(dir))
|
|
26
|
+
continue;
|
|
27
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.prompt'))) {
|
|
30
|
+
const rawName = entry.name.replace(/\.(md|prompt)$/i, '').toLowerCase();
|
|
31
|
+
const filePath = path.join(dir, entry.name);
|
|
32
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
33
|
+
const { description, template } = this.parseCommandFile(content, rawName);
|
|
34
|
+
commandMap.set(rawName, {
|
|
35
|
+
name: rawName,
|
|
36
|
+
cmd: `/${rawName}`,
|
|
37
|
+
desc: description || `Custom command (${path.basename(dir)}/${entry.name})`,
|
|
38
|
+
promptTemplate: template,
|
|
39
|
+
sourcePath: filePath
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
}
|
|
46
|
+
this.cachedCommands = Array.from(commandMap.values());
|
|
47
|
+
this.lastScanTime = now;
|
|
48
|
+
return this.cachedCommands;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Finds a custom command by slash name (e.g. "/deploy" or "deploy")
|
|
52
|
+
*/
|
|
53
|
+
static async findCommand(cmdName) {
|
|
54
|
+
const clean = cmdName.replace(/^\//, '').toLowerCase().trim();
|
|
55
|
+
const list = await this.listCommands();
|
|
56
|
+
return list.find(c => c.name === clean) || null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Expands arguments into the prompt template
|
|
60
|
+
*/
|
|
61
|
+
static expandTemplate(template, args) {
|
|
62
|
+
const trimmedArgs = args.trim();
|
|
63
|
+
if (!template.includes('$ARG') && !template.includes('$*') && !template.includes('$1')) {
|
|
64
|
+
return trimmedArgs ? `${template}\n\nUser Arguments: ${trimmedArgs}` : template;
|
|
65
|
+
}
|
|
66
|
+
let expanded = template;
|
|
67
|
+
expanded = expanded.replace(/\$ARG/g, () => trimmedArgs);
|
|
68
|
+
expanded = expanded.replace(/\$\*/g, () => trimmedArgs);
|
|
69
|
+
// Support positional parameters: $1, $2, etc.
|
|
70
|
+
const parts = trimmedArgs.split(/\s+/);
|
|
71
|
+
for (let i = 0; i < parts.length; i++) {
|
|
72
|
+
expanded = expanded.replace(new RegExp(`\\$${i + 1}`, 'g'), () => parts[i]);
|
|
73
|
+
}
|
|
74
|
+
return expanded;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parses frontmatter (YAML description) and template content
|
|
78
|
+
*/
|
|
79
|
+
static parseCommandFile(rawContent, defaultName) {
|
|
80
|
+
let description = '';
|
|
81
|
+
let template = rawContent;
|
|
82
|
+
const frontmatterMatch = rawContent.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
83
|
+
if (frontmatterMatch) {
|
|
84
|
+
const frontmatter = frontmatterMatch[1];
|
|
85
|
+
template = frontmatterMatch[2].trim();
|
|
86
|
+
const descMatch = frontmatter.match(/description:\s*([^\r\n]+)/i);
|
|
87
|
+
if (descMatch) {
|
|
88
|
+
description = descMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Check for first-line Markdown heading or comment: # Description
|
|
93
|
+
const firstLine = rawContent.split('\n')[0].trim();
|
|
94
|
+
if (firstLine.startsWith('# ')) {
|
|
95
|
+
description = firstLine.replace(/^#\s*/, '').trim();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { description, template };
|
|
99
|
+
}
|
|
100
|
+
}
|
package/dist/core/history.js
CHANGED
|
@@ -16,6 +16,16 @@ export class History {
|
|
|
16
16
|
getMessages() {
|
|
17
17
|
return [...this.messages];
|
|
18
18
|
}
|
|
19
|
+
popLastTurn() {
|
|
20
|
+
// Pop assistant and tool messages from the end of history
|
|
21
|
+
while (this.messages.length > 1 && this.messages[this.messages.length - 1].role !== 'user') {
|
|
22
|
+
this.messages.pop();
|
|
23
|
+
}
|
|
24
|
+
// Pop the triggering user message
|
|
25
|
+
if (this.messages.length > 1 && this.messages[this.messages.length - 1].role === 'user') {
|
|
26
|
+
this.messages.pop();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
19
29
|
clear() {
|
|
20
30
|
this.messages = [];
|
|
21
31
|
}
|
package/dist/core/loop.js
CHANGED
|
@@ -94,9 +94,13 @@ export class Agent {
|
|
|
94
94
|
response = chunk.response;
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
if (response
|
|
97
|
+
if (response) {
|
|
98
98
|
break;
|
|
99
99
|
}
|
|
100
|
+
if (!receivedAnyChunk) {
|
|
101
|
+
throw new Error('Provider stream ended unexpectedly without receiving any data.');
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
100
104
|
}
|
|
101
105
|
else {
|
|
102
106
|
response = await this.provider.chat(request);
|
package/dist/core/models.js
CHANGED
|
@@ -33,7 +33,7 @@ export function getModelContextLimit(modelName) {
|
|
|
33
33
|
return cache[clean];
|
|
34
34
|
if (cache[short])
|
|
35
35
|
return cache[short];
|
|
36
|
-
const baseName = short.replace(
|
|
36
|
+
const baseName = short.replace(/-\d{4,8}$/, '').replace(/:latest$/, '');
|
|
37
37
|
if (cache[baseName])
|
|
38
38
|
return cache[baseName];
|
|
39
39
|
if (clean.includes('kimi-k2') || clean.includes('kimi'))
|
package/dist/core/pricing.js
CHANGED
|
@@ -42,7 +42,7 @@ export function getModelPricing(modelName) {
|
|
|
42
42
|
return cache[clean];
|
|
43
43
|
if (cache[short])
|
|
44
44
|
return cache[short];
|
|
45
|
-
const baseName = short.replace(
|
|
45
|
+
const baseName = short.replace(/-\d{4,8}$/, '').replace(/:latest$/, '');
|
|
46
46
|
if (cache[baseName])
|
|
47
47
|
return cache[baseName];
|
|
48
48
|
if (clean.includes('gpt-4o-mini'))
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const IGNORED_DIRS = new Set([
|
|
4
|
+
'.git', 'node_modules', 'dist', 'build', 'out', 'coverage',
|
|
5
|
+
'.next', '.nuxt', '.cache', '.gemini', '.antigravity', '.vscode', '.idea'
|
|
6
|
+
]);
|
|
7
|
+
const EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.mjs', '.cjs', '.rs', '.go', '.c', '.cpp', '.h', '.java']);
|
|
8
|
+
export class RepoMapGenerator {
|
|
9
|
+
/**
|
|
10
|
+
* Generates a compact, compressed tree representation of project files and key symbols
|
|
11
|
+
*/
|
|
12
|
+
static async generate(rootDir = process.cwd(), maxFiles = 60) {
|
|
13
|
+
try {
|
|
14
|
+
const files = await this.scanFiles(rootDir, maxFiles);
|
|
15
|
+
if (files.length === 0)
|
|
16
|
+
return '';
|
|
17
|
+
const lines = ['Project Structure & Key Symbols (Repo Map):'];
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const symbolStr = file.symbols.length > 0 ? ` (${file.symbols.slice(0, 5).join(', ')}${file.symbols.length > 5 ? ', ...' : ''})` : '';
|
|
20
|
+
lines.push(`• ${file.relPath}${symbolStr}`);
|
|
21
|
+
}
|
|
22
|
+
return lines.join('\n');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
static async scanFiles(dir, maxFiles) {
|
|
29
|
+
const results = [];
|
|
30
|
+
async function walk(currentDir) {
|
|
31
|
+
if (results.length >= maxFiles)
|
|
32
|
+
return;
|
|
33
|
+
try {
|
|
34
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (results.length >= maxFiles)
|
|
37
|
+
break;
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
if (!IGNORED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
40
|
+
await walk(path.join(currentDir, entry.name));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (entry.isFile()) {
|
|
44
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
45
|
+
if (EXTENSIONS.has(ext)) {
|
|
46
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
47
|
+
const relPath = path.relative(dir, fullPath).replace(/\\/g, '/');
|
|
48
|
+
const symbols = await RepoMapGenerator.extractSymbols(fullPath, ext);
|
|
49
|
+
results.push({ relPath, symbols });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
56
|
+
await walk(dir);
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
static async extractSymbols(filePath, ext) {
|
|
60
|
+
const symbols = [];
|
|
61
|
+
try {
|
|
62
|
+
// Read at most 16KB of each file for super-fast symbol extraction
|
|
63
|
+
const stat = await fs.stat(filePath);
|
|
64
|
+
if (stat.size > 256 * 1024)
|
|
65
|
+
return []; // Skip giant generated files
|
|
66
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
67
|
+
const lines = content.split('\n').slice(0, 300); // Only examine top 300 lines
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
const trimmed = line.trim();
|
|
70
|
+
if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx' || ext === '.mjs') {
|
|
71
|
+
// JS/TS exports: function, class, interface, type, const
|
|
72
|
+
const fnMatch = trimmed.match(/^export\s+(?:async\s+)?function\s+([a-zA-Z0-9_$]+)/);
|
|
73
|
+
if (fnMatch) {
|
|
74
|
+
symbols.push(`fn ${fnMatch[1]}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const classMatch = trimmed.match(/^export\s+(?:abstract\s+)?class\s+([a-zA-Z0-9_$]+)/);
|
|
78
|
+
if (classMatch) {
|
|
79
|
+
symbols.push(`class ${classMatch[1]}`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const ifaceMatch = trimmed.match(/^export\s+interface\s+([a-zA-Z0-9_$]+)/);
|
|
83
|
+
if (ifaceMatch) {
|
|
84
|
+
symbols.push(`interface ${ifaceMatch[1]}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const typeMatch = trimmed.match(/^export\s+type\s+([a-zA-Z0-9_$]+)/);
|
|
88
|
+
if (typeMatch) {
|
|
89
|
+
symbols.push(`type ${typeMatch[1]}`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const constMatch = trimmed.match(/^export\s+const\s+([a-zA-Z0-9_$]+)/);
|
|
93
|
+
if (constMatch && !constMatch[1].startsWith('_')) {
|
|
94
|
+
symbols.push(constMatch[1]);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (ext === '.py') {
|
|
99
|
+
// Python classes and functions
|
|
100
|
+
const pyClass = trimmed.match(/^class\s+([a-zA-Z0-9_]+)/);
|
|
101
|
+
if (pyClass) {
|
|
102
|
+
symbols.push(`class ${pyClass[1]}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const pyFn = trimmed.match(/^(?:async\s+)?def\s+([a-zA-Z0-9_]+)/);
|
|
106
|
+
if (pyFn && !pyFn[1].startsWith('__')) {
|
|
107
|
+
symbols.push(`def ${pyFn[1]}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (ext === '.rs') {
|
|
112
|
+
const rsFn = trimmed.match(/^pub\s+(?:async\s+)?fn\s+([a-zA-Z0-9_]+)/);
|
|
113
|
+
if (rsFn) {
|
|
114
|
+
symbols.push(`fn ${rsFn[1]}`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const rsStruct = trimmed.match(/^pub\s+struct\s+([a-zA-Z0-9_]+)/);
|
|
118
|
+
if (rsStruct) {
|
|
119
|
+
symbols.push(`struct ${rsStruct[1]}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (ext === '.go') {
|
|
124
|
+
const goFn = trimmed.match(/^func\s+(?:\([^\)]+\)\s+)?([a-zA-Z0-9_]+)/);
|
|
125
|
+
if (goFn) {
|
|
126
|
+
symbols.push(`func ${goFn[1]}`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
return symbols.slice(0, 8);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { getCurrentTheme } from '../cli/theme.js';
|
|
3
|
+
function stripAnsi(str) {
|
|
4
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
5
|
+
}
|
|
6
|
+
export class UsageTracker {
|
|
7
|
+
static instance;
|
|
8
|
+
bytesSent = 0;
|
|
9
|
+
bytesReceived = 0;
|
|
10
|
+
requestsCount = 0;
|
|
11
|
+
promptTokens = 0;
|
|
12
|
+
completionTokens = 0;
|
|
13
|
+
totalCost = 0;
|
|
14
|
+
dataSaverLimitMB;
|
|
15
|
+
warnedThreshold = false;
|
|
16
|
+
constructor() { }
|
|
17
|
+
static getInstance() {
|
|
18
|
+
if (!UsageTracker.instance) {
|
|
19
|
+
UsageTracker.instance = new UsageTracker();
|
|
20
|
+
}
|
|
21
|
+
return UsageTracker.instance;
|
|
22
|
+
}
|
|
23
|
+
setLimit(limitMB) {
|
|
24
|
+
this.dataSaverLimitMB = limitMB;
|
|
25
|
+
}
|
|
26
|
+
recordRequest(payloadBytes) {
|
|
27
|
+
this.bytesSent += payloadBytes;
|
|
28
|
+
this.requestsCount += 1;
|
|
29
|
+
}
|
|
30
|
+
recordResponseChunk(chunkBytes) {
|
|
31
|
+
this.bytesReceived += chunkBytes;
|
|
32
|
+
}
|
|
33
|
+
recordTokens(promptTokens, completionTokens, cost) {
|
|
34
|
+
this.promptTokens += promptTokens;
|
|
35
|
+
this.completionTokens += completionTokens;
|
|
36
|
+
this.totalCost += cost;
|
|
37
|
+
}
|
|
38
|
+
getSummary() {
|
|
39
|
+
return {
|
|
40
|
+
bytesSent: this.bytesSent,
|
|
41
|
+
bytesReceived: this.bytesReceived,
|
|
42
|
+
totalBytes: this.bytesSent + this.bytesReceived,
|
|
43
|
+
requestsCount: this.requestsCount,
|
|
44
|
+
promptTokens: this.promptTokens,
|
|
45
|
+
completionTokens: this.completionTokens,
|
|
46
|
+
totalTokens: this.promptTokens + this.completionTokens,
|
|
47
|
+
totalCost: this.totalCost,
|
|
48
|
+
dataSaverLimitMB: this.dataSaverLimitMB
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
static formatBytes(bytes) {
|
|
52
|
+
if (bytes < 1024)
|
|
53
|
+
return `${bytes} B`;
|
|
54
|
+
if (bytes < 1024 * 1024)
|
|
55
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
56
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
57
|
+
}
|
|
58
|
+
static formatTokens(n) {
|
|
59
|
+
if (n >= 1000000)
|
|
60
|
+
return `${(n / 1000000).toFixed(2)}M`;
|
|
61
|
+
if (n >= 1000)
|
|
62
|
+
return `${(n / 1000).toFixed(1)}k`;
|
|
63
|
+
return `${n}`;
|
|
64
|
+
}
|
|
65
|
+
renderUsageCard() {
|
|
66
|
+
const theme = getCurrentTheme();
|
|
67
|
+
const summary = this.getSummary();
|
|
68
|
+
const cols = Math.min(process.stdout.columns || 80, 75);
|
|
69
|
+
const boxWidth = Math.max(38, cols - 4);
|
|
70
|
+
const innerWidth = boxWidth - 6;
|
|
71
|
+
const padRow = (label, value) => {
|
|
72
|
+
const visibleValLen = stripAnsi(value).length;
|
|
73
|
+
const plainLen = label.length + visibleValLen;
|
|
74
|
+
const spaces = Math.max(1, innerWidth - plainLen);
|
|
75
|
+
return `│ ${pc.bold(label)}${' '.repeat(spaces)}${value} │`;
|
|
76
|
+
};
|
|
77
|
+
const header = theme.colorFn('┌─ ') + pc.bold('📊 Session Bandwidth & Usage Monitor') + ' ' + theme.colorFn('─'.repeat(Math.max(2, boxWidth - 41)) + '┐');
|
|
78
|
+
const divider = theme.colorFn('├' + '─'.repeat(boxWidth - 2) + '┤');
|
|
79
|
+
const footer = theme.colorFn('└' + '─'.repeat(boxWidth - 2) + '┘');
|
|
80
|
+
const totalBandwidthStr = pc.cyan(UsageTracker.formatBytes(summary.totalBytes));
|
|
81
|
+
const sentStr = pc.dim(UsageTracker.formatBytes(summary.bytesSent));
|
|
82
|
+
const recvStr = pc.dim(UsageTracker.formatBytes(summary.bytesReceived));
|
|
83
|
+
const reqStr = pc.yellow(`${summary.requestsCount}`);
|
|
84
|
+
const tokenStr = pc.magenta(`${UsageTracker.formatTokens(summary.totalTokens)} tokens`);
|
|
85
|
+
const promptTokenStr = pc.dim(`${UsageTracker.formatTokens(summary.promptTokens)} in`);
|
|
86
|
+
const compTokenStr = pc.dim(`${UsageTracker.formatTokens(summary.completionTokens)} out`);
|
|
87
|
+
const costStr = pc.green(`$${summary.totalCost.toFixed(4)}`);
|
|
88
|
+
const lines = [
|
|
89
|
+
header,
|
|
90
|
+
padRow('Total Network Traffic:', totalBandwidthStr),
|
|
91
|
+
padRow(' • Upload (Sent):', sentStr),
|
|
92
|
+
padRow(' • Download (Recv):', recvStr),
|
|
93
|
+
padRow('API Requests Count:', reqStr),
|
|
94
|
+
divider,
|
|
95
|
+
padRow('Tokens Consumed:', tokenStr),
|
|
96
|
+
padRow(' • Breakdown:', `${promptTokenStr} / ${compTokenStr}`),
|
|
97
|
+
padRow('Session Cost:', costStr)
|
|
98
|
+
];
|
|
99
|
+
if (summary.dataSaverLimitMB) {
|
|
100
|
+
const usedMB = summary.totalBytes / (1024 * 1024);
|
|
101
|
+
const pct = Math.min(100, Math.round((usedMB / summary.dataSaverLimitMB) * 100));
|
|
102
|
+
const limitStr = pct >= 90 ? pc.red(`${usedMB.toFixed(1)} / ${summary.dataSaverLimitMB} MB (${pct}%)`) : pc.cyan(`${usedMB.toFixed(1)} / ${summary.dataSaverLimitMB} MB (${pct}%)`);
|
|
103
|
+
lines.push(divider);
|
|
104
|
+
lines.push(padRow('Data Saver Limit:', limitStr));
|
|
105
|
+
}
|
|
106
|
+
lines.push(footer);
|
|
107
|
+
return lines.join('\n') + '\n';
|
|
108
|
+
}
|
|
109
|
+
checkThresholdWarning() {
|
|
110
|
+
if (!this.dataSaverLimitMB || this.warnedThreshold)
|
|
111
|
+
return null;
|
|
112
|
+
const usedMB = (this.bytesSent + this.bytesReceived) / (1024 * 1024);
|
|
113
|
+
if (usedMB >= this.dataSaverLimitMB) {
|
|
114
|
+
this.warnedThreshold = true;
|
|
115
|
+
return `⚠️ [DATA SAVER WARNING] Session network traffic has reached ${usedMB.toFixed(1)} MB (Limit: ${this.dataSaverLimitMB} MB)!`;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|