@cantinasecurity/apex-cli 0.1.42 → 0.1.44

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.
@@ -62,6 +62,8 @@ If the Apex MCP server is not configured, fall back to the local CLI:
62
62
  - `apex-trial-status` is MCP-only; do not invoke it as a CLI command. In a CLI-only flow, use the `claimDeadline` returned by `apex redeem <code> --json` without repeating or persisting the secret code.
63
63
  - After a guest scan, tell the user to run `apex claim` on the same machine before that returned claim deadline. If the deadline is unavailable or uncertain, do not guess or start a replacement login; run `apex claim` and let the server validate the proof.
64
64
  - Claim moves the guest workspace, scans, and findings into the currently selected Cantina workspace when the account can run scans there. Personal workspace owners receive the trial in their existing workspace instead of a duplicate. If the selected workspace cannot run scans, tell the user to switch to one that can and retry before the claim deadline.
65
+ - Cantina company access requires current, activated membership; pending invitations and removed or disabled memberships do not grant access. After membership changes, refresh the account with `apex login` in a terminal, checking `apex-trial-status` first and preserving the claim-first workflow above. For temporary company-access verification failures, keep credentials and retry after recovery.
66
+ - For GitHub connection, use `apex-connect-provider` with `provider: "github"` and pass the returned URL unchanged to the user. They must open it in the browser signed in to the same Cantina account as the CLI and complete GitHub authorization. After an organization owner approves an installation request, call the tool again to finish company linking. CLI fallback: `apex connect github`. Automation must not assume the returned URL is hosted on github.com.
65
67
  - Service-key creation, listing, and revocation have no MCP tools. Never ask a user to paste a service key into a prompt, and never place one in a tool argument, tool result, source file, or log. Hosted MCP clients may read it from a secret-backed environment variable for the HTTP Authorization header.
66
68
  - If a user explicitly needs a new service key, tell them to run `apex login` if needed, then run `apex service-key create --name <name> --scope <scope[,scope]>` directly in their own terminal. Use only the required scopes: `scans:create` to query credit availability or trigger, `scans:read` to poll status, and `findings:read` to retrieve findings. A P1-only adapter can use `--scope scans:read,findings:read`; omitting `--scope` grants all three. The CLI automatically selects the company when only one is available and prompts when there are several; use `--company <id-or-handle>` to skip that prompt or resolve a non-interactive command. Service-key commands never initiate login, and the secret is shown once.
67
69
  - For a hosted MCP client, connect to `https://ai.cantina.xyz/mcp` and send `APEX_SERVICE_KEY` as a Bearer header. The service key authenticates hosted MCP, but not the local CLI or stdio MCP; those use device login.
@@ -74,7 +76,7 @@ If the Apex MCP server is not configured, fall back to the local CLI:
74
76
  - `apex credits` reports standard credits plus audit, fix review, and Lite scan entitlements when Bedrock returns those balances.
75
77
  - Work from the target repository directory so Apex can resolve `.apex/workspace.json`.
76
78
  - `apex scan` scans the current working directory by default; pass `--repo` only when the user asks to scan explicit alternate roots.
77
- - `apex-doctor` is read-only for guest sessions and reports the local snapshot plan without reserving a workspace. For account sessions it reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
79
+ - `apex-doctor` reports the running CLI version and detects conflicting npm/pnpm global installations. It is read-only for guest sessions and reports the local snapshot plan without reserving a workspace. For account sessions it reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
78
80
  - Plain local directories and dirty git worktrees can scan through local snapshot uploads without provider access.
79
81
  - Audit scans use `--mode audit` in user-facing CLI calls and request payloads. The legacy `ultra` mode remains accepted as an alias, but audit scans still require provider-backed GitHub, GitLab or company-enabled Forgejo sources.
80
82
  - Lite scans use `--mode lite` in CLI calls or `mode: "lite"` in MCP, require Lite Scan beta access, and require provider-backed GitHub or GitLab sources without local snapshot fallback.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Cantina agent plugins for security review workflows.",
9
- "version": "0.1.42"
9
+ "version": "0.1.44"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -14,10 +14,10 @@
14
14
  "source": {
15
15
  "source": "npm",
16
16
  "package": "@cantinasecurity/apex-cli",
17
- "version": "0.1.42"
17
+ "version": "0.1.44"
18
18
  },
19
19
  "description": "Run Apex security scans and review findings from Claude Code.",
20
- "version": "0.1.42",
20
+ "version": "0.1.44",
21
21
  "author": {
22
22
  "name": "Cantina",
23
23
  "email": "support@cantina.xyz"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apex-cli",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "description": "Run Apex security scans and review findings from Claude Code.",
5
5
  "author": {
6
6
  "name": "Cantina",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apex-cli",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "description": "Run Apex security scans and review findings from Codex.",
5
5
  "author": {
6
6
  "name": "Cantina",
package/.mcp.claude.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "args": [
6
6
  "-y",
7
7
  "-p",
8
- "@cantinasecurity/apex-cli@0.1.42",
8
+ "@cantinasecurity/apex-cli@0.1.44",
9
9
  "apex-mcp"
10
10
  ],
11
11
  "env": {
package/.mcp.codex.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "args": [
5
5
  "-y",
6
6
  "-p",
7
- "@cantinasecurity/apex-cli@0.1.42",
7
+ "@cantinasecurity/apex-cli@0.1.44",
8
8
  "apex-mcp"
9
9
  ],
10
10
  "env": {
package/MARKETPLACE.md CHANGED
@@ -13,7 +13,7 @@ This package is prepared as both a Claude Code plugin and a Codex plugin. The pu
13
13
  The plugin MCP configs launch the pinned npm CLI package with:
14
14
 
15
15
  ```bash
16
- npx -y -p @cantinasecurity/apex-cli@0.1.42 apex-mcp
16
+ npx -y -p @cantinasecurity/apex-cli@0.1.44 apex-mcp
17
17
  ```
18
18
 
19
19
  That keeps marketplace installs independent of a user's global `apex` install.
package/README.md CHANGED
@@ -66,6 +66,14 @@ cannot adopt the new merge and generation-safety rules until it restarts.
66
66
 
67
67
  When `apex` is run in an interactive terminal, it checks for updates periodically and offers to install them.
68
68
 
69
+ `apex update` updates the package-manager installation that owns the running
70
+ executable and verifies that installation actually changed before reporting
71
+ success. `apex doctor` reports the running version and package manager. If npm
72
+ and pnpm contain different global Apex versions, doctor identifies the stale
73
+ active copy and prints the command that repairs it. The same installation
74
+ diagnostic is included in `apex doctor --json`, including when another doctor
75
+ check fails.
76
+
69
77
  ## Local Development
70
78
 
71
79
  1. Install dependencies:
@@ -158,30 +166,30 @@ unclaimed local trial proof so `apex claim` can still use it.
158
166
  Agent-safe copy/paste prompt (substitute the issued code):
159
167
 
160
168
  ```text
161
- Install and run Cantina Apex CLI 0.1.42 to start one free standard security scan of this repository immediately.
169
+ Install and run Cantina Apex CLI 0.1.44 to start one free standard security scan of this repository immediately.
162
170
 
163
171
  Use this trial code only where <TRIAL_CODE> appears below: SPBT-DEVREL-XXXXXXXX
164
172
 
165
173
  Run the commands yourself from the repository root; do not stop after setup or only explain the steps. Use terminal CLI commands only. Do not use an Apex MCP tool or run `apex setup`, `apex mcp`, `apex login`, `apex-auth-start`, or `apex-auth-wait`. Never use `--replace` or `--force`.
166
174
 
167
175
  Use this exact command prefix for every Apex command:
168
- npx -y -p @cantinasecurity/apex-cli@0.1.42 apex
176
+ npx -y -p @cantinasecurity/apex-cli@0.1.44 apex
169
177
 
170
178
  1. Before redeeming, confirm every command will use the same persistent, user-accessible machine or container, HOME, filesystem, and repository root. Confirm Node.js 20+ and npx are available. If `APEX_BASE_URL` is set, continue only when its origin is `https://ai.cantina.xyz`. Run the exact prefix with `--help` and continue only if help lists both `redeem` and `claim`. If any check fails, STOP before sending the code; do not install system packages.
171
179
  2. Redeem, with at most one response-loss recovery retry:
172
- npx -y -p @cantinasecurity/apex-cli@0.1.42 apex redeem <TRIAL_CODE> --json
180
+ npx -y -p @cantinasecurity/apex-cli@0.1.44 apex redeem <TRIAL_CODE> --json
173
181
  This terminal command is the only allowed tool call containing the code. Never echo or restate the code, store it in a file or environment variable, put it in a URL or MCP call, or include it in your response. Parse the JSON from stdout only; do not merge stderr into stdout, because npm may write warnings to stderr. Continue when the command exits 0 and stdout is JSON with `redeemed: true` and `outcome` equal to `redeemed` or `idempotent`. `idempotent` means the existing guest session resumed and it may already own a workspace. If and only if the first attempt ends with an ambiguous transport/connection failure or an interrupted/missing response and no typed server error code, retry this exact same command once on the same machine, HOME, and repository root. Never retry a typed server rejection and never make more than one recovery retry. If the allowed retry does not return a successful JSON outcome, STOP.
174
182
  3. Immediately start exactly one scan; do not wait for another instruction:
175
- npx -y -p @cantinasecurity/apex-cli@0.1.42 apex scan --mode standard --non-interactive --no-open --json
183
+ npx -y -p @cantinasecurity/apex-cli@0.1.44 apex scan --mode standard --non-interactive --no-open --json
176
184
  The CLI validates the complete guest local archive before it reserves a workspace. An unbound renamed checkout may recover only the sole empty guest workspace when no explicit workspace name was supplied. On any error or nonzero exit, STOP. After a successful scan JSON response, inspect `localPersistence.workspaceBindingSaved`. If it is `false`, the server scan already started: capture and report `binding.lastScanId`, then STOP; do not run status or findings until local repository permissions are repaired. Otherwise capture `binding.lastScanId` from the JSON as <SCAN_ID>; if it is absent, STOP.
177
185
  4. About every 60 seconds, poll that exact scan:
178
- npx -y -p @cantinasecurity/apex-cli@0.1.42 apex status --scan <SCAN_ID> --json
186
+ npx -y -p @cantinasecurity/apex-cli@0.1.44 apex status --scan <SCAN_ID> --json
179
187
  Read `progress.progress.status`. Wait while it is `created`, `pending`, `queued`, `starting`, `running`, `in_progress`, or `processing`. Continue on `completed`. STOP on `failed`, `cancelled`, a missing or unknown status, or a command failure.
180
188
  5. Fetch that exact scan's findings:
181
- npx -y -p @cantinasecurity/apex-cli@0.1.42 apex findings --scan <SCAN_ID> --json
189
+ npx -y -p @cantinasecurity/apex-cli@0.1.44 apex findings --scan <SCAN_ID> --json
182
190
  On success, summarize findings by severity, most severe first, including identifier, title, status, and confidence. This command does not return file locations, so do not invent them. On failure, STOP.
183
191
  6. Do not claim automatically. Tell me the claim deadline from step 2 and remind me to run this exact command myself from the same repository root, machine, and HOME:
184
- npx -y -p @cantinasecurity/apex-cli@0.1.42 apex claim
192
+ npx -y -p @cantinasecurity/apex-cli@0.1.44 apex claim
185
193
  It may open a browser so I can create or sign in to a Cantina account and keep the results. Claim moves the trial into the currently selected Cantina workspace when I can run scans there, or into my existing personal workspace. If the selected workspace cannot run scans, tell me to switch to one that can and retry before the claim deadline.
186
194
 
187
195
  Never paste raw command transcripts. For any failure, report only the step, exit code, and sanitized error. Redact the trial code and every `apex_at_`, `apex_rt_`, or `apex_ct_` value from all reports.
@@ -495,6 +503,19 @@ Useful flags:
495
503
 
496
504
  Unauthenticated `--json`, `--non-interactive`, and non-TTY commands fail immediately with `apex login` guidance. Run login explicitly before automation. Temporary refresh/network failures preserve credentials; retry after the service recovers. Provider connection commands return URLs without opening browsers in automation.
497
505
 
506
+ Cantina company access requires a current, activated membership. Removed or
507
+ disabled members and pending invitations do not grant access. After membership
508
+ changes, run `apex login` again to refresh the company snapshot. If company-access
509
+ verification is temporarily unavailable, keep the saved credentials and retry
510
+ after the service recovers.
511
+
512
+ For `apex connect github`, open the returned URL in a browser signed in to the
513
+ same Cantina account as the CLI, then complete GitHub authorization. After an
514
+ organization owner approves an installation request, run `apex connect github`
515
+ again to finish linking the installation to the selected company. Local MCP
516
+ users repeat `apex-connect-provider` with `provider: "github"` to obtain the new
517
+ URL; automation must not assume the URL points directly to github.com.
518
+
498
519
  `auto` is the default. `remote` requires Apex to materialize from a remote repository. `local` forces a local snapshot upload even when a clean remote path is available.
499
520
 
500
521
  Audit scans use `audit` as the scan mode and still require provider-backed GitHub, GitLab or company-enabled Forgejo repositories that Apex can materialize remotely without a local snapshot fallback. `ultra` remains accepted as a backwards-compatible alias.
@@ -687,7 +708,7 @@ Telemetry event posts do not include bearer tokens. The telemetry endpoint is `P
687
708
  {
688
709
  "schemaVersion": 1,
689
710
  "event": "apex.invocation.completed",
690
- "cliVersion": "0.1.42",
711
+ "cliVersion": "0.1.44",
691
712
  "invocation": {
692
713
  "surface": "mcp",
693
714
  "command": "apex-scan",
@@ -718,7 +739,7 @@ The npm package also includes marketplace-ready plugin artifacts:
718
739
  - `.claude-plugin/plugin.json` and `.mcp.claude.json` for Claude Code plugin installs
719
740
  - `.claude-plugin/marketplace.json` for a Claude marketplace entry backed by the public npm package
720
741
 
721
- These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.42 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
742
+ These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.44 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
722
743
 
723
744
  The repository also includes `.agents/plugins/marketplace.json` for local Codex marketplace testing from a checkout.
724
745
 
package/dist/commands.js CHANGED
@@ -19,11 +19,11 @@ import { createWorkspaceBindingFromSummary, fetchCompanyCredits, fetchCompanyWor
19
19
  import { loadWorkspaceBinding, saveWorkspaceBinding } from "./workspace-binding.js";
20
20
  import { commandSetup as runCliSetup } from "./setup.js";
21
21
  import { getTelemetryStatus, setTelemetryAuthKind, setTelemetryEnabled, setTelemetryOnboardingOutcome, } from "./telemetry.js";
22
- import { commandUpdate as runCliUpdate } from "./update.js";
22
+ import { commandUpdate as runCliUpdate, getCliInstallDiagnostics, } from "./update.js";
23
23
  import { mkdir, writeFile } from "node:fs/promises";
24
24
  import path from "node:path";
25
25
  import { z } from "zod";
26
- function getRequestedWorkspaceSrDoctorPayload(me, flags) {
26
+ function getRequestedWorkspaceSrDoctorPayload(me, flags, cliInstallation) {
27
27
  const requested = getFlagString(flags, "company")?.trim().toLowerCase();
28
28
  if (!requested)
29
29
  return null;
@@ -48,6 +48,7 @@ function getRequestedWorkspaceSrDoctorPayload(me, flags) {
48
48
  const company = scopes[0];
49
49
  const workspaceIds = [...new Set(scopes.map((scope) => scope.workspaceId))];
50
50
  return {
51
+ cliInstallation,
51
52
  authenticatedAs: me.user.username ?? me.user.id,
52
53
  accessMode: "workspace_sr",
53
54
  company: {
@@ -1035,6 +1036,21 @@ export async function commandTelemetry(flags, target) {
1035
1036
  return payload;
1036
1037
  }
1037
1038
  export async function commandDoctor(client, cwd, flags) {
1039
+ const cliInstallation = await getCliInstallDiagnostics().catch(() => null);
1040
+ if (!isJsonMode(flags) && cliInstallation) {
1041
+ const manager = cliInstallation.packageManager
1042
+ ? ` via ${cliInstallation.packageManager}`
1043
+ : cliInstallation.installKind === "git-checkout"
1044
+ ? " from a Git checkout"
1045
+ : "";
1046
+ logLine(`CLI: ${escapeTerminalControlCharacters(cliInstallation.currentVersion)}${manager}`, flags);
1047
+ if (cliInstallation.warning) {
1048
+ logLine(`CLI installation warning: ${escapeTerminalControlCharacters(cliInstallation.warning)}`, flags);
1049
+ }
1050
+ if (cliInstallation.repairCommand) {
1051
+ logLine(`Repair: ${escapeTerminalControlCharacters(cliInstallation.repairCommand)}`, flags);
1052
+ }
1053
+ }
1038
1054
  try {
1039
1055
  const serviceKeyGuidance = getServiceKeyCliAuthGuidance();
1040
1056
  const existingMe = serviceKeyGuidance ? await getMe(client) : null;
@@ -1044,7 +1060,7 @@ export async function commandDoctor(client, cwd, flags) {
1044
1060
  const me = existingMe ?? (await ensureAuthenticated(client, flags));
1045
1061
  const authState = await getCommandAuthState(client, me);
1046
1062
  setTelemetryAuthKind(authState.authKind);
1047
- const workspaceSrPayload = getRequestedWorkspaceSrDoctorPayload(me, flags);
1063
+ const workspaceSrPayload = getRequestedWorkspaceSrDoctorPayload(me, flags, cliInstallation);
1048
1064
  if (workspaceSrPayload) {
1049
1065
  if (isJsonMode(flags)) {
1050
1066
  printJson(workspaceSrPayload);
@@ -1069,6 +1085,7 @@ export async function commandDoctor(client, cwd, flags) {
1069
1085
  });
1070
1086
  const localSnapshotCount = result.resolve.plannedSources.filter((source) => source.sourceKind === "local_archive").length;
1071
1087
  const payload = {
1088
+ cliInstallation,
1072
1089
  authenticatedAs: me.user.username ?? me.user.id,
1073
1090
  accessMode: "company",
1074
1091
  company: result.company,
@@ -1089,10 +1106,14 @@ export async function commandDoctor(client, cwd, flags) {
1089
1106
  return payload;
1090
1107
  }
1091
1108
  catch (error) {
1109
+ if (error instanceof Error) {
1110
+ error.cliInstallation = cliInstallation;
1111
+ }
1092
1112
  if (isJsonMode(flags)) {
1093
1113
  const payload = {
1094
1114
  ok: false,
1095
1115
  error: formatApiError(error),
1116
+ cliInstallation,
1096
1117
  };
1097
1118
  printJson(payload);
1098
1119
  if (error instanceof Error) {
package/dist/help.js CHANGED
@@ -27,7 +27,7 @@ export const CLI_HELP_TEXT = `Usage:
27
27
  Bind this directory to an existing Apex workspace
28
28
  apex cancel-scan [scan-id] Cancel a running scan
29
29
  apex status Show progress for the latest or selected scan
30
- apex doctor Validate auth, repos, connections, and workspace binding
30
+ apex doctor Validate CLI installation, auth, repos, connections, and workspace binding
31
31
  apex login Sign in to Apex
32
32
  apex redeem <code> Redeem a Standard-scan code without device login
33
33
  apex claim Link a guest workspace and findings to a Cantina account
@@ -104,6 +104,8 @@ Tips:
104
104
  An unbound renamed checkout may recover only the sole empty guest workspace unless you pass --workspace-name.
105
105
  After a guest scan, run apex claim on the same machine within the displayed claim deadline to keep the results.
106
106
  Claim moves trial results into the selected Cantina workspace when you can run scans there, or into your existing personal workspace.
107
+ Cantina company access requires current, activated membership. After membership changes, run apex login again.
108
+ For GitHub connection, use the same Cantina account in the browser. After owner approval, rerun apex connect github to finish linking.
107
109
  Service-key commands require an existing login; run apex login first when needed.
108
110
  APEX_SERVICE_KEY authenticates https://ai.cantina.xyz/mcp and hosted REST; it does not replace apex login for local CLI or stdio MCP commands.
109
111
  Apex automatically selects your company when only one is available. With multiple companies, choose one interactively or pass --company.
@@ -178,7 +180,7 @@ Commands:
178
180
  /workspaces List accessible workspaces for the active company
179
181
  /cancel-scan [scan-id] Cancel a running or most recent scan
180
182
  /status [scan-id] Show progress for the most recent or selected scan
181
- /doctor Validate auth, repos, connections, and workspace binding
183
+ /doctor Validate CLI installation, auth, repos, connections, and workspace binding
182
184
  /telemetry [status|enable|disable]
183
185
  Show or change local anonymous usage telemetry settings
184
186
  /update Update the local Apex CLI install and exit the shell
package/dist/mcp.js CHANGED
@@ -30,6 +30,8 @@ Suggested workflow:
30
30
  Notes:
31
31
  - Pass cwd explicitly for repository-specific operations. repoPaths accepts exact files, directories, or HTTPS repository URLs; remote URLs never fall back to uploading cwd. Use Standard for local changes and unconnected public repositories.
32
32
  - Tool calls are always non-interactive and never auto-open a browser.
33
+ - Cantina company access requires current, activated membership. After membership changes, check apex-trial-status and preserve the claim-first workflow before refreshing account login. Temporary membership-verification failures should be retried after recovery without discarding credentials.
34
+ - For GitHub, return the apex-connect-provider URL unchanged. The user must open it in the browser signed in to the same Cantina account as the CLI and complete GitHub authorization. After an organization owner approves an installation request, call apex-connect-provider again to finish company linking.
33
35
  - Finding write tools use the same Apex device-login credentials as read tools. Never ask for browser cookies or CANTINA_AUTH_TOKEN. An error requiring CANTINA_AUTH_TOKEN proves the MCP server is outdated; update Apex CLI, re-run apex setup for this client, restart it, and retry.
34
36
  - Organization service-key creation, listing, and revocation are intentionally scripted-CLI-only and are not exposed as MCP tools. Never ask a user to paste a service key into a prompt or tool argument. A hosted MCP client supplies it only as the HTTP Authorization header.
35
37
  - Production agents that do not need local filesystem access can connect to https://ai.cantina.xyz/mcp over Streamable HTTP with Authorization: Bearer <service-key>. Serverless and cron applications can instead use the REST API documented at https://ai.cantina.xyz/docs/apex-api. This local stdio server continues to use device login.
@@ -56,6 +58,11 @@ const PR_MISSING_PROVIDER_CONNECTIONS_ERROR_PREFIX = "PR scans require connected
56
58
  const MULTIPLE_ACTIVE_SCANS_ERROR = "Multiple active scans are running. Run `apex scans` to list scan IDs, then re-run `apex cancel-scan <scan-id>`.";
57
59
  const DUPLICATE_SCAN_FORCE_GUIDANCE = "Re-run with --force to start another scan.";
58
60
  const MULTIPLE_COMPANIES_ERROR = "Multiple companies available. Re-run with --company <id-or-handle>.";
61
+ const LOCAL_NOT_AUTHENTICATED_ERROR = "Apex is not authenticated. Run `apex login` explicitly, then retry this command.";
62
+ const LOCAL_CLAIM_PROOF_ERROR = "Your guest trial session is no longer available, but its claim proof is stored on this machine. Run `apex claim` to keep the trial results.";
63
+ const SERVICE_KEY_LOCAL_AUTH_GUIDANCE = "service keys authenticate the hosted MCP and REST endpoints, not the local Apex CLI";
64
+ const MCP_NOT_AUTHENTICATED_ERROR = "Not authenticated. If the user supplied a Cantina trial code, run `apex redeem <code>` in a terminal instead of starting device login; never put the code in an MCP tool call. Otherwise call `apex-auth-start`, ask the user to approve the device login, then call `apex-auth-wait`.";
65
+ const MCP_CLAIM_PROOF_ERROR = "Your guest trial session is no longer available, but its claim proof is stored on this machine. Run `apex claim` in a terminal to keep the trial results before starting another login; claim is intentionally not exposed through MCP.";
59
66
  function resolveCwd(cwd) {
60
67
  return path.resolve(cwd && cwd.trim().length > 0 ? cwd : process.cwd());
61
68
  }
@@ -143,6 +150,15 @@ function successResult(summary, value) {
143
150
  }
144
151
  function formatMcpErrorMessage(toolName, error) {
145
152
  const message = formatApiError(error);
153
+ if (message === LOCAL_NOT_AUTHENTICATED_ERROR) {
154
+ return MCP_NOT_AUTHENTICATED_ERROR;
155
+ }
156
+ if (message === LOCAL_CLAIM_PROOF_ERROR) {
157
+ return MCP_CLAIM_PROOF_ERROR;
158
+ }
159
+ if (message.includes(SERVICE_KEY_LOCAL_AUTH_GUIDANCE)) {
160
+ return `${message} ${MCP_NOT_AUTHENTICATED_ERROR}`;
161
+ }
146
162
  if (message === WORKSPACE_BINDING_ERROR) {
147
163
  return "No workspace is bound to this directory. Call `apex-workspace-use` to bind an existing workspace, or call `apex-scan` to create or resolve one first.";
148
164
  }
@@ -165,16 +181,23 @@ function formatMcpErrorMessage(toolName, error) {
165
181
  }
166
182
  function errorResult(toolName, error) {
167
183
  const message = formatMcpErrorMessage(toolName, error);
184
+ const cliInstallation = error instanceof Error && "cliInstallation" in error
185
+ ? error.cliInstallation
186
+ : undefined;
187
+ const diagnosis = cliInstallation
188
+ ? `\n\nCLI installation diagnosis:\n${JSON.stringify(cliInstallation, null, 2)}`
189
+ : "";
168
190
  return {
169
191
  content: [
170
192
  {
171
193
  type: "text",
172
- text: message,
194
+ text: `${message}${diagnosis}`,
173
195
  },
174
196
  ],
175
197
  structuredContent: {
176
198
  ok: false,
177
199
  error: message,
200
+ ...(cliInstallation ? { cliInstallation } : {}),
178
201
  },
179
202
  isError: true,
180
203
  };
@@ -199,9 +222,9 @@ async function requireAuthenticated(client) {
199
222
  if (!me) {
200
223
  const credentials = await client.getCredentials();
201
224
  if (isClaimProofAvailable(credentials)) {
202
- throw new Error("Your guest trial session is no longer available, but its claim proof is stored on this machine. Run `apex claim` in a terminal to keep the trial results before starting another login; claim is intentionally not exposed through MCP.");
225
+ throw new Error(MCP_CLAIM_PROOF_ERROR);
203
226
  }
204
- throw new Error("Not authenticated. If the user supplied a Cantina trial code, run `apex redeem <code>` in a terminal instead of starting device login; never put the code in an MCP tool call. Otherwise call `apex-auth-start`, ask the user to approve the device login, then call `apex-auth-wait`.");
227
+ throw new Error(MCP_NOT_AUTHENTICATED_ERROR);
205
228
  }
206
229
  return me;
207
230
  }
@@ -396,7 +419,7 @@ function registerTools(server, auditSetupEnabled = false) {
396
419
  });
397
420
  server.registerTool("apex-doctor", {
398
421
  title: "Run Apex Doctor",
399
- description: "Validate auth, local source detection, workspace binding, and the planned remote-vs-local materialization strategy.",
422
+ description: "Validate the CLI installation, auth, local source detection, workspace binding, and the planned remote-vs-local materialization strategy.",
400
423
  inputSchema: {
401
424
  cwd: z.string().optional(),
402
425
  company: z.string().optional(),
@@ -406,7 +429,6 @@ function registerTools(server, auditSetupEnabled = false) {
406
429
  },
407
430
  }, async ({ cwd, company, workspaceName, repoPaths, sourceMode }) => runTool("apex-doctor", async () => {
408
431
  const client = new ApexApiClient();
409
- await requireAuthenticated(client);
410
432
  const targetCwd = resolveCwd(cwd);
411
433
  const payload = await commandDoctor(client, targetCwd, buildFlags({
412
434
  company,
@@ -885,7 +907,7 @@ function registerTools(server, auditSetupEnabled = false) {
885
907
  }));
886
908
  server.registerTool("apex-connect-provider", {
887
909
  title: "Get Apex Provider Connection URL",
888
- description: "Return the browser URL a user needs to connect GitHub, GitLab or company-enabled Forgejo access for Apex. This tool never opens a browser.",
910
+ description: "Return the browser URL a user needs to connect GitHub, GitLab or company-enabled Forgejo access for Apex. This tool never opens a browser. For GitHub, use the same Cantina account as the CLI in the browser and repeat this tool after organization-owner approval to finish linking.",
889
911
  inputSchema: {
890
912
  cwd: z.string().optional(),
891
913
  company: z.string().optional(),
package/dist/update.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { execFile as execFileCallback, spawn } from "node:child_process";
2
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -51,19 +51,23 @@ async function readPackageVersion(packageRoot) {
51
51
  }
52
52
  return parsed.version.trim();
53
53
  }
54
- async function execText(command, args, cwd) {
54
+ async function canonicalPath(targetPath) {
55
+ return realpath(targetPath).catch(() => path.resolve(targetPath));
56
+ }
57
+ async function execText(command, args, cwd, timeout) {
55
58
  const result = await execFile(command, args, {
56
59
  cwd,
57
60
  encoding: "utf8",
61
+ timeout,
58
62
  });
59
63
  return {
60
64
  stdout: result.stdout.trim(),
61
65
  stderr: result.stderr.trim(),
62
66
  };
63
67
  }
64
- async function tryExecText(command, args, cwd) {
68
+ async function tryExecText(command, args, cwd, timeout) {
65
69
  try {
66
- return await execText(command, args, cwd);
70
+ return await execText(command, args, cwd, timeout);
67
71
  }
68
72
  catch {
69
73
  return null;
@@ -93,25 +97,121 @@ async function detectProjectPackageManager(packageRoot) {
93
97
  }
94
98
  return null;
95
99
  }
96
- async function detectGlobalInstaller() {
97
- if (await tryExecText("pnpm", ["--version"])) {
100
+ function globalInstallCommand(packageManager, packageSpec = REPO_SPEC) {
101
+ if (packageManager === "pnpm") {
98
102
  return {
99
103
  command: "pnpm",
100
- args: ["add", "-g", REPO_SPEC],
101
- label: `pnpm add -g ${REPO_SPEC}`,
104
+ args: ["add", "-g", packageSpec],
105
+ label: `pnpm add -g ${packageSpec}`,
102
106
  };
103
107
  }
104
- if (await tryExecText("npm", ["--version"])) {
108
+ return {
109
+ command: "npm",
110
+ args: ["install", "-g", packageSpec],
111
+ label: `npm install -g ${packageSpec}`,
112
+ };
113
+ }
114
+ function globalRemoveCommand(packageManager) {
115
+ if (packageManager === "pnpm") {
105
116
  return {
106
- command: "npm",
107
- args: ["install", "-g", REPO_SPEC],
108
- label: `npm install -g ${REPO_SPEC}`,
117
+ command: "pnpm",
118
+ args: ["remove", "-g", REPO_SPEC],
119
+ label: `pnpm remove -g ${REPO_SPEC}`,
109
120
  };
110
121
  }
111
122
  return {
112
- command: "pnpm",
113
- args: ["add", "-g", REPO_SPEC],
114
- label: `pnpm add -g ${REPO_SPEC}`,
123
+ command: "npm",
124
+ args: ["uninstall", "-g", REPO_SPEC],
125
+ label: `npm uninstall -g ${REPO_SPEC}`,
126
+ };
127
+ }
128
+ async function getGlobalPackageManagerRoots() {
129
+ const candidates = await Promise.all(["pnpm", "npm"].map(async (packageManager) => {
130
+ const result = await tryExecText(packageManager, ["root", "-g"], undefined, 2_000);
131
+ if (!result?.stdout)
132
+ return null;
133
+ return { packageManager, root: path.resolve(result.stdout) };
134
+ }));
135
+ return candidates.filter((candidate) => candidate !== null);
136
+ }
137
+ async function inspectGlobalInstallations(packageRoot) {
138
+ const [activeRoot, roots] = await Promise.all([
139
+ canonicalPath(packageRoot),
140
+ getGlobalPackageManagerRoots(),
141
+ ]);
142
+ const installations = (await Promise.all(roots.map(async ({ packageManager, root }) => {
143
+ const candidateRoot = path.join(root, ...PACKAGE_NAME.split("/"));
144
+ const version = await readPackageVersion(candidateRoot).catch(() => null);
145
+ if (!version)
146
+ return null;
147
+ const resolvedCandidateRoot = await canonicalPath(candidateRoot);
148
+ return {
149
+ packageManager,
150
+ version,
151
+ packageRoot: candidateRoot,
152
+ active: resolvedCandidateRoot === activeRoot,
153
+ };
154
+ }))).filter((installation) => installation !== null);
155
+ return { roots, installations };
156
+ }
157
+ async function detectGlobalInstaller(packageRoot) {
158
+ const { installations } = await inspectGlobalInstallations(packageRoot);
159
+ const owner = installations.find((installation) => installation.active);
160
+ if (owner) {
161
+ return {
162
+ ...globalInstallCommand(owner.packageManager),
163
+ packageRoot: owner.packageRoot,
164
+ };
165
+ }
166
+ throw new Error("Apex could not determine which package manager owns this installation. Run `npm install -g @cantinasecurity/apex-cli` or `pnpm add -g @cantinasecurity/apex-cli` explicitly.");
167
+ }
168
+ export async function getCliInstallDiagnostics(packageRoot = PACKAGE_ROOT) {
169
+ const currentVersion = await readPackageVersion(packageRoot);
170
+ const gitCheckout = await pathExists(path.join(packageRoot, ".git"));
171
+ if (gitCheckout) {
172
+ return {
173
+ currentVersion,
174
+ installKind: "git-checkout",
175
+ packageManager: null,
176
+ healthy: true,
177
+ installations: [],
178
+ warning: null,
179
+ repairCommand: null,
180
+ };
181
+ }
182
+ const { installations } = await inspectGlobalInstallations(packageRoot);
183
+ const active = installations.find((installation) => installation.active) ?? null;
184
+ const duplicates = installations.filter((installation) => !installation.active);
185
+ const newerDuplicate = duplicates.find((installation) => compareVersions(currentVersion, installation.version) < 0);
186
+ const mismatchedTransientCopies = active
187
+ ? []
188
+ : installations.filter((installation) => installation.version !== currentVersion);
189
+ const warning = active && newerDuplicate
190
+ ? `This process is Apex CLI ${currentVersion}, but ${newerDuplicate.packageManager} also has newer Apex CLI ${newerDuplicate.version}. Your shell is resolving the older installation.`
191
+ : active && duplicates.length > 0
192
+ ? `Multiple global Apex CLI installations were found (${installations.map((installation) => `${installation.packageManager} ${installation.version}`).join(", ")}). They can drift and cause the shell and MCP clients to run different versions.`
193
+ : mismatchedTransientCopies.length > 0
194
+ ? `This process is Apex CLI ${currentVersion} from a temporary package, but global Apex installations use ${installations.map((installation) => `${installation.packageManager} ${installation.version}`).join(", ")}. Terminal commands may resolve a different version.`
195
+ : null;
196
+ const repairCommand = active && duplicates.length > 0
197
+ ? [
198
+ globalInstallCommand(active.packageManager, `${REPO_SPEC}@latest`).label,
199
+ ...duplicates.map((installation) => globalRemoveCommand(installation.packageManager).label),
200
+ ].join(" && ")
201
+ : mismatchedTransientCopies.length > 0
202
+ ? installations
203
+ .map((installation) => globalInstallCommand(installation.packageManager, `${REPO_SPEC}@latest`).label)
204
+ .join(" && ")
205
+ : null;
206
+ const publicInstallations = installations.map(({ packageRoot: _packageRoot, ...installation }) => installation);
207
+ return {
208
+ currentVersion,
209
+ installKind: "package-install",
210
+ packageManager: active?.packageManager ?? null,
211
+ healthy: warning === null,
212
+ installations: publicInstallations,
213
+ warning,
214
+ repairCommand,
115
215
  };
116
216
  }
117
217
  async function resolveInstallContext(packageRoot = PACKAGE_ROOT) {
@@ -146,23 +246,27 @@ async function resolveInstallContext(packageRoot = PACKAGE_ROOT) {
146
246
  upstreamRef: upstream?.stdout ?? null,
147
247
  updateCommands,
148
248
  updateCommand: display.join(" && "),
249
+ verificationPackageRoot: null,
149
250
  };
150
251
  }
151
252
  }
152
- const installer = await detectGlobalInstaller();
253
+ const installer = await detectGlobalInstaller(packageRoot).catch(() => null);
153
254
  return {
154
255
  packageRoot,
155
256
  currentVersion,
156
257
  currentRevision: null,
157
258
  installKind: "package-install",
158
259
  upstreamRef: null,
159
- updateCommands: [
160
- {
161
- command: installer.command,
162
- args: installer.args,
163
- },
164
- ],
165
- updateCommand: installer.label,
260
+ updateCommands: installer
261
+ ? [
262
+ {
263
+ command: installer.command,
264
+ args: installer.args,
265
+ },
266
+ ]
267
+ : [],
268
+ updateCommand: installer?.label ?? null,
269
+ verificationPackageRoot: installer?.packageRoot ?? null,
166
270
  };
167
271
  }
168
272
  async function loadUpdateState() {
@@ -431,7 +535,7 @@ export async function commandUpdate(flags, packageRoot = PACKAGE_ROOT) {
431
535
  return status;
432
536
  }
433
537
  if (!context.updateCommand || context.updateCommands.length === 0) {
434
- throw new Error("No update command is available for this Apex CLI installation.");
538
+ throw new Error("Apex could not determine which package manager owns this installation. Run `npm install -g @cantinasecurity/apex-cli` or `pnpm add -g @cantinasecurity/apex-cli` explicitly.");
435
539
  }
436
540
  await ensureCleanGitCheckout(context);
437
541
  if (!isJsonMode(flags)) {
@@ -445,13 +549,24 @@ export async function commandUpdate(flags, packageRoot = PACKAGE_ROOT) {
445
549
  ...(await getUpdateStatus(packageRoot, true)),
446
550
  updated: true,
447
551
  }
448
- : {
449
- ...status,
450
- currentVersion: status.latestVersion ?? status.currentVersion,
451
- currentRevision: status.latestRevision ?? status.currentRevision,
452
- available: false,
453
- updated: true,
454
- };
552
+ : await (async () => {
553
+ const installedVersion = await readPackageVersion(context.verificationPackageRoot ?? packageRoot);
554
+ if (status.latestVersion &&
555
+ compareVersions(installedVersion, status.latestVersion) < 0) {
556
+ const diagnostics = await getCliInstallDiagnostics(packageRoot).catch(() => null);
557
+ const repair = diagnostics?.repairCommand
558
+ ? ` Run \`${diagnostics.repairCommand}\` to update the installation used by this shell.`
559
+ : " Update the package-manager installation selected by `which apex`.";
560
+ throw new Error(`The installer finished, but this Apex executable is still ${installedVersion}; expected ${status.latestVersion}.${repair}`);
561
+ }
562
+ return {
563
+ ...status,
564
+ currentVersion: installedVersion,
565
+ currentRevision: status.latestRevision ?? status.currentRevision,
566
+ available: false,
567
+ updated: true,
568
+ };
569
+ })();
455
570
  if (isJsonMode(flags)) {
456
571
  printJson(payload);
457
572
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cantinasecurity/apex-cli",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "description": "Standalone CLI and MCP server for Apex.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -67,6 +67,8 @@ Guidelines:
67
67
  - Do not rely on interactive CLI prompts. The MCP tools are intentionally non-interactive.
68
68
  - The MCP-only `apex-trial-status` tool may report local guest and claim deadlines, but it must never return a trial code or claim token. `claimAvailable` becomes false at a known passed deadline; missing or invalid local deadline metadata remains conservative and server-authoritative. There is intentionally no MCP redemption or claim tool.
69
69
  - When MCP remains available after a guest scan, remind the user to run `apex claim` on the same machine while `claimAvailable` is true; after a known passed deadline, continue with normal account login.
70
+ - Cantina company access requires current, activated membership; pending invitations and removed or disabled memberships do not grant access. After membership changes, refresh the account with `apex login` in a terminal, checking `apex-trial-status` first and preserving the claim-first workflow above. For temporary company-access verification failures, keep credentials and retry after recovery.
71
+ - For GitHub connection, use `apex-connect-provider` with `provider: "github"` and pass the returned URL unchanged to the user. They must open it in the browser signed in to the same Cantina account as the CLI and complete GitHub authorization. After an organization owner approves an installation request, call the tool again to finish company linking. CLI fallback: `apex connect github`. Automation must not assume the returned URL is hosted on github.com.
70
72
  - Service-key creation, listing, and revocation have no MCP tools. Never ask a user to paste a service key into a prompt, and never place one in a tool argument, tool result, source file, or log. Hosted MCP clients may read it from a secret-backed environment variable for the HTTP Authorization header.
71
73
  - If a user explicitly needs a new service key, tell them to run `apex login` if needed, then run `apex service-key create --name <name> --scope <scope[,scope]>` directly in their own terminal. Use only the required scopes: `scans:create` to query credit availability or trigger, `scans:read` to poll status, and `findings:read` to retrieve findings. A P1-only adapter can use `--scope scans:read,findings:read`; omitting `--scope` grants all three. The CLI automatically selects the company when only one is available and prompts when there are several; use `--company <id-or-handle>` to skip that prompt or resolve a non-interactive command. Service-key commands never initiate login, and the secret is shown once.
72
74
  - For a hosted MCP client, connect to `https://ai.cantina.xyz/mcp` and send `APEX_SERVICE_KEY` as a Bearer header. The service key authenticates hosted MCP, but not the local CLI or stdio MCP; those use device login.
@@ -76,7 +78,7 @@ Guidelines:
76
78
  - Hosted repository scan triggers require `Idempotency-Key`. Reuse one stable key only for an identical transport/timeout/5xx retry. After correcting a definitive 4xx response, or for any new intended repository scan, use a new key. Apex Black stored start failures follow the support escalation rule above; never change keys to bypass them.
77
79
  - `apex-credits` reports standard credits plus audit, fix review, and Lite scan entitlements when Bedrock returns those balances.
78
80
  - `apex-scan` scans the provided `cwd` by default; pass `repoPaths` only when the user asks to scan explicit alternate roots.
79
- - `apex-doctor` is read-only for guest sessions and reports the local snapshot plan without reserving a workspace. For account sessions it reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
81
+ - `apex-doctor` reports the running CLI version and detects conflicting npm/pnpm global installations. It is read-only for guest sessions and reports the local snapshot plan without reserving a workspace. For account sessions it reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
80
82
  - Apex can scan plain local directories and dirty git worktrees without provider connections by using local snapshot uploads.
81
83
  - Audit scans use `mode: "audit"` in user-facing instructions and request payloads. The legacy `ultra` mode remains accepted as an alias, but audit scans still require provider-backed GitHub, GitLab or company-enabled Forgejo sources.
82
84
  - Lite scans use `mode: "lite"`, require Lite Scan beta access, and require provider-backed GitHub or GitLab sources without local snapshot fallback.