aica-cli 0.0.1
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 +11 -0
- package/bin/aica.js +147 -0
- package/lib/actions.js +131 -0
- package/lib/backup.js +40 -0
- package/lib/logger.js +63 -0
- package/lib/parser.js +94 -0
- package/lib/password.js +8 -0
- package/lib/security.js +121 -0
- package/lib/server.js +699 -0
- package/lib/tunnel.js +150 -0
- package/lib/ui.js +221 -0
- package/package.json +21 -0
package/lib/tunnel.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import natUpnp from 'nat-upnp';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import http from 'http';
|
|
6
|
+
|
|
7
|
+
const { createClient } = natUpnp;
|
|
8
|
+
|
|
9
|
+
// Получение внешнего IP
|
|
10
|
+
export async function getExternalIP() {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const services = [
|
|
13
|
+
{ host: 'api.ipify.org', path: '/?format=json', protocol: 'https' },
|
|
14
|
+
{ host: 'ifconfig.me', path: '/', protocol: 'https' },
|
|
15
|
+
{ host: 'api.ipinfo.io', path: '/ip', protocol: 'https' },
|
|
16
|
+
{ host: 'api.ipify.org', path: '/?format=json', protocol: 'http' },
|
|
17
|
+
{ host: 'ifconfig.me', path: '/', protocol: 'http' }
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
let currentIndex = 0;
|
|
21
|
+
|
|
22
|
+
function tryNext() {
|
|
23
|
+
if (currentIndex >= services.length) {
|
|
24
|
+
reject(new Error('Не удалось получить внешний IP'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const service = services[currentIndex];
|
|
29
|
+
currentIndex++;
|
|
30
|
+
|
|
31
|
+
const client = service.protocol === 'https' ? https : http;
|
|
32
|
+
|
|
33
|
+
const req = client.get({
|
|
34
|
+
hostname: service.host,
|
|
35
|
+
path: service.path,
|
|
36
|
+
timeout: 5000
|
|
37
|
+
}, (res) => {
|
|
38
|
+
let data = '';
|
|
39
|
+
res.on('data', chunk => data += chunk);
|
|
40
|
+
res.on('end', () => {
|
|
41
|
+
try {
|
|
42
|
+
const json = JSON.parse(data);
|
|
43
|
+
const ip = json.ip || json.query || data.trim();
|
|
44
|
+
if (ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
|
|
45
|
+
resolve(ip);
|
|
46
|
+
} else {
|
|
47
|
+
tryNext();
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
const ip = data.trim();
|
|
51
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
|
|
52
|
+
resolve(ip);
|
|
53
|
+
} else {
|
|
54
|
+
tryNext();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
req.on('error', () => tryNext());
|
|
61
|
+
req.on('timeout', () => {
|
|
62
|
+
req.destroy();
|
|
63
|
+
tryNext();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
tryNext();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// UPnP проброс порта
|
|
72
|
+
export async function setupUPnP(port) {
|
|
73
|
+
const client = createClient();
|
|
74
|
+
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
console.log(chalk.gray(' 🔌 Проброс порта через UPnP...'));
|
|
77
|
+
|
|
78
|
+
client.portMapping({
|
|
79
|
+
public: port,
|
|
80
|
+
private: port,
|
|
81
|
+
description: 'aica agent',
|
|
82
|
+
ttl: 0
|
|
83
|
+
}, (err) => {
|
|
84
|
+
if (err) {
|
|
85
|
+
reject(new Error(`UPnP не сработал: ${err.message}`));
|
|
86
|
+
} else {
|
|
87
|
+
console.log(chalk.green(` ✅ Порт ${port} проброшен через UPnP`));
|
|
88
|
+
resolve(client);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Cloudflare Tunnel
|
|
95
|
+
export async function createCloudflareTunnel(port) {
|
|
96
|
+
console.log(chalk.gray(' 🌐 Создание туннеля через cloudflared...'));
|
|
97
|
+
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const proc = spawn('cloudflared', [
|
|
100
|
+
'tunnel',
|
|
101
|
+
'--url', `http://localhost:${port}`,
|
|
102
|
+
'--no-autoupdate',
|
|
103
|
+
'--metrics', 'localhost:0'
|
|
104
|
+
], {
|
|
105
|
+
env: { ...process.env, TUNNEL_LOGLEVEL: 'info' }
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
let url = null;
|
|
109
|
+
|
|
110
|
+
function checkUrl(line) {
|
|
111
|
+
const match = line.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
112
|
+
if (match && !url) {
|
|
113
|
+
url = match[0];
|
|
114
|
+
console.log(chalk.green(` 🌍 Туннель: ${url}`));
|
|
115
|
+
resolve({ url, process: proc });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
proc.stdout.on('data', (data) => {
|
|
120
|
+
checkUrl(data.toString());
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
proc.stderr.on('data', (data) => {
|
|
124
|
+
checkUrl(data.toString());
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
proc.on('error', (err) => {
|
|
128
|
+
if (err.code === 'ENOENT') {
|
|
129
|
+
reject(new Error('cloudflared не найден'));
|
|
130
|
+
} else {
|
|
131
|
+
reject(err);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
proc.on('close', (code) => {
|
|
136
|
+
if (!url) {
|
|
137
|
+
reject(new Error(`cloudflared завершился с кодом ${code}`));
|
|
138
|
+
} else {
|
|
139
|
+
console.log(chalk.yellow(` ⚠️ Туннель закрыт (код ${code})`));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
if (!url) {
|
|
145
|
+
proc.kill();
|
|
146
|
+
reject(new Error('Timeout создания туннеля'));
|
|
147
|
+
}
|
|
148
|
+
}, 30000);
|
|
149
|
+
});
|
|
150
|
+
}
|
package/lib/ui.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
3
|
+
|
|
4
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5
|
+
|
|
6
|
+
export function ask(q) {
|
|
7
|
+
return new Promise(res => {
|
|
8
|
+
process.stdout.write('\n' + q);
|
|
9
|
+
rl.once('line', (answer) => {
|
|
10
|
+
res(answer);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function sanitize(text) {
|
|
16
|
+
if (!text) return '';
|
|
17
|
+
return text
|
|
18
|
+
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
|
19
|
+
.replace(/\x1b\].*?\x07/g, '')
|
|
20
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
|
21
|
+
.replace(/\r/g, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function showStartupUI({ role, workdir, port, password, tunnelUrl, externalIP, publicUrl, useUPnP, useIP, useCloudflared, requestMode, autoMode }) {
|
|
25
|
+
console.log();
|
|
26
|
+
console.log(chalk.bgCyan.black(` 🤖 aica — агент "${role}" `));
|
|
27
|
+
console.log(chalk.gray(` 📁 ${workdir}`));
|
|
28
|
+
console.log(chalk.gray(` 🌐 localhost:${port}`));
|
|
29
|
+
|
|
30
|
+
if (useUPnP && externalIP) console.log(chalk.green(` 🌍 UPnP: ${externalIP}:${port}`));
|
|
31
|
+
if (useIP && externalIP) console.log(chalk.green(` 🌍 IP: ${externalIP}:${port}`));
|
|
32
|
+
if (useCloudflared && tunnelUrl) console.log(chalk.green(` 🌍 Tunnel: ${tunnelUrl}`));
|
|
33
|
+
if (autoMode) console.log(chalk.yellow(` 🤖 Автономный режим`));
|
|
34
|
+
|
|
35
|
+
console.log(chalk.gray(` 🔑 ${password}`));
|
|
36
|
+
|
|
37
|
+
// Определяем URL для промпта
|
|
38
|
+
let serverUrl = publicUrl || `http://localhost:${port}`;
|
|
39
|
+
if (!publicUrl) {
|
|
40
|
+
if (useCloudflared && tunnelUrl) serverUrl = tunnelUrl;
|
|
41
|
+
else if (useUPnP && externalIP) serverUrl = `http://${externalIP}:${port}`;
|
|
42
|
+
else if (useIP && externalIP) serverUrl = `http://${externalIP}:${port}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Убеждаемся что есть протокол
|
|
46
|
+
if (!serverUrl.startsWith('http://') && !serverUrl.startsWith('https://')) {
|
|
47
|
+
serverUrl = `http://${serverUrl}`;
|
|
48
|
+
}
|
|
49
|
+
// Убираем trailing slash
|
|
50
|
+
serverUrl = serverUrl.replace(/\/$/, '');
|
|
51
|
+
|
|
52
|
+
// Формируем подсказку в зависимости от режима
|
|
53
|
+
let helpHint;
|
|
54
|
+
if (requestMode === 'post') {
|
|
55
|
+
helpHint = `POST ${serverUrl}/help (Auth: Bearer ${password})`;
|
|
56
|
+
} else {
|
|
57
|
+
// get или mixed — для /help используем GET с password в query
|
|
58
|
+
helpHint = `GET ${serverUrl}/help?password=${password}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Минимальный промпт с протоколом и режимом
|
|
62
|
+
console.log();
|
|
63
|
+
console.log(chalk.gray(' ─── промпт для LLM ───'));
|
|
64
|
+
|
|
65
|
+
console.log(`Ты работаешь с aica-агентом в проекте. Твоя роль: ${role}.`);
|
|
66
|
+
console.log(`Сервер: ${serverUrl}`);
|
|
67
|
+
console.log(`Пароль: ${password}`);
|
|
68
|
+
console.log(`Используй доступные HTTP инструменты для запросов к серверу.`);
|
|
69
|
+
console.log(`Получи справку: ${helpHint}`);
|
|
70
|
+
console.log(`Следуй инструкциям из ответа сервера.`);
|
|
71
|
+
|
|
72
|
+
console.log(chalk.gray(' ────────────────────────'));
|
|
73
|
+
|
|
74
|
+
if (!useUPnP && !useIP && !useCloudflared && !publicUrl) {
|
|
75
|
+
console.log(chalk.yellow(' ⚠ localhost only'));
|
|
76
|
+
console.log(chalk.gray(' --upnp | --ip | --cloudflared | --url <URL>'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log();
|
|
80
|
+
console.log(chalk.gray(' ⏳ ожидание...'));
|
|
81
|
+
console.log();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function showPatchUI(id, action, recentRequests) {
|
|
85
|
+
console.log();
|
|
86
|
+
console.log(chalk.bgBlue.black(` 📨 ${action.action.toUpperCase()} `) + chalk.gray(` #${id}`));
|
|
87
|
+
if (action.description) console.log(chalk.bold(action.description));
|
|
88
|
+
console.log(chalk.gray(` 📁 ${action.file || action.path}`));
|
|
89
|
+
if (action.reason) console.log(chalk.gray(` 💡 ${action.reason}`));
|
|
90
|
+
|
|
91
|
+
showRecentRequests(recentRequests);
|
|
92
|
+
showActionSummary(action);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function showSequenceUI(id, action, recentRequests) {
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(chalk.bgMagenta.black(` 📨 SEQUENCE `) + chalk.gray(` #${id}`));
|
|
98
|
+
if (action.description) console.log(chalk.bold(action.description));
|
|
99
|
+
if (action.reason) console.log(chalk.gray(` 💡 ${action.reason}`));
|
|
100
|
+
|
|
101
|
+
showRecentRequests(recentRequests);
|
|
102
|
+
|
|
103
|
+
console.log(chalk.gray(' 📋 шаги:'));
|
|
104
|
+
for (const step of action.steps) {
|
|
105
|
+
const icon = step.action === 'exec' ? '⚙️' :
|
|
106
|
+
step.action === 'patch' ? '🔧' :
|
|
107
|
+
step.action === 'create' ? '📄' :
|
|
108
|
+
step.action === 'delete' ? '🗑️' :
|
|
109
|
+
step.action === 'replace' ? '🔄' :
|
|
110
|
+
step.action === 'rename' ? '📝' :
|
|
111
|
+
step.action === 'append' ? '➕' : '❓';
|
|
112
|
+
|
|
113
|
+
let detail = '';
|
|
114
|
+
if (step.action === 'exec') {
|
|
115
|
+
detail = chalk.yellow(`exec: ${step.command}`);
|
|
116
|
+
} else {
|
|
117
|
+
detail = `${step.action} ${step.file || step.path}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(` ${step.stepIndex}. ${icon} ${detail}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const firstFileStep = action.steps.find(s => s.diff || s.content);
|
|
124
|
+
if (firstFileStep) {
|
|
125
|
+
console.log(chalk.gray(' ─── preview ───'));
|
|
126
|
+
showActionSummary(firstFileStep);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function showRecentRequests(recentRequests) {
|
|
131
|
+
if (recentRequests.length > 0) {
|
|
132
|
+
console.log(chalk.gray(' 📊 последние:'));
|
|
133
|
+
for (const req of recentRequests) {
|
|
134
|
+
const time = req.time?.slice(11, 19) || '??:??:??';
|
|
135
|
+
const icon = req.endpoint === '/get-file' ? '📖' :
|
|
136
|
+
req.endpoint === '/grep' ? '🔍' :
|
|
137
|
+
req.endpoint === '/list-files' ? '📂' :
|
|
138
|
+
req.endpoint === '/tree' ? '🌳' : '📡';
|
|
139
|
+
console.log(chalk.gray(` ${time} ${icon} ${req.endpoint} ${req.path || ''}`));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function showActionSummary(action) {
|
|
145
|
+
if (action.diff) {
|
|
146
|
+
showDiffSummary(action.diff);
|
|
147
|
+
} else if (action.content) {
|
|
148
|
+
showContentPreview(action.content);
|
|
149
|
+
} else if (action.action === 'delete') {
|
|
150
|
+
console.log(chalk.red(` ⚠️ удаление: ${action.file}`));
|
|
151
|
+
} else if (action.action === 'rename') {
|
|
152
|
+
console.log(chalk.yellow(` 📝 ${action.file} → ${action.to}`));
|
|
153
|
+
} else if (action.action === 'exec') {
|
|
154
|
+
console.log(chalk.yellow(` ⚙️ ${action.command}`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function showDiffSummary(diff) {
|
|
159
|
+
const sanitized = sanitize(diff);
|
|
160
|
+
const lines = sanitized.split('\n');
|
|
161
|
+
|
|
162
|
+
let files = new Set();
|
|
163
|
+
let additions = 0;
|
|
164
|
+
let deletions = 0;
|
|
165
|
+
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
if (line.startsWith('+++ ') || line.startsWith('--- ')) {
|
|
168
|
+
const m = line.match(/[ab]\/(.+)$/);
|
|
169
|
+
if (m) files.add(m[1]);
|
|
170
|
+
} else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
171
|
+
additions++;
|
|
172
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
173
|
+
deletions++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(chalk.gray(` 📊 ${files.size} файлов, +${additions} -${deletions}`));
|
|
178
|
+
|
|
179
|
+
if (files.size > 0 && files.size <= 5) {
|
|
180
|
+
console.log(chalk.gray(' ' + Array.from(files).join(', ')));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function showContentPreview(content) {
|
|
185
|
+
const sanitized = sanitize(content);
|
|
186
|
+
const lines = sanitized.split('\n');
|
|
187
|
+
|
|
188
|
+
console.log(chalk.gray(` 📊 ${lines.length} строк, ${content.length} байт`));
|
|
189
|
+
|
|
190
|
+
if (lines.length > 0) {
|
|
191
|
+
const preview = lines.slice(0, 3);
|
|
192
|
+
for (const line of preview) {
|
|
193
|
+
console.log(chalk.gray(` ${line.slice(0, 70)}`));
|
|
194
|
+
}
|
|
195
|
+
if (lines.length > 3) {
|
|
196
|
+
console.log(chalk.gray(` ... ещё ${lines.length - 3}`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function showResult(id, status, notify = true) {
|
|
202
|
+
console.log();
|
|
203
|
+
if (status === 'applied') {
|
|
204
|
+
console.log(chalk.green(' ✅ применён'));
|
|
205
|
+
} else if (status === 'rejected') {
|
|
206
|
+
console.log(chalk.red(' ❌ отклонён'));
|
|
207
|
+
} else if (status.startsWith('partial')) {
|
|
208
|
+
console.log(chalk.yellow(` ⚠️ частично: ${status}`));
|
|
209
|
+
} else {
|
|
210
|
+
console.log(chalk.red(` ❌ ошибка: ${status}`));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (notify) {
|
|
214
|
+
if (status === 'applied' || status === 'rejected') {
|
|
215
|
+
console.log(chalk.bold(`${status}:${id}`));
|
|
216
|
+
} else {
|
|
217
|
+
console.log(chalk.bold(`error:${id}:${status}`));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
console.log();
|
|
221
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aica-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "AI-ассистент мост между LLM и локальным кодом",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aica": "./bin/aica.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/aica.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"chalk": "^5.3.0",
|
|
14
|
+
"diff": "^5.2.0",
|
|
15
|
+
"express": "^4.19.0",
|
|
16
|
+
"nat-upnp": "^1.1.0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["ai", "agent", "patch", "llm"],
|
|
19
|
+
"author": "Vopilovskiy Konstantin <flash.vkv@gmail.com>",
|
|
20
|
+
"license": "MIT"
|
|
21
|
+
}
|