vexp-cli 3.2.5 → 3.3.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 +4 -4
- package/dist/agent-config.js +411 -69
- package/dist/agent-consent.js +244 -0
- package/dist/cli.js +197 -95
- package/dist/codex-trust.js +608 -0
- package/dist/doctor.js +1105 -107
- package/dist/forget.js +4 -2
- package/dist/hook-template.js +78 -23
- package/dist/license-activate.js +90 -0
- package/dist/license.js +577 -125
- package/mcp/mcp-server.cjs +48 -48
- package/package.json +7 -7
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,11 @@ import * as fs from "fs";
|
|
|
8
8
|
import * as net from "net";
|
|
9
9
|
import { checkbox, confirm } from "@inquirer/prompts";
|
|
10
10
|
import { getBinaryPath, getInstalledVersion, getMcpServerPath, binaryEnv } from "./binary.js";
|
|
11
|
-
import {
|
|
11
|
+
import { getAgentList, configureSelectedAgents, resolveAgentName, suggestAgentName, setGuardMode, setInterventionMode, plannedWrites, takeSkippedConfigs, takeUnreachableTargets, takeCodexHookApprovals, codexHookApprovalStep, planAgentSetup, nonInteractiveAgentChoice, setupAgentsCommand } from "./agent-config.js";
|
|
12
|
+
import { recordAgentConsent } from "./agent-consent.js";
|
|
12
13
|
import { CLI_VERSION } from "./version.js";
|
|
13
|
-
import {
|
|
14
|
+
import { deactivateLicense, readLicenseLimits, readDeviceBlocked, refreshFromDisk, } from "./license.js";
|
|
15
|
+
import { runActivation, planLabel as licensePlanLabel, isLifetimePlan, printCheckInStatus, } from "./license-activate.js";
|
|
14
16
|
import { checkForUpdate } from "./update-check.js";
|
|
15
17
|
import { ensureMcpHttpServer, mcpHttpStatus } from "./mcp-supervisor.js";
|
|
16
18
|
import { installAutostart, uninstallAutostart, autostartStatus, migrateClaudeUnpinIfNeeded } from "./autostart.js";
|
|
@@ -67,6 +69,13 @@ program.hook("preAction", async (_thisCmd, actionCmd) => {
|
|
|
67
69
|
return;
|
|
68
70
|
if (AUTOSTART_SKIP.has(actionCmd.name()))
|
|
69
71
|
return;
|
|
72
|
+
// `capsule --explain` runs in the engine's own process, read-only, and
|
|
73
|
+
// promises to write nothing. Booting a daemon for it installed git hooks,
|
|
74
|
+
// wrote .vexp/.gitignore, synced the index and left a daemon running on a
|
|
75
|
+
// workspace whose daemon was down (review of the explain stream,
|
|
76
|
+
// 2026-09-25).
|
|
77
|
+
if (actionCmd.name() === "capsule" && (actionCmd.opts().explain ?? []).length > 0)
|
|
78
|
+
return;
|
|
70
79
|
// Effective workspace: inside a connected secondary repo this resolves to
|
|
71
80
|
// the parent, so we boot (or reuse) the parent daemon instead of spawning
|
|
72
81
|
// a conflicting one for the child.
|
|
@@ -430,6 +439,10 @@ program
|
|
|
430
439
|
.option("--repos <repos>", "Comma-separated repo aliases")
|
|
431
440
|
.option("--max-tokens <n>", "Max tokens", "8000")
|
|
432
441
|
.option("--format <fmt>", "Output format: text, json", "text")
|
|
442
|
+
.option("--explain <path>", "Follow a file through every stage of the retrieval and say where it left and why " +
|
|
443
|
+
"(never a candidate, not admitted, cut at the ten, under the floor, past the pivot cap, published at rank k). " +
|
|
444
|
+
"A repo-relative path, a path suffix, or a file name (every indexed file of that name). " +
|
|
445
|
+
"Repeatable. Writes nothing: the next real answer is unchanged", (value, previous) => previous.concat([value]), [])
|
|
433
446
|
.action(async (query, opts) => {
|
|
434
447
|
const binaryPath = ensureBinary();
|
|
435
448
|
const args = ["capsule", query, "--max-tokens", opts.maxTokens];
|
|
@@ -438,6 +451,9 @@ program
|
|
|
438
451
|
// The Rust binary's clap arg is `--format <json|markdown>` (there is no
|
|
439
452
|
// `--json` flag). Map the CLI's "text"/"json" to the binary's vocabulary.
|
|
440
453
|
args.push("--format", opts.format === "json" ? "json" : "markdown");
|
|
454
|
+
// One flag per file, in the order given: the binary expands a file name.
|
|
455
|
+
for (const p of opts.explain)
|
|
456
|
+
args.push("--explain", p);
|
|
441
457
|
runBinary(binaryPath, args);
|
|
442
458
|
});
|
|
443
459
|
program
|
|
@@ -714,11 +730,40 @@ program
|
|
|
714
730
|
.action(async (opts) => {
|
|
715
731
|
await runDoctor({ report: opts.report });
|
|
716
732
|
});
|
|
733
|
+
/**
|
|
734
|
+
* How the agent lists name an agent. Pre-selected (●) are only the agents
|
|
735
|
+
* the project itself shows and the ones chosen for it before; a tool vexp
|
|
736
|
+
* finds only on this machine is labelled so and waits for a pick. Enter used
|
|
737
|
+
* to set up everything found, which put Codex and Antigravity into the
|
|
738
|
+
* project of a user who ran neither (field report, 2026-09-24).
|
|
739
|
+
*/
|
|
740
|
+
/**
|
|
741
|
+
* The one step vexp cannot take for the user: approving its Codex hook. Said
|
|
742
|
+
* whenever this run wrote the hook for the first time or changed its
|
|
743
|
+
* definition, because until then Codex does not run it
|
|
744
|
+
* (codexHookApprovalStep).
|
|
745
|
+
*/
|
|
746
|
+
function printCodexHookApprovals() {
|
|
747
|
+
for (const root of takeCodexHookApprovals()) {
|
|
748
|
+
console.log("");
|
|
749
|
+
console.log(chalk.yellow(` ! ${codexHookApprovalStep(root)}`));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function agentMark(plan, agent) {
|
|
753
|
+
const pre = plan.configure.includes(agent);
|
|
754
|
+
const kind = plan.kinds[agent];
|
|
755
|
+
if (kind === "project")
|
|
756
|
+
return chalk.green(" ● detected");
|
|
757
|
+
if (kind === "machine") {
|
|
758
|
+
return pre ? chalk.green(" ● (found on this machine, chosen for this project)") : chalk.dim(" (found on this machine)");
|
|
759
|
+
}
|
|
760
|
+
return pre ? chalk.green(" ● (chosen for this project)") : "";
|
|
761
|
+
}
|
|
717
762
|
program
|
|
718
763
|
.command("setup [dir]")
|
|
719
764
|
.description("Complete one-command setup: index, detect agents, configure MCP")
|
|
720
765
|
.option("--no-index", "Skip indexing (if already indexed)")
|
|
721
|
-
.option("--agents [list]", "
|
|
766
|
+
.option("--agents [list]", "Agents to configure (comma-separated). Without it: the ones this project shows, plus the ones chosen for it before")
|
|
722
767
|
.option("--dry-run", "Show what would be configured without writing files")
|
|
723
768
|
.option("--personal", "Personal mode: index locally without writing agent configs or git hooks to the shared repo")
|
|
724
769
|
.option("--guard-strict", "Install the Grep/Glob deny hooks (opt-in since 2.3; default setup removes them)")
|
|
@@ -743,6 +788,9 @@ program
|
|
|
743
788
|
}
|
|
744
789
|
// Set once the agents are known: only editor-hosted ones need a restart.
|
|
745
790
|
let editorHosted = false;
|
|
791
|
+
// Whether any agent was set up: a run that set up none must not end on
|
|
792
|
+
// "Your AI coding agents are configured".
|
|
793
|
+
let agentsWritten = false;
|
|
746
794
|
const isPersonal = !!opts.personal;
|
|
747
795
|
if (isPersonal) {
|
|
748
796
|
console.log(chalk.cyan(" Personal mode: skipping agent configs and git hooks\n"));
|
|
@@ -783,9 +831,13 @@ program
|
|
|
783
831
|
if (!isPersonal) {
|
|
784
832
|
// Step 3: Select agents
|
|
785
833
|
const allAgents = getAgentList();
|
|
786
|
-
const
|
|
787
|
-
const detectedNames = new Set(detected.map((a) => a.agent));
|
|
834
|
+
const plan = planAgentSetup(workspaceRoot);
|
|
788
835
|
let selectedNames;
|
|
836
|
+
// What the user picked (the flag, the list, Enter on the list) is
|
|
837
|
+
// remembered as "yes" for this project, so the extension sets up and
|
|
838
|
+
// refreshes the same agents instead of asking again. The non-TTY
|
|
839
|
+
// default is not a pick: nobody chose anything there.
|
|
840
|
+
let picked = false;
|
|
789
841
|
if (typeof opts.agents === "string") {
|
|
790
842
|
// Explicit list from --agents flag. Resolve every name BEFORE writing
|
|
791
843
|
// anything: matching is exact downstream, so an unrecognised name used
|
|
@@ -805,6 +857,7 @@ program
|
|
|
805
857
|
process.exit(1);
|
|
806
858
|
}
|
|
807
859
|
selectedNames = requested.map((n) => resolveAgentName(n));
|
|
860
|
+
picked = true;
|
|
808
861
|
console.log(chalk.dim(` Agents (from flag): ${selectedNames.join(", ")}`));
|
|
809
862
|
}
|
|
810
863
|
else if (!process.stdin.isTTY) {
|
|
@@ -813,31 +866,39 @@ program
|
|
|
813
866
|
// reading that line — cannot answer a prompt: it hangs until it is
|
|
814
867
|
// killed, which is worse than failing (field report, 2026-09-07: a
|
|
815
868
|
// tester followed doctor's advice and had to abandon the command).
|
|
816
|
-
// Configure what
|
|
817
|
-
|
|
818
|
-
|
|
869
|
+
// Configure what the project shows (or was chosen for it), and say
|
|
870
|
+
// that is what happened. A tool found only on this machine is named,
|
|
871
|
+
// never set up: that default put Codex and Antigravity into the
|
|
872
|
+
// project of a user who ran neither (field report, 2026-09-24).
|
|
873
|
+
// Exit 1 only when no agent was found at all (nonInteractiveAgentChoice).
|
|
874
|
+
const choice = nonInteractiveAgentChoice(plan, dir);
|
|
875
|
+
selectedNames = choice.selected;
|
|
876
|
+
for (const note of choice.notes)
|
|
877
|
+
console.log(chalk.dim(` ${note}`));
|
|
878
|
+
if (choice.noAgentFound) {
|
|
819
879
|
console.error(chalk.yellow(" No AI agent detected here and stdin is not a terminal — nothing to configure."));
|
|
820
|
-
console.error(chalk.dim(
|
|
880
|
+
console.error(chalk.dim(` Name them explicitly: ${setupAgentsCommand(["Claude Code", "Codex"], dir)}`));
|
|
821
881
|
process.exit(1);
|
|
822
882
|
}
|
|
823
|
-
|
|
883
|
+
if (selectedNames.length > 0) {
|
|
884
|
+
console.log(chalk.dim(` Agents (detected, non-interactive): ${selectedNames.join(", ")}`));
|
|
885
|
+
}
|
|
824
886
|
}
|
|
825
887
|
else {
|
|
826
888
|
// Interactive selection
|
|
827
889
|
console.log(chalk.bold("\n Select AI agents to configure:\n"));
|
|
828
890
|
for (let i = 0; i < allAgents.length; i++) {
|
|
829
|
-
|
|
830
|
-
const marker = isDetected ? chalk.green(" ● detected") : "";
|
|
831
|
-
console.log(` ${chalk.cyan(String(i + 1).padStart(2))}. ${allAgents[i].agent}${marker}`);
|
|
891
|
+
console.log(` ${chalk.cyan(String(i + 1).padStart(2))}. ${allAgents[i].agent}${agentMark(plan, allAgents[i].agent)}`);
|
|
832
892
|
}
|
|
833
893
|
const readline = await import("readline");
|
|
834
894
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
835
|
-
const answer = await new Promise((resolve) => rl.question(chalk.cyan("\n Select (numbers separated by spaces, Enter for
|
|
895
|
+
const answer = await new Promise((resolve) => rl.question(chalk.cyan("\n Select (numbers separated by spaces, Enter for the ● ones): "), resolve));
|
|
836
896
|
rl.close();
|
|
837
897
|
const trimmed = answer.trim();
|
|
898
|
+
picked = true;
|
|
838
899
|
if (trimmed === "") {
|
|
839
|
-
// Enter with no input →
|
|
840
|
-
selectedNames =
|
|
900
|
+
// Enter with no input → the pre-selected agents
|
|
901
|
+
selectedNames = allAgents.filter((a) => plan.configure.includes(a.agent)).map((a) => a.agent);
|
|
841
902
|
if (selectedNames.length === 0) {
|
|
842
903
|
console.log(chalk.yellow("\n No agents detected. Skipping agent configuration."));
|
|
843
904
|
console.log(chalk.dim(" Tip: run 'vexp setup-agents' to choose agents manually.\n"));
|
|
@@ -871,6 +932,10 @@ program
|
|
|
871
932
|
const version = (await getInstalledVersion()) ?? CLI_VERSION;
|
|
872
933
|
const mcpServerPath = getMcpServerPath();
|
|
873
934
|
const result = configureSelectedAgents(workspaceRoot, binaryPath, version, selectedNames, mcpServerPath);
|
|
935
|
+
if (picked)
|
|
936
|
+
for (const name of selectedNames)
|
|
937
|
+
recordAgentConsent(workspaceRoot, name, "yes");
|
|
938
|
+
agentsWritten = true;
|
|
874
939
|
spinner4.succeed("Agent configurations written");
|
|
875
940
|
// Only editor-hosted agents need the host restarted. Telling a
|
|
876
941
|
// Codex-only or Claude-Code-only user to restart VS Code invites
|
|
@@ -932,6 +997,7 @@ program
|
|
|
932
997
|
for (const f of unreachable)
|
|
933
998
|
console.log(chalk.yellow(` ${f}`));
|
|
934
999
|
}
|
|
1000
|
+
printCodexHookApprovals();
|
|
935
1001
|
}
|
|
936
1002
|
}
|
|
937
1003
|
}
|
|
@@ -997,7 +1063,7 @@ program
|
|
|
997
1063
|
console.log("MCP tools are available locally via your agent's user-scope config.");
|
|
998
1064
|
}
|
|
999
1065
|
else {
|
|
1000
|
-
console.log("Your AI coding agents are configured to use vexp MCP tools.");
|
|
1066
|
+
console.log(agentsWritten ? "Your AI coding agents are configured to use vexp MCP tools." : "No AI agent was set up for this project (see above).");
|
|
1001
1067
|
console.log("The vexp daemon is running and ready to serve context.");
|
|
1002
1068
|
}
|
|
1003
1069
|
console.log("");
|
|
@@ -1026,18 +1092,16 @@ async function runSetupAgents(dir) {
|
|
|
1026
1092
|
console.log(chalk.bold(" Setup AI Coding Agents"));
|
|
1027
1093
|
console.log(chalk.dim(" Use ↑↓ to navigate, Space to toggle, Enter to confirm\n"));
|
|
1028
1094
|
const agents = getAgentList();
|
|
1095
|
+
// The same detection and answers `vexp setup` uses (see agentMark): an
|
|
1096
|
+
// agent found only on this machine is shown, labelled, not pre-checked.
|
|
1097
|
+
const plan = planAgentSetup(workspaceRoot);
|
|
1029
1098
|
const selectedNames = await checkbox({
|
|
1030
1099
|
message: "Select agents to configure",
|
|
1031
|
-
choices: agents.map((a) => {
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
: a.agent,
|
|
1037
|
-
value: a.agent,
|
|
1038
|
-
checked: detected,
|
|
1039
|
-
};
|
|
1040
|
-
}),
|
|
1100
|
+
choices: agents.map((a) => ({
|
|
1101
|
+
name: `${a.agent}${agentMark(plan, a.agent)}`,
|
|
1102
|
+
value: a.agent,
|
|
1103
|
+
checked: plan.configure.includes(a.agent),
|
|
1104
|
+
})),
|
|
1041
1105
|
theme: { prefix: chalk.cyan(" ") },
|
|
1042
1106
|
});
|
|
1043
1107
|
if (selectedNames.length === 0) {
|
|
@@ -1047,6 +1111,8 @@ async function runSetupAgents(dir) {
|
|
|
1047
1111
|
const spinner = ora(" Writing agent configurations...").start();
|
|
1048
1112
|
const version = (await getInstalledVersion()) ?? CLI_VERSION;
|
|
1049
1113
|
const result = configureSelectedAgents(workspaceRoot, binaryPath, version, selectedNames);
|
|
1114
|
+
for (const name of selectedNames)
|
|
1115
|
+
recordAgentConsent(workspaceRoot, name, "yes");
|
|
1050
1116
|
spinner.succeed("Agent configurations written");
|
|
1051
1117
|
console.log("");
|
|
1052
1118
|
for (const agent of result.agents) {
|
|
@@ -1067,6 +1133,7 @@ async function runSetupAgents(dir) {
|
|
|
1067
1133
|
console.log(chalk.dim(` → ${mcpFile}`));
|
|
1068
1134
|
}
|
|
1069
1135
|
}
|
|
1136
|
+
printCodexHookApprovals();
|
|
1070
1137
|
console.log(chalk.green(`\n ✓ ${result.agents.length} agent(s) configured\n`));
|
|
1071
1138
|
}
|
|
1072
1139
|
/** Full setup from REPL — reuses the existing readline to avoid stdin conflicts. */
|
|
@@ -1094,19 +1161,16 @@ async function runSetupInteractive(rl) {
|
|
|
1094
1161
|
rl.resume();
|
|
1095
1162
|
// Step 2: Select agents
|
|
1096
1163
|
const allAgents = getAgentList();
|
|
1097
|
-
const
|
|
1098
|
-
const detectedNames = new Set(detected.map((a) => a.agent));
|
|
1164
|
+
const plan = planAgentSetup(workspaceRoot);
|
|
1099
1165
|
console.log(chalk.bold("\n Select AI agents to configure:\n"));
|
|
1100
1166
|
for (let i = 0; i < allAgents.length; i++) {
|
|
1101
|
-
|
|
1102
|
-
const marker = isDetected ? chalk.green(" ● detected") : "";
|
|
1103
|
-
console.log(` ${chalk.cyan(String(i + 1).padStart(2))}. ${allAgents[i].agent}${marker}`);
|
|
1167
|
+
console.log(` ${chalk.cyan(String(i + 1).padStart(2))}. ${allAgents[i].agent}${agentMark(plan, allAgents[i].agent)}`);
|
|
1104
1168
|
}
|
|
1105
|
-
const answer = await ask(rl, chalk.cyan("\n Select (numbers separated by spaces, Enter for
|
|
1169
|
+
const answer = await ask(rl, chalk.cyan("\n Select (numbers separated by spaces, Enter for the ● ones): "));
|
|
1106
1170
|
const trimmed = answer.trim();
|
|
1107
1171
|
let selectedNames;
|
|
1108
1172
|
if (trimmed === "") {
|
|
1109
|
-
selectedNames =
|
|
1173
|
+
selectedNames = allAgents.filter((a) => plan.configure.includes(a.agent)).map((a) => a.agent);
|
|
1110
1174
|
if (selectedNames.length === 0) {
|
|
1111
1175
|
console.log(chalk.yellow("\n No agents detected. Skipping."));
|
|
1112
1176
|
return;
|
|
@@ -1121,6 +1185,8 @@ async function runSetupInteractive(rl) {
|
|
|
1121
1185
|
if (selectedNames.length > 0) {
|
|
1122
1186
|
const version = (await getInstalledVersion()) ?? CLI_VERSION;
|
|
1123
1187
|
const result = configureSelectedAgents(workspaceRoot, binaryPath, version, selectedNames);
|
|
1188
|
+
for (const name of selectedNames)
|
|
1189
|
+
recordAgentConsent(workspaceRoot, name, "yes");
|
|
1124
1190
|
console.log("");
|
|
1125
1191
|
for (const agent of result.agents) {
|
|
1126
1192
|
const actionLabel = agent.action === "created" ? chalk.green("created")
|
|
@@ -1132,6 +1198,7 @@ async function runSetupInteractive(rl) {
|
|
|
1132
1198
|
if (result.mcpConfigs.length > 0) {
|
|
1133
1199
|
console.log(chalk.dim(`\n MCP configs: ${result.mcpConfigs.join(", ")}`));
|
|
1134
1200
|
}
|
|
1201
|
+
printCodexHookApprovals();
|
|
1135
1202
|
console.log(chalk.green(`\n ✓ Setup complete — ${result.agents.length} agent(s) configured\n`));
|
|
1136
1203
|
}
|
|
1137
1204
|
// Login autostart — opt-in only. The daemon already cold-starts on demand,
|
|
@@ -1176,30 +1243,17 @@ program
|
|
|
1176
1243
|
isTTY: process.stdin.isTTY === true,
|
|
1177
1244
|
});
|
|
1178
1245
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
// devices immediately after activating and reasonably thought something
|
|
1190
|
-
// had failed. Best effort by design: the refresh has its own short
|
|
1191
|
-
// timeout and never throws, so an offline activation still succeeds and
|
|
1192
|
-
// simply registers later.
|
|
1193
|
-
try {
|
|
1194
|
-
await tryOnlineRefresh(key.trim());
|
|
1195
|
-
}
|
|
1196
|
-
catch { /* offline or server down: registration happens on a later run */ }
|
|
1197
|
-
console.log("");
|
|
1198
|
-
}
|
|
1199
|
-
catch (err) {
|
|
1200
|
-
console.error(chalk.red(`✗ ${err instanceof Error ? err.message : err}`));
|
|
1246
|
+
// Register this machine NOW, while the user is watching, and say whether
|
|
1247
|
+
// it worked. The dashboard tells people to "open vexp on any machine to
|
|
1248
|
+
// register it" and activating IS that — a tier-4 user read 0 of 10
|
|
1249
|
+
// devices immediately after activating and reasonably thought something
|
|
1250
|
+
// had failed. An offline activation still succeeds and registers later;
|
|
1251
|
+
// the line printed says which of the two happened. Same helper as the
|
|
1252
|
+
// interactive menu, so the two can never drift apart again.
|
|
1253
|
+
const report = await runActivation(key, { indent: "" });
|
|
1254
|
+
console.log("");
|
|
1255
|
+
if (!report.ok)
|
|
1201
1256
|
process.exit(1);
|
|
1202
|
-
}
|
|
1203
1257
|
});
|
|
1204
1258
|
program
|
|
1205
1259
|
.command("deactivate")
|
|
@@ -1211,17 +1265,18 @@ program
|
|
|
1211
1265
|
program
|
|
1212
1266
|
.command("license")
|
|
1213
1267
|
.description("Show current license status")
|
|
1214
|
-
.action(() => {
|
|
1215
|
-
|
|
1268
|
+
.action(async () => {
|
|
1269
|
+
// Check in first (throttled to once a day), so `vexp license` is also the
|
|
1270
|
+
// way to register this machine and the date printed below is current.
|
|
1271
|
+
const outcome = await refreshFromDisk();
|
|
1272
|
+
const limits = readLicenseLimits({ refresh: false });
|
|
1216
1273
|
// AppSumo lifetime tiers print as the raw internal string ("tier3") with a
|
|
1217
1274
|
// "Renews" date one month out — which is the local token's expiry, not the
|
|
1218
1275
|
// licence's. It rolls forward by itself on every check-in. A tier-3 buyer
|
|
1219
1276
|
// read that as a monthly deadline and re-activated his key by hand every
|
|
1220
1277
|
// month for nothing, and he was right to be alarmed by what it said.
|
|
1221
|
-
const ltd =
|
|
1222
|
-
const planLabel =
|
|
1223
|
-
? `Lifetime · Tier ${limits.plan.slice(4)}`
|
|
1224
|
-
: limits.plan;
|
|
1278
|
+
const ltd = isLifetimePlan(limits.plan);
|
|
1279
|
+
const planLabel = licensePlanLabel(limits.plan);
|
|
1225
1280
|
console.log(chalk.bold("\nvexp License Status\n"));
|
|
1226
1281
|
console.log(` Plan: ${planLabel}`);
|
|
1227
1282
|
if (limits.email)
|
|
@@ -1238,11 +1293,12 @@ program
|
|
|
1238
1293
|
`current one valid to ${limits.renewsAt.toLocaleDateString()})`
|
|
1239
1294
|
: ` Renews: ${limits.renewsAt.toLocaleDateString()}`);
|
|
1240
1295
|
}
|
|
1241
|
-
|
|
1296
|
+
printCheckInStatus(outcome, " ");
|
|
1297
|
+
const blocked = outcome.kind === "device_limit_exceeded" ? null : readDeviceBlocked();
|
|
1242
1298
|
if (blocked) {
|
|
1243
1299
|
console.log("");
|
|
1244
1300
|
console.log(chalk.yellow(` ⚠ This machine is not registered (${blocked.maxDevices}/${blocked.maxDevices} slots used).`));
|
|
1245
|
-
console.log(chalk.dim(` Free a slot at ${blocked.manageUrl} and run
|
|
1301
|
+
console.log(chalk.dim(` Free a slot at ${blocked.manageUrl} and run \`vexp license\`\n to re-register this device.`));
|
|
1246
1302
|
}
|
|
1247
1303
|
console.log("");
|
|
1248
1304
|
});
|
|
@@ -1300,34 +1356,81 @@ program
|
|
|
1300
1356
|
}
|
|
1301
1357
|
});
|
|
1302
1358
|
program
|
|
1303
|
-
.command("use
|
|
1304
|
-
.description("
|
|
1305
|
-
.
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
if (!fs.existsSync(manifest)) {
|
|
1309
|
-
console.error(chalk.red(`No .vexp/manifest.json at ${resolved}. Run 'vexp setup' there first.`));
|
|
1310
|
-
process.exit(1);
|
|
1311
|
-
}
|
|
1359
|
+
.command("use [workspacePath]")
|
|
1360
|
+
.description("Set Codex up for a configured workspace; --pin makes every Codex session use its index, --unpin undoes that")
|
|
1361
|
+
.option("--pin", "pin Codex's global vexp entry to this workspace, whatever folder a session starts in")
|
|
1362
|
+
.option("--unpin", "drop the pin: Codex starts vexp in each session's own folder (the default)")
|
|
1363
|
+
.action(async (workspacePath, opts) => {
|
|
1312
1364
|
const binaryPath = ensureBinary();
|
|
1313
1365
|
const mcpServerPath = getMcpServerPath();
|
|
1314
1366
|
// Import lazily so configureCodexGlobal's side effects (TOML rewrite + legacy cleanup) only fire here.
|
|
1315
|
-
const { configureSelectedAgents, codexGlobalSectionIsUserManaged } = await import("./agent-config.js");
|
|
1367
|
+
const { configureSelectedAgents, configureCodexGlobal, codexGlobalSectionIsUserManaged, codexGlobalExplicitPin } = await import("./agent-config.js");
|
|
1316
1368
|
// A hand-written [mcp_servers.vexp] is never rewritten (no-clobber), and
|
|
1317
1369
|
// this command used to print its success line anyway — the user then
|
|
1318
1370
|
// "repointed" Codex and watched it launch from the old cwd (field
|
|
1319
1371
|
// report, 2026-09-19). Say what did not happen and how to do it by hand.
|
|
1320
1372
|
if (codexGlobalSectionIsUserManaged()) {
|
|
1321
1373
|
console.error(chalk.yellow(`~/.codex/config.toml has a hand-written [mcp_servers.vexp] section, and vexp never rewrites one: Codex keeps launching it exactly as written, so nothing changed.`));
|
|
1322
|
-
|
|
1374
|
+
if (opts.pin && workspacePath) {
|
|
1375
|
+
const resolved = path.resolve(workspacePath);
|
|
1376
|
+
console.error(chalk.dim(` Point it yourself — under [mcp_servers.vexp] add cwd = ${JSON.stringify(resolved)}, and under [mcp_servers.vexp.env] add VEXP_WORKSPACE = ${JSON.stringify(resolved)} — or delete the section and run 'vexp use' again.`));
|
|
1377
|
+
}
|
|
1378
|
+
else {
|
|
1379
|
+
console.error(chalk.dim(" Delete the section and run 'vexp use' again to have vexp manage it."));
|
|
1380
|
+
}
|
|
1381
|
+
process.exit(1);
|
|
1382
|
+
}
|
|
1383
|
+
if (opts.unpin) {
|
|
1384
|
+
// The HTTP transport routes to one workspace's daemon by URL: there is
|
|
1385
|
+
// no project-free form of it to go back to.
|
|
1386
|
+
if ((process.env.VEXP_CODEX_TRANSPORT ?? "direct").toLowerCase() === "http") {
|
|
1387
|
+
console.error(chalk.yellow("VEXP_CODEX_TRANSPORT=http: the HTTP entry always names one project. Unset it to use the direct transport, which follows each Codex session's folder."));
|
|
1388
|
+
process.exit(1);
|
|
1389
|
+
}
|
|
1390
|
+
configureCodexGlobal(binaryPath, mcpServerPath, undefined, { unpin: true });
|
|
1391
|
+
console.log(chalk.green("OK: codex now starts vexp in each session's own folder"));
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (!workspacePath) {
|
|
1395
|
+
console.error(chalk.red("Name the workspace ('vexp use <path>', add --pin to pin Codex to it), or pass --unpin."));
|
|
1396
|
+
process.exit(1);
|
|
1397
|
+
}
|
|
1398
|
+
const resolved = path.resolve(workspacePath);
|
|
1399
|
+
const manifest = path.join(resolved, ".vexp", "manifest.json");
|
|
1400
|
+
if (!fs.existsSync(manifest)) {
|
|
1401
|
+
console.error(chalk.red(`No .vexp/manifest.json at ${resolved}. Run 'vexp setup' there first.`));
|
|
1323
1402
|
process.exit(1);
|
|
1324
1403
|
}
|
|
1325
1404
|
const version = (await getInstalledVersion()) ?? CLI_VERSION;
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1328
|
-
//
|
|
1405
|
+
// Codex's entry names no project any more: Codex starts vexp in each
|
|
1406
|
+
// session's folder, and pinning the project set up last broke every
|
|
1407
|
+
// other one. Setting Codex up here therefore pins nothing: doctor sends
|
|
1408
|
+
// Codex users here, and a machine-wide pin is not what they asked for. A
|
|
1409
|
+
// pin is --pin, and a pin made that way follows this command to the next
|
|
1410
|
+
// project named, as 'vexp use' always did.
|
|
1411
|
+
const pin = opts.pin || codexGlobalExplicitPin() !== undefined;
|
|
1329
1412
|
configureSelectedAgents(resolved, binaryPath, version, ["Codex"], mcpServerPath);
|
|
1330
|
-
|
|
1413
|
+
if (pin)
|
|
1414
|
+
configureCodexGlobal(binaryPath, mcpServerPath, resolved, { pin: true });
|
|
1415
|
+
// Setting Codex up for a project is a choice of Codex for it: remembered,
|
|
1416
|
+
// so the extension keeps it set up there instead of asking.
|
|
1417
|
+
recordAgentConsent(resolved, "Codex", "yes");
|
|
1418
|
+
// The HTTP entry is a URL routed to one workspace's daemon: whatever
|
|
1419
|
+
// was asked, it now names this project for every session.
|
|
1420
|
+
const http = (process.env.VEXP_CODEX_TRANSPORT ?? "direct").toLowerCase() === "http";
|
|
1421
|
+
if (pin) {
|
|
1422
|
+
console.log(chalk.green(`OK: codex now points at ${resolved}`));
|
|
1423
|
+
console.log(chalk.dim(" every Codex session, in any folder, queries this project's index; 'vexp use --unpin' undoes it"));
|
|
1424
|
+
}
|
|
1425
|
+
else if (http) {
|
|
1426
|
+
console.log(chalk.green(`OK: codex set up for ${resolved}`));
|
|
1427
|
+
console.log(chalk.dim(" VEXP_CODEX_TRANSPORT=http: the HTTP entry routes every Codex session, in any folder, to this project's index. Unset it to use the direct transport, which follows each session's folder."));
|
|
1428
|
+
}
|
|
1429
|
+
else {
|
|
1430
|
+
console.log(chalk.green(`OK: codex set up for ${resolved}`));
|
|
1431
|
+
console.log(chalk.dim(` Codex starts vexp in each session's own folder; 'vexp use ${JSON.stringify(resolved)} --pin' would make every session use this project instead`));
|
|
1432
|
+
}
|
|
1433
|
+
printCodexHookApprovals();
|
|
1331
1434
|
});
|
|
1332
1435
|
// ────────────────────────────────────────────────────
|
|
1333
1436
|
// Version
|
|
@@ -1352,7 +1455,7 @@ const MENU_CONFIG = [
|
|
|
1352
1455
|
];
|
|
1353
1456
|
// ── Sub-menu: Explore & Analyze ──
|
|
1354
1457
|
const MENU_EXPLORE = [
|
|
1355
|
-
{ key: "1", label: "capsule", hint: "Generate context capsule for a task", description: "Given a natural-language query, returns the most relevant files\n and symbols as a compressed context capsule (markdown)
|
|
1458
|
+
{ key: "1", label: "capsule", hint: "Generate context capsule for a task", description: "Given a natural-language query, returns the most relevant files\n and symbols as a compressed context capsule (markdown),\n much smaller than reading the full files." },
|
|
1356
1459
|
{ key: "2", label: "skeleton", hint: "Show a file's token-efficient skeleton", description: "Prints a compact outline of a file (functions, classes, signatures).\n Detail levels: minimal / standard / detailed." },
|
|
1357
1460
|
{ key: "3", label: "impact", hint: "Impact graph for a symbol", description: "Shows every symbol that depends on a given function/class\n (direct + transitive).\n Output: tree, list, or mermaid diagram." },
|
|
1358
1461
|
{ key: "4", label: "flow", hint: "Logic flow between two symbols", description: "Finds execution paths (call chains) from symbol A to symbol B.\n Output: up to N shortest paths with cost." },
|
|
@@ -1370,7 +1473,7 @@ const MENU_REPOS = [
|
|
|
1370
1473
|
// ── Sub-menu: License ──
|
|
1371
1474
|
const MENU_LICENSE = [
|
|
1372
1475
|
{ key: "1", label: "status", hint: "Show current license plan and limits", description: "Displays your current plan (free / pro / team), node limits,\n repo limits, and license expiry." },
|
|
1373
|
-
{ key: "2", label: "activate", hint: "Activate
|
|
1476
|
+
{ key: "2", label: "activate", hint: "Activate your license key", description: "Enter your license key (Pro, Team or lifetime) to unlock your plan.\n Find your key at https://vexp.dev/account" },
|
|
1374
1477
|
{ key: "3", label: "deactivate", hint: "Remove current license", description: "Removes the active license. Free plan limits will apply." },
|
|
1375
1478
|
];
|
|
1376
1479
|
/** Format a duration as a compact uptime string (e.g. "2h 14m", "47s"). */
|
|
@@ -1872,11 +1975,12 @@ async function executeCommand(label, rl, outCtl) {
|
|
|
1872
1975
|
}
|
|
1873
1976
|
// ── License commands ──
|
|
1874
1977
|
case "status": {
|
|
1875
|
-
const
|
|
1978
|
+
const outcome = await refreshFromDisk();
|
|
1979
|
+
const limits = readLicenseLimits({ refresh: false });
|
|
1876
1980
|
// Same wording as `vexp license`: a lifetime tier must not be shown as
|
|
1877
1981
|
// a raw "tier3" with a monthly renewal date it does not have.
|
|
1878
|
-
const ltd =
|
|
1879
|
-
console.log(` Plan: ${
|
|
1982
|
+
const ltd = isLifetimePlan(limits.plan);
|
|
1983
|
+
console.log(` Plan: ${licensePlanLabel(limits.plan)}`);
|
|
1880
1984
|
if (limits.email)
|
|
1881
1985
|
console.log(` Email: ${limits.email}`);
|
|
1882
1986
|
console.log(` Max nodes: ${limits.maxNodes === 0 ? "unlimited" : limits.maxNodes.toLocaleString()}`);
|
|
@@ -1890,21 +1994,19 @@ async function executeCommand(label, rl, outCtl) {
|
|
|
1890
1994
|
? ` Renewal: none — lifetime licence (token auto-refreshes, valid to ${limits.renewsAt.toLocaleDateString()})`
|
|
1891
1995
|
: ` Renews: ${limits.renewsAt.toLocaleDateString()}`);
|
|
1892
1996
|
}
|
|
1997
|
+
printCheckInStatus(outcome, " ");
|
|
1998
|
+
const blocked = outcome.kind === "device_limit_exceeded" ? null : readDeviceBlocked();
|
|
1999
|
+
if (blocked) {
|
|
2000
|
+
console.log(chalk.yellow(` This machine is not registered (${blocked.maxDevices}/${blocked.maxDevices} slots used). Free a slot at ${blocked.manageUrl}.`));
|
|
2001
|
+
}
|
|
1893
2002
|
break;
|
|
1894
2003
|
}
|
|
1895
2004
|
case "activate": {
|
|
1896
2005
|
const key = await askSecretOn(rl, chalk.cyan(" License key: "), outCtl, process.stdout);
|
|
1897
2006
|
if (key.trim()) {
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
console.log(` Plan: ${claims.plan}`);
|
|
1902
|
-
console.log(` Email: ${claims.sub}`);
|
|
1903
|
-
console.log(` Expires: ${new Date(claims.exp * 1000).toLocaleDateString()}`);
|
|
1904
|
-
}
|
|
1905
|
-
catch (err) {
|
|
1906
|
-
console.error(chalk.red(` ✗ ${err instanceof Error ? err.message : err}`));
|
|
1907
|
-
}
|
|
2007
|
+
// Awaits the check-in exactly like `vexp activate`: this path used
|
|
2008
|
+
// to write the key and stop, so the device never registered.
|
|
2009
|
+
await runActivation(key, { indent: " " });
|
|
1908
2010
|
}
|
|
1909
2011
|
break;
|
|
1910
2012
|
}
|