termux-dev 1.4.18 → 1.5.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 +8 -0
- package/assets/banner.svg +1 -1
- package/assets/preview.png +0 -0
- package/bin/devx.js +0 -0
- package/dist/cli/banner.js +41 -0
- package/dist/cli/diff.js +64 -0
- package/dist/cli/doctor.js +2 -1
- package/dist/cli/headless.js +8 -5
- package/dist/cli/history_search.js +111 -0
- package/dist/cli/index.js +87 -107
- package/dist/cli/modeborder.js +31 -0
- package/dist/cli/prompt.js +104 -67
- package/dist/cli/prompt_palette.js +37 -0
- package/dist/cli/server.js +2 -1
- package/dist/cli/statusline.js +107 -0
- package/dist/cli/telemetry.js +105 -0
- package/dist/core/types.js +1 -1
- package/dist/mcp/client.js +2 -1
- package/dist/permissions/guard.js +9 -4
- package/dist/providers/openai.js +5 -4
- package/dist/tools/fs.js +41 -7
- package/dist/tools/packages.js +35 -11
- package/dist/tools/search.js +24 -5
- package/package.json +5 -2
- package/dist/cli/index (# Edit conflict 2026-08-29 pe1jzcC #).js +0 -1966
package/dist/cli/prompt.js
CHANGED
|
@@ -3,6 +3,9 @@ import { scanProjectFiles } from './files.js';
|
|
|
3
3
|
import { saveClipboardImage, processPastedFilePath } from './clipboard.js';
|
|
4
4
|
import { getCurrentTheme } from './theme.js';
|
|
5
5
|
import { CustomCommandManager } from '../core/commands.js';
|
|
6
|
+
import { formatPromptHeader } from './modeborder.js';
|
|
7
|
+
import { filterCommandPalette, estimatePromptTokens, formatPromptTokenBadge } from './prompt_palette.js';
|
|
8
|
+
import { PromptHistoryNavigator } from './history_search.js';
|
|
6
9
|
export const SLASH_COMMANDS = [
|
|
7
10
|
{ cmd: '/new', desc: 'Start a new clean chat session' },
|
|
8
11
|
{ cmd: '/resume', desc: 'Resume a previous chat session' },
|
|
@@ -39,11 +42,17 @@ function stripAnsi(str) {
|
|
|
39
42
|
}
|
|
40
43
|
export function askPrompt(opts = {}) {
|
|
41
44
|
return new Promise((resolve) => {
|
|
42
|
-
const msg = opts.message
|
|
43
|
-
? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`
|
|
44
|
-
: `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
|
|
45
45
|
const theme = getCurrentTheme();
|
|
46
|
-
|
|
46
|
+
if (opts.ui?.modeBorder) {
|
|
47
|
+
const header = formatPromptHeader(!!opts.planMode, !!opts.isYolo, opts.message);
|
|
48
|
+
console.log(header);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const msg = opts.message
|
|
52
|
+
? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`
|
|
53
|
+
: `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
|
|
54
|
+
console.log(theme.colorFn('◆') + ' ' + pc.bold(msg));
|
|
55
|
+
}
|
|
47
56
|
let availableFiles = [];
|
|
48
57
|
let customCommandsList = [];
|
|
49
58
|
scanProjectFiles().then((f) => (availableFiles = f)).catch(() => { });
|
|
@@ -54,8 +63,14 @@ export function askPrompt(opts = {}) {
|
|
|
54
63
|
let cursorPos = input.length;
|
|
55
64
|
let selectedIndex = 0;
|
|
56
65
|
let lastRenderedDropdownLines = 0;
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
const historyNav = new PromptHistoryNavigator(GLOBAL_PROMPT_HISTORY, !!opts.ui?.promptHistoryFuzzy);
|
|
67
|
+
PromptHistoryNavigator.loadPersistent().then((persisted) => {
|
|
68
|
+
for (const p of persisted) {
|
|
69
|
+
if (!GLOBAL_PROMPT_HISTORY.includes(p)) {
|
|
70
|
+
GLOBAL_PROMPT_HISTORY.push(p);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}).catch(() => { });
|
|
59
74
|
const imageAttachments = [];
|
|
60
75
|
const usedImageNames = new Set();
|
|
61
76
|
const pastedBlocks = [];
|
|
@@ -133,18 +148,34 @@ export function askPrompt(opts = {}) {
|
|
|
133
148
|
function getDropdownItems() {
|
|
134
149
|
if (input.startsWith('/')) {
|
|
135
150
|
const q = input.trim().toLowerCase();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
if (opts.ui?.commandPalette) {
|
|
152
|
+
const baseList = [
|
|
153
|
+
...SLASH_COMMANDS.map((c) => ({ cmd: c.cmd, desc: c.desc })),
|
|
154
|
+
...customCommandsList.map((c) => ({ cmd: c.cmd, desc: `(custom) ${c.desc}` }))
|
|
155
|
+
];
|
|
156
|
+
const filtered = filterCommandPalette(input, baseList).slice(0, 15);
|
|
157
|
+
return filtered.map((item) => ({
|
|
158
|
+
label: item.cmd,
|
|
159
|
+
desc: item.desc,
|
|
160
|
+
replacement: `${item.cmd} `,
|
|
161
|
+
replaceStart: 0,
|
|
162
|
+
replaceLen: input.length
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const baseList = [
|
|
167
|
+
...SLASH_COMMANDS.map((c) => ({ label: c.cmd, desc: c.desc, replacement: c.cmd })),
|
|
168
|
+
...customCommandsList.map((c) => ({ label: c.cmd, desc: `(custom) ${c.desc}`, replacement: c.cmd }))
|
|
169
|
+
];
|
|
170
|
+
const filtered = baseList.filter((c) => c.label.toLowerCase().startsWith(q) || q === '/');
|
|
171
|
+
return filtered.map((item) => ({
|
|
172
|
+
label: item.label,
|
|
173
|
+
desc: item.desc,
|
|
174
|
+
replacement: item.replacement,
|
|
175
|
+
replaceStart: 0,
|
|
176
|
+
replaceLen: input.length
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
148
179
|
}
|
|
149
180
|
const beforeCursor = input.slice(0, cursorPos);
|
|
150
181
|
const atMatch = beforeCursor.match(/@([a-zA-Z0-9_\-./\u0400-\u04FF]*)$/);
|
|
@@ -178,9 +209,12 @@ export function askPrompt(opts = {}) {
|
|
|
178
209
|
if (selectedIndex < 0)
|
|
179
210
|
selectedIndex = 0;
|
|
180
211
|
// Limit dropdown height gracefully so it always fits comfortably
|
|
181
|
-
const maxDropdownRows = Math.max(2, Math.min(rows - 3,
|
|
212
|
+
const maxDropdownRows = Math.max(2, Math.min(rows - 3, 5));
|
|
182
213
|
const pageSize = Math.min(4, Math.max(2, Math.min(maxDropdownRows - 2, Math.floor(rows / 4))));
|
|
183
|
-
|
|
214
|
+
// Strict boundary: left margin (2) + box borders (2) + inner width <= cols - 4
|
|
215
|
+
// This mathematically prevents terminal auto-wrapping on narrow mobile screens
|
|
216
|
+
const boxWidth = Math.max(12, Math.min(cols - 6, 46));
|
|
217
|
+
const innerWidth = Math.max(8, boxWidth - 2);
|
|
184
218
|
const total = items.length;
|
|
185
219
|
let startIndex = 0;
|
|
186
220
|
if (total > pageSize) {
|
|
@@ -191,52 +225,53 @@ export function askPrompt(opts = {}) {
|
|
|
191
225
|
const endIndex = Math.min(startIndex + pageSize, total);
|
|
192
226
|
const hasMoreUp = startIndex > 0;
|
|
193
227
|
const hasMoreDown = endIndex < total;
|
|
194
|
-
let topBorderStr = '─'.repeat(
|
|
228
|
+
let topBorderStr = '─'.repeat(innerWidth);
|
|
195
229
|
if (hasMoreUp) {
|
|
196
|
-
const mid = Math.max(0, Math.floor(
|
|
197
|
-
topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0,
|
|
230
|
+
const mid = Math.max(0, Math.floor(innerWidth / 2) - 2);
|
|
231
|
+
topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0, innerWidth - mid - 3));
|
|
198
232
|
}
|
|
199
|
-
let botBorderStr = '─'.repeat(
|
|
233
|
+
let botBorderStr = '─'.repeat(innerWidth);
|
|
200
234
|
if (hasMoreDown) {
|
|
201
|
-
const mid = Math.max(0, Math.floor(
|
|
202
|
-
botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0,
|
|
235
|
+
const mid = Math.max(0, Math.floor(innerWidth / 2) - 2);
|
|
236
|
+
botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, innerWidth - mid - 3));
|
|
203
237
|
}
|
|
204
|
-
dropdownLines.push(pc.dim('│') +
|
|
238
|
+
dropdownLines.push(pc.dim('│ ') + pc.dim('╭' + topBorderStr + '╮') + '\x1b[0m');
|
|
205
239
|
for (let i = startIndex; i < endIndex; i++) {
|
|
206
240
|
const item = items[i];
|
|
207
241
|
const isSelected = i === selectedIndex;
|
|
208
242
|
const pointer = isSelected ? '› ' : ' ';
|
|
209
|
-
const
|
|
210
|
-
let
|
|
211
|
-
if (
|
|
212
|
-
const labelStr = item.label.length >
|
|
213
|
-
? item.label.slice(0,
|
|
214
|
-
: item.label.padEnd(
|
|
215
|
-
|
|
216
|
-
row = isSelected
|
|
217
|
-
? theme.badgeFn(plain) + '\x1b[0m'
|
|
218
|
-
: pointer + theme.boldFn(labelStr) + '\x1b[0m';
|
|
243
|
+
const avail = Math.max(4, innerWidth - pointer.length);
|
|
244
|
+
let rowText = '';
|
|
245
|
+
if (avail < 16) {
|
|
246
|
+
const labelStr = item.label.length > avail
|
|
247
|
+
? item.label.slice(0, avail - 1) + '…'
|
|
248
|
+
: item.label.padEnd(avail, ' ');
|
|
249
|
+
rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr);
|
|
219
250
|
}
|
|
220
251
|
else {
|
|
221
|
-
const labelMax = Math.min(
|
|
252
|
+
const labelMax = Math.min(12, Math.max(6, Math.floor(avail * 0.38)));
|
|
222
253
|
const labelStr = item.label.length > labelMax
|
|
223
254
|
? item.label.slice(0, labelMax - 1) + '…'
|
|
224
255
|
: item.label.padEnd(labelMax, ' ');
|
|
225
|
-
const descMax = Math.max(
|
|
256
|
+
const descMax = Math.max(3, avail - labelMax - 1);
|
|
226
257
|
const descStr = item.desc.length > descMax
|
|
227
258
|
? item.desc.slice(0, descMax - 1) + '…'
|
|
228
259
|
: item.desc.padEnd(descMax, ' ');
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
260
|
+
rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr) + ' ' + pc.gray(descStr);
|
|
261
|
+
}
|
|
262
|
+
// Calculate visible plain length without ANSI
|
|
263
|
+
const plainLen = rowText.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').length;
|
|
264
|
+
const padCount = Math.max(0, innerWidth - plainLen);
|
|
265
|
+
const fullRow = rowText + ' '.repeat(padCount);
|
|
266
|
+
if (isSelected) {
|
|
267
|
+
const cleanFull = fullRow.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
268
|
+
dropdownLines.push(pc.dim('│ ') + pc.dim('│') + theme.badgeFn(cleanFull) + pc.dim('│') + '\x1b[0m');
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
dropdownLines.push(pc.dim('│ ') + pc.dim('│') + fullRow + pc.dim('│') + '\x1b[0m');
|
|
236
272
|
}
|
|
237
|
-
dropdownLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│') + '\x1b[0m');
|
|
238
273
|
}
|
|
239
|
-
dropdownLines.push(pc.dim('│') +
|
|
274
|
+
dropdownLines.push(pc.dim('│ ') + pc.dim('╰' + botBorderStr + '╯') + '\x1b[0m');
|
|
240
275
|
}
|
|
241
276
|
// 1. Format and write the Prompt line safely
|
|
242
277
|
let inputDisplay = pc.dim('│') + ' ';
|
|
@@ -248,9 +283,11 @@ export function askPrompt(opts = {}) {
|
|
|
248
283
|
renderCursorCol = 3;
|
|
249
284
|
}
|
|
250
285
|
else {
|
|
251
|
-
const
|
|
286
|
+
const tokenBadge = opts.ui?.inputTokenCounter ? formatPromptTokenBadge(estimatePromptTokens(input), 40) : '';
|
|
287
|
+
const badgeSuffix = tokenBadge ? ` ${tokenBadge}` : '';
|
|
288
|
+
const maxInputLen = Math.max(10, cols - 6 - (tokenBadge ? 12 : 0));
|
|
252
289
|
if (input.length <= maxInputLen) {
|
|
253
|
-
inputDisplay += formatInputWithBadges(input) + '\x1b[0m';
|
|
290
|
+
inputDisplay += formatInputWithBadges(input) + badgeSuffix + '\x1b[0m';
|
|
254
291
|
renderCursorCol = 3 + cursorPos;
|
|
255
292
|
}
|
|
256
293
|
else {
|
|
@@ -259,7 +296,7 @@ export function askPrompt(opts = {}) {
|
|
|
259
296
|
const visibleChunk = input.slice(start, start + maxInputLen);
|
|
260
297
|
const prefix = start > 0 ? '…' : '';
|
|
261
298
|
const suffix = start + maxInputLen < input.length ? '…' : '';
|
|
262
|
-
inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + '\x1b[0m';
|
|
299
|
+
inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + badgeSuffix + '\x1b[0m';
|
|
263
300
|
renderCursorCol = 3 + (prefix ? 1 : 0) + (cursorPos - start);
|
|
264
301
|
}
|
|
265
302
|
}
|
|
@@ -361,6 +398,10 @@ export function askPrompt(opts = {}) {
|
|
|
361
398
|
handlePaste(str);
|
|
362
399
|
return;
|
|
363
400
|
}
|
|
401
|
+
// Ignore EOF / Ctrl+D (\x04)
|
|
402
|
+
if (str === '\x04') {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
364
405
|
// Ctrl+C / SIGINT
|
|
365
406
|
if (str === '\x03') {
|
|
366
407
|
if (lastRenderedDropdownLines > 0) {
|
|
@@ -465,14 +506,13 @@ export function askPrompt(opts = {}) {
|
|
|
465
506
|
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
|
|
466
507
|
render();
|
|
467
508
|
}
|
|
468
|
-
else
|
|
469
|
-
|
|
470
|
-
|
|
509
|
+
else {
|
|
510
|
+
const prev = historyNav.navigateUp(input);
|
|
511
|
+
if (prev !== null) {
|
|
512
|
+
input = prev;
|
|
513
|
+
cursorPos = input.length;
|
|
514
|
+
render();
|
|
471
515
|
}
|
|
472
|
-
historyIndex--;
|
|
473
|
-
input = GLOBAL_PROMPT_HISTORY[historyIndex];
|
|
474
|
-
cursorPos = input.length;
|
|
475
|
-
render();
|
|
476
516
|
}
|
|
477
517
|
return;
|
|
478
518
|
}
|
|
@@ -482,16 +522,13 @@ export function askPrompt(opts = {}) {
|
|
|
482
522
|
selectedIndex = (selectedIndex + 1) % items.length;
|
|
483
523
|
render();
|
|
484
524
|
}
|
|
485
|
-
else
|
|
486
|
-
|
|
487
|
-
if (
|
|
488
|
-
input =
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
input = GLOBAL_PROMPT_HISTORY[historyIndex];
|
|
525
|
+
else {
|
|
526
|
+
const next = historyNav.navigateDown();
|
|
527
|
+
if (next !== null) {
|
|
528
|
+
input = next;
|
|
529
|
+
cursorPos = input.length;
|
|
530
|
+
render();
|
|
492
531
|
}
|
|
493
|
-
cursorPos = input.length;
|
|
494
|
-
render();
|
|
495
532
|
}
|
|
496
533
|
return;
|
|
497
534
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
export function filterCommandPalette(query, commands) {
|
|
3
|
+
if (!query || query === '/') {
|
|
4
|
+
return [...commands];
|
|
5
|
+
}
|
|
6
|
+
const q = query.toLowerCase().replace(/^\//, '');
|
|
7
|
+
return commands.filter((c) => {
|
|
8
|
+
const name = c.cmd.toLowerCase().replace(/^\//, '');
|
|
9
|
+
return name.includes(q) || c.desc.toLowerCase().includes(q);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export function estimatePromptTokens(text) {
|
|
13
|
+
if (!text)
|
|
14
|
+
return 0;
|
|
15
|
+
let tokens = 0;
|
|
16
|
+
for (let i = 0; i < text.length; i++) {
|
|
17
|
+
const code = text.charCodeAt(i);
|
|
18
|
+
if (code >= 0x0400 && code <= 0x04ff) {
|
|
19
|
+
tokens += 0.8;
|
|
20
|
+
}
|
|
21
|
+
else if (code > 0x07ff) {
|
|
22
|
+
tokens += 1.2;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
tokens += 0.25;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return Math.max(1, Math.round(tokens));
|
|
29
|
+
}
|
|
30
|
+
export function formatPromptTokenBadge(tokens, threshold = 30) {
|
|
31
|
+
if (tokens < threshold)
|
|
32
|
+
return '';
|
|
33
|
+
if (tokens >= 1000) {
|
|
34
|
+
return pc.dim(`(~${(tokens / 1000).toFixed(1)}k tok)`);
|
|
35
|
+
}
|
|
36
|
+
return pc.dim(`(~${tokens} tok)`);
|
|
37
|
+
}
|
package/dist/cli/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { spawn } from 'child_process';
|
|
|
7
7
|
import pc from 'picocolors';
|
|
8
8
|
import qrcode from 'qrcode-terminal';
|
|
9
9
|
import { getTheme } from './theme.js';
|
|
10
|
+
import { DEVX_VERSION } from '../core/types.js';
|
|
10
11
|
const MIME_TYPES = {
|
|
11
12
|
'.html': 'text/html; charset=utf-8',
|
|
12
13
|
'.htm': 'text/html; charset=utf-8',
|
|
@@ -139,7 +140,7 @@ function renderDirectoryHtml(dirPath, relPath, files, port) {
|
|
|
139
140
|
${parentLink}
|
|
140
141
|
${items || '<li style="padding: 20px; text-align: center; color: #6e7681;">No visible files in this directory</li>'}
|
|
141
142
|
</ul>
|
|
142
|
-
<div class="footer">devx
|
|
143
|
+
<div class="footer">devx v${DEVX_VERSION} • Terminal-Native AI Assistant</div>
|
|
143
144
|
</div>
|
|
144
145
|
</body>
|
|
145
146
|
</html>`;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { getCurrentTheme } from './theme.js';
|
|
4
|
+
export class GitStatusCache {
|
|
5
|
+
static cachedInfo = undefined;
|
|
6
|
+
static lastCheckTime = 0;
|
|
7
|
+
static TTL_MS = 10000; // 10s fallback TTL
|
|
8
|
+
static getStatus(forceRefresh = false) {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
if (!forceRefresh && GitStatusCache.cachedInfo !== undefined && (now - GitStatusCache.lastCheckTime < GitStatusCache.TTL_MS)) {
|
|
11
|
+
return GitStatusCache.cachedInfo;
|
|
12
|
+
}
|
|
13
|
+
GitStatusCache.lastCheckTime = now;
|
|
14
|
+
try {
|
|
15
|
+
const branch = execSync('git branch --show-current', {
|
|
16
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
timeout: 1000
|
|
19
|
+
}).trim();
|
|
20
|
+
if (!branch) {
|
|
21
|
+
GitStatusCache.cachedInfo = null;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const status = execSync('git status --porcelain', {
|
|
25
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
timeout: 1000
|
|
28
|
+
}).trim();
|
|
29
|
+
const isDirty = status.length > 0;
|
|
30
|
+
GitStatusCache.cachedInfo = { branch, isDirty };
|
|
31
|
+
return GitStatusCache.cachedInfo;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
GitStatusCache.cachedInfo = null;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
static invalidate() {
|
|
39
|
+
GitStatusCache.cachedInfo = undefined;
|
|
40
|
+
GitStatusCache.lastCheckTime = 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function renderProgressBar(percent, barWidth = 10) {
|
|
44
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
45
|
+
const filledCount = Math.round((clamped / 100) * barWidth);
|
|
46
|
+
const emptyCount = Math.max(0, barWidth - filledCount);
|
|
47
|
+
const bar = '█'.repeat(filledCount) + '░'.repeat(emptyCount);
|
|
48
|
+
if (clamped >= 90)
|
|
49
|
+
return pc.red(`[${bar}] ${clamped}%`);
|
|
50
|
+
if (clamped >= 75)
|
|
51
|
+
return pc.yellow(`[${bar}] ${clamped}%`);
|
|
52
|
+
return pc.cyan(`[${bar}] ${clamped}%`);
|
|
53
|
+
}
|
|
54
|
+
export function formatCostBadge(cost, maxCostUSD) {
|
|
55
|
+
const costFormatted = `$${cost.toFixed(4)}`;
|
|
56
|
+
if (!maxCostUSD || maxCostUSD <= 0) {
|
|
57
|
+
return pc.green(costFormatted);
|
|
58
|
+
}
|
|
59
|
+
const ratio = cost / maxCostUSD;
|
|
60
|
+
if (ratio >= 1.0) {
|
|
61
|
+
return pc.red(costFormatted);
|
|
62
|
+
}
|
|
63
|
+
if (ratio >= 0.8) {
|
|
64
|
+
return pc.yellow(costFormatted);
|
|
65
|
+
}
|
|
66
|
+
return pc.green(costFormatted);
|
|
67
|
+
}
|
|
68
|
+
export function renderPowerlineStatus(opts) {
|
|
69
|
+
const cols = opts.cols || process.stdout.columns || 80;
|
|
70
|
+
const theme = getCurrentTheme();
|
|
71
|
+
// Mode badge
|
|
72
|
+
let modeBadge = '';
|
|
73
|
+
if (opts.isYolo) {
|
|
74
|
+
modeBadge = pc.bgRed(pc.bold(pc.white(' YOLO ')));
|
|
75
|
+
}
|
|
76
|
+
else if (opts.mode === 'PLAN') {
|
|
77
|
+
modeBadge = pc.bgCyan(pc.bold(pc.black(' PLAN ')));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
modeBadge = pc.bgGreen(pc.bold(pc.black(' AGENT ')));
|
|
81
|
+
}
|
|
82
|
+
// Model shortening for narrow screens
|
|
83
|
+
let displayModel = opts.model;
|
|
84
|
+
if (cols < 75 && displayModel.length > 18) {
|
|
85
|
+
const parts = displayModel.split('/');
|
|
86
|
+
displayModel = parts.length > 1 ? parts.slice(1).join('/') : displayModel;
|
|
87
|
+
if (displayModel.length > 18) {
|
|
88
|
+
displayModel = displayModel.slice(0, 15) + '…';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Git info
|
|
92
|
+
const git = GitStatusCache.getStatus();
|
|
93
|
+
const gitStr = git ? ` · ${pc.magenta(git.branch)}${git.isDirty ? pc.yellow('✱') : ''}` : '';
|
|
94
|
+
// Context progress
|
|
95
|
+
const usagePercent = Math.min(100, Math.round((opts.currentTokens / Math.max(1, opts.maxTokens)) * 100));
|
|
96
|
+
const barWidth = cols < 60 ? 6 : 10;
|
|
97
|
+
const progressStr = renderProgressBar(usagePercent, barWidth);
|
|
98
|
+
const costStr = formatCostBadge(opts.cost, opts.maxCostUSD);
|
|
99
|
+
if (cols < 65) {
|
|
100
|
+
// 2-line layout for mobile Termux
|
|
101
|
+
const line1 = `${modeBadge} ${pc.bold(displayModel)}${gitStr}`;
|
|
102
|
+
const line2 = `${pc.dim('│')} Context ${progressStr} · ${costStr}`;
|
|
103
|
+
return `${line1}\n${line2}`;
|
|
104
|
+
}
|
|
105
|
+
// 1-line powerline layout for desktop
|
|
106
|
+
return `${modeBadge} ${pc.bold(displayModel)}${gitStr} · Context ${progressStr} · ${costStr}`;
|
|
107
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
export class LiveTelemetryTracker {
|
|
3
|
+
startTime = 0;
|
|
4
|
+
phase = 'connecting';
|
|
5
|
+
totalTokens = 0;
|
|
6
|
+
samples = [];
|
|
7
|
+
windowMs = 2000;
|
|
8
|
+
constructor(windowMs = 2000) {
|
|
9
|
+
this.windowMs = windowMs;
|
|
10
|
+
}
|
|
11
|
+
start(phase = 'connecting') {
|
|
12
|
+
this.startTime = Date.now();
|
|
13
|
+
this.phase = phase;
|
|
14
|
+
this.totalTokens = 0;
|
|
15
|
+
this.samples = [];
|
|
16
|
+
}
|
|
17
|
+
setPhase(phase) {
|
|
18
|
+
this.phase = phase;
|
|
19
|
+
}
|
|
20
|
+
recordTokens(count) {
|
|
21
|
+
if (count <= 0)
|
|
22
|
+
return;
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
this.totalTokens += count;
|
|
25
|
+
this.samples.push({ timestamp: now, tokens: count });
|
|
26
|
+
this.pruneOldSamples(now);
|
|
27
|
+
}
|
|
28
|
+
estimateTokensFromText(text) {
|
|
29
|
+
if (!text)
|
|
30
|
+
return 0;
|
|
31
|
+
// Fast token estimation: ~0.8 tokens per Cyrillic char, ~0.25 per ASCII
|
|
32
|
+
let tokens = 0;
|
|
33
|
+
for (let i = 0; i < text.length; i++) {
|
|
34
|
+
const code = text.charCodeAt(i);
|
|
35
|
+
if (code >= 0x0400 && code <= 0x04ff) {
|
|
36
|
+
tokens += 0.8;
|
|
37
|
+
}
|
|
38
|
+
else if (code > 0x07ff) {
|
|
39
|
+
tokens += 1.2;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
tokens += 0.25;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return Math.max(1, Math.round(tokens));
|
|
46
|
+
}
|
|
47
|
+
pruneOldSamples(now) {
|
|
48
|
+
const threshold = now - this.windowMs;
|
|
49
|
+
while (this.samples.length > 0 && this.samples[0].timestamp < threshold) {
|
|
50
|
+
this.samples.shift();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
getTokensPerSecond() {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
this.pruneOldSamples(now);
|
|
56
|
+
if (this.samples.length === 0)
|
|
57
|
+
return 0;
|
|
58
|
+
const windowTokens = this.samples.reduce((acc, s) => acc + s.tokens, 0);
|
|
59
|
+
const earliest = this.samples[0].timestamp;
|
|
60
|
+
const durationSec = Math.max(0.2, (now - earliest) / 1000);
|
|
61
|
+
return Math.round(windowTokens / durationSec);
|
|
62
|
+
}
|
|
63
|
+
getElapsedSeconds() {
|
|
64
|
+
if (this.startTime === 0)
|
|
65
|
+
return 0;
|
|
66
|
+
return Math.max(0, Math.round((Date.now() - this.startTime) / 1000));
|
|
67
|
+
}
|
|
68
|
+
getTotalTokens() {
|
|
69
|
+
return this.totalTokens;
|
|
70
|
+
}
|
|
71
|
+
static formatTokenCount(n) {
|
|
72
|
+
if (n >= 1_000_000)
|
|
73
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
74
|
+
if (n >= 1_000)
|
|
75
|
+
return `${(n / 1_000).toFixed(1)}k`;
|
|
76
|
+
return `${Math.round(n)}`;
|
|
77
|
+
}
|
|
78
|
+
formatLabel() {
|
|
79
|
+
const secs = `${this.getElapsedSeconds()}s`;
|
|
80
|
+
const tokens = LiveTelemetryTracker.formatTokenCount(this.totalTokens);
|
|
81
|
+
const tps = this.getTokensPerSecond();
|
|
82
|
+
if (this.phase === 'connecting') {
|
|
83
|
+
return `connecting… ${pc.dim(secs)}`;
|
|
84
|
+
}
|
|
85
|
+
if (this.phase === 'thinking') {
|
|
86
|
+
if (this.totalTokens > 0) {
|
|
87
|
+
return `thinking… ${pc.dim(`${secs} · ${tokens} tok · ${tps} tok/s`)}`;
|
|
88
|
+
}
|
|
89
|
+
return `thinking… ${pc.dim(secs)}`;
|
|
90
|
+
}
|
|
91
|
+
if (this.phase === 'generating') {
|
|
92
|
+
if (this.totalTokens > 0) {
|
|
93
|
+
return `generating… ${pc.dim(`${secs} · ${tokens} tok · ${tps} tok/s`)}`;
|
|
94
|
+
}
|
|
95
|
+
return `generating… ${pc.dim(secs)}`;
|
|
96
|
+
}
|
|
97
|
+
return `executing… ${pc.dim(secs)}`;
|
|
98
|
+
}
|
|
99
|
+
reset() {
|
|
100
|
+
this.startTime = 0;
|
|
101
|
+
this.phase = 'connecting';
|
|
102
|
+
this.totalTokens = 0;
|
|
103
|
+
this.samples = [];
|
|
104
|
+
}
|
|
105
|
+
}
|
package/dist/core/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const DEVX_VERSION = '1.5.0';
|
package/dist/mcp/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { DEVX_VERSION } from '../core/types.js';
|
|
2
3
|
export class MCPClient {
|
|
3
4
|
name;
|
|
4
5
|
config;
|
|
@@ -63,7 +64,7 @@ export class MCPClient {
|
|
|
63
64
|
capabilities: {},
|
|
64
65
|
clientInfo: {
|
|
65
66
|
name: 'devx',
|
|
66
|
-
version:
|
|
67
|
+
version: DEVX_VERSION
|
|
67
68
|
}
|
|
68
69
|
});
|
|
69
70
|
if (!initResult) {
|
|
@@ -25,14 +25,19 @@ function isDangerousBashCommand(commandStr) {
|
|
|
25
25
|
return false;
|
|
26
26
|
// Normalize whitespace: collapse multiple spaces/tabs into single space
|
|
27
27
|
const norm = commandStr.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
28
|
-
// Pattern matching rm with combined flags (-rf, -fr, -r -f, etc.) targeting root, home, termux prefix, or
|
|
29
|
-
const isDangerousRm = /rm\s+-(?:[a-z]*r[a-z]*f|[a-z]*f[a-z]*r)\s+(
|
|
30
|
-
/rm\s+-[a-z]*r[a-z]*\s+-[a-z]*f[a-z]*\s+(
|
|
31
|
-
/rm\s+-[a-z]*f[a-z]*\s+-[a-z]*r[a-z]*\s+(
|
|
28
|
+
// Pattern matching rm with combined flags (-rf, -fr, -r -f, etc.) targeting root, home, termux prefix, glob, or current/parent dir
|
|
29
|
+
const isDangerousRm = /rm\s+-(?:[a-z]*r[a-z]*f|[a-z]*f[a-z]*r)\s+(?:\/|\/\*|~|~\/|\*|\.|\.\/|\.\.\/?.*|\$home|\$prefix|\/data\/data\/com\.termux)/i.test(norm) ||
|
|
30
|
+
/rm\s+-[a-z]*r[a-z]*\s+-[a-z]*f[a-z]*\s+(?:\/|\/\*|~|~\/|\*|\.|\.\/|\.\.\/?.*|\$home|\$prefix|\/data\/data\/com\.termux)/i.test(norm) ||
|
|
31
|
+
/rm\s+-[a-z]*f[a-z]*\s+-[a-z]*r[a-z]*\s+(?:\/|\/\*|~|~\/|\*|\.|\.\/|\.\.\/?.*|\$home|\$prefix|\/data\/data\/com\.termux)/i.test(norm);
|
|
32
32
|
return (isDangerousRm ||
|
|
33
33
|
norm.includes('rm -rf /') ||
|
|
34
34
|
norm.includes('rm -rf ~') ||
|
|
35
35
|
norm.includes('rm -rf *') ||
|
|
36
|
+
norm.includes('rm -rf .') ||
|
|
37
|
+
norm.includes('rm -rf ./') ||
|
|
38
|
+
norm.includes('rm -rf ..') ||
|
|
39
|
+
norm.includes('rm -fr .') ||
|
|
40
|
+
norm.includes('rm -fr ./') ||
|
|
36
41
|
norm.includes('rm -rf $prefix') ||
|
|
37
42
|
norm.includes('rm -rf $home') ||
|
|
38
43
|
norm.includes('rm -rf /data/data/com.termux') ||
|
package/dist/providers/openai.js
CHANGED
|
@@ -220,11 +220,12 @@ export class OpenAIProvider {
|
|
|
220
220
|
const trimmed = line.trim();
|
|
221
221
|
if (!trimmed || trimmed.startsWith(':'))
|
|
222
222
|
continue;
|
|
223
|
-
if (trimmed
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
if (trimmed.startsWith('data:')) {
|
|
224
|
+
const dataPayload = trimmed.replace(/^data:\s*/, '');
|
|
225
|
+
if (dataPayload === '[DONE]')
|
|
226
|
+
continue;
|
|
226
227
|
try {
|
|
227
|
-
const json = JSON.parse(
|
|
228
|
+
const json = JSON.parse(dataPayload);
|
|
228
229
|
if (json.error) {
|
|
229
230
|
const errMsg = json.error.message || json.error.code || JSON.stringify(json.error);
|
|
230
231
|
throw new Error(`Provider Stream Error: ${errMsg}`);
|