neoagent 1.0.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/.env.example +28 -0
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/bin/neoagent.js +8 -0
- package/com.neoagent.plist +45 -0
- package/docs/configuration.md +45 -0
- package/docs/skills.md +45 -0
- package/lib/manager.js +459 -0
- package/package.json +61 -0
- package/server/db/database.js +239 -0
- package/server/index.js +442 -0
- package/server/middleware/auth.js +35 -0
- package/server/public/app.html +559 -0
- package/server/public/css/app.css +608 -0
- package/server/public/css/styles.css +472 -0
- package/server/public/favicon.svg +17 -0
- package/server/public/js/app.js +3283 -0
- package/server/public/login.html +313 -0
- package/server/routes/agents.js +125 -0
- package/server/routes/auth.js +105 -0
- package/server/routes/browser.js +116 -0
- package/server/routes/mcp.js +164 -0
- package/server/routes/memory.js +193 -0
- package/server/routes/messaging.js +153 -0
- package/server/routes/protocols.js +87 -0
- package/server/routes/scheduler.js +63 -0
- package/server/routes/settings.js +98 -0
- package/server/routes/skills.js +107 -0
- package/server/routes/store.js +1192 -0
- package/server/services/ai/compaction.js +82 -0
- package/server/services/ai/engine.js +1690 -0
- package/server/services/ai/models.js +46 -0
- package/server/services/ai/multiStep.js +112 -0
- package/server/services/ai/providers/anthropic.js +181 -0
- package/server/services/ai/providers/base.js +40 -0
- package/server/services/ai/providers/google.js +187 -0
- package/server/services/ai/providers/grok.js +121 -0
- package/server/services/ai/providers/ollama.js +162 -0
- package/server/services/ai/providers/openai.js +167 -0
- package/server/services/ai/toolRunner.js +218 -0
- package/server/services/browser/controller.js +320 -0
- package/server/services/cli/executor.js +204 -0
- package/server/services/mcp/client.js +260 -0
- package/server/services/memory/embeddings.js +126 -0
- package/server/services/memory/manager.js +431 -0
- package/server/services/messaging/base.js +23 -0
- package/server/services/messaging/discord.js +238 -0
- package/server/services/messaging/manager.js +328 -0
- package/server/services/messaging/telegram.js +243 -0
- package/server/services/messaging/telnyx.js +693 -0
- package/server/services/messaging/whatsapp.js +304 -0
- package/server/services/scheduler/cron.js +312 -0
- package/server/services/websocket.js +191 -0
- package/server/utils/security.js +71 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
const SCREENSHOTS_DIR = path.join(__dirname, '..', '..', '..', 'data', 'screenshots');
|
|
5
|
+
if (!fs.existsSync(SCREENSHOTS_DIR)) fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
6
|
+
|
|
7
|
+
class BrowserController {
|
|
8
|
+
constructor(io) {
|
|
9
|
+
this.io = io;
|
|
10
|
+
this.browser = null;
|
|
11
|
+
this.page = null;
|
|
12
|
+
this.launching = false;
|
|
13
|
+
this.headless = true; // can be toggled via setHeadless()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async setHeadless(val) {
|
|
17
|
+
const wasHeadless = this.headless;
|
|
18
|
+
this.headless = val !== false && val !== 'false';
|
|
19
|
+
// Close browser so it relaunches with new setting next time
|
|
20
|
+
if (wasHeadless !== this.headless) {
|
|
21
|
+
await this.close().catch(() => { });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Alias used by graceful shutdown in server.js
|
|
26
|
+
async closeBrowser() {
|
|
27
|
+
return this.close();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async ensureBrowser() {
|
|
31
|
+
if (this.browser && this.browser.isConnected()) return;
|
|
32
|
+
if (this.launching) {
|
|
33
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.launching = true;
|
|
38
|
+
try {
|
|
39
|
+
const puppeteer = require('puppeteer-extra');
|
|
40
|
+
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
|
|
41
|
+
puppeteer.use(StealthPlugin());
|
|
42
|
+
|
|
43
|
+
this.browser = await puppeteer.launch({
|
|
44
|
+
headless: this.headless ? 'new' : false,
|
|
45
|
+
args: [
|
|
46
|
+
'--no-sandbox',
|
|
47
|
+
'--disable-setuid-sandbox',
|
|
48
|
+
'--disable-dev-shm-usage',
|
|
49
|
+
'--disable-gpu',
|
|
50
|
+
'--window-size=1280,800'
|
|
51
|
+
],
|
|
52
|
+
defaultViewport: { width: 1280, height: 800 }
|
|
53
|
+
});
|
|
54
|
+
this.page = await this.browser.newPage();
|
|
55
|
+
|
|
56
|
+
const userAgents = [
|
|
57
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
58
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
59
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0',
|
|
60
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0',
|
|
61
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15'
|
|
62
|
+
];
|
|
63
|
+
const randomUA = userAgents[Math.floor(Math.random() * userAgents.length)];
|
|
64
|
+
await this.page.setUserAgent(randomUA);
|
|
65
|
+
this._currentUserAgent = randomUA;
|
|
66
|
+
} finally {
|
|
67
|
+
this.launching = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async ensurePage() {
|
|
72
|
+
await this.ensureBrowser();
|
|
73
|
+
if (!this.page || this.page.isClosed()) {
|
|
74
|
+
this.page = await this.browser.newPage();
|
|
75
|
+
if (this._currentUserAgent) {
|
|
76
|
+
await this.page.setUserAgent(this._currentUserAgent);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return this.page;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async takeScreenshot(options = {}) {
|
|
83
|
+
const page = await this.ensurePage();
|
|
84
|
+
const filename = `screenshot_${Date.now()}.png`;
|
|
85
|
+
const filepath = path.join(SCREENSHOTS_DIR, filename);
|
|
86
|
+
|
|
87
|
+
const screenshotOptions = { path: filepath, type: 'png' };
|
|
88
|
+
if (options.fullPage) screenshotOptions.fullPage = true;
|
|
89
|
+
if (options.selector) {
|
|
90
|
+
const element = await page.$(options.selector);
|
|
91
|
+
if (element) {
|
|
92
|
+
await element.screenshot(screenshotOptions);
|
|
93
|
+
} else {
|
|
94
|
+
await page.screenshot(screenshotOptions);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
await page.screenshot(screenshotOptions);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { screenshotPath: `/screenshots/${filename}`, filename, fullPath: filepath };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async navigate(url, options = {}) {
|
|
104
|
+
const page = await this.ensurePage();
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const response = await page.goto(url, {
|
|
108
|
+
waitUntil: 'networkidle2',
|
|
109
|
+
timeout: 30000
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (options.waitFor) {
|
|
113
|
+
await page.waitForSelector(options.waitFor, { timeout: 10000 }).catch(() => { });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const title = await page.title();
|
|
117
|
+
const currentUrl = page.url();
|
|
118
|
+
|
|
119
|
+
let screenshot = null;
|
|
120
|
+
if (options.screenshot !== false) {
|
|
121
|
+
screenshot = await this.takeScreenshot({ fullPage: options.fullPage });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const bodyText = await page.evaluate(() => {
|
|
125
|
+
const body = document.body;
|
|
126
|
+
if (!body) return '';
|
|
127
|
+
const clone = body.cloneNode(true);
|
|
128
|
+
const scripts = clone.querySelectorAll('script, style, noscript');
|
|
129
|
+
scripts.forEach(s => s.remove());
|
|
130
|
+
return clone.innerText.slice(0, 10000);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
title,
|
|
135
|
+
url: currentUrl,
|
|
136
|
+
status: response?.status() || 0,
|
|
137
|
+
bodyText,
|
|
138
|
+
screenshotPath: screenshot?.screenshotPath || null
|
|
139
|
+
};
|
|
140
|
+
} catch (err) {
|
|
141
|
+
let screenshot = null;
|
|
142
|
+
try { screenshot = await this.takeScreenshot(); } catch { }
|
|
143
|
+
return {
|
|
144
|
+
error: err.message,
|
|
145
|
+
url,
|
|
146
|
+
screenshotPath: screenshot?.screenshotPath || null
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async click(selector, text, screenshot = true) {
|
|
152
|
+
const page = await this.ensurePage();
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
if (text && !selector) {
|
|
156
|
+
const elements = await page.$$('a, button, [role="button"], input[type="submit"], [onclick]');
|
|
157
|
+
let found = false;
|
|
158
|
+
for (const el of elements) {
|
|
159
|
+
const elText = await page.evaluate(e => e.innerText || e.value || e.getAttribute('aria-label') || '', el);
|
|
160
|
+
if (elText.toLowerCase().includes(text.toLowerCase())) {
|
|
161
|
+
await el.click();
|
|
162
|
+
found = true;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!found) {
|
|
167
|
+
return { error: `No clickable element found with text: ${text}` };
|
|
168
|
+
}
|
|
169
|
+
} else if (selector) {
|
|
170
|
+
await page.click(selector);
|
|
171
|
+
} else {
|
|
172
|
+
return { error: 'Either selector or text required' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
176
|
+
|
|
177
|
+
let screenshotResult = null;
|
|
178
|
+
if (screenshot) {
|
|
179
|
+
screenshotResult = await this.takeScreenshot();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const currentUrl = page.url();
|
|
183
|
+
const title = await page.title();
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
success: true,
|
|
187
|
+
url: currentUrl,
|
|
188
|
+
title,
|
|
189
|
+
screenshotPath: screenshotResult?.screenshotPath || null
|
|
190
|
+
};
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return { error: err.message };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async type(selector, text, options = {}) {
|
|
197
|
+
const page = await this.ensurePage();
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
if (options.clear !== false) {
|
|
201
|
+
await page.click(selector, { clickCount: 3 });
|
|
202
|
+
await page.keyboard.press('Backspace');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Simulate human typing speeds (between 30ms and 150ms per keystroke)
|
|
206
|
+
for (const char of text) {
|
|
207
|
+
const charDelay = Math.floor(Math.random() * (150 - 30 + 1) + 30);
|
|
208
|
+
await page.type(selector, char, { delay: charDelay });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (options.pressEnter) {
|
|
212
|
+
await page.keyboard.press('Enter');
|
|
213
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let screenshotResult = null;
|
|
217
|
+
if (options.screenshot !== false) {
|
|
218
|
+
screenshotResult = await this.takeScreenshot();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
success: true,
|
|
223
|
+
typed: text,
|
|
224
|
+
screenshotPath: screenshotResult?.screenshotPath || null
|
|
225
|
+
};
|
|
226
|
+
} catch (err) {
|
|
227
|
+
return { error: err.message };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async extract(selector, attribute, all = false) {
|
|
232
|
+
const page = await this.ensurePage();
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
if (all) {
|
|
236
|
+
const results = await page.$$eval(selector || 'body', (elements, attr) => {
|
|
237
|
+
return elements.map(el => {
|
|
238
|
+
if (attr === 'innerHTML') return el.innerHTML;
|
|
239
|
+
if (attr === 'outerHTML') return el.outerHTML;
|
|
240
|
+
if (attr) return el.getAttribute(attr) || '';
|
|
241
|
+
return el.innerText || '';
|
|
242
|
+
});
|
|
243
|
+
}, attribute);
|
|
244
|
+
return { results: results.slice(0, 100) };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const result = await page.$eval(selector || 'body', (el, attr) => {
|
|
248
|
+
if (attr === 'innerHTML') return el.innerHTML;
|
|
249
|
+
if (attr === 'outerHTML') return el.outerHTML;
|
|
250
|
+
if (attr) return el.getAttribute(attr) || '';
|
|
251
|
+
return el.innerText || '';
|
|
252
|
+
}, attribute);
|
|
253
|
+
|
|
254
|
+
return { result: typeof result === 'string' ? result.slice(0, 50000) : result };
|
|
255
|
+
} catch (err) {
|
|
256
|
+
return { error: err.message };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async evaluate(script) {
|
|
261
|
+
const page = await this.ensurePage();
|
|
262
|
+
try {
|
|
263
|
+
const result = await page.evaluate(script);
|
|
264
|
+
return { result: typeof result === 'object' ? JSON.stringify(result) : String(result) };
|
|
265
|
+
} catch (err) {
|
|
266
|
+
return { error: err.message };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async screenshot(options = {}) {
|
|
271
|
+
return await this.takeScreenshot(options);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async launch(options = {}) {
|
|
275
|
+
await this.ensureBrowser();
|
|
276
|
+
return { success: true };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
isLaunched() {
|
|
280
|
+
return !!(this.browser && this.browser.isConnected());
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
getPageCount() {
|
|
284
|
+
if (!this.browser) return 0;
|
|
285
|
+
try { return this.browser.pages ? 1 : 0; } catch { return 0; }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async fill(selector, value) {
|
|
289
|
+
return this.type(selector, String(value));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async extractContent(options = {}) {
|
|
293
|
+
return this.extract(options.selector, options.attribute, options.all);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async executeJS(code) {
|
|
297
|
+
return this.evaluate(code);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async getPageInfo() {
|
|
301
|
+
if (!this.page || this.page.isClosed()) return { url: null, title: null };
|
|
302
|
+
return {
|
|
303
|
+
url: this.page.url(),
|
|
304
|
+
title: await this.page.title()
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async close() {
|
|
309
|
+
if (this.page && !this.page.isClosed()) {
|
|
310
|
+
await this.page.close().catch(() => { });
|
|
311
|
+
}
|
|
312
|
+
if (this.browser) {
|
|
313
|
+
await this.browser.close().catch(() => { });
|
|
314
|
+
this.browser = null;
|
|
315
|
+
this.page = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = { BrowserController };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const { spawn, execFileSync } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
let _cachedLoginPath = null;
|
|
5
|
+
|
|
6
|
+
class CLIExecutor {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.activeProcesses = new Map();
|
|
9
|
+
this.defaultShell = process.env.SHELL || '/bin/zsh';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
_getLoginPath() {
|
|
13
|
+
if (_cachedLoginPath) return _cachedLoginPath;
|
|
14
|
+
try {
|
|
15
|
+
const raw = execFileSync(this.defaultShell, ['-l', '-c', 'echo $PATH'], {
|
|
16
|
+
timeout: 5000,
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
19
|
+
});
|
|
20
|
+
_cachedLoginPath = raw.trim();
|
|
21
|
+
} catch {
|
|
22
|
+
_cachedLoginPath = process.env.PATH || '/usr/local/bin:/usr/bin:/bin';
|
|
23
|
+
}
|
|
24
|
+
return _cachedLoginPath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_buildEnv(extra = {}) {
|
|
28
|
+
const loginPath = this._getLoginPath();
|
|
29
|
+
const current = (process.env.PATH || '').split(':');
|
|
30
|
+
const login = loginPath.split(':');
|
|
31
|
+
const merged = [...new Set([...login, ...current])].join(':');
|
|
32
|
+
return { ...process.env, PATH: merged, ...extra };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async execute(command, options = {}) {
|
|
36
|
+
const cwd = options.cwd || process.env.HOME;
|
|
37
|
+
const timeout = options.timeout || 60000;
|
|
38
|
+
const stdinInput = options.stdinInput;
|
|
39
|
+
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
let stdout = '';
|
|
42
|
+
let stderr = '';
|
|
43
|
+
let killed = false;
|
|
44
|
+
|
|
45
|
+
const proc = spawn(this.defaultShell, ['-l', '-c', command], {
|
|
46
|
+
cwd,
|
|
47
|
+
env: this._buildEnv(options.env),
|
|
48
|
+
timeout,
|
|
49
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const pid = proc.pid;
|
|
53
|
+
this.activeProcesses.set(pid, proc);
|
|
54
|
+
|
|
55
|
+
proc.stdout.on('data', (data) => {
|
|
56
|
+
stdout += data.toString();
|
|
57
|
+
if (stdout.length > 500000) {
|
|
58
|
+
stdout = stdout.slice(-250000);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
proc.stderr.on('data', (data) => {
|
|
63
|
+
stderr += data.toString();
|
|
64
|
+
if (stderr.length > 100000) {
|
|
65
|
+
stderr = stderr.slice(-50000);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (stdinInput) {
|
|
70
|
+
proc.stdin.write(stdinInput);
|
|
71
|
+
proc.stdin.end();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
killed = true;
|
|
76
|
+
proc.kill('SIGTERM');
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
if (!proc.killed) proc.kill('SIGKILL');
|
|
79
|
+
}, 5000);
|
|
80
|
+
}, timeout);
|
|
81
|
+
|
|
82
|
+
proc.on('close', (code) => {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
this.activeProcesses.delete(pid);
|
|
85
|
+
|
|
86
|
+
const truncate = (str, max) => {
|
|
87
|
+
if (str.length > max) return str.slice(0, max) + `\n...[truncated, ${str.length} total chars]`;
|
|
88
|
+
return str;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
resolve({
|
|
92
|
+
exitCode: code,
|
|
93
|
+
stdout: truncate(stdout.trim(), 50000),
|
|
94
|
+
stderr: truncate(stderr.trim(), 10000),
|
|
95
|
+
killed,
|
|
96
|
+
command,
|
|
97
|
+
cwd
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
proc.on('error', (err) => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
this.activeProcesses.delete(pid);
|
|
104
|
+
resolve({
|
|
105
|
+
exitCode: -1,
|
|
106
|
+
stdout: '',
|
|
107
|
+
stderr: err.message,
|
|
108
|
+
killed: false,
|
|
109
|
+
command,
|
|
110
|
+
cwd,
|
|
111
|
+
error: err.message
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async executeInteractive(command, inputs = [], options = {}) {
|
|
118
|
+
const cwd = options.cwd || process.env.HOME;
|
|
119
|
+
const timeout = options.timeout || 120000;
|
|
120
|
+
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
let output = '';
|
|
123
|
+
let inputIndex = 0;
|
|
124
|
+
let killed = false;
|
|
125
|
+
|
|
126
|
+
let pty;
|
|
127
|
+
try {
|
|
128
|
+
pty = require('node-pty');
|
|
129
|
+
} catch {
|
|
130
|
+
return this.execute(command, { ...options, stdinInput: inputs.join('\n') + '\n' }).then(resolve);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const proc = pty.spawn(this.defaultShell, ['-l', '-c', command], {
|
|
134
|
+
name: 'xterm-256color',
|
|
135
|
+
cols: 120,
|
|
136
|
+
rows: 30,
|
|
137
|
+
cwd,
|
|
138
|
+
env: { ...this._buildEnv(), TERM: 'xterm-256color' }
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const pid = proc.pid;
|
|
142
|
+
this.activeProcesses.set(pid, proc);
|
|
143
|
+
|
|
144
|
+
proc.onData((data) => {
|
|
145
|
+
output += data;
|
|
146
|
+
|
|
147
|
+
if (inputIndex < inputs.length) {
|
|
148
|
+
const inputItem = inputs[inputIndex];
|
|
149
|
+
if (typeof inputItem === 'object' && inputItem.waitFor) {
|
|
150
|
+
if (output.includes(inputItem.waitFor)) {
|
|
151
|
+
proc.write(inputItem.input + '\r');
|
|
152
|
+
inputIndex++;
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
proc.write(inputItem + '\r');
|
|
157
|
+
inputIndex++;
|
|
158
|
+
}, 200);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const timer = setTimeout(() => {
|
|
164
|
+
killed = true;
|
|
165
|
+
proc.kill();
|
|
166
|
+
}, timeout);
|
|
167
|
+
|
|
168
|
+
proc.onExit(({ exitCode }) => {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
this.activeProcesses.delete(pid);
|
|
171
|
+
|
|
172
|
+
const cleanOutput = output.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '').trim();
|
|
173
|
+
resolve({
|
|
174
|
+
exitCode,
|
|
175
|
+
stdout: cleanOutput.slice(0, 50000),
|
|
176
|
+
stderr: '',
|
|
177
|
+
killed,
|
|
178
|
+
command,
|
|
179
|
+
cwd,
|
|
180
|
+
interactive: true
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
kill(pid) {
|
|
187
|
+
const proc = this.activeProcesses.get(pid);
|
|
188
|
+
if (proc) {
|
|
189
|
+
proc.kill?.('SIGTERM') || proc.kill?.();
|
|
190
|
+
this.activeProcesses.delete(pid);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
killAll() {
|
|
197
|
+
for (const [pid, proc] of this.activeProcesses) {
|
|
198
|
+
proc.kill?.('SIGTERM') || proc.kill?.();
|
|
199
|
+
}
|
|
200
|
+
this.activeProcesses.clear();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = { CLIExecutor };
|