termux-dev 1.0.2 → 1.1.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 CHANGED
@@ -7,8 +7,9 @@
7
7
  **Ultra-fast, autonomous AI pair-programmer & vibe-coding terminal agent.**
8
8
  Built specifically for **Android Termux**, **Windows**, **macOS**, and **Linux**.
9
9
 
10
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
10
+ [![npm version](https://img.shields.io/npm/v/termux-dev.svg?color=blue)](https://www.npmjs.com/package/termux-dev)
11
11
  [![CI](https://github.com/apvcode/Termux-Dev/actions/workflows/ci.yml/badge.svg)](https://github.com/apvcode/Termux-Dev/actions/workflows/ci.yml)
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
12
13
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20.0.0-brightgreen.svg)](https://nodejs.org)
13
14
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.4-blue.svg)](https://www.typescriptlang.org)
14
15
  [![Platform](https://img.shields.io/badge/Platform-Termux%20%7C%20Windows%20%7C%20macOS%20%7C%20Linux-orange.svg)](#)
package/assets/banner.svg CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  <!-- Tagline & Badge -->
27
27
  <text x="425" y="142" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="600" fill="#8b949e" text-anchor="middle" letter-spacing="3">
28
- THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.0.2</tspan>
28
+ THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.1.0</tspan>
29
29
  </text>
30
30
 
31
31
  <!-- Top Accent Line -->
package/dist/cli/index.js CHANGED
@@ -20,6 +20,7 @@ import { ALL_PROVIDERS } from './providers.js';
20
20
  import { getModelContextLimit } from '../core/models.js';
21
21
  import { SessionManager } from '../core/session.js';
22
22
  import { MarkdownStreamer, renderMarkdown } from './markdown.js';
23
+ import { SmoothStreamer } from './smooth.js';
23
24
  import { askPrompt } from './prompt.js';
24
25
  import { resolveAtMentions } from './files.js';
25
26
  import { runStartupUpdateCheck, checkForUpdates, performSelfUpdate } from './updater.js';
@@ -377,7 +378,7 @@ function drawLogo() {
377
378
  pc.cyan(' █▀▀▄ █▀▀▀ █ █ █ █'),
378
379
  pc.cyan(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
379
380
  pc.cyan(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
380
- ' ' + pc.cyan(pc.bold('v1.0.2')),
381
+ ' ' + pc.cyan(pc.bold('v1.1.0')),
381
382
  ''
382
383
  ];
383
384
  for (const line of logo) {
@@ -392,7 +393,7 @@ function drawLogo() {
392
393
  indent + pc.cyan('▀▀▀█▀▀▀ █▀▀▀ █▀▀█ █▄ ▄█ █ █ ▀▄ ▄▀ █▀▀▄ █▀▀▀ █ █'),
393
394
  indent + pc.cyan(' █ █▀▀▀ █▄▄▀ █ █ █ █ █ █ ▀▀ █ █ █▀▀▀ █ █'),
394
395
  indent + pc.cyan(' █ █▄▄▄ █ ▀▄ █ █ ▀▄▄▀ ▄▀ ▀▄ █▄▄▀ █▄▄▄ ▀▄▀ '),
395
- indent + pc.cyan(pc.bold('v1.0.2')),
396
+ indent + pc.cyan(pc.bold('v1.1.0')),
396
397
  ''
397
398
  ];
398
399
  for (const line of logo) {
@@ -587,7 +588,7 @@ export async function main() {
587
588
  const maxIter = config.maxIterations || 100;
588
589
  const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
589
590
  const choice = await select({
590
- message: `${pc.bold('⚙️ Settings')} ${pc.dim('(devx v1.0.2 • by ApvCode)')}`,
591
+ message: `${pc.bold('⚙️ Settings')} ${pc.dim('(devx v1.1.0 • by ApvCode)')}`,
591
592
  choices: [
592
593
  {
593
594
  name: `${config.pureBlackTheme !== false ? pc.green('🎨 Pure Black Background: ON') : pc.yellow('🎨 Pure Black Background: OFF')}`,
@@ -623,7 +624,7 @@ export async function main() {
623
624
  description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
624
625
  },
625
626
  {
626
- name: `${pc.cyan('✨ About devx')} ${pc.dim('(v1.0.2 by ApvCode)')}`,
627
+ name: `${pc.cyan('✨ About devx')} ${pc.dim('(v1.1.0 by ApvCode)')}`,
627
628
  value: 'about',
628
629
  description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
629
630
  },
@@ -635,7 +636,7 @@ export async function main() {
635
636
  ]
636
637
  });
637
638
  if (choice === 'about') {
638
- p.note(`⚡ devx v1.0.2 — Terminal-Native AI Coding Agent\n` +
639
+ p.note(`⚡ devx v1.1.0 — Terminal-Native AI Coding Agent\n` +
639
640
  `👤 Author: ApvCode (https://github.com/apvcode)\n` +
640
641
  `🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
641
642
  `📜 License: MIT License (2026)\n` +
@@ -1276,9 +1277,13 @@ export async function main() {
1276
1277
  let thinkingStartTime = 0;
1277
1278
  let textActive = false;
1278
1279
  const streamer = new MarkdownStreamer();
1280
+ const thoughtStreamer = new SmoothStreamer((chunk) => {
1281
+ process.stdout.write(pc.dim(chunk));
1282
+ });
1279
1283
  const finishThinking = (extraNewline = true) => {
1280
1284
  if (!thinkingActive)
1281
1285
  return;
1286
+ thoughtStreamer.flush();
1282
1287
  const elapsedSec = thinkingStartTime > 0 ? ((Date.now() - thinkingStartTime) / 1000).toFixed(1) : '0.0';
1283
1288
  const width = Math.min(process.stdout.columns || 40, 52);
1284
1289
  const label = `─── Thought for ${elapsedSec}s `;
@@ -1298,6 +1303,7 @@ export async function main() {
1298
1303
  s.stop();
1299
1304
  spinnerActive = false;
1300
1305
  }
1306
+ thoughtStreamer.reset();
1301
1307
  finishThinking(true);
1302
1308
  streamer.finish();
1303
1309
  if (textActive) {
@@ -1338,7 +1344,7 @@ export async function main() {
1338
1344
  process.stdout.write('\n' + pc.bold(pc.white('› Thought:')) + '\n');
1339
1345
  thinkingActive = true;
1340
1346
  }
1341
- process.stdout.write(pc.dim(event.delta));
1347
+ thoughtStreamer.push(event.delta);
1342
1348
  }
1343
1349
  else if (event.type === 'text_delta') {
1344
1350
  if (spinnerActive) {
@@ -1384,8 +1390,8 @@ export async function main() {
1384
1390
  process.stdout.write('\n');
1385
1391
  textActive = false;
1386
1392
  }
1387
- console.log(pc.cyan(`\n${event.actionDesc}`));
1388
- if (event.name !== 'ask_questions') {
1393
+ console.log('\n' + pc.bold(pc.white(event.actionDesc)));
1394
+ if (event.name !== 'ask_questions' && event.name !== 'todo_list') {
1389
1395
  s.start('Executing...');
1390
1396
  spinnerActive = true;
1391
1397
  }
@@ -1396,15 +1402,85 @@ export async function main() {
1396
1402
  spinnerActive = false;
1397
1403
  }
1398
1404
  if (event.result) {
1399
- const firstLine = event.result.trim().split('\n')[0];
1400
- if (firstLine.includes('+') || firstLine.includes('lines') || firstLine.includes('Successfully')) {
1401
- console.log(pc.green(` └─ ${firstLine}`));
1405
+ try {
1406
+ const parsed = JSON.parse(event.result);
1407
+ if (parsed.type === 'todo_list' && parsed.displayCard) {
1408
+ process.stdout.write(parsed.displayCard);
1409
+ }
1410
+ else if (parsed.action === 'edit' && parsed.diffLines) {
1411
+ const cols = Math.min(process.stdout.columns || 80, 80);
1412
+ const borderChar = '─';
1413
+ const boxWidth = Math.max(20, Math.min(cols - 4, 66));
1414
+ console.log(pc.dim('┌' + borderChar.repeat(boxWidth) + '┐'));
1415
+ const maxShown = Math.min(parsed.diffLines.length, 12);
1416
+ for (let i = 0; i < maxShown; i++) {
1417
+ const line = parsed.diffLines[i];
1418
+ if (line.includes(' - ')) {
1419
+ console.log(pc.dim('│ ') + pc.red(line));
1420
+ }
1421
+ else if (line.includes(' + ')) {
1422
+ console.log(pc.dim('│ ') + pc.green(line));
1423
+ }
1424
+ else {
1425
+ console.log(pc.dim('│ ') + pc.white(line));
1426
+ }
1427
+ }
1428
+ if (parsed.diffLines.length > maxShown) {
1429
+ console.log(pc.dim(`│ ... +${parsed.diffLines.length - maxShown} more lines`));
1430
+ }
1431
+ console.log(pc.dim('└' + borderChar.repeat(boxWidth) + '┘'));
1432
+ console.log(pc.green(` └─ ${parsed.summary}`));
1433
+ }
1434
+ else if (parsed.summary) {
1435
+ console.log(pc.green(` └─ ${parsed.summary}`));
1436
+ }
1437
+ else {
1438
+ const firstLine = event.result.trim().split('\n')[0];
1439
+ if (firstLine.includes('+') || firstLine.includes('lines') || firstLine.includes('Successfully')) {
1440
+ console.log(pc.green(` └─ ${firstLine}`));
1441
+ }
1442
+ }
1443
+ }
1444
+ catch {
1445
+ const firstLine = event.result.trim().split('\n')[0];
1446
+ if (firstLine.includes('+') || firstLine.includes('lines') || firstLine.includes('Successfully')) {
1447
+ console.log(pc.green(` └─ ${firstLine}`));
1448
+ }
1402
1449
  }
1403
1450
  }
1404
1451
  }
1405
1452
  else if (event.type === 'usage') {
1406
1453
  totalSessionCost += event.usage.cost || 0;
1407
1454
  }
1455
+ else if (event.type === 'reconnecting') {
1456
+ if (spinnerActive) {
1457
+ s.stop();
1458
+ spinnerActive = false;
1459
+ }
1460
+ if (thinkingActive) {
1461
+ finishThinking(false);
1462
+ }
1463
+ if (textActive) {
1464
+ process.stdout.write('\n');
1465
+ textActive = false;
1466
+ }
1467
+ streamer.reset();
1468
+ console.log();
1469
+ console.log(pc.bold(pc.yellow(`⚠️ Интернет-соединение нестабильно (${event.reason})`)));
1470
+ console.log(pc.cyan(`🔄 Попытка переподключения [${event.attempt}/${event.maxAttempts}] (пауза ${(event.delayMs / 1000).toFixed(1)}с)...`));
1471
+ s.start(pc.cyan(`Ожидание сети [${event.attempt}/${event.maxAttempts}]...`));
1472
+ spinnerActive = true;
1473
+ }
1474
+ else if (event.type === 'reconnected') {
1475
+ if (spinnerActive) {
1476
+ s.stop();
1477
+ spinnerActive = false;
1478
+ }
1479
+ console.log(pc.bold(pc.green(`✔ Соединение восстановлено! Возобновляем генерацию...`)));
1480
+ console.log();
1481
+ s.start('Connecting...');
1482
+ spinnerActive = true;
1483
+ }
1408
1484
  else if (event.type === 'error') {
1409
1485
  if (spinnerActive) {
1410
1486
  s.stop();
@@ -28,10 +28,38 @@ export class MarkdownStreamer {
28
28
  tableBuffer = [];
29
29
  push(chunk) {
30
30
  this.buffer += chunk;
31
- const lines = this.buffer.split('\n');
32
- this.buffer = lines.pop() || '';
33
- for (const line of lines) {
34
- this.processLine(line);
31
+ // 1. Process completed lines
32
+ if (this.buffer.includes('\n')) {
33
+ const lines = this.buffer.split('\n');
34
+ this.buffer = lines.pop() || '';
35
+ for (const line of lines) {
36
+ this.processLine(line);
37
+ }
38
+ }
39
+ // 2. Stream regular prose words immediately in real-time
40
+ if (!this.inCodeBlock && !this.inTable && this.buffer.length > 0) {
41
+ const trimmed = this.buffer.trimStart();
42
+ const isSpecialLine = trimmed.startsWith('#') ||
43
+ trimmed.startsWith('|') ||
44
+ trimmed.startsWith('```') ||
45
+ trimmed.startsWith('- [') ||
46
+ trimmed.startsWith('* [') ||
47
+ trimmed.startsWith('[') ||
48
+ trimmed.startsWith('→') ||
49
+ trimmed.startsWith('- ') ||
50
+ trimmed.startsWith('* ') ||
51
+ /^\d+\.\s/.test(trimmed) ||
52
+ trimmed.startsWith('> ') ||
53
+ trimmed === '---' ||
54
+ trimmed === '***';
55
+ if (!isSpecialLine) {
56
+ const lastSpaceIdx = Math.max(this.buffer.lastIndexOf(' '), this.buffer.lastIndexOf('\t'));
57
+ if (lastSpaceIdx > 0) {
58
+ const wordsToFlush = this.buffer.slice(0, lastSpaceIdx + 1);
59
+ this.buffer = this.buffer.slice(lastSpaceIdx + 1);
60
+ process.stdout.write(this.formatInline(wordsToFlush));
61
+ }
62
+ }
35
63
  }
36
64
  }
37
65
  processLine(line) {
@@ -67,7 +95,28 @@ export class MarkdownStreamer {
67
95
  this.flushTable();
68
96
  this.inTable = false;
69
97
  }
70
- // 4. Regular line fast formatting (Headers, Bullets, Bold, Dims)
98
+ // 4. OpenCode Checklists & Todos ([✓], [ ], [/], [>])
99
+ const todoMatch = trimmed.match(/^(\s*(?:[-*]\s*)?)\[([ x✓X>/])\]\s*(.*)$/);
100
+ if (todoMatch) {
101
+ const mark = todoMatch[2].toLowerCase();
102
+ const taskText = this.formatInline(todoMatch[3]);
103
+ if (mark === 'x' || mark === '✓') {
104
+ process.stdout.write(' ' + pc.green('[✓] ') + pc.dim(taskText) + '\n');
105
+ }
106
+ else if (mark === '/' || mark === '>') {
107
+ process.stdout.write(' ' + pc.bold(pc.green('[ ] ' + taskText)) + '\n');
108
+ }
109
+ else {
110
+ process.stdout.write(' ' + pc.dim('[ ] ') + pc.white(taskText) + '\n');
111
+ }
112
+ return;
113
+ }
114
+ // 5. OpenCode Action arrows (→ Read ..., → Edit ...)
115
+ if (trimmed.startsWith('→ ')) {
116
+ process.stdout.write(pc.bold(pc.cyan('→ ')) + pc.white(this.formatInline(trimmed.slice(2))) + '\n');
117
+ return;
118
+ }
119
+ // 6. Regular line fast formatting (Headers, Bullets, Bold, Dims)
71
120
  if (trimmed.startsWith('# ')) {
72
121
  process.stdout.write('\n' + pc.bold(pc.cyan(trimmed.slice(2))) + '\n\n');
73
122
  }
@@ -123,6 +172,13 @@ export class MarkdownStreamer {
123
172
  this.tableBuffer = [];
124
173
  }
125
174
  }
175
+ reset() {
176
+ this.buffer = '';
177
+ this.inCodeBlock = false;
178
+ this.codeBlockLang = '';
179
+ this.inTable = false;
180
+ this.tableBuffer = [];
181
+ }
126
182
  finish() {
127
183
  if (this.buffer) {
128
184
  const trimmed = this.buffer.trim();
@@ -0,0 +1,77 @@
1
+ /**
2
+ * High-performance fluid token streamer for terminal vibe-coding.
3
+ * Turns jerky SSE network chunk bursts into buttery-smooth word-by-word and character-by-character flow.
4
+ */
5
+ export class SmoothStreamer {
6
+ queue = [];
7
+ timer = null;
8
+ onWrite;
9
+ constructor(onWrite) {
10
+ this.onWrite = onWrite;
11
+ }
12
+ push(text) {
13
+ if (!text)
14
+ return;
15
+ this.queue.push(text);
16
+ if (!this.timer) {
17
+ this.startLoop();
18
+ }
19
+ }
20
+ startLoop() {
21
+ this.timer = setInterval(() => {
22
+ this.tick();
23
+ }, 10);
24
+ }
25
+ tick() {
26
+ if (this.queue.length === 0) {
27
+ if (this.timer) {
28
+ clearInterval(this.timer);
29
+ this.timer = null;
30
+ }
31
+ return;
32
+ }
33
+ const fullStr = this.queue.join('');
34
+ this.queue = [];
35
+ // Adaptive chunk size based on remaining buffer backlog:
36
+ // Small backlog (< 30 chars) -> 3-6 chars / tick (~60-100 fps typing effect)
37
+ // Medium backlog (30-100 chars) -> 12-20 chars / tick
38
+ // Large backlog (> 100 chars) -> flush 50-100 chars / tick (never lags behind fast LLMs)
39
+ let take = 4;
40
+ if (fullStr.length > 200) {
41
+ take = fullStr.length;
42
+ }
43
+ else if (fullStr.length > 100) {
44
+ take = 25;
45
+ }
46
+ else if (fullStr.length > 40) {
47
+ take = 10;
48
+ }
49
+ else {
50
+ take = 4;
51
+ }
52
+ const chunk = fullStr.slice(0, take);
53
+ const rest = fullStr.slice(take);
54
+ if (rest) {
55
+ this.queue.unshift(rest);
56
+ }
57
+ this.onWrite(chunk);
58
+ }
59
+ flush() {
60
+ if (this.timer) {
61
+ clearInterval(this.timer);
62
+ this.timer = null;
63
+ }
64
+ if (this.queue.length > 0) {
65
+ const remaining = this.queue.join('');
66
+ this.queue = [];
67
+ this.onWrite(remaining);
68
+ }
69
+ }
70
+ reset() {
71
+ if (this.timer) {
72
+ clearInterval(this.timer);
73
+ this.timer = null;
74
+ }
75
+ this.queue = [];
76
+ }
77
+ }
package/dist/core/loop.js CHANGED
@@ -1,33 +1,40 @@
1
+ import { isNetworkOrRetryableError, getHumanReadableNetworkError, waitDelay } from './network.js';
1
2
  function getActionDescription(toolName, args) {
2
3
  const t = toolName.toLowerCase();
3
4
  if (t === 'read_file' || t === 'view_file' || t === 'read') {
4
- return `📖 Reading: ${args.path || args.filePath || args.targetFile || ''}`;
5
+ return `→ Read ${args.path || args.filePath || args.targetFile || ''}`;
5
6
  }
6
7
  if (t === 'write_file' || t === 'create_file' || t === 'write_to_file' || t === 'write') {
7
- return `📝 Writing: ${args.path || args.filePath || args.targetFile || ''}`;
8
+ return `→ Write ${args.path || args.filePath || args.targetFile || ''}`;
8
9
  }
9
10
  if (t === 'edit_file' || t === 'replace_file_content' || t === 'patch') {
10
- return `✏️ Editing: ${args.path || args.filePath || args.targetFile || ''}`;
11
+ return `→ Edit ${args.path || args.filePath || args.targetFile || ''}`;
11
12
  }
12
13
  if (t === 'delete_file' || t === 'remove_file' || t === 'rm') {
13
- return `🗑️ Deleting: ${args.path || args.filePath || ''}`;
14
+ return `→ Delete ${args.path || args.filePath || ''}`;
14
15
  }
15
16
  if (t === 'make_dir' || t === 'mkdir') {
16
- return `📁 Creating directory: ${args.path || args.dirPath || ''}`;
17
+ return `→ Mkdir ${args.path || args.dirPath || ''}`;
17
18
  }
18
19
  if (t === 'list_dir' || t === 'list_files' || t === 'find_files' || t === 'ls') {
19
- return `📂 Listing: ${args.path || args.dirPath || '.'}`;
20
+ return `→ List ${args.path || args.dirPath || '.'}`;
20
21
  }
21
22
  if (t === 'search' || t === 'grep_search' || t === 'grep') {
22
- return `🔍 Searching: "${args.query || args.pattern || ''}" in ${args.dir || args.path || '.'}`;
23
+ return `→ Search "${args.query || args.pattern || ''}" in ${args.dir || args.path || '.'}`;
23
24
  }
24
25
  if (t === 'ask_questions' || t === 'questions') {
25
- return `📋 Asking clarifying questions...`;
26
+ return `→ Clarify: Asking questions...`;
26
27
  }
27
28
  if (t === 'bash' || t === 'execute_command' || t === 'run_command' || t === 'exec') {
28
- return `⚡ Running command: ${args.command || args.cmd || ''}`;
29
+ return `→ Run: ${args.command || args.cmd || ''}`;
29
30
  }
30
- return `🔧 Executing ${toolName}...`;
31
+ if (t === 'todo_list' || t === 'update_todos') {
32
+ return `→ Update Plan & Tasks`;
33
+ }
34
+ if (t === 'plan_ready') {
35
+ return `→ Finalize Plan`;
36
+ }
37
+ return `→ Executing ${toolName}...`;
31
38
  }
32
39
  export class Agent {
33
40
  config;
@@ -61,36 +68,68 @@ export class Agent {
61
68
  delete request.tools;
62
69
  }
63
70
  let response;
64
- try {
65
- if (this.provider.chatStream) {
66
- for await (const chunk of this.provider.chatStream(request)) {
67
- if (signal?.aborted) {
68
- return;
69
- }
70
- if (chunk.type === 'reasoning_delta') {
71
- yield { type: 'reasoning_delta', delta: chunk.delta };
72
- }
73
- else if (chunk.type === 'content_delta') {
74
- yield { type: 'text_delta', delta: chunk.delta };
75
- }
76
- else if (chunk.type === 'tool_generating') {
77
- yield { type: 'tool_generating', name: chunk.name, bytes: chunk.bytes };
71
+ let attempt = 0;
72
+ const maxRetries = 4;
73
+ while (true) {
74
+ if (signal?.aborted)
75
+ return;
76
+ try {
77
+ if (this.provider.chatStream) {
78
+ let receivedAnyChunk = false;
79
+ for await (const chunk of this.provider.chatStream(request)) {
80
+ if (signal?.aborted) {
81
+ return;
82
+ }
83
+ receivedAnyChunk = true;
84
+ if (chunk.type === 'reasoning_delta') {
85
+ yield { type: 'reasoning_delta', delta: chunk.delta };
86
+ }
87
+ else if (chunk.type === 'content_delta') {
88
+ yield { type: 'text_delta', delta: chunk.delta };
89
+ }
90
+ else if (chunk.type === 'tool_generating') {
91
+ yield { type: 'tool_generating', name: chunk.name, bytes: chunk.bytes };
92
+ }
93
+ else if (chunk.type === 'done') {
94
+ response = chunk.response;
95
+ }
78
96
  }
79
- else if (chunk.type === 'done') {
80
- response = chunk.response;
97
+ if (response || receivedAnyChunk) {
98
+ break;
81
99
  }
82
100
  }
101
+ else {
102
+ response = await this.provider.chat(request);
103
+ break;
104
+ }
83
105
  }
84
- else {
85
- response = await this.provider.chat(request);
86
- }
87
- }
88
- catch (err) {
89
- if (signal?.aborted || err.name === 'AbortError' || err.message?.includes('aborted')) {
106
+ catch (err) {
107
+ if (signal?.aborted || err.name === 'AbortError' || err.message?.includes('aborted')) {
108
+ return;
109
+ }
110
+ const isNetwork = isNetworkOrRetryableError(err);
111
+ if (isNetwork && attempt < maxRetries) {
112
+ attempt++;
113
+ const delayMs = Math.min(1500 * Math.pow(1.5, attempt - 1), 8000);
114
+ yield {
115
+ type: 'reconnecting',
116
+ attempt,
117
+ maxAttempts: maxRetries,
118
+ delayMs,
119
+ reason: getHumanReadableNetworkError(err)
120
+ };
121
+ await waitDelay(delayMs, signal);
122
+ if (signal?.aborted)
123
+ return;
124
+ yield { type: 'reconnected', attempt };
125
+ continue;
126
+ }
127
+ const errMsg = isNetwork
128
+ ? `Ошибка сети: Не удалось восстановить интернет-соединение после ${attempt} попыток (${err.message}).`
129
+ : `API Error: ${err.message}`;
130
+ yield { type: 'error', message: errMsg, isFatal: true };
90
131
  return;
91
132
  }
92
- yield { type: 'error', message: `API Error: ${err.message}`, isFatal: true };
93
- return;
94
133
  }
95
134
  if (signal?.aborted) {
96
135
  return;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Network Resilience & Error Utilities for devx
3
+ */
4
+ export function isNetworkOrRetryableError(err) {
5
+ if (!err)
6
+ return false;
7
+ const msg = (err.message || String(err)).toLowerCase();
8
+ const code = (err.code || err.cause?.code || '').toLowerCase();
9
+ const status = err.status || err.statusCode || 0;
10
+ // HTTP status codes that are retryable (Rate limits, server errors, timeouts)
11
+ if (status === 429 ||
12
+ status === 500 ||
13
+ status === 502 ||
14
+ status === 503 ||
15
+ status === 504 ||
16
+ status === 520 ||
17
+ status === 521 ||
18
+ status === 522 ||
19
+ status === 524) {
20
+ return true;
21
+ }
22
+ // Network / Socket error codes
23
+ const networkCodes = [
24
+ 'econnreset',
25
+ 'etimedout',
26
+ 'enotfound',
27
+ 'econnrefused',
28
+ 'ehostunreach',
29
+ 'enetunreach',
30
+ 'eai_again',
31
+ 'und_err_socket',
32
+ 'und_err_connect_timeout',
33
+ 'und_err_headers_timeout',
34
+ 'und_err_body_timeout',
35
+ 'und_err_request_timeout',
36
+ 'err_network'
37
+ ];
38
+ if (networkCodes.some(c => code.includes(c))) {
39
+ return true;
40
+ }
41
+ // Common network error messages & fragments
42
+ const networkPatterns = [
43
+ 'fetch failed',
44
+ 'terminated',
45
+ 'socket hang up',
46
+ 'network error',
47
+ 'network timeout',
48
+ 'failed to fetch',
49
+ 'premature close',
50
+ 'other side closed',
51
+ 'connection reset',
52
+ 'connection refused',
53
+ 'timeout',
54
+ 'time out',
55
+ 'econnreset',
56
+ 'etimedout',
57
+ 'enotfound',
58
+ 'dns lookup',
59
+ 'temporary failure',
60
+ 'server error',
61
+ 'bad gateway',
62
+ 'gateway timeout',
63
+ 'overloaded',
64
+ 'service unavailable',
65
+ 'empty stream',
66
+ 'connection closed before receiving response',
67
+ 'rate limit'
68
+ ];
69
+ return networkPatterns.some(p => msg.includes(p));
70
+ }
71
+ export function getHumanReadableNetworkError(err) {
72
+ if (!err)
73
+ return 'Неизвестная ошибка сети (Network error)';
74
+ const msg = (err.message || String(err)).toLowerCase();
75
+ const code = (err.code || err.cause?.code || '').toLowerCase();
76
+ const status = err.status || err.statusCode || 0;
77
+ if (status === 429 || msg.includes('429') || msg.includes('rate limit')) {
78
+ return 'Превышен лимит запросов API (Rate limit / 429)';
79
+ }
80
+ if (status === 502 || msg.includes('502') || msg.includes('bad gateway')) {
81
+ return 'Шлюз провайдера временно недоступен (Bad Gateway / 502)';
82
+ }
83
+ if (status === 503 || msg.includes('503') || msg.includes('service unavailable')) {
84
+ return 'Сервер провайдера перегружен (Service Unavailable / 503)';
85
+ }
86
+ if (status === 504 || msg.includes('504') || msg.includes('gateway timeout')) {
87
+ return 'Таймаут шлюза провайдера (Gateway Timeout / 504)';
88
+ }
89
+ if (code.includes('enotfound') || msg.includes('enotfound') || code.includes('eai_again')) {
90
+ return 'Нет подключения к сети или DNS (Host not found / Offline)';
91
+ }
92
+ if (code.includes('econnreset') ||
93
+ msg.includes('econnreset') ||
94
+ msg.includes('connection reset') ||
95
+ msg.includes('socket hang up') ||
96
+ msg.includes('terminated')) {
97
+ return 'Обрыв соединения с сервером (Connection reset / Socket closed)';
98
+ }
99
+ if (code.includes('etimedout') ||
100
+ msg.includes('timeout') ||
101
+ msg.includes('time out') ||
102
+ code.includes('und_err_connect_timeout')) {
103
+ return 'Таймаут сетевого соединения (Network timeout)';
104
+ }
105
+ if (code.includes('econnrefused') || msg.includes('connection refused')) {
106
+ return 'Сервер отклонил подключение (Connection refused)';
107
+ }
108
+ if (msg.includes('fetch failed')) {
109
+ return 'Сбой сетевого соединения (Fetch failed / Connection dropped)';
110
+ }
111
+ return err.message || 'Сбой интернет-соединения';
112
+ }
113
+ export function waitDelay(ms, signal) {
114
+ return new Promise((resolve) => {
115
+ if (signal?.aborted) {
116
+ resolve();
117
+ return;
118
+ }
119
+ const timer = setTimeout(() => {
120
+ resolve();
121
+ }, ms);
122
+ if (signal) {
123
+ signal.addEventListener('abort', () => {
124
+ clearTimeout(timer);
125
+ resolve();
126
+ }, { once: true });
127
+ }
128
+ });
129
+ }
@@ -20,17 +20,23 @@ export async function buildSystemPrompt(planMode) {
20
20
  prompt += ` - Whenever you want to ask the user ANY question, clarify requirements, or get choices (genre, stack, mechanics, UI, design), you MUST NEVER write questions as plain text in your markdown output!\n`;
21
21
  prompt += ` - You MUST call the 'ask_questions' tool function call with the list of questions and options.\n`;
22
22
  prompt += ` - Outputting numbered questions in plain markdown text without calling 'ask_questions' is strictly prohibited.\n`;
23
- prompt += `5. FORMULATING THE PLAN: Deeply analyze the problem, consider edge cases, and design a solid, clean, step-by-step plan. Present a structured **FINAL PLAN** in your response:\n`;
24
- prompt += ` - **🎯 Цель и архитектурный обзор**\n`;
25
- prompt += ` - **📁 Пошаговый список изменений по файлам (какие файлы создаём/правим и какую логику реализуем в них, БЕЗ написания полного кода)**\n`;
26
- prompt += ` - **🧪 План тестирования и проверки**\n`;
27
- prompt += `6. CALL 'plan_ready' TOOL: When your architectural plan is finalized and ready for execution, you MUST call the 'plan_ready' tool with the summary, target files list, and roadmap steps. Do NOT ask the user to type trigger words (like "начинай", "делай", "выполняй") — devx automatically presents the user with an interactive [🚀 Go / ✏️ Other] selection!\n`;
28
- prompt += `7. EXECUTION TRIGGER: When the user confirms with [🚀 Go], devx switches to AGENT mode and sends "Go!" to begin implementing the approved plan.\n`;
23
+ prompt += `5. FORMULATING THE PLAN & TODO LIST:\n`;
24
+ prompt += ` - For multi-step tasks, formulate a clear, actionable todo checklist using the 'todo_list' tool or markdown checkboxes:\n`;
25
+ prompt += ` [ ] Step 1 description\n`;
26
+ prompt += ` [ ] Step 2 description\n`;
27
+ prompt += ` - Present a structured **FINAL PLAN** in your response:\n`;
28
+ prompt += ` - **🎯 Цель и архитектурный обзор**\n`;
29
+ prompt += ` - **📁 Пошаговый список изменений по файлам** (какие файлы создаём/правим и какую логику реализуем в них, БЕЗ полного кода)\n`;
30
+ prompt += ` - **🧪 План тестирования и проверки**\n`;
31
+ prompt += `6. CALL 'plan_ready' TOOL: When your architectural plan is finalized and ready for execution, call the 'plan_ready' tool. Do NOT ask the user to type trigger words — devx automatically presents the user with an interactive [🚀 Go / ✏️ Other] prompt!\n`;
32
+ prompt += `7. EXECUTION TRIGGER: When the user confirms with [🚀 Go], devx switches to AGENT mode to begin implementing the approved plan.\n`;
29
33
  }
30
34
  else {
31
35
  prompt += `MODE: AGENT (CODER / EXECUTOR MODE)\n`;
32
36
  prompt += `You are in AGENT mode. You have full access to bash, file creation, editing, package installation, diagnostics, and search tools.\n`;
33
- prompt += `Execute tasks and approved plans step by step with high code quality.\n`;
37
+ prompt += `EXECUTION & TODO PROGRESS TRACKING:\n`;
38
+ prompt += `- For multi-step tasks, you can use the 'todo_list' tool to update the live task checklist, marking finished steps as 'completed' ([✓]) and the active step as 'in_progress' ([ ]).\n`;
39
+ prompt += `- Execute tasks and approved plans step by step with high code quality.\n`;
34
40
  prompt += `SELF-HEALING & VERIFICATION:\n`;
35
41
  prompt += `- Whenever you modify or create files, use the 'diagnose_code' tool to check for any syntax or type errors.\n`;
36
42
  prompt += `- If any error is found, automatically fix it with 'edit_file' until all diagnostics pass cleanly.\n`;
@@ -91,7 +91,9 @@ export class OpenAIProvider {
91
91
  }
92
92
  }
93
93
  catch { }
94
- throw new Error(`Provider HTTP Error ${res.status}: ${errMsg}`);
94
+ const err = new Error(`Provider HTTP Error ${res.status}: ${errMsg}`);
95
+ err.status = res.status;
96
+ throw err;
95
97
  }
96
98
  const data = await res.json();
97
99
  if (data.error) {
@@ -152,7 +154,9 @@ export class OpenAIProvider {
152
154
  }
153
155
  }
154
156
  catch { }
155
- throw new Error(`Provider HTTP Error ${res.status}: ${errMsg}`);
157
+ const err = new Error(`Provider HTTP Error ${res.status}: ${errMsg}`);
158
+ err.status = res.status;
159
+ throw err;
156
160
  }
157
161
  const reader = res.body?.getReader();
158
162
  if (!reader) {
package/dist/tools/fs.js CHANGED
@@ -65,12 +65,24 @@ export const writeFileTool = {
65
65
  await fs.writeFile(args.path, args.content, 'utf8');
66
66
  const newLines = args.content.split('\n').length;
67
67
  if (!existed) {
68
- return `Successfully created ${args.path} (+${newLines} lines)`;
68
+ return JSON.stringify({
69
+ status: 'success',
70
+ action: 'create',
71
+ path: args.path,
72
+ addedCount: newLines,
73
+ summary: `Successfully created ${args.path} (+${newLines} lines)`
74
+ });
69
75
  }
70
76
  else {
71
77
  const diff = newLines - oldLines;
72
78
  const diffStr = diff >= 0 ? `+${diff}` : `${diff}`;
73
- return `Successfully updated ${args.path} (${diffStr} lines, ${newLines} total)`;
79
+ return JSON.stringify({
80
+ status: 'success',
81
+ action: 'write',
82
+ path: args.path,
83
+ addedCount: newLines,
84
+ summary: `Successfully updated ${args.path} (${diffStr} lines, ${newLines} total)`
85
+ });
74
86
  }
75
87
  }
76
88
  catch (err) {
@@ -118,11 +130,29 @@ export const editFileTool = {
118
130
  throw new Error(`Target text to replace was not found in ${args.path}. Ensure exact indentation and characters match.`);
119
131
  }
120
132
  }
133
+ const targetIndex = content.indexOf(target);
134
+ const startLine = content.slice(0, targetIndex).split('\n').length;
135
+ const removedArr = target.split('\n');
136
+ const addedArr = args.replacement.split('\n');
137
+ const diffLines = [];
138
+ removedArr.forEach((l, i) => {
139
+ diffLines.push(`${startLine + i} - ${l}`);
140
+ });
141
+ addedArr.forEach((l, i) => {
142
+ diffLines.push(`${startLine + i} + ${l}`);
143
+ });
121
144
  const newContent = content.replace(target, args.replacement);
122
145
  await fs.writeFile(args.path, newContent, 'utf8');
123
- const removedLines = target.split('\n').length;
124
- const addedLines = args.replacement.split('\n').length;
125
- return `Successfully edited ${args.path} (+${addedLines} -${removedLines} lines)`;
146
+ return JSON.stringify({
147
+ status: 'success',
148
+ action: 'edit',
149
+ path: args.path,
150
+ startLine,
151
+ removedCount: removedArr.length,
152
+ addedCount: addedArr.length,
153
+ diffLines,
154
+ summary: `Successfully edited ${args.path} (+${addedArr.length} -${removedArr.length} lines)`
155
+ });
126
156
  }
127
157
  catch (err) {
128
158
  throw new Error(`Failed to edit file: ${err.message}`);
@@ -7,11 +7,12 @@ import { diagnoseCodeTool } from './diagnostics.js';
7
7
  import { installPackageTool } from './packages.js';
8
8
  import { saveMemoryTool } from '../core/memory.js';
9
9
  import { planReadyTool, lastPlanReady, resetPlanReady } from './plan.js';
10
+ import { todoListTool, currentTodoList, resetTodoList } from './todo.js';
10
11
  export function getTools(planMode) {
11
- const baseTools = [readFileTool, listDirTool, searchTool, askQuestionsTool, webSearchTool, fetchUrlTool, saveMemoryTool, planReadyTool];
12
+ const baseTools = [readFileTool, listDirTool, searchTool, askQuestionsTool, webSearchTool, fetchUrlTool, saveMemoryTool, planReadyTool, todoListTool];
12
13
  if (planMode) {
13
14
  return baseTools;
14
15
  }
15
16
  return [...baseTools, writeFileTool, editFileTool, mkdirTool, bashTool, diagnoseCodeTool, installPackageTool];
16
17
  }
17
- export { webSearchTool, fetchUrlTool, diagnoseCodeTool, installPackageTool, saveMemoryTool, planReadyTool, lastPlanReady, resetPlanReady };
18
+ export { webSearchTool, fetchUrlTool, diagnoseCodeTool, installPackageTool, saveMemoryTool, planReadyTool, lastPlanReady, resetPlanReady, todoListTool, currentTodoList, resetTodoList };
@@ -0,0 +1,79 @@
1
+ import pc from 'picocolors';
2
+ export let currentTodoList = [];
3
+ export function resetTodoList() {
4
+ currentTodoList = [];
5
+ }
6
+ export function formatTodoListCard(tasks) {
7
+ if (!tasks || tasks.length === 0)
8
+ return '';
9
+ const cols = Math.min(process.stdout.columns || 80, 80);
10
+ const boxWidth = Math.max(30, Math.min(cols - 4, 65));
11
+ const borderChar = '─';
12
+ const lines = [];
13
+ lines.push('\n' + pc.bold(pc.cyan('┌─ 📋 Plan & Tasks ' + borderChar.repeat(Math.max(2, boxWidth - 19)) + '┐')));
14
+ for (const task of tasks) {
15
+ let taskLine = '';
16
+ if (task.status === 'completed') {
17
+ taskLine = pc.green('[✓] ') + pc.dim(task.text);
18
+ }
19
+ else if (task.status === 'in_progress') {
20
+ taskLine = pc.bold(pc.green('[ ] ' + task.text));
21
+ }
22
+ else {
23
+ taskLine = pc.dim('[ ] ') + pc.white(task.text);
24
+ }
25
+ lines.push(pc.cyan('│ ') + taskLine);
26
+ }
27
+ lines.push(pc.cyan('└' + borderChar.repeat(boxWidth) + '┘\n'));
28
+ return lines.join('\n');
29
+ }
30
+ export const todoListTool = {
31
+ name: 'todo_list',
32
+ definition: {
33
+ name: 'todo_list',
34
+ description: 'Update the live interactive task checklist. Use this to plan multi-step implementations, mark completed steps as "completed", and set the current active step to "in_progress".',
35
+ parameters: {
36
+ type: 'object',
37
+ properties: {
38
+ tasks: {
39
+ type: 'array',
40
+ items: {
41
+ type: 'object',
42
+ properties: {
43
+ text: {
44
+ type: 'string',
45
+ description: 'Short, clear description of the task step'
46
+ },
47
+ status: {
48
+ type: 'string',
49
+ enum: ['pending', 'in_progress', 'completed'],
50
+ description: 'Current status of the task'
51
+ }
52
+ },
53
+ required: ['text', 'status']
54
+ },
55
+ description: 'The updated list of tasks'
56
+ }
57
+ },
58
+ required: ['tasks']
59
+ }
60
+ },
61
+ validateArgs: (args) => {
62
+ if (!args || !Array.isArray(args.tasks)) {
63
+ throw new Error('tasks must be an array of task objects');
64
+ }
65
+ },
66
+ execute: async (args) => {
67
+ currentTodoList = args.tasks;
68
+ const card = formatTodoListCard(args.tasks);
69
+ const completedCount = args.tasks.filter(t => t.status === 'completed').length;
70
+ const totalCount = args.tasks.length;
71
+ return JSON.stringify({
72
+ status: 'success',
73
+ type: 'todo_list',
74
+ tasks: args.tasks,
75
+ displayCard: card,
76
+ summary: `Updated plan progress: ${completedCount}/${totalCount} tasks completed`
77
+ });
78
+ }
79
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termux-dev",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Ultra-fast, terminal-native AI coding assistant and agent built for Android Termux, Windows, macOS, and Linux.",
5
5
  "main": "dist/cli/index.js",
6
6
  "type": "module",