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.
- package/LICENSE +21 -0
- package/README.md +290 -0
- package/assets/banner.svg +33 -0
- package/assets/preview.png +0 -0
- package/bin/devx.js +2 -0
- package/dist/cli/clipboard.js +136 -0
- package/dist/cli/files.js +93 -0
- package/dist/cli/index.js +1506 -0
- package/dist/cli/markdown.js +147 -0
- package/dist/cli/prompt.js +553 -0
- package/dist/cli/providers.js +892 -0
- package/dist/cli/server.js +137 -0
- package/dist/cli/updater.js +245 -0
- package/dist/core/history.js +121 -0
- package/dist/core/loop.js +164 -0
- package/dist/core/memory.js +68 -0
- package/dist/core/models.js +72 -0
- package/dist/core/pricing.js +65 -0
- package/dist/core/session.js +129 -0
- package/dist/core/snapshot.js +88 -0
- package/dist/core/types.js +1 -0
- package/dist/permissions/guard.js +104 -0
- package/dist/prompts/builder.js +69 -0
- package/dist/providers/index.js +7 -0
- package/dist/providers/openai.js +318 -0
- package/dist/tools/bash.js +51 -0
- package/dist/tools/diagnostics.js +63 -0
- package/dist/tools/fs.js +185 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/packages.js +80 -0
- package/dist/tools/plan.js +52 -0
- package/dist/tools/questions.js +101 -0
- package/dist/tools/search.js +90 -0
- package/dist/tools/web.js +155 -0
- package/package.json +64 -0
|
@@ -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
|
+
}
|