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 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
- * Lines pasted together arrive within milliseconds of each other;
35
- * a 30 ms debounce collects them all before resolving.
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(rl, prompt) {
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
- rl.removeListener('line', onLine);
47
- rl.removeListener('close', onClose);
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
- rl.removeListener('line', onLine);
89
+ iface.removeListener('line', onLine);
60
90
  reject(Object.assign(new Error('stdin closed'), { name: 'ExitPromptError' }));
61
91
  }
62
92
  };
63
- rl.on('line', onLine);
64
- rl.once('close', onClose);
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(chatRl, userPrompt);
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.0';
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vexi-cli",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Open-source AI coding agent for your terminal. Bring your own key, zero config, multilingual.",
5
5
  "type": "module",
6
6
  "bin": {