vexi-cli 0.5.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elomami1976 (Vexi contributors)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,317 @@
1
+ <div align="center">
2
+
3
+ ```
4
+ ██╗ ██╗███████╗██╗ ██╗██╗
5
+ ██║ ██║██╔════╝╚██╗██╔╝██║
6
+ ██║ ██║█████╗ ╚███╔╝ ██║
7
+ ╚██╗ ██╔╝██╔══╝ ██╔██╗ ██║
8
+ ╚████╔╝ ███████╗██╔╝ ██╗██║
9
+ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝
10
+ ```
11
+
12
+ **Open-source AI coding agent for your terminal.**
13
+ Bring your own key · Zero config · Multilingual · 100% local
14
+
15
+ [![npm](https://img.shields.io/npm/v/vexi?color=2979FF)](https://www.npmjs.com/package/vexi)
16
+ [![license](https://img.shields.io/badge/license-MIT-2979FF)](LICENSE)
17
+ [![node](https://img.shields.io/badge/node-%E2%89%A518-2979FF)](package.json)
18
+
19
+ **English** · [العربية](#-العربية) · [Español](#-español) · [Português](#-português) · [Français](#-français)
20
+
21
+ </div>
22
+
23
+ ---
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install -g vexi
29
+ vexi
30
+ ```
31
+
32
+ That's it. No login, no signup, no server, no database. Everything runs locally.
33
+
34
+ ## BYOK — Bring Your Own Key
35
+
36
+ On first run, paste any API key. Vexi **auto-detects the provider** from the key format:
37
+
38
+ | Key prefix | Provider |
39
+ | ------------------- | ----------------- |
40
+ | `sk-ant-...` | Anthropic (Claude)|
41
+ | `sk-or-...` | OpenRouter |
42
+ | `gsk_...` | Groq |
43
+ | `AIza...` | Google Gemini |
44
+ | `sk-...` / `sk-proj-...` | OpenAI |
45
+
46
+ If detection is ambiguous, Vexi simply asks you to pick the provider. Your key is stored **locally** in `~/.vexi/config.json` with owner-only file permissions (`chmod 600`).
47
+
48
+ ## Multilingual
49
+
50
+ ```bash
51
+ vexi --lang ar # العربية
52
+ vexi --lang es # Español
53
+ vexi --lang pt # Português
54
+ vexi --lang fr # Français
55
+ vexi --lang en # English
56
+ ```
57
+
58
+ Vexi auto-detects your system language on first run.
59
+
60
+ > **Note on Arabic:** terminals render RTL text incorrectly, so the interactive UI stays in English while AI replies, generated explanations and HTML exports (coming in Phase 3) are in fluent Arabic — where RTL renders perfectly.
61
+
62
+ ## Usage
63
+
64
+ ```bash
65
+ vexi # start a chat session in the current project
66
+ vexi --lang es # start in Spanish
67
+ vexi config # show config location + provider + model
68
+ vexi config reset # delete the stored API key
69
+ vexi skill list # show active skills
70
+ vexi skill add <src> # add a skill (local .md file or GitHub URL)
71
+ vexi skill remove <n> # remove a skill
72
+ vexi replay # list recorded sessions
73
+ vexi replay --export # export a session as an animated HTML replay
74
+ vexi explain auth.ts --ar # explain a file in Arabic (opens RTL HTML)
75
+ vexi explain src/ --es # explain a folder in Spanish (in terminal)
76
+ vexi graph --visual # interactive dependency graph in your browser
77
+ vexi mcp list # manage external MCP servers
78
+ vexi --mcp-server # expose Vexi as an MCP server (stdio)
79
+ vexi learn # learn your coding style from past sessions
80
+ vexi learn --apply # save it as a skill (injected in every session)
81
+ ```
82
+
83
+ Inside the chat:
84
+
85
+ ```
86
+ /help show available commands
87
+ /model switch model (e.g. /model gpt-4o)
88
+ /memory show compressed project memory
89
+ /clear clear conversation history
90
+ /exit quit
91
+ ```
92
+
93
+ ## 🧠 Project memory — Context Compression Engine
94
+
95
+ Most AI coding tools forget earlier decisions once the conversation gets long.
96
+ Vexi doesn't delete old messages — it **compresses** them:
97
+
98
+ - Recent messages always stay in full.
99
+ - Older messages are folded into a *running summary* + key decision points
100
+ (e.g. *"User chose JWT for authentication"*) stored in `.vexi/memory.json`.
101
+ - Memory is loaded automatically on every session start — Vexi remembers
102
+ your decisions **across sessions**, even in large projects.
103
+ - Inspect it anytime with `/memory`.
104
+
105
+ ## 🗺️ Full project understanding
106
+
107
+ On startup Vexi scans your whole project (not just the open file) and injects
108
+ a compact map into every prompt: languages, frameworks, and architecture
109
+ layers (frontend / backend / database / auth / devops).
110
+
111
+ Scanner safeguards: respects `.gitignore`, always skips `node_modules`,
112
+ `.git`, `dist`, `build`, `coverage`, and ignores files larger than 500KB —
113
+ so it never floods the context window.
114
+
115
+ ## 🎯 Custom Skills
116
+
117
+ Teach Vexi *your* conventions with plain markdown files in `.vexi/skills/`:
118
+
119
+ ```
120
+ .vexi/skills/
121
+ api-style.md "All API endpoints follow REST + Zod validation"
122
+ arabic-docs.md "All documentation written in Arabic"
123
+ my-stack.md "Always use Next.js + Supabase + Tailwind"
124
+ ```
125
+
126
+ Every skill is injected into the system prompt on session start, so generated
127
+ code follows your style automatically. Share skills via GitHub:
128
+
129
+ ```bash
130
+ vexi skill add https://github.com/user/react-best-practices
131
+ vexi skill add ./docs/conventions.md
132
+ ```
133
+
134
+ ## 🎬 Vexi Replay
135
+
136
+ Every chat session is automatically recorded to `.vexi/sessions/` (locally,
137
+ nothing leaves your machine). Export any session as a **single standalone
138
+ HTML file**:
139
+
140
+ ```bash
141
+ vexi replay --export # latest session
142
+ vexi replay --export --lang ar # full RTL Arabic replay
143
+ ```
144
+
145
+ The generated page has play/pause and 1×/2×/4× speed controls, messages
146
+ appear with their real timing and a character-by-character typing effect,
147
+ and it ends with a session summary (duration, messages, model). An
148
+ **Export video** button records the replay right in the browser
149
+ (MediaRecorder — no ffmpeg, the CLI stays lightweight). Share it anywhere.
150
+
151
+ ## 🌍 Explain code in your native language
152
+
153
+ > The first AI tool that explains any code in your native language.
154
+
155
+ ```bash
156
+ vexi explain auth.ts --ar # Arabic — opens a beautiful RTL HTML page
157
+ vexi explain src/ --es # Spanish — streams into the terminal
158
+ vexi explain app.py --fr # French
159
+ ```
160
+
161
+ Structured output: file purpose → function-by-function breakdown with line
162
+ numbers → how the pieces fit together. Latin-script languages stream
163
+ directly in the terminal; Arabic is written to `.md` + `.html` (dir="rtl")
164
+ and opened in your browser, where it renders perfectly.
165
+
166
+ ## 🗺️ Visual code graph
167
+
168
+ ```bash
169
+ vexi graph --visual
170
+ ```
171
+
172
+ Generates a **single HTML file** (no server) and opens it in your browser:
173
+ an interactive d3 force-directed graph of your modules with zoom, drag and
174
+ search. Node heat shows how many files depend on each module, and clicking
175
+ a node runs **impact analysis** — highlighting every file that breaks if
176
+ you change it.
177
+
178
+ ## 🔌 MCP support
179
+
180
+ **Vexi as MCP client** — connect external tools and the AI can call them
181
+ mid-conversation (works with every provider, no function-calling API needed):
182
+
183
+ ```bash
184
+ vexi mcp add github npx -y @modelcontextprotocol/server-github
185
+ vexi mcp list
186
+ ```
187
+
188
+ Configuration lives in `~/.vexi/mcp.json` (same shape as Claude Desktop's
189
+ config, copy entries verbatim).
190
+
191
+ **Vexi as MCP server** — the unique part: Vexi exposes its own capabilities
192
+ to *other* AI agents (Claude Desktop, Claude Code, Cursor…), built with the
193
+ official `@modelcontextprotocol/sdk`:
194
+
195
+ | Capability | Type |
196
+ | --- | --- |
197
+ | `vexi://project` — project structure map | Resource |
198
+ | `vexi://memory` — decisions & summary from past sessions | Resource |
199
+ | `vexi://sessions` — recorded session list | Resource |
200
+ | `scan_project` / `project_memory` / `explain_code` (5 languages) | Tools |
201
+
202
+ ```jsonc
203
+ // Claude Desktop config
204
+ { "mcpServers": { "vexi": { "command": "vexi", "args": ["--mcp-server"] } } }
205
+ ```
206
+
207
+ Vexi **complements** Claude Code instead of competing: its project memory
208
+ becomes a shared memory layer usable by any agent.
209
+
210
+ ## 🧠 Vexi Learn
211
+
212
+ > The agent gets more *you* over time.
213
+
214
+ ```bash
215
+ vexi learn # analyze your recent sessions, preview the learned style
216
+ vexi learn --apply # save it as .vexi/skills/learned-style.md
217
+ ```
218
+
219
+ Vexi mines your own recorded sessions for the strongest style signal there
220
+ is: **your corrections to the AI** — “don't use classes”, “always use
221
+ async/await”, “prefer named exports”, “لا تستخدم مكتبات خارجية” (signal
222
+ detection works in all 5 languages). It distills them into a markdown skill
223
+ file that is automatically injected into the system prompt of every future
224
+ session, so you stop repeating yourself. Everything stays local — the only
225
+ network call is to your own model provider, and you always preview before
226
+ saving.
227
+
228
+ ## Roadmap
229
+
230
+ | Phase | Feature | Status |
231
+ | ----- | ------- | ------ |
232
+ | 1 | BYOK · easy install · terminal chat | ✅ done |
233
+ | 2 | AI Context Compression (running summary memory) · full project understanding · custom skills | ✅ done |
234
+ | 3 | **Vexi Replay** (export sessions as animated HTML) · multilingual code explanation | ✅ done |
235
+ | 4 | Visual code graph · MCP support (client **and** server mode) | ✅ done |
236
+ | 5 | **Vexi Learn** — adapts to your personal coding style | ✅ done |
237
+
238
+ ## Why Vexi?
239
+
240
+ | | Vexi | OpenCode | Claude Code | Cursor |
241
+ | --- | --- | --- | --- | --- |
242
+ | Install | `npm i -g vexi` | binary/script | `npm i -g` | desktop app |
243
+ | BYOK (any provider) | ✅ 5 providers, auto-detect | ✅ | ❌ Anthropic only | partial |
244
+ | Works fully offline/local | ✅ no server, no account | ✅ | ❌ account | ❌ account |
245
+ | Native-language code explanations | ✅ ar/es/pt/fr | ❌ | ❌ | ❌ |
246
+ | Session replay export | ✅ | ❌ | ❌ | ❌ |
247
+ | Persistent project memory | ✅ | partial | partial | ✅ |
248
+ | Learns your personal coding style | ✅ from your own sessions | ❌ | ❌ | partial |
249
+ | MCP server mode (be a tool for other agents) | ✅ | ❌ | ❌ | ❌ |
250
+ | License | MIT | MIT | proprietary | proprietary |
251
+
252
+ Vexi **complements** tools like Claude Code instead of competing: its project memory and multilingual explanations will be exposed over MCP so any agent can use them.
253
+
254
+ ## Privacy & security
255
+
256
+ - Your API key never leaves your machine — requests go **directly** to your provider.
257
+ - `~/.vexi/config.json` is written atomically with `0600` permissions.
258
+ - No telemetry, no analytics, no accounts.
259
+
260
+ ## Contributing
261
+
262
+ PRs welcome! The codebase is small, modular and heavily commented:
263
+
264
+ ```
265
+ src/
266
+ ├── index.ts entry point
267
+ ├── cli.ts CLI definition (commander)
268
+ ├── agent.ts chat loop + first-run onboarding
269
+ ├── config.ts ~/.vexi/config.json (atomic, chmod 600)
270
+ ├── providers/ key detection + streaming API clients
271
+ ├── scanner/ project mapper (.gitignore-aware, size-capped)
272
+ ├── memory/ Context Compression Engine (.vexi/memory.json)
273
+ ├── skills/ custom skills loader (.vexi/skills/*.md)
274
+ ├── replay/ session recorder + HTML replay export
275
+ ├── explain/ multilingual code explanation (RTL HTML for Arabic)
276
+ ├── graph/ dependency graph + interactive d3 visualization
277
+ ├── mcp/ MCP client (tools in chat) + server mode
278
+ ├── learn/ Vexi Learn — style mining from your own sessions
279
+ ├── i18n/ 5-language UI strings + RTL strategy
280
+ ├── ui/ terminal branding (chalk, ora)
281
+ └── utils/ atomic JSON writes, cross-platform open
282
+ ```
283
+
284
+ 1. Fork & clone
285
+ 2. `npm install && npm run build`
286
+ 3. `node dist/index.js`
287
+ 4. Open a PR
288
+
289
+ To add support for a new key format, edit a single file: `src/providers/detect.ts`.
290
+
291
+ ---
292
+
293
+ ## 🌍 العربية
294
+
295
+ **Vexi** — وكيل برمجة بالذكاء الاصطناعي مفتوح المصدر يعمل في الطرفية. ثبّته بأمر واحد (`npm install -g vexi`)، الصق مفتاح API الخاص بك مرة واحدة، وابدأ فورًا. لا تسجيل، لا خادم، كل شيء يعمل محليًا على جهازك. يشرح Vexi أي كود بالعربية الفصحى (`vexi explain auth.ts --ar`) في ملفات HTML تدعم الاتجاه من اليمين لليسار بشكل مثالي، ويتعلّم أسلوبك البرمجي الشخصي من جلساتك السابقة (`vexi learn`).
296
+
297
+ ## 🌍 Español
298
+
299
+ **Vexi** es un agente de programación con IA, de código abierto, que vive en tu terminal. Instálalo con un solo comando (`npm install -g vexi`), pega tu clave API una vez y empieza al instante. Sin registro, sin servidor: todo se ejecuta localmente. Vexi detecta tu proveedor automáticamente y habla tu idioma.
300
+
301
+ ## 🌍 Português
302
+
303
+ **Vexi** é um agente de programação com IA, de código aberto, que vive no seu terminal. Instale com um único comando (`npm install -g vexi`), cole sua chave de API uma vez e comece imediatamente. Sem cadastro, sem servidor: tudo roda localmente. O Vexi detecta seu provedor automaticamente e fala o seu idioma.
304
+
305
+ ## 🌍 Français
306
+
307
+ **Vexi** est un agent de codage IA open source qui vit dans votre terminal. Installez-le en une seule commande (`npm install -g vexi`), collez votre clé API une fois et commencez immédiatement. Pas de compte, pas de serveur : tout s'exécute localement. Vexi détecte automatiquement votre fournisseur et parle votre langue.
308
+
309
+ ---
310
+
311
+ <div align="center">
312
+
313
+ **MIT License** · Made with ⚡ by the Vexi community
314
+
315
+ `npm install -g vexi`
316
+
317
+ </div>
package/dist/agent.js ADDED
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Vexi agent: first-run onboarding (BYOK) + interactive chat loop.
3
+ *
4
+ * Phase 2 additions:
5
+ * - Full project understanding: the scanner maps the codebase on startup
6
+ * and a compact summary is injected into every AI prompt.
7
+ * - Context Compression Engine: older messages are folded into a running
8
+ * summary in .vexi/memory.json instead of being deleted.
9
+ * - Custom Skills: .vexi/skills/*.md conventions are injected into the
10
+ * system prompt.
11
+ *
12
+ * Phase 4 additions:
13
+ * - MCP client: tools from servers configured in ~/.vexi/mcp.json are
14
+ * offered to the model (text-based tool calls, provider-agnostic) and
15
+ * executed over stdio.
16
+ */
17
+ import { basename } from 'node:path';
18
+ import { platform } from 'node:os';
19
+ import { input, select, confirm } from '@inquirer/prompts';
20
+ import ora from 'ora';
21
+ import { loadConfig, saveConfig, CONFIG_PATH } from './config.js';
22
+ import { createProvider, detectProvider, sanitizeKey, PROVIDER_INFO, ProviderError, } from './providers/index.js';
23
+ import { scanProject, projectSummary } from './scanner/index.js';
24
+ import { loadMemory, saveMemory, memoryBlock, compressIntoMemory, KEEP_RECENT, COMPRESS_INTERVAL, } from './memory/index.js';
25
+ import { loadSkills, skillsBlock } from './skills/index.js';
26
+ import { SessionRecorder } from './replay/recorder.js';
27
+ import { McpManager, parseToolCall } from './mcp/client.js';
28
+ import { loadMcpConfig } from './mcp/config.js';
29
+ import { ARABIC_RTL_NOTE, getStrings, t } from './i18n/index.js';
30
+ import { accent, dim, err, ok, printBanner, printStatusLine, userPrompt, vexiLabel, warn } from './ui/index.js';
31
+ export async function runAgent(opts) {
32
+ const s = getStrings(opts.lang);
33
+ printBanner(opts.version);
34
+ if (opts.lang === 'ar') {
35
+ console.log(warn(ARABIC_RTL_NOTE) + '\n');
36
+ }
37
+ // ── Load or create config (first-run experience) ──────────────────────
38
+ let config = await loadConfig();
39
+ if (!config) {
40
+ config = await firstRunSetup(s, opts.lang);
41
+ }
42
+ // Persist language override so future runs remember it
43
+ if (config.lang !== opts.lang) {
44
+ config.lang = opts.lang;
45
+ await saveConfig(config);
46
+ }
47
+ let provider = createProvider(config.provider, config.apiKey, config.model);
48
+ const root = process.cwd();
49
+ // ── Full project understanding: scan + load memory + load skills ──────
50
+ const scanSpinner = ora({ text: dim(s.scanning), spinner: 'dots' }).start();
51
+ let project = null;
52
+ try {
53
+ project = await scanProject(root);
54
+ scanSpinner.succeed(dim(t(s.scanned, { files: String(project.fileCount) })));
55
+ }
56
+ catch {
57
+ scanSpinner.stop(); // scanning is best-effort — chat works without it
58
+ }
59
+ let memory = await loadMemory(root);
60
+ const skills = await loadSkills(root);
61
+ // ── MCP client: connect configured external tool servers ────────────
62
+ const mcp = new McpManager();
63
+ const mcpConfig = await loadMcpConfig();
64
+ if (Object.keys(mcpConfig.mcpServers).length > 0) {
65
+ const mcpSpinner = ora({ text: dim(s.mcpConnecting), spinner: 'dots' }).start();
66
+ const { connected, failed } = await mcp.connect();
67
+ mcpSpinner.stop();
68
+ if (connected.length > 0) {
69
+ console.log(dim(t(s.mcpConnected, { servers: connected.join(', '), tools: String(mcp.tools.length) })));
70
+ }
71
+ for (const f of failed)
72
+ console.log(warn(t(s.mcpFailed, { name: f.name })));
73
+ }
74
+ // ── Session recording (Vexi Replay) — saved after every turn ─────────
75
+ const recorder = new SessionRecorder(root, {
76
+ project: basename(root),
77
+ provider: PROVIDER_INFO[config.provider].label,
78
+ model: provider.model,
79
+ lang: opts.lang,
80
+ });
81
+ printStatusLine({
82
+ project: project?.stack.length
83
+ ? `${basename(root)} (${project.stack.slice(0, 4).join(', ')})`
84
+ : basename(root),
85
+ provider: PROVIDER_INFO[config.provider].label,
86
+ model: provider.model,
87
+ lang: opts.lang,
88
+ });
89
+ if (memory.summary || memory.decisions.length > 0) {
90
+ console.log(dim(t(s.memoryLoaded, { decisions: String(memory.decisions.length) })));
91
+ }
92
+ if (skills.length > 0) {
93
+ console.log(dim(t(s.skillsLoaded, { names: skills.map((sk) => sk.name).join(', ') })));
94
+ }
95
+ console.log(dim(s.chatHint) + '\n');
96
+ // ── Chat loop ────────────────────────────────────────────────────────
97
+ const history = [];
98
+ const projectBlock = project ? projectSummary(project) : '';
99
+ const skillsText = skillsBlock(skills);
100
+ let compressing = false;
101
+ /**
102
+ * The system prompt is rebuilt every turn because the memory block
103
+ * changes as the Context Compression Engine folds in old messages.
104
+ */
105
+ const buildSystem = () => ({
106
+ role: 'system',
107
+ content: buildSystemPrompt(opts.lang, projectBlock, skillsText, memoryBlock(memory), mcp.promptBlock()),
108
+ });
109
+ /**
110
+ * Running-summary compression: when enough messages have accumulated
111
+ * beyond the keep-window, fold the oldest into .vexi/memory.json in the
112
+ * background. Recent messages always stay verbatim.
113
+ */
114
+ const maybeCompress = () => {
115
+ if (compressing || history.length < KEEP_RECENT + COMPRESS_INTERVAL)
116
+ return;
117
+ const archived = history.splice(0, history.length - KEEP_RECENT);
118
+ compressing = true;
119
+ compressIntoMemory(provider, memory, archived)
120
+ .then(async (updated) => {
121
+ memory = updated;
122
+ await saveMemory(root, updated);
123
+ })
124
+ .catch(() => { }) // compression must never break the chat
125
+ .finally(() => {
126
+ compressing = false;
127
+ });
128
+ };
129
+ while (true) {
130
+ let line;
131
+ try {
132
+ line = await input({ message: userPrompt, theme: { prefix: '' } });
133
+ }
134
+ catch {
135
+ // Ctrl+C / closed stdin
136
+ console.log('\n' + ok(s.goodbye));
137
+ return;
138
+ }
139
+ const text = line.trim();
140
+ if (!text)
141
+ continue;
142
+ // ── Slash commands ──
143
+ if (text.startsWith('/')) {
144
+ const [cmd, ...rest] = text.split(/\s+/);
145
+ switch (cmd) {
146
+ case '/exit':
147
+ case '/quit':
148
+ console.log(ok(s.goodbye));
149
+ await mcp.close();
150
+ return;
151
+ case '/help':
152
+ console.log(dim(s.helpText) + '\n');
153
+ continue;
154
+ case '/clear':
155
+ history.length = 0;
156
+ console.log(ok(s.historyCleared) + '\n');
157
+ continue;
158
+ case '/memory': {
159
+ if (memory.summary || memory.decisions.length > 0) {
160
+ if (memory.summary)
161
+ console.log(dim(memory.summary));
162
+ for (const decision of memory.decisions)
163
+ console.log(accent('• ') + decision);
164
+ console.log();
165
+ }
166
+ else {
167
+ console.log(dim(s.memoryEmpty) + '\n');
168
+ }
169
+ continue;
170
+ }
171
+ case '/model': {
172
+ const model = rest.join(' ').trim();
173
+ if (model) {
174
+ config.model = model;
175
+ await saveConfig(config);
176
+ provider = createProvider(config.provider, config.apiKey, model);
177
+ console.log(ok(t(s.modelSwitched, { model })) + '\n');
178
+ }
179
+ else {
180
+ console.log(dim(`model: ${provider.model}`) + '\n');
181
+ }
182
+ continue;
183
+ }
184
+ default:
185
+ console.log(warn(`Unknown command: ${cmd}`) + ' ' + dim('(/help)') + '\n');
186
+ continue;
187
+ }
188
+ }
189
+ // ── Send to the AI (with MCP tool-call loop) ──
190
+ history.push({ role: 'user', content: text });
191
+ recorder.add('user', text);
192
+ const spinner = ora({ text: dim(s.thinking), spinner: 'dots' }).start();
193
+ let started = false;
194
+ try {
195
+ // Up to 5 tool-call rounds per user turn, then a final plain answer.
196
+ for (let round = 0; round < 6; round++) {
197
+ const reply = await provider.stream([buildSystem(), ...history], (chunk) => {
198
+ if (!started) {
199
+ spinner.stop();
200
+ process.stdout.write(vexiLabel + ' ');
201
+ started = true;
202
+ }
203
+ process.stdout.write(chunk);
204
+ });
205
+ if (!started)
206
+ spinner.stop(); // empty reply edge case
207
+ process.stdout.write('\n\n');
208
+ history.push({ role: 'assistant', content: reply });
209
+ recorder.add('assistant', reply);
210
+ // MCP tool call requested by the model?
211
+ const call = mcp.tools.length > 0 && round < 5 ? parseToolCall(reply) : null;
212
+ if (!call)
213
+ break;
214
+ const toolSpinner = ora({
215
+ text: dim(t(s.mcpRunningTool, { tool: `${call.server}/${call.tool}` })),
216
+ spinner: 'dots',
217
+ }).start();
218
+ let result;
219
+ try {
220
+ result = await mcp.callTool(call.server, call.tool, call.arguments);
221
+ }
222
+ catch (e) {
223
+ result = `TOOL ERROR: ${e instanceof Error ? e.message : String(e)}`;
224
+ }
225
+ toolSpinner.stop();
226
+ const toolMessage = `TOOL RESULT (${call.server}/${call.tool}):\n${result.slice(0, 8000)}`;
227
+ history.push({ role: 'user', content: toolMessage });
228
+ recorder.add('user', toolMessage);
229
+ started = false; // next round streams with a fresh label
230
+ }
231
+ void recorder.save(); // fire-and-forget, atomic
232
+ maybeCompress();
233
+ }
234
+ catch (e) {
235
+ spinner.stop();
236
+ if (started)
237
+ process.stdout.write('\n');
238
+ history.pop(); // drop the failed user turn so it can be retried
239
+ if (e instanceof ProviderError && e.isAuthError) {
240
+ console.log(err(s.invalidKey));
241
+ const retry = await confirm({ message: s.reenterKey, default: true }).catch(() => false);
242
+ if (retry) {
243
+ config = await firstRunSetup(s, opts.lang);
244
+ provider = createProvider(config.provider, config.apiKey, config.model);
245
+ }
246
+ }
247
+ else {
248
+ const message = e instanceof Error ? e.message : String(e);
249
+ console.log(err(t(s.apiError, { message })) + '\n');
250
+ }
251
+ }
252
+ }
253
+ }
254
+ /** Ask for the API key, auto-detect the provider, save the config. */
255
+ async function firstRunSetup(s, lang) {
256
+ console.log(accent(s.welcome));
257
+ console.log(dim(s.firstRunIntro) + '\n');
258
+ // 1. Get a non-empty, sanitized key
259
+ let key = '';
260
+ while (!key) {
261
+ const raw = await input({ message: s.enterApiKey }).catch(() => {
262
+ console.log('\n' + ok(s.goodbye));
263
+ process.exit(0);
264
+ });
265
+ key = sanitizeKey(raw ?? '');
266
+ if (!key)
267
+ console.log(warn(s.emptyKey));
268
+ }
269
+ // 2. Auto-detect the provider, fall back to manual selection
270
+ let providerId = detectProvider(key);
271
+ if (providerId) {
272
+ console.log(ok(t(s.detectedProvider, { provider: PROVIDER_INFO[providerId].label })));
273
+ }
274
+ else {
275
+ console.log(warn(s.detectFailed));
276
+ providerId = await select({
277
+ message: s.selectProvider,
278
+ choices: Object.keys(PROVIDER_INFO).map((id) => ({
279
+ name: PROVIDER_INFO[id].label,
280
+ value: id,
281
+ })),
282
+ });
283
+ }
284
+ // 3. Save locally (atomic write, mode 600)
285
+ const config = { provider: providerId, apiKey: key, lang };
286
+ await saveConfig(config);
287
+ console.log(ok(t(s.configSaved, { path: CONFIG_PATH })) + '\n');
288
+ return config;
289
+ }
290
+ /** System prompt for the chat session. */
291
+ function buildSystemPrompt(lang, projectBlock, skillsText, memoryText, mcpText) {
292
+ const langNames = {
293
+ en: 'English',
294
+ ar: 'Arabic',
295
+ es: 'Spanish',
296
+ pt: 'Portuguese',
297
+ fr: 'French',
298
+ };
299
+ const parts = [
300
+ 'You are Vexi, an open-source AI coding agent running in the user\'s terminal.',
301
+ 'Be concise, technical and direct. Prefer code over prose.',
302
+ 'Format code in fenced Markdown blocks with the language tag.',
303
+ `Environment: OS=${platform()}, cwd=${process.cwd()}.`,
304
+ `The user's preferred language is ${langNames[lang]}; reply in that language unless asked otherwise (code and identifiers stay in English).`,
305
+ ];
306
+ if (projectBlock)
307
+ parts.push('', '## Project map', projectBlock);
308
+ if (memoryText)
309
+ parts.push('', memoryText);
310
+ if (skillsText)
311
+ parts.push('', skillsText);
312
+ if (mcpText)
313
+ parts.push('', mcpText);
314
+ return parts.join('\n');
315
+ }