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 +2 -1
- package/assets/banner.svg +1 -1
- package/dist/cli/index.js +87 -11
- package/dist/cli/markdown.js +61 -5
- package/dist/cli/smooth.js +77 -0
- package/dist/core/loop.js +73 -34
- package/dist/core/network.js +129 -0
- package/dist/prompts/builder.js +13 -7
- package/dist/providers/openai.js +6 -2
- package/dist/tools/fs.js +35 -5
- package/dist/tools/index.js +3 -2
- package/dist/tools/todo.js +79 -0
- package/package.json +1 -1
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
|
-
[](https://www.npmjs.com/package/termux-dev)
|
|
11
11
|
[](https://github.com/apvcode/Termux-Dev/actions/workflows/ci.yml)
|
|
12
|
+
[](LICENSE)
|
|
12
13
|
[](https://nodejs.org)
|
|
13
14
|
[](https://www.typescriptlang.org)
|
|
14
15
|
[](#)
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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();
|
package/dist/cli/markdown.js
CHANGED
|
@@ -28,10 +28,38 @@ export class MarkdownStreamer {
|
|
|
28
28
|
tableBuffer = [];
|
|
29
29
|
push(chunk) {
|
|
30
30
|
this.buffer += chunk;
|
|
31
|
-
|
|
32
|
-
this.buffer
|
|
33
|
-
|
|
34
|
-
this.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
11
|
+
return `→ Edit ${args.path || args.filePath || args.targetFile || ''}`;
|
|
11
12
|
}
|
|
12
13
|
if (t === 'delete_file' || t === 'remove_file' || t === 'rm') {
|
|
13
|
-
return
|
|
14
|
+
return `→ Delete ${args.path || args.filePath || ''}`;
|
|
14
15
|
}
|
|
15
16
|
if (t === 'make_dir' || t === 'mkdir') {
|
|
16
|
-
return
|
|
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
|
|
20
|
+
return `→ List ${args.path || args.dirPath || '.'}`;
|
|
20
21
|
}
|
|
21
22
|
if (t === 'search' || t === 'grep_search' || t === 'grep') {
|
|
22
|
-
return
|
|
23
|
+
return `→ Search "${args.query || args.pattern || ''}" in ${args.dir || args.path || '.'}`;
|
|
23
24
|
}
|
|
24
25
|
if (t === 'ask_questions' || t === 'questions') {
|
|
25
|
-
return
|
|
26
|
+
return `→ Clarify: Asking questions...`;
|
|
26
27
|
}
|
|
27
28
|
if (t === 'bash' || t === 'execute_command' || t === 'run_command' || t === 'exec') {
|
|
28
|
-
return
|
|
29
|
+
return `→ Run: ${args.command || args.cmd || ''}`;
|
|
29
30
|
}
|
|
30
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
package/dist/prompts/builder.js
CHANGED
|
@@ -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
|
|
24
|
-
prompt += ` -
|
|
25
|
-
prompt += `
|
|
26
|
-
prompt += `
|
|
27
|
-
prompt += `
|
|
28
|
-
prompt += `
|
|
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 += `
|
|
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`;
|
package/dist/providers/openai.js
CHANGED
|
@@ -91,7 +91,9 @@ export class OpenAIProvider {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
catch { }
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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}`);
|
package/dist/tools/index.js
CHANGED
|
@@ -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