termux-dev 1.0.2

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.
@@ -0,0 +1,137 @@
1
+ import http from 'http';
2
+ import fs from 'fs/promises';
3
+ import fsSync from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import { spawn } from 'child_process';
7
+ const MIME_TYPES = {
8
+ '.html': 'text/html; charset=utf-8',
9
+ '.htm': 'text/html; charset=utf-8',
10
+ '.css': 'text/css; charset=utf-8',
11
+ '.js': 'application/javascript; charset=utf-8',
12
+ '.mjs': 'application/javascript; charset=utf-8',
13
+ '.json': 'application/json; charset=utf-8',
14
+ '.png': 'image/png',
15
+ '.jpg': 'image/jpeg',
16
+ '.jpeg': 'image/jpeg',
17
+ '.gif': 'image/gif',
18
+ '.svg': 'image/svg+xml',
19
+ '.ico': 'image/x-icon',
20
+ '.webp': 'image/webp',
21
+ '.wav': 'audio/wav',
22
+ '.mp3': 'audio/mpeg',
23
+ '.ogg': 'audio/ogg',
24
+ '.wasm': 'application/wasm',
25
+ '.txt': 'text/plain; charset=utf-8'
26
+ };
27
+ let activeServer = null;
28
+ let activePort = 3000;
29
+ function getLocalIp() {
30
+ const interfaces = os.networkInterfaces();
31
+ for (const name of Object.keys(interfaces)) {
32
+ for (const iface of interfaces[name] || []) {
33
+ if (iface.family === 'IPv4' && !iface.internal) {
34
+ return iface.address;
35
+ }
36
+ }
37
+ }
38
+ return 'localhost';
39
+ }
40
+ export function isServerRunning() {
41
+ return activeServer !== null;
42
+ }
43
+ export function getServerPort() {
44
+ return activePort;
45
+ }
46
+ export function stopServer() {
47
+ if (activeServer) {
48
+ activeServer.close();
49
+ activeServer = null;
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+ export async function startServer(preferredPort = 3000) {
55
+ if (activeServer) {
56
+ stopServer();
57
+ }
58
+ activePort = preferredPort;
59
+ return new Promise((resolve, reject) => {
60
+ const server = http.createServer(async (req, res) => {
61
+ try {
62
+ let reqPath = decodeURIComponent(req.url?.split('?')[0] || '/');
63
+ if (reqPath === '/') {
64
+ reqPath = '/index.html';
65
+ }
66
+ const filePath = path.join(process.cwd(), reqPath);
67
+ // Security check: ensure path is within cwd
68
+ if (!filePath.startsWith(process.cwd())) {
69
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
70
+ res.end('Forbidden');
71
+ return;
72
+ }
73
+ if (!fsSync.existsSync(filePath)) {
74
+ // If html file requested without extension
75
+ const withHtml = `${filePath}.html`;
76
+ if (fsSync.existsSync(withHtml)) {
77
+ const content = await fs.readFile(withHtml);
78
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
79
+ res.end(content);
80
+ return;
81
+ }
82
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
83
+ res.end(`404 Not Found: ${reqPath}`);
84
+ return;
85
+ }
86
+ const stat = await fs.stat(filePath);
87
+ if (stat.isDirectory()) {
88
+ const indexPath = path.join(filePath, 'index.html');
89
+ if (fsSync.existsSync(indexPath)) {
90
+ const content = await fs.readFile(indexPath);
91
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
92
+ res.end(content);
93
+ return;
94
+ }
95
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
96
+ res.end('Directory listing disabled');
97
+ return;
98
+ }
99
+ const ext = path.extname(filePath).toLowerCase();
100
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
101
+ const content = await fs.readFile(filePath);
102
+ res.writeHead(200, {
103
+ 'Content-Type': contentType,
104
+ 'Access-Control-Allow-Origin': '*'
105
+ });
106
+ res.end(content);
107
+ }
108
+ catch (err) {
109
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
110
+ res.end(`Internal Server Error: ${err.message}`);
111
+ }
112
+ });
113
+ server.on('error', (err) => {
114
+ if (err.code === 'EADDRINUSE') {
115
+ server.listen(activePort + 1);
116
+ activePort++;
117
+ }
118
+ else {
119
+ reject(err);
120
+ }
121
+ });
122
+ server.listen(activePort, () => {
123
+ activeServer = server;
124
+ const localIp = getLocalIp();
125
+ const localUrl = `http://localhost:${activePort}`;
126
+ const networkUrl = `http://${localIp}:${activePort}`;
127
+ // If in Android Termux, try to open in browser
128
+ if (process.env.PREFIX?.includes('com.termux')) {
129
+ try {
130
+ spawn('termux-open-url', [localUrl], { stdio: 'ignore' });
131
+ }
132
+ catch { }
133
+ }
134
+ resolve({ port: activePort, localUrl, networkUrl });
135
+ });
136
+ });
137
+ }
@@ -0,0 +1,245 @@
1
+ import { execSync, spawn } from 'child_process';
2
+ import path from 'path';
3
+ import fs from 'fs/promises';
4
+ import pc from 'picocolors';
5
+ import { select } from '@inquirer/prompts';
6
+ import { fileURLToPath } from 'url';
7
+ const GITHUB_REPO_URL = 'https://github.com/apvcode/Termux-Dev';
8
+ const REMOTE_PKG_URL = 'https://raw.githubusercontent.com/apvcode/Termux-Dev/main/package.json';
9
+ const REMOTE_RELEASE_URL = 'https://api.github.com/repos/apvcode/Termux-Dev/releases/latest';
10
+ // Compare semantic versions (e.g. 1.1.0 > 1.0.0)
11
+ export function isNewerVersion(remote, current) {
12
+ const clean = (v) => v.replace(/^v/, '').trim();
13
+ const rParts = clean(remote).split('.').map(n => parseInt(n, 10) || 0);
14
+ const cParts = clean(current).split('.').map(n => parseInt(n, 10) || 0);
15
+ for (let i = 0; i < Math.max(rParts.length, cParts.length); i++) {
16
+ const r = rParts[i] || 0;
17
+ const c = cParts[i] || 0;
18
+ if (r > c)
19
+ return true;
20
+ if (r < c)
21
+ return false;
22
+ }
23
+ return false;
24
+ }
25
+ export async function getCurrentVersion() {
26
+ try {
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = path.dirname(__filename);
29
+ const pkgPath = path.resolve(__dirname, '../../package.json');
30
+ const data = await fs.readFile(pkgPath, 'utf8');
31
+ const parsed = JSON.parse(data);
32
+ return parsed.version || '1.0.0';
33
+ }
34
+ catch {
35
+ return '1.0.0';
36
+ }
37
+ }
38
+ export async function checkForUpdates(timeoutMs = 10000) {
39
+ const currentVersion = await getCurrentVersion();
40
+ const controller = new AbortController();
41
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
42
+ try {
43
+ // 1. Try fetching latest package.json with cache-busting timestamp
44
+ const cacheBusterUrl = `${REMOTE_PKG_URL}?t=${Date.now()}`;
45
+ const res = await fetch(cacheBusterUrl, {
46
+ signal: controller.signal,
47
+ headers: {
48
+ 'User-Agent': 'devx-updater',
49
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
50
+ 'Pragma': 'no-cache'
51
+ }
52
+ });
53
+ clearTimeout(timer);
54
+ let latestVersion = '';
55
+ let releaseNotes = '';
56
+ if (res.ok) {
57
+ const pkg = await res.json();
58
+ latestVersion = (pkg.version || '').replace(/^v/, '').trim();
59
+ }
60
+ // 2. Also check latest GitHub release tag
61
+ try {
62
+ const relRes = await fetch(`${REMOTE_RELEASE_URL}?t=${Date.now()}`, {
63
+ headers: { 'User-Agent': 'devx-updater' }
64
+ });
65
+ if (relRes.ok) {
66
+ const relData = await relRes.json();
67
+ const relTag = (relData.tag_name || relData.name || '').replace(/^v/, '').trim();
68
+ if (relTag && (!latestVersion || isNewerVersion(relTag, latestVersion))) {
69
+ latestVersion = relTag;
70
+ }
71
+ releaseNotes = relData.body || '';
72
+ }
73
+ }
74
+ catch { }
75
+ if (latestVersion && isNewerVersion(latestVersion, currentVersion)) {
76
+ return {
77
+ updateAvailable: true,
78
+ currentVersion,
79
+ latestVersion,
80
+ releaseNotes
81
+ };
82
+ }
83
+ return {
84
+ updateAvailable: false,
85
+ currentVersion,
86
+ latestVersion: latestVersion || currentVersion
87
+ };
88
+ return {
89
+ updateAvailable: false,
90
+ currentVersion,
91
+ latestVersion: currentVersion,
92
+ error: `HTTP ${res.status}`
93
+ };
94
+ }
95
+ catch (err) {
96
+ clearTimeout(timer);
97
+ return {
98
+ updateAvailable: false,
99
+ currentVersion,
100
+ latestVersion: currentVersion,
101
+ error: err.name === 'AbortError' ? 'Timeout (10s)' : err.message
102
+ };
103
+ }
104
+ }
105
+ export async function performSelfUpdate(latestVersion) {
106
+ const __filename = fileURLToPath(import.meta.url);
107
+ const projectRoot = path.resolve(path.dirname(__filename), '../../');
108
+ console.log('\n' + pc.bold(pc.cyan('─── 🚀 Starting devx Update ──────────────────────────')));
109
+ const steps = [
110
+ {
111
+ title: '📦 Pulling latest updates from GitHub repository...',
112
+ action: () => {
113
+ try {
114
+ execSync('git pull origin main --quiet', { cwd: projectRoot, stdio: 'pipe' });
115
+ }
116
+ catch {
117
+ // If not git clone or detached, try global git install or fetch
118
+ try {
119
+ execSync('git pull --quiet', { cwd: projectRoot, stdio: 'pipe' });
120
+ }
121
+ catch {
122
+ execSync(`npm install -g git+${GITHUB_REPO_URL}.git --quiet`, { stdio: 'pipe' });
123
+ }
124
+ }
125
+ }
126
+ },
127
+ {
128
+ title: '🔨 Building and compiling TypeScript sources...',
129
+ action: () => {
130
+ execSync('npm install --quiet', { cwd: projectRoot, stdio: 'pipe' });
131
+ execSync('npm run build --quiet', { cwd: projectRoot, stdio: 'pipe' });
132
+ }
133
+ },
134
+ {
135
+ title: '🔑 Ensuring binary execution permissions & linking...',
136
+ action: () => {
137
+ try {
138
+ execSync('chmod +x bin/* 2>/dev/null || true', { cwd: projectRoot, stdio: 'pipe' });
139
+ }
140
+ catch { }
141
+ try {
142
+ execSync('npm link --quiet', { cwd: projectRoot, stdio: 'pipe' });
143
+ }
144
+ catch { }
145
+ }
146
+ }
147
+ ];
148
+ for (let i = 0; i < steps.length; i++) {
149
+ const step = steps[i];
150
+ process.stdout.write(` ${pc.cyan('⚡')} [${i + 1}/${steps.length}] ${pc.white(step.title)}\n`);
151
+ // Animation spinner for each step
152
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
153
+ let fIdx = 0;
154
+ const interval = setInterval(() => {
155
+ process.stdout.write(`\r ${pc.cyan(frames[fIdx++ % frames.length])} ${pc.dim(step.title)} `);
156
+ }, 80);
157
+ try {
158
+ await new Promise(resolve => setTimeout(resolve, 300));
159
+ step.action();
160
+ clearInterval(interval);
161
+ process.stdout.write(`\r ${pc.green('✔')} ${pc.bold(pc.white(step.title))}\n`);
162
+ }
163
+ catch (err) {
164
+ clearInterval(interval);
165
+ process.stdout.write(`\r ${pc.red('✖')} ${pc.red(step.title)} - ${err.message}\n`);
166
+ console.log(pc.red(`\nUpdate failed during step ${i + 1}: ${err.message}`));
167
+ return false;
168
+ }
169
+ }
170
+ console.log(pc.bold(pc.green(`\n✅ Successfully updated to v${latestVersion}! Restarting devx...\n`)));
171
+ console.log(pc.bold(pc.cyan('──────────────────────────────────────────────────────\n')));
172
+ await new Promise(r => setTimeout(r, 1000));
173
+ // Spawn new devx process and exit current
174
+ try {
175
+ const child = spawn(process.argv[0], process.argv.slice(1), {
176
+ stdio: 'inherit',
177
+ detached: true
178
+ });
179
+ child.unref();
180
+ process.exit(0);
181
+ }
182
+ catch {
183
+ process.exit(0);
184
+ }
185
+ return true;
186
+ }
187
+ export async function runStartupUpdateCheck(config) {
188
+ // If updates checking is disabled in settings, return immediately
189
+ if (config.checkUpdates === false) {
190
+ return;
191
+ }
192
+ const currentVersion = await getCurrentVersion();
193
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
194
+ let fIdx = 0;
195
+ let isDone = false;
196
+ const spinner = setInterval(() => {
197
+ if (isDone)
198
+ return;
199
+ const frame = pc.cyan(frames[fIdx++ % frames.length]);
200
+ process.stdout.write(`\r ${frame} ${pc.dim(`Checking for updates from`)} ${pc.cyan('apvcode/Termux-Dev')} ${pc.dim(`(v${currentVersion})...`)} `);
201
+ }, 75);
202
+ const result = await checkForUpdates(10000);
203
+ isDone = true;
204
+ clearInterval(spinner);
205
+ // Clear spinner line
206
+ process.stdout.write('\r\x1b[K');
207
+ if (result.updateAvailable) {
208
+ console.log();
209
+ console.log(pc.bold(pc.cyan(' ┌───────────────────────────────────────────────────────────┐')));
210
+ console.log(pc.bold(pc.cyan(' │')) + pc.bold(pc.yellow(` 🚀 A new update is available: `)) + pc.dim(`v${result.currentVersion}`) + pc.bold(pc.green(` ➔ v${result.latestVersion}`)) + ' '.repeat(Math.max(1, 23 - result.currentVersion.length - result.latestVersion.length)) + pc.bold(pc.cyan('│')));
211
+ console.log(pc.bold(pc.cyan(' │')) + pc.dim(` Repository: https://github.com/apvcode/Termux-Dev `) + pc.bold(pc.cyan('│')));
212
+ console.log(pc.bold(pc.cyan(' └───────────────────────────────────────────────────────────┘')));
213
+ console.log();
214
+ try {
215
+ const choice = await select({
216
+ message: pc.bold('Do you want to update devx now?'),
217
+ choices: [
218
+ {
219
+ name: `⚡ Update now (Pull v${result.latestVersion}, rebuild & restart)`,
220
+ value: 'update',
221
+ description: 'Download latest code, compile TypeScript, and restart devx'
222
+ },
223
+ {
224
+ name: '⏭ Skip for now (Continue to chat)',
225
+ value: 'skip',
226
+ description: 'Keep current version and start session'
227
+ }
228
+ ]
229
+ });
230
+ if (choice === 'update') {
231
+ await performSelfUpdate(result.latestVersion);
232
+ }
233
+ }
234
+ catch {
235
+ // If user presses Ctrl+C / cancels prompt, continue
236
+ }
237
+ }
238
+ else {
239
+ // Briefly show up to date message
240
+ process.stdout.write(` ${pc.green('✔')} ${pc.dim(`devx is up to date (v${result.currentVersion})`)}\n`);
241
+ await new Promise(r => setTimeout(r, 450));
242
+ // Clean up line
243
+ process.stdout.write('\x1b[1A\x1b[2K');
244
+ }
245
+ }
@@ -0,0 +1,121 @@
1
+ export class History {
2
+ messages = [];
3
+ constructor() { }
4
+ addMessage(msg) {
5
+ this.messages.push(msg);
6
+ }
7
+ updateSystemPrompt(content) {
8
+ const idx = this.messages.findIndex(m => m.role === 'system');
9
+ if (idx >= 0) {
10
+ this.messages[idx].content = content;
11
+ }
12
+ else {
13
+ this.messages.unshift({ role: 'system', content });
14
+ }
15
+ }
16
+ getMessages() {
17
+ return [...this.messages];
18
+ }
19
+ clear() {
20
+ this.messages = [];
21
+ }
22
+ getTotalTokens() {
23
+ return this.estimateTokens(this.messages);
24
+ }
25
+ getConversationTokens() {
26
+ const chatOnly = this.messages.filter(m => m.role !== 'system');
27
+ return this.estimateTokens(chatOnly);
28
+ }
29
+ // Эвристика: кол-во токенов = длина всех строк / 3.5
30
+ estimateTokens(messages) {
31
+ let chars = 0;
32
+ for (const m of messages) {
33
+ chars += m.content?.length || 0;
34
+ if (m.tool_calls) {
35
+ for (const tc of m.tool_calls) {
36
+ chars += (tc.name.length + JSON.stringify(tc.arguments).length);
37
+ }
38
+ }
39
+ }
40
+ return Math.ceil(chars / 3.5);
41
+ }
42
+ groupIntoBlocks(messages) {
43
+ const blocks = [];
44
+ let i = 0;
45
+ while (i < messages.length) {
46
+ const msg = messages[i];
47
+ if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
48
+ // Начинаем Блок типа B
49
+ const block = [msg];
50
+ i++;
51
+ // Собираем все следующие tool-сообщения
52
+ while (i < messages.length && messages[i].role === 'tool') {
53
+ block.push(messages[i]);
54
+ i++;
55
+ }
56
+ blocks.push(block);
57
+ }
58
+ else {
59
+ // Блок типа A
60
+ blocks.push([msg]);
61
+ i++;
62
+ }
63
+ }
64
+ return blocks;
65
+ }
66
+ pruneToLimit(maxTokens) {
67
+ if (this.estimateTokens(this.messages) <= maxTokens) {
68
+ return false;
69
+ }
70
+ let compacted = false;
71
+ const blocks = this.groupIntoBlocks(this.messages);
72
+ // Определяем pinned блоки
73
+ const pinnedIndices = new Set();
74
+ let foundUser = false;
75
+ for (let i = 0; i < blocks.length; i++) {
76
+ const firstMsg = blocks[i][0];
77
+ if (firstMsg.role === 'system') {
78
+ pinnedIndices.add(i);
79
+ }
80
+ else if (firstMsg.role === 'user' && !foundUser) {
81
+ pinnedIndices.add(i);
82
+ foundUser = true;
83
+ }
84
+ }
85
+ const numProtectLast = 2;
86
+ for (let i = Math.max(0, blocks.length - numProtectLast); i < blocks.length; i++) {
87
+ pinnedIndices.add(i);
88
+ }
89
+ // Фаза 1: Сжатие tool-сообщений
90
+ const TOOL_TRUNCATE_THRESHOLD = 1500;
91
+ for (let i = 0; i < blocks.length; i++) {
92
+ if (!pinnedIndices.has(i)) {
93
+ const block = blocks[i];
94
+ for (let j = 0; j < block.length; j++) {
95
+ const msg = block[j];
96
+ if (msg.role === 'tool' && msg.content && msg.content.length > TOOL_TRUNCATE_THRESHOLD) {
97
+ msg.content = `[Content truncated to save context window. Original length: ${msg.content.length} chars...]`;
98
+ }
99
+ }
100
+ }
101
+ }
102
+ if (this.estimateTokens(blocks.flat()) <= maxTokens) {
103
+ this.messages = blocks.flat();
104
+ return true;
105
+ }
106
+ // Фаза 2: Удаление незащищенных блоков целиком (от старых к новым)
107
+ let currentTokens = this.estimateTokens(blocks.flat());
108
+ for (let i = 0; i < blocks.length; i++) {
109
+ if (currentTokens <= maxTokens) {
110
+ break;
111
+ }
112
+ if (!pinnedIndices.has(i)) {
113
+ const blockTokens = this.estimateTokens(blocks[i]);
114
+ currentTokens -= blockTokens;
115
+ blocks[i] = []; // помечаем как удаленный
116
+ }
117
+ }
118
+ this.messages = blocks.filter(b => b.length > 0).flat();
119
+ return true;
120
+ }
121
+ }
@@ -0,0 +1,164 @@
1
+ function getActionDescription(toolName, args) {
2
+ const t = toolName.toLowerCase();
3
+ if (t === 'read_file' || t === 'view_file' || t === 'read') {
4
+ return `📖 Reading: ${args.path || args.filePath || args.targetFile || ''}`;
5
+ }
6
+ if (t === 'write_file' || t === 'create_file' || t === 'write_to_file' || t === 'write') {
7
+ return `📝 Writing: ${args.path || args.filePath || args.targetFile || ''}`;
8
+ }
9
+ if (t === 'edit_file' || t === 'replace_file_content' || t === 'patch') {
10
+ return `✏️ Editing: ${args.path || args.filePath || args.targetFile || ''}`;
11
+ }
12
+ if (t === 'delete_file' || t === 'remove_file' || t === 'rm') {
13
+ return `🗑️ Deleting: ${args.path || args.filePath || ''}`;
14
+ }
15
+ if (t === 'make_dir' || t === 'mkdir') {
16
+ return `📁 Creating directory: ${args.path || args.dirPath || ''}`;
17
+ }
18
+ if (t === 'list_dir' || t === 'list_files' || t === 'find_files' || t === 'ls') {
19
+ return `📂 Listing: ${args.path || args.dirPath || '.'}`;
20
+ }
21
+ if (t === 'search' || t === 'grep_search' || t === 'grep') {
22
+ return `🔍 Searching: "${args.query || args.pattern || ''}" in ${args.dir || args.path || '.'}`;
23
+ }
24
+ if (t === 'ask_questions' || t === 'questions') {
25
+ return `📋 Asking clarifying questions...`;
26
+ }
27
+ if (t === 'bash' || t === 'execute_command' || t === 'run_command' || t === 'exec') {
28
+ return `⚡ Running command: ${args.command || args.cmd || ''}`;
29
+ }
30
+ return `🔧 Executing ${toolName}...`;
31
+ }
32
+ export class Agent {
33
+ config;
34
+ provider;
35
+ tools;
36
+ history;
37
+ guard;
38
+ constructor(config, provider, tools, history, guard) {
39
+ this.config = config;
40
+ this.provider = provider;
41
+ this.tools = new Map(tools.map((t) => [t.name, t]));
42
+ this.history = history;
43
+ this.guard = guard;
44
+ }
45
+ async *run(signal) {
46
+ let iterations = 0;
47
+ while (iterations < this.config.maxIterations) {
48
+ if (signal?.aborted) {
49
+ return;
50
+ }
51
+ const wasCompacted = this.history.pruneToLimit(this.config.maxContextTokens);
52
+ if (wasCompacted) {
53
+ yield { type: 'system', message: 'Context compacted to fit model context window.' };
54
+ }
55
+ const request = {
56
+ messages: this.history.getMessages(),
57
+ tools: Array.from(this.tools.values()).map((t) => t.definition),
58
+ signal,
59
+ };
60
+ if (request.tools.length === 0) {
61
+ delete request.tools;
62
+ }
63
+ 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 };
78
+ }
79
+ else if (chunk.type === 'done') {
80
+ response = chunk.response;
81
+ }
82
+ }
83
+ }
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')) {
90
+ return;
91
+ }
92
+ yield { type: 'error', message: `API Error: ${err.message}`, isFatal: true };
93
+ return;
94
+ }
95
+ if (signal?.aborted) {
96
+ return;
97
+ }
98
+ if (!response) {
99
+ response = { content: '' };
100
+ }
101
+ const assistantMsg = {
102
+ role: 'assistant',
103
+ content: response.content || '',
104
+ };
105
+ if (response.toolCalls && response.toolCalls.length > 0) {
106
+ assistantMsg.tool_calls = response.toolCalls;
107
+ }
108
+ this.history.addMessage(assistantMsg);
109
+ if (response.usage) {
110
+ yield { type: 'usage', usage: response.usage };
111
+ }
112
+ // If streaming wasn't used or to notify completed text
113
+ if (assistantMsg.content && !this.provider.chatStream) {
114
+ yield { type: 'text', content: assistantMsg.content };
115
+ }
116
+ if (!response.toolCalls || response.toolCalls.length === 0) {
117
+ break;
118
+ }
119
+ for (const call of response.toolCalls) {
120
+ if (signal?.aborted) {
121
+ return;
122
+ }
123
+ const actionDesc = getActionDescription(call.name, call.arguments);
124
+ yield { type: 'tool_start', id: call.id, name: call.name, argsRaw: JSON.stringify(call.arguments), actionDesc };
125
+ let result = '';
126
+ const tool = this.tools.get(call.name);
127
+ if (!tool) {
128
+ result = `Error: Tool '${call.name}' not found.`;
129
+ }
130
+ else {
131
+ try {
132
+ tool.validateArgs(call.arguments);
133
+ if (this.guard.check(tool.name, call.arguments)) {
134
+ const allowed = await this.guard.askUser(tool.name, call.arguments);
135
+ if (allowed) {
136
+ result = await tool.execute(call.arguments, this.config);
137
+ }
138
+ else {
139
+ result = 'Action denied by user.';
140
+ }
141
+ }
142
+ else {
143
+ result = await tool.execute(call.arguments, this.config);
144
+ }
145
+ }
146
+ catch (err) {
147
+ result = `Error executing tool: ${err.message}`;
148
+ }
149
+ }
150
+ yield { type: 'tool_end', id: call.id, name: call.name, result };
151
+ this.history.addMessage({
152
+ role: 'tool',
153
+ content: result,
154
+ tool_call_id: call.id,
155
+ name: call.name,
156
+ });
157
+ }
158
+ iterations++;
159
+ }
160
+ if (iterations >= this.config.maxIterations) {
161
+ yield { type: 'system', message: `Reached max iterations (${this.config.maxIterations}).` };
162
+ }
163
+ }
164
+ }