vexi-cli 0.5.2 → 0.5.4
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/dist/agent.js +76 -15
- package/dist/cli.js +1 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { basename } from 'node:path';
|
|
18
18
|
import { platform } from 'node:os';
|
|
19
|
+
import { exec as cpExec } from 'node:child_process';
|
|
19
20
|
import { input, select, confirm } from '@inquirer/prompts';
|
|
20
21
|
import * as nodeRl from 'node:readline';
|
|
21
22
|
import ora from 'ora';
|
|
@@ -29,22 +30,51 @@ import { McpManager, parseToolCall } from './mcp/client.js';
|
|
|
29
30
|
import { loadMcpConfig } from './mcp/config.js';
|
|
30
31
|
import { ARABIC_RTL_NOTE, getStrings, t } from './i18n/index.js';
|
|
31
32
|
import { accent, dim, err, ok, printBanner, printStatusLine, userPrompt, vexiLabel, warn } from './ui/index.js';
|
|
33
|
+
/** Extract shell commands from fenced code blocks in an AI reply. */
|
|
34
|
+
function extractShellBlocks(reply) {
|
|
35
|
+
const blocks = [];
|
|
36
|
+
const re = /```(?:bash|sh|shell|cmd|powershell|ps1)\n([\s\S]*?)```/gi;
|
|
37
|
+
let m;
|
|
38
|
+
while ((m = re.exec(reply)) !== null) {
|
|
39
|
+
const code = m[1].trim();
|
|
40
|
+
if (code)
|
|
41
|
+
blocks.push(code);
|
|
42
|
+
}
|
|
43
|
+
return blocks;
|
|
44
|
+
}
|
|
45
|
+
/** Run a shell command and return { stdout, stderr, code }. */
|
|
46
|
+
function runCommand(cmd, cwd) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
cpExec(cmd, { cwd, shell: process.platform === 'win32' ? 'powershell.exe' : '/bin/sh', maxBuffer: 1024 * 1024 * 4 }, (err, stdout, stderr) => {
|
|
49
|
+
resolve({ stdout: stdout ?? '', stderr: stderr ?? '', code: err?.code ?? 0 });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
32
53
|
/**
|
|
33
54
|
* Read a user message with multi-line paste support.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
55
|
+
* Creates a fresh readline interface per call so it never conflicts
|
|
56
|
+
* with inquirer's internal readline. Lines pasted together arrive
|
|
57
|
+
* within ms of each other; a 30 ms debounce collects them all.
|
|
36
58
|
*/
|
|
37
|
-
function readMessage(
|
|
59
|
+
function readMessage(prompt) {
|
|
38
60
|
return new Promise((resolve, reject) => {
|
|
61
|
+
// Inquirer may have paused stdin — resume before creating our interface.
|
|
62
|
+
process.stdin.resume();
|
|
39
63
|
const lines = [];
|
|
40
64
|
let settled = false;
|
|
41
65
|
let timer = null;
|
|
66
|
+
const iface = nodeRl.createInterface({
|
|
67
|
+
input: process.stdin,
|
|
68
|
+
output: process.stdout,
|
|
69
|
+
terminal: true,
|
|
70
|
+
});
|
|
42
71
|
const finish = () => {
|
|
43
72
|
if (settled)
|
|
44
73
|
return;
|
|
45
74
|
settled = true;
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
iface.removeListener('line', onLine);
|
|
76
|
+
iface.removeListener('close', onClose);
|
|
77
|
+
iface.close();
|
|
48
78
|
resolve(lines.join('\n'));
|
|
49
79
|
};
|
|
50
80
|
const onLine = (line) => {
|
|
@@ -56,12 +86,17 @@ function readMessage(rl, prompt) {
|
|
|
56
86
|
const onClose = () => {
|
|
57
87
|
if (!settled) {
|
|
58
88
|
settled = true;
|
|
59
|
-
|
|
89
|
+
iface.removeListener('line', onLine);
|
|
60
90
|
reject(Object.assign(new Error('stdin closed'), { name: 'ExitPromptError' }));
|
|
61
91
|
}
|
|
62
92
|
};
|
|
63
|
-
|
|
64
|
-
|
|
93
|
+
iface.on('SIGINT', () => {
|
|
94
|
+
settled = true;
|
|
95
|
+
iface.close();
|
|
96
|
+
reject(Object.assign(new Error('ExitPromptError'), { name: 'ExitPromptError' }));
|
|
97
|
+
});
|
|
98
|
+
iface.on('line', onLine);
|
|
99
|
+
iface.once('close', onClose);
|
|
65
100
|
process.stdout.write(prompt + ' ');
|
|
66
101
|
});
|
|
67
102
|
}
|
|
@@ -83,10 +118,6 @@ export async function runAgent(opts) {
|
|
|
83
118
|
}
|
|
84
119
|
let provider = createProvider(config.provider, config.apiKey, config.model);
|
|
85
120
|
const root = process.cwd();
|
|
86
|
-
// Create a persistent readline interface for the chat loop.
|
|
87
|
-
// Must be created AFTER all inquirer prompts (first-run setup) are done.
|
|
88
|
-
const chatRl = nodeRl.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
89
|
-
chatRl.on('SIGINT', () => process.exit(0));
|
|
90
121
|
// ── Full project understanding: scan + load memory + load skills ──────
|
|
91
122
|
const scanSpinner = ora({ text: dim(s.scanning), spinner: 'dots' }).start();
|
|
92
123
|
let project = null;
|
|
@@ -170,12 +201,11 @@ export async function runAgent(opts) {
|
|
|
170
201
|
while (true) {
|
|
171
202
|
let line;
|
|
172
203
|
try {
|
|
173
|
-
line = await readMessage(
|
|
204
|
+
line = await readMessage(userPrompt);
|
|
174
205
|
}
|
|
175
206
|
catch {
|
|
176
207
|
// Ctrl+C / closed stdin
|
|
177
208
|
console.log('\n' + ok(s.goodbye));
|
|
178
|
-
chatRl.close();
|
|
179
209
|
return;
|
|
180
210
|
}
|
|
181
211
|
const text = line.trim();
|
|
@@ -188,7 +218,6 @@ export async function runAgent(opts) {
|
|
|
188
218
|
case '/exit':
|
|
189
219
|
case '/quit':
|
|
190
220
|
console.log(ok(s.goodbye));
|
|
191
|
-
chatRl.close();
|
|
192
221
|
await mcp.close();
|
|
193
222
|
return;
|
|
194
223
|
case '/help':
|
|
@@ -250,6 +279,31 @@ export async function runAgent(opts) {
|
|
|
250
279
|
process.stdout.write('\n\n');
|
|
251
280
|
history.push({ role: 'assistant', content: reply });
|
|
252
281
|
recorder.add('assistant', reply);
|
|
282
|
+
// ── Auto-run shell commands suggested by the AI ──────────────
|
|
283
|
+
const shellBlocks = extractShellBlocks(reply);
|
|
284
|
+
for (const cmd of shellBlocks) {
|
|
285
|
+
console.log(accent('▶ run? ') + dim(cmd.slice(0, 120) + (cmd.length > 120 ? '…' : '')));
|
|
286
|
+
const yes = await confirm({ message: 'Execute', default: true }).catch(() => false);
|
|
287
|
+
if (!yes) {
|
|
288
|
+
history.push({ role: 'user', content: `COMMAND SKIPPED: ${cmd}` });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const runSpinner = ora({ text: dim('running…'), spinner: 'dots' }).start();
|
|
292
|
+
const { stdout, stderr, code } = await runCommand(cmd, root);
|
|
293
|
+
runSpinner.stop();
|
|
294
|
+
const output = [
|
|
295
|
+
stdout.trim() ? `STDOUT:\n${stdout.trim()}` : '',
|
|
296
|
+
stderr.trim() ? `STDERR:\n${stderr.trim()}` : '',
|
|
297
|
+
`EXIT CODE: ${code}`,
|
|
298
|
+
].filter(Boolean).join('\n');
|
|
299
|
+
console.log(code === 0 ? ok('✓ done') : err(`✗ exit ${code}`));
|
|
300
|
+
if (stdout.trim())
|
|
301
|
+
console.log(dim(stdout.trim().slice(0, 800)));
|
|
302
|
+
if (stderr.trim())
|
|
303
|
+
console.log(warn(stderr.trim().slice(0, 400)));
|
|
304
|
+
history.push({ role: 'user', content: `COMMAND RESULT (${cmd.slice(0, 60)}):\n${output.slice(0, 6000)}` });
|
|
305
|
+
recorder.add('user', `COMMAND RESULT:\n${output.slice(0, 6000)}`);
|
|
306
|
+
}
|
|
253
307
|
// MCP tool call requested by the model?
|
|
254
308
|
const call = mcp.tools.length > 0 && round < 5 ? parseToolCall(reply) : null;
|
|
255
309
|
if (!call)
|
|
@@ -345,6 +399,13 @@ function buildSystemPrompt(lang, projectBlock, skillsText, memoryText, mcpText)
|
|
|
345
399
|
'Format code in fenced Markdown blocks with the language tag.',
|
|
346
400
|
`Environment: OS=${platform()}, cwd=${process.cwd()}.`,
|
|
347
401
|
`The user's preferred language is ${langNames[lang]}; reply in that language unless asked otherwise (code and identifiers stay in English).`,
|
|
402
|
+
'',
|
|
403
|
+
'## Command execution',
|
|
404
|
+
'You can run shell commands directly. Wrap any command you want to execute in a fenced code block tagged `bash` or `sh`.',
|
|
405
|
+
'Vexi will show the command to the user, ask for confirmation, execute it, and report the output back to you.',
|
|
406
|
+
'Use this to: install dependencies, build projects, run tests, start servers, scaffold files, etc.',
|
|
407
|
+
'Example: to install deps write: ```bash\nnpm install\n```',
|
|
408
|
+
'After seeing the output, continue helping based on the result.',
|
|
348
409
|
];
|
|
349
410
|
if (projectBlock)
|
|
350
411
|
parts.push('', '## Project map', projectBlock);
|
package/dist/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ import { createProvider, PROVIDER_INFO } from './providers/index.js';
|
|
|
32
32
|
import { openInDefaultApp } from './utils/open.js';
|
|
33
33
|
import { detectSystemLang, getStrings, normalizeLang, t, SUPPORTED_LANGS } from './i18n/index.js';
|
|
34
34
|
import { accent, dim, err, ok } from './ui/index.js';
|
|
35
|
-
export const VERSION = '0.5.
|
|
35
|
+
export const VERSION = '0.5.2';
|
|
36
36
|
/** Resolve the active language: --lang flag > saved config > system locale. */
|
|
37
37
|
async function resolveLang(flag) {
|
|
38
38
|
if (flag) {
|