vexi-cli 0.5.5 → 0.8.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/README.md +120 -14
- package/dist/agent.js +84 -8
- package/dist/cli.js +130 -9
- package/dist/config.js +18 -4
- package/dist/endpoint.js +174 -0
- package/dist/endpoint.test.js +146 -0
- package/dist/git/index.js +142 -0
- package/dist/git/index.test.js +178 -0
- package/dist/i18n/index.js +44 -12
- package/dist/mcp/client.js +8 -2
- package/dist/mcp/server.js +4 -3
- package/dist/memory/compress.test.js +52 -0
- package/dist/memory/index.js +10 -5
- package/dist/onboarding.js +105 -0
- package/dist/onboarding.test.js +76 -0
- package/dist/providers/anthropic.js +4 -3
- package/dist/providers/detect.js +18 -3
- package/dist/providers/index.js +43 -7
- package/dist/providers/openai-compat.js +1 -0
- package/dist/providers/types.js +11 -2
- package/dist/snapshots/index.js +261 -0
- package/dist/snapshots/index.test.js +105 -0
- package/dist/update/index.js +190 -0
- package/dist/version.js +4 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ Bring your own key · Zero config · Multilingual · 100% local
|
|
|
15
15
|
[](https://www.npmjs.com/package/vexi-cli)
|
|
16
16
|
[](LICENSE)
|
|
17
17
|
[](package.json)
|
|
18
|
+
[](https://vexi.pro)
|
|
18
19
|
|
|
19
20
|
**English** · [العربية](#-العربية) · [Español](#-español) · [Português](#-português) · [Français](#-français)
|
|
20
21
|
|
|
@@ -31,19 +32,89 @@ vexi
|
|
|
31
32
|
|
|
32
33
|
That's it. No login, no signup, no server, no database. Everything runs locally.
|
|
33
34
|
|
|
35
|
+
## Update
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
vexi update
|
|
39
|
+
# or directly:
|
|
40
|
+
npm install -g vexi-cli@latest
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Vexi checks once a day whether a newer version is published on npm and prints a
|
|
44
|
+
one-line notice at startup if one is available. To disable the check:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
vexi --no-update-check # skip for this run
|
|
48
|
+
VEXI_NO_UPDATE_CHECK=1 vexi # skip via environment variable
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Uninstall
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
vexi uninstall # removes Vexi, keeps your ~/.vexi config
|
|
55
|
+
vexi uninstall --purge # also deletes ~/.vexi (config, keys, memory)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or directly, without running Vexi:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm uninstall -g vexi-cli
|
|
62
|
+
rm -rf ~/.vexi # optional -- removes config, keys, and memory
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
On Windows:
|
|
66
|
+
|
|
67
|
+
```cmd
|
|
68
|
+
npm uninstall -g vexi-cli
|
|
69
|
+
rmdir /s /q %USERPROFILE%\.vexi
|
|
70
|
+
```
|
|
71
|
+
|
|
34
72
|
## BYOK — Bring Your Own Key
|
|
35
73
|
|
|
36
74
|
On first run, paste any API key. Vexi **auto-detects the provider** from the key format:
|
|
37
75
|
|
|
38
|
-
| Key prefix
|
|
39
|
-
|
|
|
40
|
-
| `sk-ant-...`
|
|
41
|
-
| `sk-or-...`
|
|
42
|
-
| `gsk_...`
|
|
43
|
-
| `AIza...`
|
|
44
|
-
| `
|
|
76
|
+
| Key prefix | Provider | Auto-detect? | Free tier? |
|
|
77
|
+
| ---------- | -------- | ------------ | ---------- |
|
|
78
|
+
| `sk-ant-...` | Anthropic (Claude) | ✅ | — |
|
|
79
|
+
| `sk-or-...` | OpenRouter | ✅ | ✅ free models |
|
|
80
|
+
| `gsk_...` | Groq | ✅ | ✅ free |
|
|
81
|
+
| `AIza...` | Google Gemini | ✅ | ✅ free |
|
|
82
|
+
| `csk-...` | Cerebras | ✅ | ✅ free |
|
|
83
|
+
| `<32-hex>.<secret>` | Zhipu AI — GLM | ✅ | ✅ free |
|
|
84
|
+
| `sk-proj-...` | OpenAI | ✅ | — |
|
|
85
|
+
| `sk-...` *(classic)* | OpenAI / DeepSeek / Kimi — pick manually | manual select | DeepSeek ✅ / Kimi ✅ |
|
|
86
|
+
| *(any)* | Qwen · Mistral · MiniMax | manual select | ✅ free |
|
|
45
87
|
|
|
46
|
-
|
|
88
|
+
> **Tip:** If Vexi guesses the wrong provider, run `vexi config reset`, paste your key again, and pick from the list.
|
|
89
|
+
|
|
90
|
+
Your key is stored **locally** in `~/.vexi/config.json` with owner-only file permissions (`chmod 600`).
|
|
91
|
+
|
|
92
|
+
### 🆓 Start for free — no credit card needed
|
|
93
|
+
|
|
94
|
+
**International:**
|
|
95
|
+
|
|
96
|
+
| Provider | Sign-up | Free model | Speed |
|
|
97
|
+
| -------- | ------- | ---------- | ----- |
|
|
98
|
+
| **Groq** | [console.groq.com](https://console.groq.com) | Llama 3.3 70B | ⚡⚡ very fast |
|
|
99
|
+
| **Google Gemini** | [aistudio.google.com](https://aistudio.google.com) | Gemini 2.5 Flash | ⚡ fast |
|
|
100
|
+
| **Cerebras** | [cloud.cerebras.ai](https://cloud.cerebras.ai) | Llama 3.3 70B | ⚡⚡⚡ fastest |
|
|
101
|
+
| **OpenRouter** | [openrouter.ai](https://openrouter.ai) | many free models | varies |
|
|
102
|
+
|
|
103
|
+
**Chinese AI (all free tier, great quality):**
|
|
104
|
+
|
|
105
|
+
| Provider | Sign-up | Free model | Notes |
|
|
106
|
+
| -------- | ------- | ---------- | ----- |
|
|
107
|
+
| **DeepSeek** | [platform.deepseek.com](https://platform.deepseek.com) | deepseek-chat (V3) | excellent coder |
|
|
108
|
+
| **Zhipu AI — GLM** | [bigmodel.cn](https://bigmodel.cn) | glm-4-flash | auto-detected |
|
|
109
|
+
| **Alibaba Qwen** | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) | qwen-turbo | manual select |
|
|
110
|
+
| **Kimi (Moonshot)** | [platform.moonshot.cn](https://platform.moonshot.cn) | moonshot-v1-8k | manual select |
|
|
111
|
+
| **MiniMax** | [platform.minimaxi.com](https://platform.minimaxi.com) | MiniMax-Text-01 | manual select |
|
|
112
|
+
|
|
113
|
+
To switch provider at any time:
|
|
114
|
+
```bash
|
|
115
|
+
vexi config reset # wipe the stored key
|
|
116
|
+
vexi # restart — prompts for a new key
|
|
117
|
+
```
|
|
47
118
|
|
|
48
119
|
## Multilingual
|
|
49
120
|
|
|
@@ -78,16 +149,23 @@ vexi mcp list # manage external MCP servers
|
|
|
78
149
|
vexi --mcp-server # expose Vexi as an MCP server (stdio)
|
|
79
150
|
vexi learn # learn your coding style from past sessions
|
|
80
151
|
vexi learn --apply # save it as a skill (injected in every session)
|
|
152
|
+
vexi undo # revert the last AI file edit
|
|
153
|
+
vexi redo # re-apply a reverted edit
|
|
154
|
+
vexi history # list recent AI file edits with timestamps
|
|
155
|
+
vexi clean # clear old snapshots (.vexi/snapshots/)
|
|
81
156
|
```
|
|
82
157
|
|
|
83
158
|
Inside the chat:
|
|
84
159
|
|
|
85
160
|
```
|
|
86
|
-
/help
|
|
87
|
-
/model
|
|
88
|
-
/memory
|
|
89
|
-
/clear
|
|
90
|
-
/
|
|
161
|
+
/help show available commands
|
|
162
|
+
/model switch model (e.g. /model gpt-4o)
|
|
163
|
+
/memory show compressed project memory
|
|
164
|
+
/clear clear conversation history
|
|
165
|
+
/undo revert last AI file edit
|
|
166
|
+
/redo re-apply last undone edit
|
|
167
|
+
/history list recent AI file edits
|
|
168
|
+
/exit quit
|
|
91
169
|
```
|
|
92
170
|
|
|
93
171
|
## ⚙️ Multi-language build support
|
|
@@ -241,6 +319,29 @@ session, so you stop repeating yourself. Everything stays local — the only
|
|
|
241
319
|
network call is to your own model provider, and you always preview before
|
|
242
320
|
saving.
|
|
243
321
|
|
|
322
|
+
## ↩️ Undo / Redo — instant rescue from any AI edit
|
|
323
|
+
|
|
324
|
+
> Approve a change, see it break things, type `vexi undo`. Done.
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
vexi undo # revert the last AI-applied file change
|
|
328
|
+
vexi redo # re-apply a reverted change
|
|
329
|
+
vexi history # list every file edit this session, with timestamps
|
|
330
|
+
vexi clean # remove old snapshot sessions to free disk space
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Or use the in-chat shortcuts without leaving the session: `/undo`, `/redo`, `/history`.
|
|
334
|
+
|
|
335
|
+
**How it works (lightweight snapshot design):**
|
|
336
|
+
|
|
337
|
+
- Before executing any confirmed shell command, Vexi inspects it for file-write patterns (`cat >`, `sed -i`, `mv`, `cp`, `tee`, `writeFileSync`, PowerShell `Set-Content`, and bare source-file tokens) and saves a copy of **only the affected files** into `.vexi/snapshots/<session>/`.
|
|
338
|
+
- `undo` restores the pre-command copy and saves the current state as a redo point — so you can bounce back and forth freely.
|
|
339
|
+
- Each session keeps a maximum of 50 snapshots; the oldest are pruned automatically.
|
|
340
|
+
- Snapshots are **per-file, not per-project** — no full-tree indexing, no slow startup, no huge disk usage (the approach OpenCode's whole-tree snapshot system suffers from).
|
|
341
|
+
- `vexi clean` wipes all previous sessions' snapshots while keeping the current one active.
|
|
342
|
+
|
|
343
|
+
This pairs naturally with the confirmation prompt: even if you approve a change that turns out to be wrong, one command gets you back.
|
|
344
|
+
|
|
244
345
|
## Roadmap
|
|
245
346
|
|
|
246
347
|
| Phase | Feature | Status |
|
|
@@ -250,19 +351,23 @@ saving.
|
|
|
250
351
|
| 3 | **Vexi Replay** (export sessions as animated HTML) · multilingual code explanation | ✅ done |
|
|
251
352
|
| 4 | Visual code graph · MCP support (client **and** server mode) | ✅ done |
|
|
252
353
|
| 5 | **Vexi Learn** — adapts to your personal coding style | ✅ done |
|
|
354
|
+
| 6 | **Multi-language builds** — auto-executes pip, gcc, javac, cargo, gradle from chat | ✅ done |
|
|
355
|
+
| 7 | **Undo / Redo** — instant one-command revert of any AI file edit, without touching git | ✅ done |
|
|
253
356
|
|
|
254
357
|
## Why Vexi?
|
|
255
358
|
|
|
256
359
|
| | Vexi | OpenCode | Claude Code | Cursor |
|
|
257
360
|
| --- | --- | --- | --- | --- |
|
|
258
361
|
| Install | `npm i -g vexi-cli` | binary/script | `npm i -g` | desktop app |
|
|
259
|
-
| BYOK (any provider) | ✅
|
|
362
|
+
| BYOK (any provider) | ✅ 12 providers incl. Chinese AI | ✅ | ❌ Anthropic only | partial |
|
|
260
363
|
| Works fully offline/local | ✅ no server, no account | ✅ | ❌ account | ❌ account |
|
|
261
364
|
| Native-language code explanations | ✅ ar/es/pt/fr | ❌ | ❌ | ❌ |
|
|
262
365
|
| Session replay export | ✅ | ❌ | ❌ | ❌ |
|
|
263
366
|
| Persistent project memory | ✅ | partial | partial | ✅ |
|
|
264
367
|
| Learns your personal coding style | ✅ from your own sessions | ❌ | ❌ | partial |
|
|
265
368
|
| MCP server mode (be a tool for other agents) | ✅ | ❌ | ❌ | ❌ |
|
|
369
|
+
| Builds any language (Python, Java, C, Rust, Go) | ✅ | ❌ | ✅ | ✅ |
|
|
370
|
+
| Instant undo/redo of AI edits (no git required) | ✅ per-file snapshots | ❌ | ❌ | ❌ |
|
|
266
371
|
| License | MIT | MIT | proprietary | proprietary |
|
|
267
372
|
|
|
268
373
|
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.
|
|
@@ -292,6 +397,7 @@ src/
|
|
|
292
397
|
├── graph/ dependency graph + interactive d3 visualization
|
|
293
398
|
├── mcp/ MCP client (tools in chat) + server mode
|
|
294
399
|
├── learn/ Vexi Learn — style mining from your own sessions
|
|
400
|
+
├── snapshots/ undo/redo engine — per-file backups before AI edits
|
|
295
401
|
├── i18n/ 5-language UI strings + RTL strategy
|
|
296
402
|
├── ui/ terminal branding (chalk, ora)
|
|
297
403
|
└── utils/ atomic JSON writes, cross-platform open
|
package/dist/agent.js
CHANGED
|
@@ -21,7 +21,8 @@ import { input, select, confirm } from '@inquirer/prompts';
|
|
|
21
21
|
import * as nodeRl from 'node:readline';
|
|
22
22
|
import ora from 'ora';
|
|
23
23
|
import { loadConfig, saveConfig, CONFIG_PATH } from './config.js';
|
|
24
|
-
import {
|
|
24
|
+
import { SnapshotManager } from './snapshots/index.js';
|
|
25
|
+
import { createProviderFromConfig, detectProvider, sanitizeKey, PROVIDER_INFO, ProviderError, } from './providers/index.js';
|
|
25
26
|
import { scanProject, projectSummary } from './scanner/index.js';
|
|
26
27
|
import { loadMemory, saveMemory, memoryBlock, compressIntoMemory, KEEP_RECENT, COMPRESS_INTERVAL, } from './memory/index.js';
|
|
27
28
|
import { loadSkills, skillsBlock } from './skills/index.js';
|
|
@@ -29,6 +30,7 @@ import { SessionRecorder } from './replay/recorder.js';
|
|
|
29
30
|
import { McpManager, parseToolCall } from './mcp/client.js';
|
|
30
31
|
import { loadMcpConfig } from './mcp/config.js';
|
|
31
32
|
import { ARABIC_RTL_NOTE, getStrings, t } from './i18n/index.js';
|
|
33
|
+
import { gitPush } from './git/index.js';
|
|
32
34
|
import { accent, dim, err, ok, printBanner, printStatusLine, userPrompt, vexiLabel, warn } from './ui/index.js';
|
|
33
35
|
/** Extract shell commands from fenced code blocks in an AI reply. */
|
|
34
36
|
function extractShellBlocks(reply) {
|
|
@@ -116,7 +118,7 @@ export async function runAgent(opts) {
|
|
|
116
118
|
config.lang = opts.lang;
|
|
117
119
|
await saveConfig(config);
|
|
118
120
|
}
|
|
119
|
-
let provider =
|
|
121
|
+
let provider = createProviderFromConfig(config);
|
|
120
122
|
const root = process.cwd();
|
|
121
123
|
// ── Full project understanding: scan + load memory + load skills ──────
|
|
122
124
|
const scanSpinner = ora({ text: dim(s.scanning), spinner: 'dots' }).start();
|
|
@@ -143,10 +145,15 @@ export async function runAgent(opts) {
|
|
|
143
145
|
for (const f of failed)
|
|
144
146
|
console.log(warn(t(s.mcpFailed, { name: f.name })));
|
|
145
147
|
}
|
|
148
|
+
// ── Snapshot engine: register this session for undo/redo ─────────────
|
|
149
|
+
const snapshotSessionId = Date.now().toString(36);
|
|
150
|
+
const snapshots = new SnapshotManager(root, snapshotSessionId);
|
|
151
|
+
await snapshots.registerAsCurrentSession().catch(() => { });
|
|
146
152
|
// ── Session recording (Vexi Replay) — saved after every turn ─────────
|
|
153
|
+
const providerLabel = config.displayName ?? PROVIDER_INFO[config.provider]?.label ?? config.provider;
|
|
147
154
|
const recorder = new SessionRecorder(root, {
|
|
148
155
|
project: basename(root),
|
|
149
|
-
provider:
|
|
156
|
+
provider: providerLabel,
|
|
150
157
|
model: provider.model,
|
|
151
158
|
lang: opts.lang,
|
|
152
159
|
});
|
|
@@ -154,7 +161,7 @@ export async function runAgent(opts) {
|
|
|
154
161
|
project: project?.stack.length
|
|
155
162
|
? `${basename(root)} (${project.stack.slice(0, 4).join(', ')})`
|
|
156
163
|
: basename(root),
|
|
157
|
-
provider:
|
|
164
|
+
provider: providerLabel,
|
|
158
165
|
model: provider.model,
|
|
159
166
|
lang: opts.lang,
|
|
160
167
|
});
|
|
@@ -164,6 +171,16 @@ export async function runAgent(opts) {
|
|
|
164
171
|
if (skills.length > 0) {
|
|
165
172
|
console.log(dim(t(s.skillsLoaded, { names: skills.map((sk) => sk.name).join(', ') })));
|
|
166
173
|
}
|
|
174
|
+
// Peek at the update check (only picks up already-resolved results; never blocks)
|
|
175
|
+
const latestVersion = opts.updateCheckPromise
|
|
176
|
+
? await Promise.race([
|
|
177
|
+
opts.updateCheckPromise,
|
|
178
|
+
new Promise((r) => setTimeout(() => r(null), 0)),
|
|
179
|
+
])
|
|
180
|
+
: null;
|
|
181
|
+
if (latestVersion) {
|
|
182
|
+
console.log(dim(`Update available: ${opts.version} -> ${latestVersion} run: npm install -g vexi-cli@latest`));
|
|
183
|
+
}
|
|
167
184
|
console.log(dim(s.chatHint) + '\n');
|
|
168
185
|
// ── Chat loop ────────────────────────────────────────────────────────
|
|
169
186
|
const history = [];
|
|
@@ -186,14 +203,18 @@ export async function runAgent(opts) {
|
|
|
186
203
|
const maybeCompress = () => {
|
|
187
204
|
if (compressing || history.length < KEEP_RECENT + COMPRESS_INTERVAL)
|
|
188
205
|
return;
|
|
189
|
-
|
|
206
|
+
// Slice (not splice) — do NOT mutate history before we know compression succeeded.
|
|
207
|
+
const archiveCount = history.length - KEEP_RECENT;
|
|
208
|
+
const archived = history.slice(0, archiveCount);
|
|
190
209
|
compressing = true;
|
|
191
210
|
compressIntoMemory(provider, memory, archived)
|
|
192
211
|
.then(async (updated) => {
|
|
212
|
+
// Only remove from history after a successful archive write.
|
|
213
|
+
history.splice(0, archiveCount);
|
|
193
214
|
memory = updated;
|
|
194
215
|
await saveMemory(root, updated);
|
|
195
216
|
})
|
|
196
|
-
.catch(() => { }) //
|
|
217
|
+
.catch(() => { }) // history intact on failure — retries next turn
|
|
197
218
|
.finally(() => {
|
|
198
219
|
compressing = false;
|
|
199
220
|
});
|
|
@@ -245,7 +266,7 @@ export async function runAgent(opts) {
|
|
|
245
266
|
if (model) {
|
|
246
267
|
config.model = model;
|
|
247
268
|
await saveConfig(config);
|
|
248
|
-
provider =
|
|
269
|
+
provider = createProviderFromConfig(config);
|
|
249
270
|
console.log(ok(t(s.modelSwitched, { model })) + '\n');
|
|
250
271
|
}
|
|
251
272
|
else {
|
|
@@ -253,6 +274,56 @@ export async function runAgent(opts) {
|
|
|
253
274
|
}
|
|
254
275
|
continue;
|
|
255
276
|
}
|
|
277
|
+
case '/undo': {
|
|
278
|
+
const entry = await snapshots.undo().catch(() => null);
|
|
279
|
+
if (!entry) {
|
|
280
|
+
console.log(dim(s.undoNone) + '\n');
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.log(ok(t(s.undoDone, { files: entry.files.join(', ') })) + '\n');
|
|
284
|
+
}
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
case '/redo': {
|
|
288
|
+
const entry = await snapshots.redo().catch(() => null);
|
|
289
|
+
if (!entry) {
|
|
290
|
+
console.log(dim(s.redoNone) + '\n');
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
console.log(ok(t(s.redoDone, { files: entry.files.join(', ') })) + '\n');
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
case '/history': {
|
|
298
|
+
const entries = await snapshots.list().catch(() => []);
|
|
299
|
+
if (entries.length === 0) {
|
|
300
|
+
console.log(dim(s.historyNone) + '\n');
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
console.log(dim(s.historyHeader));
|
|
304
|
+
for (const e of entries) {
|
|
305
|
+
const time = new Date(e.at).toLocaleTimeString();
|
|
306
|
+
console.log(accent(` ${time}`) + dim(` ${e.files.join(', ')}`) + dim(` — ${e.label.slice(0, 60)}`));
|
|
307
|
+
}
|
|
308
|
+
console.log();
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
case '/push': {
|
|
313
|
+
const firstArg = rest[0] ?? '';
|
|
314
|
+
const pushOnly = firstArg === '--only';
|
|
315
|
+
const message = pushOnly || rest.length === 0 ? undefined : rest.join(' ').trim() || undefined;
|
|
316
|
+
await gitPush({
|
|
317
|
+
cwd: root,
|
|
318
|
+
run: runCommand,
|
|
319
|
+
confirm: (m) => confirm({ message: m, default: true }).catch(() => false),
|
|
320
|
+
provider,
|
|
321
|
+
message,
|
|
322
|
+
pushOnly,
|
|
323
|
+
});
|
|
324
|
+
console.log();
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
256
327
|
default:
|
|
257
328
|
console.log(warn(`Unknown command: ${cmd}`) + ' ' + dim('(/help)') + '\n');
|
|
258
329
|
continue;
|
|
@@ -288,6 +359,11 @@ export async function runAgent(opts) {
|
|
|
288
359
|
history.push({ role: 'user', content: `COMMAND SKIPPED: ${cmd}` });
|
|
289
360
|
continue;
|
|
290
361
|
}
|
|
362
|
+
// Silent snapshot of any files the command is about to modify
|
|
363
|
+
const filesToSnap = SnapshotManager.extractFilePaths(cmd, root);
|
|
364
|
+
if (filesToSnap.length > 0) {
|
|
365
|
+
await snapshots.takeSnapshot(filesToSnap, cmd.slice(0, 80)).catch(() => { });
|
|
366
|
+
}
|
|
291
367
|
const runSpinner = ora({ text: dim('running…'), spinner: 'dots' }).start();
|
|
292
368
|
const { stdout, stderr, code } = await runCommand(cmd, root);
|
|
293
369
|
runSpinner.stop();
|
|
@@ -338,7 +414,7 @@ export async function runAgent(opts) {
|
|
|
338
414
|
const retry = await confirm({ message: s.reenterKey, default: true }).catch(() => false);
|
|
339
415
|
if (retry) {
|
|
340
416
|
config = await firstRunSetup(s, opts.lang);
|
|
341
|
-
provider =
|
|
417
|
+
provider = createProviderFromConfig(config);
|
|
342
418
|
}
|
|
343
419
|
}
|
|
344
420
|
else {
|
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* vexi learn [--apply] learn your coding style from past sessions
|
|
18
18
|
*/
|
|
19
19
|
import { Command } from 'commander';
|
|
20
|
+
import { VERSION } from './version.js';
|
|
20
21
|
import ora from 'ora';
|
|
22
|
+
import { checkForUpdate, runUpdate, runUninstall } from './update/index.js';
|
|
21
23
|
import { runAgent } from './agent.js';
|
|
22
24
|
import { loadConfig, resetConfig, CONFIG_PATH } from './config.js';
|
|
23
25
|
import { loadSkills, addSkill, removeSkill } from './skills/index.js';
|
|
@@ -28,11 +30,13 @@ import { buildGraph } from './graph/index.js';
|
|
|
28
30
|
import { exportGraphHtml } from './graph/html.js';
|
|
29
31
|
import { loadMcpConfig, saveMcpConfig, MCP_CONFIG_PATH } from './mcp/config.js';
|
|
30
32
|
import { learn, applyLearned, DEFAULT_MAX_SESSIONS } from './learn/index.js';
|
|
31
|
-
import {
|
|
33
|
+
import { createProviderFromConfig, PROVIDER_INFO } from './providers/index.js';
|
|
34
|
+
import { runOnboarding } from './onboarding.js';
|
|
35
|
+
import * as nodeRlPromises from 'node:readline/promises';
|
|
32
36
|
import { openInDefaultApp } from './utils/open.js';
|
|
37
|
+
import { SnapshotManager } from './snapshots/index.js';
|
|
33
38
|
import { detectSystemLang, getStrings, normalizeLang, t, SUPPORTED_LANGS } from './i18n/index.js';
|
|
34
39
|
import { accent, dim, err, ok } from './ui/index.js';
|
|
35
|
-
export const VERSION = '0.5.5';
|
|
36
40
|
/** Resolve the active language: --lang flag > saved config > system locale. */
|
|
37
41
|
async function resolveLang(flag) {
|
|
38
42
|
if (flag) {
|
|
@@ -54,15 +58,18 @@ export function buildCli() {
|
|
|
54
58
|
.version(VERSION, '-v, --version')
|
|
55
59
|
.option('-l, --lang <lang>', `UI language (${SUPPORTED_LANGS.join('/')})`)
|
|
56
60
|
.option('--mcp-server', 'run Vexi as an MCP server over stdio (for Claude Desktop, Cursor, etc.)')
|
|
61
|
+
.option('--no-update-check', 'skip the daily background update check')
|
|
57
62
|
.action(async (options) => {
|
|
58
63
|
if (options.mcpServer) {
|
|
59
|
-
// stdout becomes the JSON-RPC channel
|
|
64
|
+
// stdout becomes the JSON-RPC channel -- no banner, no prompts.
|
|
60
65
|
const { runMcpServer } = await import('./mcp/server.js');
|
|
61
66
|
await runMcpServer();
|
|
62
67
|
return;
|
|
63
68
|
}
|
|
64
69
|
const lang = await resolveLang(options.lang);
|
|
65
|
-
|
|
70
|
+
const noUpdateCheck = !options.updateCheck || !!process.env['VEXI_NO_UPDATE_CHECK'];
|
|
71
|
+
const updateCheckPromise = noUpdateCheck ? Promise.resolve(null) : checkForUpdate();
|
|
72
|
+
await runAgent({ lang, version: VERSION, updateCheckPromise });
|
|
66
73
|
});
|
|
67
74
|
const config = program.command('config').description('Manage Vexi configuration');
|
|
68
75
|
config
|
|
@@ -72,12 +79,16 @@ export function buildCli() {
|
|
|
72
79
|
const cfg = await loadConfig();
|
|
73
80
|
console.log(dim('config: ') + accent(CONFIG_PATH));
|
|
74
81
|
if (cfg) {
|
|
75
|
-
|
|
76
|
-
console.log(dim('
|
|
82
|
+
const providerLabel = cfg.displayName ?? PROVIDER_INFO[cfg.provider]?.label ?? cfg.provider;
|
|
83
|
+
console.log(dim('provider: ') + accent(providerLabel));
|
|
84
|
+
const defaultModel = PROVIDER_INFO[cfg.provider]?.defaultModel ?? 'unknown';
|
|
85
|
+
console.log(dim('model: ') + accent(cfg.model ?? defaultModel));
|
|
86
|
+
if (cfg.baseUrl)
|
|
87
|
+
console.log(dim('baseUrl: ') + accent(cfg.baseUrl));
|
|
77
88
|
console.log(dim('lang: ') + accent(cfg.lang ?? 'auto'));
|
|
78
89
|
}
|
|
79
90
|
else {
|
|
80
|
-
console.log(dim('No configuration yet — run `vexi` to
|
|
91
|
+
console.log(dim('No configuration yet — run `vexi` or `vexi setup` to configure.'));
|
|
81
92
|
}
|
|
82
93
|
});
|
|
83
94
|
config
|
|
@@ -184,7 +195,7 @@ export function buildCli() {
|
|
|
184
195
|
process.exitCode = 1;
|
|
185
196
|
return;
|
|
186
197
|
}
|
|
187
|
-
const provider =
|
|
198
|
+
const provider = createProviderFromConfig(cfg);
|
|
188
199
|
const spinner = ora({ text: dim(s.explaining), spinner: 'dots' }).start();
|
|
189
200
|
let started = false;
|
|
190
201
|
try {
|
|
@@ -296,7 +307,7 @@ export function buildCli() {
|
|
|
296
307
|
process.exitCode = 1;
|
|
297
308
|
return;
|
|
298
309
|
}
|
|
299
|
-
const provider =
|
|
310
|
+
const provider = createProviderFromConfig(cfg);
|
|
300
311
|
const maxSessions = Math.max(1, parseInt(options.sessions ?? '', 10) || DEFAULT_MAX_SESSIONS);
|
|
301
312
|
const spinner = ora({ text: dim(s.learnAnalyzing), spinner: 'dots' }).start();
|
|
302
313
|
try {
|
|
@@ -329,5 +340,115 @@ export function buildCli() {
|
|
|
329
340
|
process.exitCode = 1;
|
|
330
341
|
}
|
|
331
342
|
});
|
|
343
|
+
// ── Undo / Redo / History / Clean (Feature 8) ────────────────────────
|
|
344
|
+
program
|
|
345
|
+
.command('undo')
|
|
346
|
+
.description('Revert the last AI file edit in the current session')
|
|
347
|
+
.action(async () => {
|
|
348
|
+
const s = getStrings(await resolveLang());
|
|
349
|
+
const mgr = await SnapshotManager.forCurrentSession(process.cwd());
|
|
350
|
+
if (!mgr) {
|
|
351
|
+
console.log(dim(s.snapshotNoSession));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const entry = await mgr.undo().catch(() => null);
|
|
355
|
+
if (!entry) {
|
|
356
|
+
console.log(dim(s.undoNone));
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
console.log(ok(t(s.undoDone, { files: entry.files.join(', ') })));
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
program
|
|
363
|
+
.command('redo')
|
|
364
|
+
.description('Re-apply the last undone AI file edit')
|
|
365
|
+
.action(async () => {
|
|
366
|
+
const s = getStrings(await resolveLang());
|
|
367
|
+
const mgr = await SnapshotManager.forCurrentSession(process.cwd());
|
|
368
|
+
if (!mgr) {
|
|
369
|
+
console.log(dim(s.snapshotNoSession));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const entry = await mgr.redo().catch(() => null);
|
|
373
|
+
if (!entry) {
|
|
374
|
+
console.log(dim(s.redoNone));
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
console.log(ok(t(s.redoDone, { files: entry.files.join(', ') })));
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
program
|
|
381
|
+
.command('history')
|
|
382
|
+
.description('List recent AI file edits in this session (with timestamps)')
|
|
383
|
+
.action(async () => {
|
|
384
|
+
const s = getStrings(await resolveLang());
|
|
385
|
+
const mgr = await SnapshotManager.forCurrentSession(process.cwd());
|
|
386
|
+
if (!mgr) {
|
|
387
|
+
console.log(dim(s.snapshotNoSession));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const entries = await mgr.list().catch(() => []);
|
|
391
|
+
if (entries.length === 0) {
|
|
392
|
+
console.log(dim(s.historyNone));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
console.log(dim(s.historyHeader));
|
|
396
|
+
for (const e of entries) {
|
|
397
|
+
const time = new Date(e.at).toLocaleTimeString();
|
|
398
|
+
console.log(accent(` ${time}`) + dim(` ${e.files.join(', ')}`) + dim(` — ${e.label.slice(0, 60)}`));
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
program
|
|
402
|
+
.command('clean')
|
|
403
|
+
.description('Clear old snapshot sessions to free disk space (.vexi/snapshots/)')
|
|
404
|
+
.action(async () => {
|
|
405
|
+
const s = getStrings(await resolveLang());
|
|
406
|
+
const mgr = await SnapshotManager.forCurrentSession(process.cwd());
|
|
407
|
+
const count = await SnapshotManager.cleanAll(process.cwd(), mgr?.sessionId);
|
|
408
|
+
console.log(ok(t(s.cleanDone, { count: String(count) })));
|
|
409
|
+
});
|
|
410
|
+
// ── URL-based provider setup (vexi setup) ────────────────────────────
|
|
411
|
+
program
|
|
412
|
+
.command('setup')
|
|
413
|
+
.description('Configure your API provider by pasting an endpoint URL')
|
|
414
|
+
.action(async () => {
|
|
415
|
+
const io = {
|
|
416
|
+
prompt: async (question) => {
|
|
417
|
+
const iface = nodeRlPromises.createInterface({
|
|
418
|
+
input: process.stdin,
|
|
419
|
+
output: process.stdout,
|
|
420
|
+
});
|
|
421
|
+
try {
|
|
422
|
+
return await iface.question(question);
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
iface.close();
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
write: (line) => console.log(line),
|
|
429
|
+
};
|
|
430
|
+
try {
|
|
431
|
+
await runOnboarding(io);
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
console.error(err(e instanceof Error ? e.message : String(e)));
|
|
435
|
+
process.exitCode = 1;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
// ── Self-update (vexi update) ─────────────────────────────────────────
|
|
439
|
+
program
|
|
440
|
+
.command('update')
|
|
441
|
+
.description('Update Vexi to the latest version (npm install -g vexi-cli@latest)')
|
|
442
|
+
.action(async () => {
|
|
443
|
+
await runUpdate();
|
|
444
|
+
});
|
|
445
|
+
// ── Self-uninstall (vexi uninstall) ──────────────────────────────────
|
|
446
|
+
program
|
|
447
|
+
.command('uninstall')
|
|
448
|
+
.description('Uninstall Vexi. Keeps ~/.vexi unless --purge is passed.')
|
|
449
|
+
.option('--purge', 'also delete ~/.vexi (config, keys, memory, sessions)')
|
|
450
|
+
.action(async (options) => {
|
|
451
|
+
await runUninstall(!!options.purge);
|
|
452
|
+
});
|
|
332
453
|
return program;
|
|
333
454
|
}
|
package/dist/config.js
CHANGED
|
@@ -10,15 +10,29 @@
|
|
|
10
10
|
import { promises as fs } from 'node:fs';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
+
import { z } from 'zod';
|
|
13
14
|
import { readJson, writeJsonAtomic } from './utils/fs-atomic.js';
|
|
15
|
+
const VexiConfigSchema = z.object({
|
|
16
|
+
provider: z.string().min(1),
|
|
17
|
+
displayName: z.string().optional(),
|
|
18
|
+
apiKey: z.string().min(1),
|
|
19
|
+
baseUrl: z.string().optional(),
|
|
20
|
+
model: z.string().optional(),
|
|
21
|
+
lang: z.string().optional(),
|
|
22
|
+
});
|
|
14
23
|
export const VEXI_DIR = join(homedir(), '.vexi');
|
|
15
24
|
export const CONFIG_PATH = join(VEXI_DIR, 'config.json');
|
|
16
|
-
/** Load the config, or `null` if it doesn't exist / is unreadable. */
|
|
25
|
+
/** Load the config, or `null` if it doesn't exist / is unreadable / fails validation. */
|
|
17
26
|
export async function loadConfig() {
|
|
18
|
-
const
|
|
19
|
-
if (!
|
|
27
|
+
const raw = await readJson(CONFIG_PATH);
|
|
28
|
+
if (!raw)
|
|
20
29
|
return null;
|
|
21
|
-
|
|
30
|
+
const result = VexiConfigSchema.safeParse(raw);
|
|
31
|
+
if (!result.success) {
|
|
32
|
+
process.stderr.write(`[vexi] config validation failed: ${result.error.message}\n`);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return result.data;
|
|
22
36
|
}
|
|
23
37
|
/** Save the config atomically with owner-only permissions. */
|
|
24
38
|
export async function saveConfig(config) {
|