vexp-cli 1.3.11 → 2.0.3

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 CHANGED
@@ -1,18 +1,24 @@
1
1
  # vexp — Context Engine for AI Coding Agents
2
2
 
3
- **Save 70% of tokens. Get 2x better answers. Your code never leaves your machine.**
3
+ **58% Less Cost. 90% Fewer Tool Calls. 100% On Your Machine.**
4
4
 
5
5
  vexp is a local-first context engine that gives AI coding agents the right code context — automatically. One install, and every agent gets smarter answers with fewer tokens.
6
6
 
7
- Works with **12+ AI coding agents** and **30 programming languages** out of the box.
7
+ Works with **14+ AI coding agents** and **30 programming languages** out of the box.
8
+
9
+ → [SWE-bench Verified: 73% pass@1 at $0.67/task](https://vexp.dev/benchmark)
8
10
 
9
11
  ## Why vexp?
10
12
 
11
- AI coding agents dump entire files into their context window, burning tokens and getting confused. vexp solves this with a local code graph that delivers only the relevant symbols, call chains, and dependencies — compressed to 70% fewer tokens.
13
+ AI coding agents waste more than half their budget exploring codebases before writing a single line of code — 23 tool calls on average just to orient themselves. vexp pre-indexes your project into a dependency graph and delivers only the code that matters, ranked by relevance.
12
14
 
15
+ - **23 tool calls → 2** — the agent stops guessing and starts coding
16
+ - **$0.78 → $0.33 per task** — measured on SWE-bench Verified
17
+ - **117s → 74s per task** — less context to process = faster answers
13
18
  - **Local-first** — your code never leaves your machine
14
19
  - **Zero config** — auto-detects your agents and configures MCP
15
20
  - **Universal** — works with any agent that supports MCP or custom rules
21
+ - **Optional local AI** — unlock +30% extra savings with `vexp-devmind`, a ~3.5 GB model that runs 100% on your machine. GPU-accelerated on NVIDIA (CUDA) and Apple Silicon (Metal). Opt in with one command.
16
22
 
17
23
  ## Supported AI Coding Agents
18
24
 
@@ -26,11 +32,15 @@ AI coding agents dump entire files into their context window, burning tokens and
26
32
  | Augment | ✓ |
27
33
  | Zed | ✓ |
28
34
  | Codex (OpenAI) | ✓ |
35
+ | Cline | ✓ |
36
+ | Aider | ✓ |
29
37
  | Kilo Code | ✓ |
30
38
  | Kiro | ✓ |
31
39
  | Antigravity | ✓ |
32
40
  | Opencode | ✓ |
33
41
 
42
+ Your agents don't compete with each other — they all share the same context layer.
43
+
34
44
  ## Supported Languages (30)
35
45
 
36
46
  TypeScript, JavaScript, Python, Go, Rust, Java, C#, C, C++, Ruby, Kotlin, Scala, Swift, Dart, Elixir, Haskell, OCaml, Lua, R, PHP, Zig, HCL/Terraform, Objective-C, Bash/Shell, Dockerfile, Clojure, F#, SQL, HTML/CSS, YAML/JSON
@@ -44,18 +54,50 @@ vexp
44
54
 
45
55
  That's it. The interactive CLI guides you through setup, indexing, and agent configuration.
46
56
 
57
+ ## What You Get
58
+
59
+ - **Semantic + graph-ranked context** — finds the right symbols even when keywords don't match
60
+ - **Intent detection in 15 languages** — English, Italian, French, German, Spanish, Portuguese, Russian, Chinese, Japanese, Korean, Arabic, Hindi, Turkish, Dutch, Polish
61
+ - **Cross-session memory** — your agent picks up exactly where it left off, even days later
62
+ - **Stale-knowledge alerts** — when code changes, outdated memory is automatically flagged
63
+ - **Pattern & anti-pattern detection** — recurring changes surface as guidelines; dead-end explorations get flagged early
64
+ - **Change awareness** — files frequently modified together are automatically linked
65
+ - **Security filter** — `.env`, credentials, and SSH keys are excluded automatically
66
+
47
67
  ## Platforms
48
68
 
49
- | Platform | Architecture |
50
- |----------|:-:|
51
- | Linux | x64, ARM64 |
52
- | macOS | x64, ARM64 (Apple Silicon) |
53
- | Windows | x64 |
69
+ Native binaries no runtime dependencies:
70
+
71
+ | Platform | Architecture | GPU Acceleration |
72
+ |----------|:-:|:-:|
73
+ | Linux | x64, ARM64 | CUDA (NVIDIA) |
74
+ | macOS | x64, ARM64 (Apple Silicon) | Metal (built-in) |
75
+ | Windows | x64 | CUDA (NVIDIA) |
76
+
77
+ GPU acceleration applies to the optional local AI layer. Vexp's core context engine runs on any modern CPU.
54
78
 
55
79
  ## VS Code Extension
56
80
 
57
81
  For the best experience, install the [vexp VS Code extension](https://marketplace.visualstudio.com/items?itemName=Vexp.vexp-vscode) — it manages the daemon automatically and adds a sidebar dashboard with token savings tracking.
58
82
 
83
+ ## Local AI — Optional +30% Savings
84
+
85
+ Turn on `vexp-devmind` — a code-specialised AI model that runs entirely on your machine — for an extra +30% token savings on top of the baseline.
86
+
87
+ - **100% local** — the model never calls out. Your code stays on your hardware.
88
+ - **GPU-accelerated** — Metal on Apple Silicon, CUDA on NVIDIA. Auto-detected.
89
+ - **One-command install** — ~3.5 GB one-time download. Minimum 6 GB RAM.
90
+ - **Always up to date** — `--check-updates` compares your local version to the published release without downloading anything.
91
+
92
+ Sharper context ranking, higher-quality session memory, smarter prompt preprocessing. All without sending a single byte to any server.
93
+
94
+ ```bash
95
+ vexp setup-llm --install # ~3.5 GB one-time download
96
+ vexp setup-llm --check-updates # no-op if already up to date
97
+ vexp setup-llm --status # show current config + hardware
98
+ vexp setup-llm --disable # turn it off any time
99
+ ```
100
+
59
101
  ## Links
60
102
 
61
103
  - [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Vexp.vexp-vscode)
@@ -9,6 +9,7 @@
9
9
  import * as fs from "fs";
10
10
  import * as path from "path";
11
11
  import * as os from "os";
12
+ import * as crypto from "crypto";
12
13
  import { VEXP_GUARD_HOOK } from "./hook-template.js";
13
14
  // ---------------------------------------------------------------------------
14
15
  // Constants
@@ -206,9 +207,9 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
206
207
  });
207
208
  }
208
209
  }
209
- // Codex: configure MCP in ~/.codex/config.toml (global, TOML format)
210
+ // Codex: configure MCP in ~/.codex/config.toml (global, HTTP + bearer token)
210
211
  if (detector.agent === "Codex") {
211
- const wrote = configureCodexGlobal(binaryPath, mcpServerPath);
212
+ const wrote = configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot);
212
213
  if (wrote)
213
214
  mcpConfigs.push("~/.codex/config.toml");
214
215
  }
@@ -315,7 +316,7 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
315
316
  installClaudeCodeHook(workspaceRoot);
316
317
  }
317
318
  if (detector.agent === "Codex") {
318
- const wrote = configureCodexGlobal(binaryPath, mcpServerPath);
319
+ const wrote = configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot);
319
320
  if (wrote)
320
321
  mcpConfigs.push("~/.codex/config.toml");
321
322
  }
@@ -449,33 +450,74 @@ function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath) {
449
450
  fs.writeFileSync(mcpConfigPath, JSON.stringify(existing, null, 2), "utf-8");
450
451
  return true;
451
452
  }
453
+ /**
454
+ * Read or generate a bearer token for MCP HTTP auth.
455
+ * Persists to ~/.vexp/mcp.token so the MCP server and Codex config share it.
456
+ */
457
+ function resolveCodexToken() {
458
+ const home = os.homedir();
459
+ const tokenPath = home ? path.join(home, ".vexp", "mcp.token") : "";
460
+ if (tokenPath) {
461
+ try {
462
+ const existing = fs.readFileSync(tokenPath, "utf-8").trim();
463
+ if (existing)
464
+ return existing;
465
+ }
466
+ catch { /* not yet created */ }
467
+ }
468
+ const token = crypto.randomUUID();
469
+ if (tokenPath) {
470
+ try {
471
+ fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
472
+ fs.writeFileSync(tokenPath, token, { mode: 0o600 });
473
+ }
474
+ catch { /* non-fatal */ }
475
+ }
476
+ return token;
477
+ }
478
+ /**
479
+ * Read the MCP HTTP port from .vexp/mcp.port, or return the default.
480
+ */
481
+ function readMcpPort(workspaceRoot) {
482
+ const DEFAULT_PORT = 7821;
483
+ if (workspaceRoot) {
484
+ try {
485
+ const portStr = fs.readFileSync(path.join(workspaceRoot, ".vexp", "mcp.port"), "utf-8").trim();
486
+ const port = parseInt(portStr, 10);
487
+ if (port > 0)
488
+ return port;
489
+ }
490
+ catch { /* file doesn't exist */ }
491
+ }
492
+ return DEFAULT_PORT;
493
+ }
452
494
  /**
453
495
  * Configure MCP in ~/.codex/config.toml (global).
454
496
  * Codex only reads the global config, not project-level .codex/config.toml.
497
+ * Uses HTTP transport with bearer token so Codex shows "Auth: BearerToken"
498
+ * instead of "Auth: Unsupported" (which is hardcoded for stdio servers).
455
499
  * Uses TOML format with [mcp_servers.<name>] sections.
456
500
  */
457
- function configureCodexGlobal(binaryPath, mcpServerPath) {
501
+ function configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot) {
458
502
  const home = os.homedir();
459
503
  if (!home)
460
504
  return false;
461
505
  const configPath = path.join(home, ".codex", "config.toml");
506
+ const mcpPort = readMcpPort(workspaceRoot);
507
+ const token = resolveCodexToken();
508
+ const newSection = `\n[mcp_servers.vexp]\nurl = "http://127.0.0.1:${mcpPort}/mcp"\ntool_timeout_sec = 120\n\n[mcp_servers.vexp.http_headers]\nAuthorization = "Bearer ${token}"\n`;
462
509
  let content = "";
463
510
  if (fs.existsSync(configPath)) {
464
511
  content = fs.readFileSync(configPath, "utf-8");
465
512
  }
466
513
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
467
- const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
468
- const cmd = useNode ? "node" : binaryPath;
469
- const arg = useNode ? mcpServerPath : "mcp";
470
- const escapedCmd = cmd.replace(/\\/g, "\\\\");
471
- const escapedArg = arg.replace(/\\/g, "\\\\");
472
- const newSection = `\n[mcp_servers.vexp]\ncommand = "${escapedCmd}"\nargs = ["${escapedArg}"]\nstartup_timeout_sec = 30\ntool_timeout_sec = 120\n`;
473
514
  if (content.includes("[mcp_servers.vexp]")) {
474
- // Already configured check if args match (path includes version, may change on update)
475
- if (content.includes(`args = ["${escapedArg}"]`))
476
- return false; // Already up to date
477
- // Remove old section and replace
478
- content = content.replace(/\n?\[mcp_servers\.vexp\][^\[]*/, "");
515
+ // Check if already up to date (same URL and token)
516
+ if (content.includes(`url = "http://127.0.0.1:${mcpPort}/mcp"`) &&
517
+ content.includes(`Authorization = "Bearer ${token}"`))
518
+ return false;
519
+ // Remove old section(s) — handles both stdio (command=) and http (url=) formats
520
+ content = content.replace(/\n?\[mcp_servers\.vexp(?:\.http_headers)?\][^\[]*/g, "");
479
521
  }
480
522
  content += newSection;
481
523
  fs.writeFileSync(configPath, content, "utf-8");
@@ -716,14 +758,9 @@ Only use Read when you need exact raw content to edit a specific line.
716
758
  - \`run_pipeline({ "task": "add auth", "observation": "using JWT" })\` — save insight in same call
717
759
 
718
760
  ### Other MCP tools (use only when run_pipeline is insufficient)
719
- - \`get_context_capsule\` — lightweight alternative for simple questions only
720
- - \`get_impact_graph\` — standalone deep impact analysis of a specific symbol
721
- - \`search_logic_flow\` — trace execution paths between two specific symbols
722
761
  - \`get_skeleton\` — **preferred over Read** for inspecting files (minimal/standard/detailed detail levels, 70-90% token savings)
723
762
  - \`index_status\` — indexing status and health check
724
- - \`get_session_context\` — recall observations from current/previous sessions
725
- - \`search_memory\` — cross-session search for past decisions
726
- - \`save_observation\` — persist insights (prefer using run_pipeline's observation param instead)
763
+ - \`expand_vexp_ref\` — expand V-REF hash placeholders in v2 compact output
727
764
 
728
765
  ### Workflow
729
766
  1. \`run_pipeline("your task")\` — ALWAYS FIRST. Returns pivots + impact + memories in 1 call
@@ -776,14 +813,9 @@ vexp returns pre-indexed, graph-ranked context in a single call.
776
813
  ### Available MCP tools
777
814
  - \`run_pipeline\` — **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
778
815
  Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix auth bug" })\`
779
- - \`get_context_capsule\` — lightweight, for simple questions only
780
- - \`get_impact_graph\` — impact analysis of a specific symbol
781
- - \`search_logic_flow\` — execution paths between functions
782
816
  - \`get_skeleton\` — compact file structure
783
817
  - \`index_status\` — indexing status
784
- - \`get_session_context\` — recall observations from sessions
785
- - \`search_memory\` — cross-session search
786
- - \`save_observation\` — persist insights (prefer run_pipeline's observation param)
818
+ - \`expand_vexp_ref\` — expand V-REF placeholders in v2 output
787
819
 
788
820
  ### Agentic search
789
821
  - Do NOT use built-in file search, grep, or codebase indexing — always call \`run_pipeline\` first
@@ -820,14 +852,9 @@ vexp returns pre-indexed, graph-ranked context in a single call.
820
852
  ### Available MCP tools
821
853
  - \`run_pipeline\` — **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
822
854
  Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix auth bug" })\`
823
- - \`get_context_capsule\` — lightweight, for simple questions only
824
- - \`get_impact_graph\` — impact analysis of a specific symbol
825
- - \`search_logic_flow\` — execution paths between functions
826
855
  - \`get_skeleton\` — compact file structure
827
856
  - \`index_status\` — indexing status
828
- - \`get_session_context\` — recall observations from sessions
829
- - \`search_memory\` — cross-session search
830
- - \`save_observation\` — persist insights (prefer run_pipeline's observation param)
857
+ - \`expand_vexp_ref\` — expand V-REF placeholders in v2 output
831
858
 
832
859
  ### Agentic search
833
860
  - Do NOT use built-in file search, grep, or codebase indexing — always call \`run_pipeline\` first
@@ -874,14 +901,9 @@ When working on this codebase:
874
901
  ### Available MCP tools
875
902
  - \`run_pipeline\` — **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
876
903
  Example: \`run_pipeline({ "task": "fix auth bug" })\`
877
- - \`get_context_capsule\` — lightweight, for simple questions only
878
- - \`get_impact_graph\` — standalone impact analysis
879
- - \`search_logic_flow\` — execution paths between functions
880
904
  - \`get_skeleton\` — token-efficient file structure
881
905
  - \`index_status\` — indexing status
882
- - \`get_session_context\` — recall observations from sessions
883
- - \`search_memory\` — cross-session search
884
- - \`save_observation\` — persist insights (prefer run_pipeline's observation param)
906
+ - \`expand_vexp_ref\` — expand V-REF placeholders in v2 output
885
907
 
886
908
  ### Agentic search
887
909
  - Do NOT use built-in file search, grep, or codebase indexing — always call \`run_pipeline\` first
@@ -909,14 +931,9 @@ vexp returns pre-indexed, graph-ranked context in a single call.
909
931
  ### Available MCP tools
910
932
  - \`run_pipeline\` — **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
911
933
  Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix auth bug" })\`
912
- - \`get_context_capsule\` — lightweight, for simple questions only
913
- - \`get_impact_graph\` — impact analysis of a specific symbol
914
- - \`search_logic_flow\` — execution paths between functions
915
934
  - \`get_skeleton\` — compact file structure
916
935
  - \`index_status\` — indexing status
917
- - \`get_session_context\` — recall observations from sessions
918
- - \`search_memory\` — cross-session search
919
- - \`save_observation\` — persist insights (prefer run_pipeline's observation param)
936
+ - \`expand_vexp_ref\` — expand V-REF placeholders in v2 output
920
937
 
921
938
  ### Agentic search
922
939
  - Do NOT use built-in file search, grep, or codebase indexing — always call \`run_pipeline\` first
@@ -944,14 +961,9 @@ vexp returns pre-indexed, graph-ranked context in a single call.
944
961
  ### Available MCP tools
945
962
  - \`run_pipeline\` — **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
946
963
  Example: \`run_pipeline({ "task": "fix auth bug" })\`
947
- - \`get_context_capsule\` — lightweight, for simple questions only
948
- - \`get_impact_graph\` — impact analysis of a specific symbol
949
- - \`search_logic_flow\` — execution paths between functions
950
964
  - \`get_skeleton\` — compact file structure
951
965
  - \`index_status\` — indexing status
952
- - \`get_session_context\` — recall observations from sessions
953
- - \`search_memory\` — cross-session search
954
- - \`save_observation\` — persist insights (prefer run_pipeline's observation param)
966
+ - \`expand_vexp_ref\` — expand V-REF placeholders in v2 output
955
967
 
956
968
  ### Agentic search
957
969
  - Do NOT use built-in file search, grep, or codebase indexing — always call \`run_pipeline\` first
@@ -979,14 +991,9 @@ vexp returns pre-indexed, graph-ranked context in a single call.
979
991
  ### Available MCP tools
980
992
  - \`run_pipeline\` — **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
981
993
  Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix auth bug" })\`
982
- - \`get_context_capsule\` — lightweight, for simple questions only
983
- - \`get_impact_graph\` — impact analysis of a specific symbol
984
- - \`search_logic_flow\` — execution paths between functions
985
994
  - \`get_skeleton\` — compact file structure
986
995
  - \`index_status\` — indexing status
987
- - \`get_session_context\` — recall observations from sessions
988
- - \`search_memory\` — cross-session search
989
- - \`save_observation\` — persist insights (prefer run_pipeline's observation param)
996
+ - \`expand_vexp_ref\` — expand V-REF placeholders in v2 output
990
997
 
991
998
  ### Agentic search
992
999
  - Do NOT use built-in file search, grep, or codebase indexing — always call \`run_pipeline\` first
@@ -1014,14 +1021,9 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1014
1021
  ## Available vexp tools
1015
1022
  - \`run_pipeline\` — **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1016
1023
  Example: \`run_pipeline({ "task": "fix auth bug" })\`
1017
- - \`get_context_capsule\` — lightweight, for simple questions only
1018
- - \`get_impact_graph\` — impact analysis of a specific symbol
1019
- - \`search_logic_flow\` — trace execution paths
1020
1024
  - \`get_skeleton\` — compact file structure
1021
1025
  - \`index_status\` — indexing status
1022
- - \`get_session_context\` — recall observations from sessions
1023
- - \`search_memory\` — cross-session search
1024
- - \`save_observation\` — persist insights (prefer run_pipeline's observation param)
1026
+ - \`expand_vexp_ref\` — expand V-REF placeholders in v2 output
1025
1027
 
1026
1028
  ## Agentic search
1027
1029
  - Do NOT use built-in file search, grep, or codebase indexing — always call \`run_pipeline\` first
@@ -1049,14 +1051,9 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1049
1051
  ### Available MCP tools
1050
1052
  - \`run_pipeline\` — **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1051
1053
  Example: \`run_pipeline({ "task": "fix auth bug" })\`
1052
- - \`get_context_capsule\` — lightweight, for simple questions only
1053
- - \`get_impact_graph\` — impact analysis of a specific symbol
1054
- - \`search_logic_flow\` — execution paths between functions
1055
1054
  - \`get_skeleton\` — compact file structure
1056
1055
  - \`index_status\` — indexing status
1057
- - \`get_session_context\` — recall observations from sessions
1058
- - \`search_memory\` — cross-session search
1059
- - \`save_observation\` — persist insights (prefer run_pipeline's observation param)
1056
+ - \`expand_vexp_ref\` — expand V-REF placeholders in v2 output
1060
1057
 
1061
1058
  ### Agentic search
1062
1059
  - Do NOT use built-in file search, grep, or codebase indexing — always call \`run_pipeline\` first
@@ -1070,4 +1067,3 @@ Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
1070
1067
  \`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
1071
1068
  <!-- /vexp -->`;
1072
1069
  }
1073
- //# sourceMappingURL=agent-config.js.map
package/dist/binary.js CHANGED
@@ -10,6 +10,24 @@ const PLATFORM_PACKAGES = {
10
10
  "win32-x64": "@vexp/core-win32-x64",
11
11
  "win32-arm64": "@vexp/core-win32-x64", // ARM64 Windows uses x64 binary via emulation
12
12
  };
13
+ // A single core binary per OS/arch decides at RUNTIME which backend to
14
+ // use (CPU, Metal, CUDA) via ggml's dynamic plugin loader. The CUDA
15
+ // plugin is downloaded on-demand by `vexp setup-llm` when an NVIDIA
16
+ // driver is detected — no separate GPU binary package.
17
+ //
18
+ // The `--gpu` / `--cuda` flags and VEXP_GPU / VEXP_CUDA env vars are
19
+ // forwarded to the Rust core as VEXP_PREFER_GPU=1, which flips
20
+ // prefer_gpu=true on the llama.cpp load. If no GPU plugin is present,
21
+ // llama.cpp silently falls back to CPU.
22
+ function gpuRequested() {
23
+ return (process.env.VEXP_GPU === "1" ||
24
+ process.env.VEXP_CUDA === "1" ||
25
+ process.argv.includes("--gpu") ||
26
+ process.argv.includes("--cuda"));
27
+ }
28
+ export function preferGpuEnv() {
29
+ return gpuRequested() ? { VEXP_PREFER_GPU: "1" } : {};
30
+ }
13
31
  export function getBinaryPath() {
14
32
  // Dev override: use VEXP_CORE_PATH env var to point to a local binary
15
33
  const envPath = process.env.VEXP_CORE_PATH;
@@ -93,4 +111,3 @@ export async function getInstalledVersion() {
93
111
  return null;
94
112
  }
95
113
  }
96
- //# sourceMappingURL=binary.js.map
package/dist/cli.js CHANGED
@@ -2,14 +2,14 @@
2
2
  import { Command } from "commander";
3
3
  import chalk from "chalk";
4
4
  import ora from "ora";
5
- import { spawn, spawnSync } from "child_process";
5
+ import { spawn, spawnSync, execFileSync } from "child_process";
6
6
  import * as path from "path";
7
7
  import * as fs from "fs";
8
8
  import { checkbox } from "@inquirer/prompts";
9
9
  import { getBinaryPath, getInstalledVersion, getMcpServerPath } from "./binary.js";
10
10
  import { detectAgents, getAgentList, configureSelectedAgents } from "./agent-config.js";
11
11
  import { CLI_VERSION } from "./version.js";
12
- import { activateLicense, deactivateLicense, readLicenseLimits, } from "./license.js";
12
+ import { activateLicense, deactivateLicense, readLicenseLimits, readDeviceBlocked, } from "./license.js";
13
13
  import { checkForUpdate } from "./update-check.js";
14
14
  const program = new Command();
15
15
  program
@@ -163,14 +163,57 @@ program
163
163
  .command("savings")
164
164
  .description("Show token savings statistics")
165
165
  .option("--days <n>", "Number of days to analyze", "30")
166
+ .option("--today", "Show savings for today only (since local midnight)")
166
167
  .option("--export", "Export report to .vexp/token-savings-report.md")
167
168
  .action(async (opts) => {
168
169
  const binaryPath = ensureBinary();
169
- const args = ["savings", "--days", opts.days];
170
+ const args = ["savings"];
171
+ if (opts.today) {
172
+ // Compute local midnight in unix seconds so the Rust core aggregates
173
+ // only events from today in the caller's timezone.
174
+ const midnight = new Date();
175
+ midnight.setHours(0, 0, 0, 0);
176
+ const sinceTs = Math.floor(midnight.getTime() / 1000);
177
+ args.push("--since-ts", String(sinceTs), "--label", "today");
178
+ }
179
+ else {
180
+ args.push("--days", opts.days);
181
+ }
170
182
  if (opts.export)
171
183
  args.push("--export");
172
184
  runBinary(binaryPath, args);
173
185
  });
186
+ program
187
+ .command("compress <file>")
188
+ .description("Compress a markdown/text file using caveman-style prose compression (backup to <name>.original.md)")
189
+ .option("--level <lvl>", "Compression level: off | lite | full | ultra", "full")
190
+ .action(async (file, opts) => {
191
+ const binaryPath = ensureBinary();
192
+ runBinary(binaryPath, ["compress", file, "--level", opts.level]);
193
+ });
194
+ program
195
+ .command("setup-llm")
196
+ .description("Configure optional local LLM compression (vexp-devmind). Runs interactive wizard by default.")
197
+ .option("--status", "Print current LLM config and readiness")
198
+ .option("--disable", "Disable LLM compression without removing model files")
199
+ .option("--install", "Non-interactive install (for CI / first-run)")
200
+ .option("--check-updates", "Check for model updates without downloading")
201
+ .option("--enable", "Enable a previously-installed LLM without re-downloading anything")
202
+ .action(async (opts) => {
203
+ const binaryPath = ensureBinary();
204
+ const args = ["setup-llm"];
205
+ if (opts.status)
206
+ args.push("--status");
207
+ if (opts.disable)
208
+ args.push("--disable");
209
+ if (opts.install)
210
+ args.push("--install");
211
+ if (opts.checkUpdates)
212
+ args.push("--check-updates");
213
+ if (opts.enable)
214
+ args.push("--enable");
215
+ runBinary(binaryPath, args);
216
+ });
174
217
  program
175
218
  .command("hooks <action>")
176
219
  .description("Manage git hooks (install, check, remove)")
@@ -273,6 +316,46 @@ program
273
316
  catch {
274
317
  spinnerDaemon.warn("Daemon already running or failed to start (non-fatal)");
275
318
  }
319
+ // Step 2.6: Start MCP HTTP server (needed for Codex and HTTP-based agents)
320
+ const mcpPath = getMcpServerPath();
321
+ if (mcpPath) {
322
+ const spinnerMcp = ora("Starting MCP HTTP server...").start();
323
+ try {
324
+ const { randomUUID } = await import("crypto");
325
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
326
+ let mcpToken = "";
327
+ const tokenPath = home ? path.join(home, ".vexp", "mcp.token") : "";
328
+ if (tokenPath) {
329
+ try {
330
+ mcpToken = fs.readFileSync(tokenPath, "utf-8").trim();
331
+ }
332
+ catch { }
333
+ }
334
+ if (!mcpToken) {
335
+ mcpToken = randomUUID();
336
+ if (tokenPath) {
337
+ fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
338
+ fs.writeFileSync(tokenPath, mcpToken, { mode: 0o600 });
339
+ }
340
+ }
341
+ const mcpChild = spawn("node", [mcpPath, "--http", "--port=7821"], {
342
+ detached: true,
343
+ stdio: "ignore",
344
+ env: {
345
+ ...process.env,
346
+ VEXP_WORKSPACE: workspaceRoot,
347
+ VEXP_HTTP: "1",
348
+ VEXP_PORT: "7821",
349
+ VEXP_MCP_TOKEN: mcpToken,
350
+ },
351
+ });
352
+ mcpChild.unref();
353
+ spinnerMcp.succeed("MCP HTTP server started (port 7821)");
354
+ }
355
+ catch {
356
+ spinnerMcp.warn("MCP HTTP server failed to start (non-fatal)");
357
+ }
358
+ }
276
359
  // In personal mode, make .vexp/ fully gitignored
277
360
  if (isPersonal) {
278
361
  const vexpGitignore = path.join(workspaceRoot, ".vexp", ".gitignore");
@@ -541,6 +624,12 @@ program
541
624
  console.log(` All tools: ${limits.allTools ? "yes" : "no (7/10)"}`);
542
625
  if (limits.renewsAt)
543
626
  console.log(` Renews: ${limits.renewsAt.toLocaleDateString()}`);
627
+ const blocked = readDeviceBlocked();
628
+ if (blocked) {
629
+ console.log("");
630
+ console.log(chalk.yellow(` ⚠ This machine is not registered (${blocked.maxDevices}/${blocked.maxDevices} slots used).`));
631
+ console.log(chalk.dim(` Free a slot at ${blocked.manageUrl} and run a vexp command\n to re-register this device.`));
632
+ }
544
633
  console.log("");
545
634
  });
546
635
  // ────────────────────────────────────────────────────
@@ -562,6 +651,7 @@ const MENU_CONFIG = [
562
651
  { key: "3", label: "setup", hint: "Full setup (index + agents + hooks)", description: "One-command bootstrap: indexes the codebase, detects AI agents,\n writes MCP configs, installs git hooks." },
563
652
  { key: "4", label: "setup-agents", hint: "Choose which AI agents to configure", description: "Select agents (Claude, Cursor, Windsurf, Copilot, …) to configure.\n Creates config directories and writes MCP settings for each." },
564
653
  { key: "5", label: "daemon-cmd", hint: "Manage daemon (start/stop/status/logs)", description: "Controls the background daemon:\n start → launches the daemon (indexes + watches for changes)\n stop → stops the running daemon\n status → shows node/edge/file counts\n logs → prints .vexp/vexp.log" },
654
+ { key: "6", label: "setup-llm", hint: "Configure local LLM compression (vexp-devmind)", description: "Install, update, or disable the optional local LLM (vexp-devmind).\n Runs entirely on your machine (~3.5 GB download).\n Enables smart prompt preprocessing and higher-quality memory compression." },
565
655
  ];
566
656
  // ── Sub-menu: Explore & Analyze ──
567
657
  const MENU_EXPLORE = [
@@ -592,10 +682,30 @@ function printBanner() {
592
682
  console.log(chalk.dim(" ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝") + chalk.dim(` v${version}`));
593
683
  console.log("");
594
684
  console.log(chalk.dim(" Context Engine for AI Coding Agents"));
685
+ // One-liner LLM status
686
+ try {
687
+ const llmBin = ensureBinary();
688
+ const stdout = execFileSync(llmBin, ["setup-llm", "--status"], {
689
+ timeout: 5000,
690
+ encoding: "utf-8",
691
+ stdio: ["pipe", "pipe", "pipe"],
692
+ });
693
+ const operational = /Operational:\s*true/.test(stdout);
694
+ const model = stdout.match(/Model:\s*(.+)/)?.[1]?.trim() ?? "";
695
+ if (operational) {
696
+ console.log(chalk.green(` ● LLM: ${model} (active)`));
697
+ }
698
+ else {
699
+ console.log(chalk.dim(" ○ LLM: not configured — type 'setup-llm' or press 1 > 6"));
700
+ }
701
+ }
702
+ catch {
703
+ // Binary not available or no local_llm feature — skip silently
704
+ }
595
705
  }
596
706
  function printMainMenu() {
597
707
  console.log("");
598
- console.log(` ${chalk.cyan.bold("1")} ${chalk.green("Setup & Config".padEnd(20))} ${chalk.dim("Initialize, index, and configure agents")}`);
708
+ console.log(` ${chalk.cyan.bold("1")} ${chalk.green("Setup & Config".padEnd(20))} ${chalk.dim("Initialize, index, configure agents & LLM")}`);
599
709
  console.log(` ${chalk.cyan.bold("2")} ${chalk.green("Explore & Analyze".padEnd(20))} ${chalk.dim("Query context, skeletons, impact, flow")}`);
600
710
  console.log(` ${chalk.cyan.bold("3")} ${chalk.green("Token Savings".padEnd(20))} ${chalk.dim("View savings stats, export report")}`);
601
711
  console.log(` ${chalk.cyan.bold("4")} ${chalk.green("License".padEnd(20))} ${chalk.dim("View or activate your license")}`);
@@ -732,6 +842,41 @@ async function executeCommand(label, rl) {
732
842
  }
733
843
  break;
734
844
  }
845
+ case "setup-llm": {
846
+ console.log("");
847
+ console.log(chalk.cyan(" Local LLM compression (vexp-devmind)"));
848
+ console.log(chalk.dim(" All processing runs locally — no data leaves your machine.\n"));
849
+ console.log(` ${chalk.bold("1")} Install / Reinstall (~3.5 GB download)`);
850
+ console.log(` ${chalk.bold("2")} Start (enable without re-downloading)`);
851
+ console.log(` ${chalk.bold("3")} Check for updates (no download if up to date)`);
852
+ console.log(` ${chalk.bold("4")} Status (show current config + hardware)`);
853
+ console.log(` ${chalk.bold("5")} Disable (keep files, stop using LLM)`);
854
+ console.log(` ${chalk.dim("b")} ${chalk.dim("Cancel")}`);
855
+ console.log("");
856
+ const llmChoice = await ask(rl, chalk.cyan(" Choose: "));
857
+ const llmBinary = ensureBinary();
858
+ switch (llmChoice.trim()) {
859
+ case "1":
860
+ console.log(chalk.dim(" Starting LLM setup (this may take several minutes)…\n"));
861
+ runBinary(llmBinary, ["setup-llm", "--install"]);
862
+ break;
863
+ case "2":
864
+ runBinary(llmBinary, ["setup-llm", "--enable"]);
865
+ break;
866
+ case "3":
867
+ runBinary(llmBinary, ["setup-llm", "--check-updates"]);
868
+ break;
869
+ case "4":
870
+ runBinary(llmBinary, ["setup-llm", "--status"]);
871
+ break;
872
+ case "5":
873
+ runBinary(llmBinary, ["setup-llm", "--disable"]);
874
+ break;
875
+ default:
876
+ console.log(chalk.dim(" Cancelled."));
877
+ }
878
+ break;
879
+ }
735
880
  // ── Explore commands ──
736
881
  case "capsule": {
737
882
  const query = await ask(rl, chalk.cyan(" Describe your task: "));
@@ -772,17 +917,33 @@ async function executeCommand(label, rl) {
772
917
  }
773
918
  case "savings": // fallthrough from direct command
774
919
  case "summary": {
775
- const daysInput = await ask(rl, chalk.cyan(" Period in days (7/14/30/60/90): "));
776
- const days = parseInt(daysInput.trim(), 10) || 30;
920
+ const input = await ask(rl, chalk.cyan(" Period (today / 7 / 14 / 30 / 60 / 90): "));
777
921
  const binaryPath = ensureBinary();
778
- runBinary(binaryPath, ["savings", "--days", String(days)]);
922
+ const trimmed = input.trim().toLowerCase();
923
+ if (trimmed === "today") {
924
+ const midnight = new Date();
925
+ midnight.setHours(0, 0, 0, 0);
926
+ runBinary(binaryPath, ["savings", "--since-ts", String(Math.floor(midnight.getTime() / 1000)), "--label", "today"]);
927
+ }
928
+ else {
929
+ const days = parseInt(trimmed, 10) || 30;
930
+ runBinary(binaryPath, ["savings", "--days", String(days)]);
931
+ }
779
932
  break;
780
933
  }
781
934
  case "export": {
782
- const daysInput = await ask(rl, chalk.cyan(" Period in days (7/14/30/60/90): "));
783
- const days = parseInt(daysInput.trim(), 10) || 30;
935
+ const input = await ask(rl, chalk.cyan(" Period (today / 7 / 14 / 30 / 60 / 90): "));
784
936
  const binaryPath = ensureBinary();
785
- runBinary(binaryPath, ["savings", "--days", String(days), "--export"]);
937
+ const trimmed = input.trim().toLowerCase();
938
+ if (trimmed === "today") {
939
+ const midnight = new Date();
940
+ midnight.setHours(0, 0, 0, 0);
941
+ runBinary(binaryPath, ["savings", "--since-ts", String(Math.floor(midnight.getTime() / 1000)), "--label", "today", "--export"]);
942
+ }
943
+ else {
944
+ const days = parseInt(trimmed, 10) || 30;
945
+ runBinary(binaryPath, ["savings", "--days", String(days), "--export"]);
946
+ }
786
947
  break;
787
948
  }
788
949
  // ── License commands ──
@@ -838,4 +999,3 @@ if (process.argv.length <= 2) {
838
999
  else {
839
1000
  program.parse();
840
1001
  }
841
- //# sourceMappingURL=cli.js.map
@@ -18,4 +18,3 @@ else
18
18
  fi
19
19
  exit 0
20
20
  `;
21
- //# sourceMappingURL=hook-template.js.map