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.
@@ -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
- // Preload project files for fast @ autocomplete
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 filtered = SLASH_COMMANDS.filter(c => c.cmd.toLowerCase().startsWith(q) || q === '/');
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,
@@ -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.2.2 &bull; Terminal-Native AI Assistant</div>
142
+ <div class="footer">devx v1.4.0 &bull; Terminal-Native AI Assistant</div>
143
143
  </div>
144
144
  </body>
145
145
  </html>`;
@@ -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
+ }
@@ -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 || receivedAnyChunk) {
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);
@@ -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(/\-\d {4,8}$/, '').replace(/:latest$/, '');
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'))
@@ -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(/\-\d {4,8}$/, '').replace(/:latest$/, '');
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
+ }