@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.
- package/.claude/skills/apex-cli/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.claude.json +1 -1
- package/.mcp.codex.json +1 -1
- package/MARKETPLACE.md +1 -1
- package/README.md +30 -9
- package/dist/commands.js +24 -3
- package/dist/help.js +4 -2
- package/dist/mcp.js +28 -6
- package/dist/update.js +146 -31
- package/package.json +1 -1
- package/skills/apex-cli/SKILL.md +3 -1
|
@@ -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.
|
|
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.
|
|
17
|
+
"version": "0.1.44"
|
|
18
18
|
},
|
|
19
19
|
"description": "Run Apex security scans and review findings from Claude Code.",
|
|
20
|
-
"version": "0.1.
|
|
20
|
+
"version": "0.1.44",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "Cantina",
|
|
23
23
|
"email": "support@cantina.xyz"
|
package/.mcp.claude.json
CHANGED
package/.mcp.codex.json
CHANGED
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
225
|
+
throw new Error(MCP_CLAIM_PROOF_ERROR);
|
|
203
226
|
}
|
|
204
|
-
throw new Error(
|
|
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
|
|
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
|
-
|
|
97
|
-
if (
|
|
100
|
+
function globalInstallCommand(packageManager, packageSpec = REPO_SPEC) {
|
|
101
|
+
if (packageManager === "pnpm") {
|
|
98
102
|
return {
|
|
99
103
|
command: "pnpm",
|
|
100
|
-
args: ["add", "-g",
|
|
101
|
-
label: `pnpm add -g ${
|
|
104
|
+
args: ["add", "-g", packageSpec],
|
|
105
|
+
label: `pnpm add -g ${packageSpec}`,
|
|
102
106
|
};
|
|
103
107
|
}
|
|
104
|
-
|
|
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: "
|
|
107
|
-
args: ["
|
|
108
|
-
label: `
|
|
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: "
|
|
113
|
-
args: ["
|
|
114
|
-
label: `
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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("
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
package/skills/apex-cli/SKILL.md
CHANGED
|
@@ -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.
|