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 +21 -0
- package/README.md +317 -0
- package/dist/agent.js +315 -0
- package/dist/cli.js +333 -0
- package/dist/config.js +36 -0
- package/dist/explain/index.js +238 -0
- package/dist/graph/html.js +225 -0
- package/dist/graph/index.js +129 -0
- package/dist/i18n/index.js +260 -0
- package/dist/index.js +25 -0
- package/dist/learn/index.js +131 -0
- package/dist/mcp/client.js +99 -0
- package/dist/mcp/config.js +27 -0
- package/dist/mcp/server.js +110 -0
- package/dist/memory/index.js +127 -0
- package/dist/providers/anthropic.js +59 -0
- package/dist/providers/detect.js +47 -0
- package/dist/providers/index.js +37 -0
- package/dist/providers/openai-compat.js +84 -0
- package/dist/providers/types.js +24 -0
- package/dist/replay/export.js +315 -0
- package/dist/replay/recorder.js +81 -0
- package/dist/scanner/gitignore.js +92 -0
- package/dist/scanner/index.js +230 -0
- package/dist/skills/index.js +121 -0
- package/dist/ui/index.js +39 -0
- package/dist/utils/fs-atomic.js +46 -0
- package/dist/utils/open.js +17 -0
- package/package.json +60 -0
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
|
+
[](https://www.npmjs.com/package/vexi)
|
|
16
|
+
[](LICENSE)
|
|
17
|
+
[](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
|
+
}
|