@cantinasecurity/apex-cli 0.1.6 → 0.1.9

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.
@@ -28,7 +28,7 @@ If the Apex MCP server is not configured, fall back to the local CLI:
28
28
  - Prefer scripted commands over the interactive shell.
29
29
  - Use `--non-interactive` and `--no-open` for automation-friendly CLI calls.
30
30
  - Use `--json` whenever structured output is helpful.
31
- - `apex credits` reports standard credits and audit scan entitlements when Bedrock returns both balances.
31
+ - `apex credits` reports standard credits plus audit and fix review scan entitlements when Bedrock returns those balances.
32
32
  - Work from the target repository directory so Apex can resolve `.apex/workspace.json`.
33
33
  - `apex scan` scans the current working directory by default; pass `--repo` only when the user asks to scan explicit alternate roots.
34
34
  - `apex-doctor` reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
@@ -37,7 +37,8 @@ If the Apex MCP server is not configured, fall back to the local CLI:
37
37
  - PR scans use `--mode pr --pr <number>` in CLI calls, or `mode: "pr"` with `pullRequests` in MCP. They require one provider-backed GitHub repository.
38
38
  - `apex-workspace-use` accepts a workspace name, prefix, or ID.
39
39
  - Use `sourceMode: "remote"` only when the user explicitly wants to forbid local snapshot fallbacks.
40
- - Finding comments, feedback, and fix review scan starts currently require `CANTINA_AUTH_TOKEN` in the MCP server environment because those writes go through the Cantina web-app routes instead of the Apex CLI bearer-token routes.
40
+ - When checking a scan that is not the workspace binding's latest scan, pass `scanId` to `apex-status`; use `apex scans` or `apex-scans` first if you need to discover scan IDs.
41
+ - Finding comments, feedback, and fix review scan starts use the same Apex device-login credentials as read tools. If a write tool reports missing auth, re-run `apex-auth-status` and complete `apex-auth-start` / `apex-auth-wait` instead of asking for browser cookies or auth tokens.
41
42
  - Invalid finding feedback requires `dismissalReason`; valid feedback can include `suggestedSeverity`, including `extreme`.
42
43
  - Fix PR callback feedback requires valid feedback with `labels: ["fixed"]` and `fixPrUrls`; start the fix review scan with `apex-finding-fix-review` after saving that feedback.
43
44
  - Finding identifiers such as `KERN2-25` resolve against the selected or latest scan for the current workspace binding. Pass an explicit `scanId` when needed, or use the finding UUID directly.
@@ -46,7 +47,7 @@ If the Apex MCP server is not configured, fall back to the local CLI:
46
47
 
47
48
  - Start a scan for the current repository or directory: run `apex-doctor`, bind a workspace if needed, then call `apex-scan`.
48
49
  - Start a PR scan: call `apex-scan` with `mode: "pr"` and `pullRequests`.
49
- - Check an active scan: call `apex-status`, then `apex-findings` when the scan completes.
50
+ - Check an active scan: call `apex-status`, passing `scanId` when the scan is not the latest binding, then `apex-findings` when the scan completes.
50
51
  - Export findings for review: call `apex-export-findings` with `format` set to `markdown`, `json`, or `gitlab-sast`.
51
52
  - Leave review feedback: call `apex-finding-comment` to add a note, or `apex-finding-feedback` with `status` set to `valid` or `invalid`.
52
53
  - Add a Fix PR and run fix review: call `apex-finding-feedback` with `status: "valid"`, `labels: ["fixed"]`, and `fixPrUrls`, then call `apex-finding-fix-review`.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Cantina agent plugins for security review workflows.",
9
- "version": "0.1.6"
9
+ "version": "0.1.9"
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.6"
17
+ "version": "0.1.9"
18
18
  },
19
19
  "description": "Run Apex security scans and review findings from Claude Code.",
20
- "version": "0.1.6",
20
+ "version": "0.1.9",
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.6",
3
+ "version": "0.1.9",
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.6",
3
+ "version": "0.1.9",
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.6",
8
+ "@cantinasecurity/apex-cli@0.1.9",
9
9
  "apex-mcp"
10
10
  ]
11
11
  }
package/.mcp.codex.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "args": [
5
5
  "-y",
6
6
  "-p",
7
- "@cantinasecurity/apex-cli@0.1.6",
7
+ "@cantinasecurity/apex-cli@0.1.9",
8
8
  "apex-mcp"
9
9
  ]
10
10
  }
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.6 apex-mcp
16
+ npx -y -p @cantinasecurity/apex-cli@0.1.9 apex-mcp
17
17
  ```
18
18
 
19
19
  That keeps marketplace installs independent of a user's global `apex` install.
package/README.md CHANGED
@@ -109,7 +109,7 @@ Supported shell commands:
109
109
  - `/export [scan-id]`
110
110
  - `/workspaces`
111
111
  - `/cancel-scan [scan-id]`
112
- - `/status`
112
+ - `/status [scan-id]`
113
113
  - `/doctor`
114
114
  - `/update`
115
115
  - `/logout`
@@ -142,7 +142,7 @@ Supported shell commands:
142
142
  - `apex workspace`
143
143
  - `apex workspace use <workspace-name|workspace-prefix|workspace-id>`
144
144
  - `apex cancel-scan [scan-id]`
145
- - `apex status`
145
+ - `apex status [--scan <scan-id>]`
146
146
  - `apex doctor`
147
147
  - `apex login`
148
148
  - `apex logout`
@@ -156,7 +156,7 @@ Helpful workspace flags:
156
156
  - `--company <id-or-handle>` to choose the Apex company when more than one is available
157
157
  - `--workspace-name <name>` to set the Apex workspace name for this directory
158
158
 
159
- `apex credits` shows standard scan credits plus audit scan entitlements when the server returns them.
159
+ `apex credits` shows standard scan credits plus audit and fix review scan entitlements when the server returns them.
160
160
 
161
161
  ## Finding Review Feedback
162
162
 
@@ -174,7 +174,7 @@ Finding review collaboration now has explicit write commands:
174
174
 
175
175
  Identifiers such as `KERN2-25` are resolved against the selected or latest scan for the current workspace binding. Pass `--scan <scan-id>` when you need a specific scan, or pass the finding UUID directly to skip workspace-based resolution.
176
176
 
177
- Finding comments, valid/invalid feedback, and fix review scan starts currently require a Cantina web session token in `CANTINA_AUTH_TOKEN`. Set it to the value of your logged-in `auth_token` cookie before using the write commands or the corresponding MCP tools.
177
+ Finding comments, valid/invalid feedback, and fix review scan starts use the same Apex login credentials as read commands. In MCP clients, `apex-auth-start` followed by `apex-auth-wait` is enough to authenticate these write tools.
178
178
 
179
179
  Invalid feedback requires a dismissal reason. Valid feedback can include `--suggested-severity extreme|critical|high|medium|low|informational`.
180
180
 
@@ -185,10 +185,7 @@ Fix review scans use a two-step callback flow for agents that create a PR outsid
185
185
 
186
186
  The matching MCP flow is `apex-finding-feedback` with `status: "valid"`, `labels: ["fixed"]`, and `fixPrUrls`, followed by `apex-finding-fix-review`.
187
187
 
188
- This is intentionally documented as a separate auth requirement because the current Apex read APIs and finding review write APIs do not accept the same credentials:
189
-
190
- - read operations such as `apex findings`, `apex export findings`, and `apex-findings` use the Apex CLI device-login bearer token
191
- - finding comments and feedback currently go through the Cantina web-app routes and require `CANTINA_AUTH_TOKEN`
188
+ The matching CLI and MCP flows intentionally use the same device-login session so agents should not ask users to paste browser cookies or auth tokens.
192
189
 
193
190
  ## Local Source Scans
194
191
 
@@ -300,7 +297,7 @@ The npm package also includes marketplace-ready plugin artifacts:
300
297
  - `.claude-plugin/plugin.json` and `.mcp.claude.json` for Claude Code plugin installs
301
298
  - `.claude-plugin/marketplace.json` for a Claude marketplace entry backed by the public npm package
302
299
 
303
- These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.6 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
300
+ These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.9 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
304
301
 
305
302
  The repository also includes `.agents/plugins/marketplace.json` for local Codex marketplace testing from a checkout.
306
303
 
@@ -328,11 +325,11 @@ The CLI uses the Apex `/api/cli/v2/**` local-source routes for scan planning and
328
325
  - `~/.config/apex/credentials.json`
329
326
  - `.apex/workspace.json`
330
327
 
331
- If a scan is already running in the current workspace, `apex scan` and `/scan` now require confirmation before starting another one. Scripted usage can opt in explicitly with `--force`.
328
+ If a scan is already running in the current workspace, `apex scan` and `/scan` now require confirmation before starting another one. Scripted usage can opt in explicitly with `--force`. Active-looking workspace scan rows are checked against the scan progress endpoint before the CLI treats them as blockers, so stale list entries do not hide terminal states such as `cancelled`.
332
329
 
333
330
  To move between existing Apex workspaces from the CLI:
334
331
 
335
332
  1. Run `apex workspaces` to list the workspaces available to your active company.
336
333
  2. Run `apex workspace use <workspace-name|workspace-prefix|workspace-id>` to bind the current directory.
337
334
  3. If the workspace name contains spaces, quote it, for example `apex workspace use "Core Platform"`.
338
- 4. Use `apex scans`, `apex findings`, and `apex export findings` against that binding.
335
+ 4. Use `apex scans`, `apex status --scan <scan-id>`, `apex findings`, and `apex export findings` against that binding.
@@ -45,6 +45,10 @@ export class ApexApiClient {
45
45
  async getCredentials() {
46
46
  return loadCredentials();
47
47
  }
48
+ async getAccessToken() {
49
+ const credentials = await this.refreshIfNeeded().catch(() => null);
50
+ return credentials?.accessToken ?? null;
51
+ }
48
52
  async clearCredentials() {
49
53
  await clearCredentials();
50
54
  }
package/dist/commands.js CHANGED
@@ -3,11 +3,11 @@ import { logout } from "./auth.js";
3
3
  import { ApiError } from "./api-client.js";
4
4
  import { openInBrowser } from "./browser.js";
5
5
  import { loadConfig, saveConfig } from "./config.js";
6
- import { createFindingComment, fetchScanExport, fetchScanFindings, isFindingUuid, normalizeFindingRef, requireCantinaAuthToken, resolveFindingSelection, resolveScanSelection, startFindingFixReviewScan, submitFindingFeedback, } from "./findings.js";
6
+ import { createFindingComment, fetchScanExport, fetchScanFindings, isFindingUuid, normalizeFindingRef, resolveFindingSelection, resolveScanSelection, startFindingFixReviewScan, submitFindingFeedback, } from "./findings.js";
7
7
  import { prepareExplicitScanSources, supportsLegacyRemoteFlow, } from "./local-source-scan.js";
8
8
  import { logLine, printJson, withLoadingIndicator } from "./output.js";
9
9
  import { confirm } from "./prompt.js";
10
- import { cancelScan, fetchWorkspaceScans, findMostRelevantActiveScan, getScanDisplayLabel, getScanDisplayId, getTrackedScanId, isActiveScanStatus, matchesScanId, selectDefaultScanToCancel, } from "./scan.js";
10
+ import { cancelScan, fetchScanProgress, fetchWorkspaceScans, findMostRelevantActiveScan, getScanDisplayLabel, getScanDisplayId, getTrackedScanId, isActiveScanStatus, matchesScanId, selectDefaultScanToCancel, } from "./scan.js";
11
11
  import { chooseCompany, createWorkspaceBinding, ensureAuthenticated, remediateMissingConnections, resolveWorkspaceSelection, resolveWorkspaceSelectionLegacy, resolveWorkspaceAllowingMissingConnections, selectWorkspaceTarget, } from "./session.js";
12
12
  import { createWorkspaceBindingFromSummary, fetchCompanyCredits, fetchCompanyWorkspaces, findWorkspaceByRef, } from "./workspaces.js";
13
13
  import { loadWorkspaceBinding, saveWorkspaceBinding } from "./workspace-binding.js";
@@ -81,6 +81,29 @@ function formatAuditScanBalance(scanBalance) {
81
81
  }
82
82
  return `Audit scans: ${detailParts.join(", ")}`;
83
83
  }
84
+ function formatFixReviewScanBalance(scanBalance) {
85
+ const purchased = getOptionalCount(scanBalance.fixReviewPurchased);
86
+ const used = getOptionalCount(scanBalance.fixReviewUsed);
87
+ const remaining = getOptionalCount(scanBalance.fixReviewRemaining);
88
+ const available = getOptionalCount(scanBalance.fixReviewAvailable) ??
89
+ remaining ??
90
+ (purchased !== null && used !== null ? Math.max(0, purchased - used) : null);
91
+ if (purchased === null && used === null && available === null) {
92
+ return null;
93
+ }
94
+ const detailParts = [];
95
+ if (purchased !== null) {
96
+ detailParts.push(`${purchased} purchased`);
97
+ }
98
+ if (used !== null) {
99
+ detailParts.push(`${used} used`);
100
+ }
101
+ if (available !== null) {
102
+ const detailSuffix = detailParts.length ? ` (${detailParts.join(", ")})` : "";
103
+ return `Fix review scans: ${available} available${detailSuffix}`;
104
+ }
105
+ return `Fix review scans: ${detailParts.join(", ")}`;
106
+ }
84
107
  function normalizeRequestedScanMode(value) {
85
108
  if (value === "ultra" || value === "audit") {
86
109
  return "audit";
@@ -450,6 +473,14 @@ export async function commandCredits(client, cwd, flags) {
450
473
  if (redeemableAuditScans && redeemableAuditScans > 0) {
451
474
  logLine(`Redeemable audit scans from standard credits: ${redeemableAuditScans}`, flags);
452
475
  }
476
+ const fixReviewBalance = formatFixReviewScanBalance(payload.scanBalance);
477
+ if (fixReviewBalance) {
478
+ logLine(fixReviewBalance, flags);
479
+ }
480
+ const redeemableFixReviewScans = getOptionalCount(payload.scanBalance.fixReviewRedeemableFromCredits);
481
+ if (redeemableFixReviewScans && redeemableFixReviewScans > 0) {
482
+ logLine(`Redeemable fix review scans from standard credits: ${redeemableFixReviewScans}`, flags);
483
+ }
453
484
  logLine(`Scans enabled: ${payload.scansEnabled ? "yes" : "no"}`, flags);
454
485
  return payload;
455
486
  }
@@ -507,24 +538,26 @@ export async function commandDoctor(client, cwd, flags) {
507
538
  export async function commandStatus(client, cwd, flags) {
508
539
  await ensureAuthenticated(client, flags);
509
540
  const binding = await requireWorkspaceBinding(cwd);
510
- if (!binding.lastScanId) {
541
+ const requestedScanId = getFlagString(flags, "scan");
542
+ const scanId = requestedScanId ?? binding.lastScanId;
543
+ if (!scanId) {
511
544
  throw new Error("No scan has been started from this directory yet. Run `apex scan` first.");
512
545
  }
513
- const progress = await withLoadingIndicator("Loading scan status...", flags, () => client.request(`/api/cli/v1/scans/${binding.lastScanId}/progress`));
546
+ const progress = await withLoadingIndicator("Loading scan status...", flags, () => fetchScanProgress(client, scanId));
514
547
  if (isJsonMode(flags)) {
515
- printJson({ binding, progress });
516
- return { binding, progress };
548
+ printJson({ binding, scanId, progress });
549
+ return { binding, scanId, progress };
517
550
  }
518
551
  logLine(`Workspace: ${binding.workspaceName}`, flags);
519
- logLine(`Scan: ${binding.lastScanId}`, flags);
552
+ logLine(`Scan: ${scanId}`, flags);
520
553
  logLine(`Status: ${progress.progress?.status ?? "unknown"}`, flags);
521
554
  if (typeof progress.progress?.progressPct === "number") {
522
555
  logLine(`Progress: ${progress.progress.progressPct}%`, flags);
523
556
  }
524
- if (binding.lastScanUrl) {
557
+ if (binding.lastScanUrl && (!requestedScanId || requestedScanId === binding.lastScanId)) {
525
558
  logLine(`View: ${binding.lastScanUrl}`, flags);
526
559
  }
527
- return { binding, progress };
560
+ return { binding, scanId, progress };
528
561
  }
529
562
  export async function commandConnect(client, cwd, flags, provider) {
530
563
  const me = await ensureAuthenticated(client, flags);
@@ -923,7 +956,6 @@ export async function commandFindingComment(client, cwd, flags, requestedFinding
923
956
  if (!trimmedContent) {
924
957
  throw new Error("Usage: apex findings comment <finding-id|finding-identifier> --content \"<markdown>\"");
925
958
  }
926
- requireCantinaAuthToken();
927
959
  const currentBinding = await loadWorkspaceBinding(cwd);
928
960
  const needsWorkspaceResolution = !isFindingUuid(findingRef);
929
961
  const binding = needsWorkspaceResolution
@@ -998,7 +1030,6 @@ export async function commandFindingFeedback(client, cwd, flags, params) {
998
1030
  if (fixPrUrls.length > 0 && labels?.[0] !== "fixed") {
999
1031
  throw new Error("Fix PR URLs require the fixed feedback label.");
1000
1032
  }
1001
- requireCantinaAuthToken();
1002
1033
  const currentBinding = await loadWorkspaceBinding(cwd);
1003
1034
  const needsWorkspaceResolution = !isFindingUuid(findingRef);
1004
1035
  const binding = needsWorkspaceResolution
@@ -1063,7 +1094,6 @@ export async function commandFindingFeedback(client, cwd, flags, params) {
1063
1094
  }
1064
1095
  export async function commandFindingFixReview(client, cwd, flags, requestedFindingRef) {
1065
1096
  const findingRef = normalizeFindingRefInput(requestedFindingRef);
1066
- requireCantinaAuthToken();
1067
1097
  const currentBinding = await loadWorkspaceBinding(cwd);
1068
1098
  const needsWorkspaceResolution = !isFindingUuid(findingRef);
1069
1099
  const binding = needsWorkspaceResolution
package/dist/findings.js CHANGED
@@ -2,7 +2,7 @@ import { fetchWorkspaceScans, getScanDisplayId, matchesScanId, } from "./scan.js
2
2
  import { ApiError } from "./api-client.js";
3
3
  const FINDING_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4
4
  const CANTINA_AUTH_TOKEN_ENV = "CANTINA_AUTH_TOKEN";
5
- const CANTINA_AUTH_TOKEN_ERROR = "Finding comments, feedback, and fix review scans currently require `CANTINA_AUTH_TOKEN` from a logged-in Cantina/Apex browser session. Export `CANTINA_AUTH_TOKEN` and retry.";
5
+ const FINDING_WRITE_AUTH_ERROR = "Finding comments, feedback, and fix review scans require Apex authentication. Run `apex login` or use MCP `apex-auth-start` followed by `apex-auth-wait`, then retry.";
6
6
  function parsePositiveInt(value) {
7
7
  if (!value)
8
8
  return null;
@@ -32,11 +32,8 @@ function normalizeCantinaAuthToken(value) {
32
32
  }
33
33
  return token.length > 0 ? token : null;
34
34
  }
35
- export function requireCantinaAuthToken() {
35
+ export function getCantinaAuthToken() {
36
36
  const token = normalizeCantinaAuthToken(process.env[CANTINA_AUTH_TOKEN_ENV]);
37
- if (!token) {
38
- throw new Error(CANTINA_AUTH_TOKEN_ERROR);
39
- }
40
37
  return token;
41
38
  }
42
39
  export function isFindingUuid(value) {
@@ -63,22 +60,68 @@ function matchesFindingRef(finding, findingRef) {
63
60
  .some((value) => value.trim().toLowerCase() === normalizedRef);
64
61
  }
65
62
  async function requestFindingAppRoute(client, path, options = {}) {
66
- const token = requireCantinaAuthToken();
67
63
  const baseUrl = await client.getBaseUrl();
68
- const headers = new Headers(options.headers ?? {});
69
- // These routes still authenticate through the web app cookie, not Apex CLI bearer auth.
70
- headers.set("Cookie", `auth_token=${token}`);
71
- headers.set("Accept", "application/json");
72
- if (options.json !== undefined) {
73
- headers.set("Content-Type", "application/json");
74
- }
75
- const response = await fetch(new URL(path, baseUrl), {
76
- ...options,
77
- headers,
78
- body: options.json !== undefined ? JSON.stringify(options.json) : options.body,
79
- });
80
- const body = await parseWebRouteResponse(response);
64
+ const accessToken = await client.getAccessToken();
65
+ const legacyCookieToken = getCantinaAuthToken();
66
+ let attemptedBearerAuth = false;
67
+ if (!accessToken && !legacyCookieToken) {
68
+ throw new Error(FINDING_WRITE_AUTH_ERROR);
69
+ }
70
+ const send = async (auth) => {
71
+ const headers = new Headers(options.headers ?? {});
72
+ headers.set("Accept", "application/json");
73
+ if (options.json !== undefined) {
74
+ headers.set("Content-Type", "application/json");
75
+ }
76
+ if (auth.type === "bearer") {
77
+ headers.set("Authorization", `Bearer ${auth.token}`);
78
+ }
79
+ else {
80
+ headers.set("Cookie", `auth_token=${auth.token}`);
81
+ }
82
+ return fetch(new URL(path, baseUrl), {
83
+ ...options,
84
+ headers,
85
+ body: options.json !== undefined ? JSON.stringify(options.json) : options.body,
86
+ });
87
+ };
88
+ const sendAndParse = async (auth) => {
89
+ if (auth.type === "bearer") {
90
+ attemptedBearerAuth = true;
91
+ }
92
+ const response = await send(auth);
93
+ const body = await parseWebRouteResponse(response);
94
+ return { response, body };
95
+ };
96
+ const primaryAuth = accessToken
97
+ ? { type: "bearer", token: accessToken }
98
+ : { type: "cookie", token: legacyCookieToken };
99
+ let { response, body } = await sendAndParse(primaryAuth);
100
+ if (!response.ok && response.status === 401 && primaryAuth.type === "bearer") {
101
+ const refreshedToken = await client
102
+ .refreshSession()
103
+ .then((credentials) => credentials?.accessToken ?? null)
104
+ .catch(() => null);
105
+ if (refreshedToken) {
106
+ ({ response, body } = await sendAndParse({
107
+ type: "bearer",
108
+ token: refreshedToken,
109
+ }));
110
+ }
111
+ }
112
+ if (!response.ok && response.status === 401 && primaryAuth.type === "bearer" && legacyCookieToken) {
113
+ ({ response, body } = await sendAndParse({
114
+ type: "cookie",
115
+ token: legacyCookieToken,
116
+ }));
117
+ }
81
118
  if (!response.ok) {
119
+ if (response.status === 401) {
120
+ if (attemptedBearerAuth) {
121
+ await client.clearCredentials();
122
+ }
123
+ throw new Error(FINDING_WRITE_AUTH_ERROR);
124
+ }
82
125
  throw new ApiError(`Request failed with ${response.status}`, response.status, body);
83
126
  }
84
127
  return body;
package/dist/help.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export const CLI_HELP_TEXT = `Usage:
2
2
  apex Open the interactive Apex shell
3
- apex credits Show scan credits and audit scan entitlements for the active company
3
+ apex credits Show scan credits plus audit and fix review scan entitlements for the active company
4
4
  apex scan Create or resolve a workspace for this directory and start a scan
5
5
  apex scans List scans for the current workspace binding
6
6
  apex findings List findings for the latest or selected scan
@@ -16,7 +16,7 @@ export const CLI_HELP_TEXT = `Usage:
16
16
  apex workspace use <workspace-name|workspace-prefix|workspace-id>
17
17
  Bind this directory to an existing Apex workspace
18
18
  apex cancel-scan [scan-id] Cancel a running scan
19
- apex status Show the latest scan progress for this directory
19
+ apex status Show progress for the latest or selected scan
20
20
  apex doctor Validate auth, repos, connections, and workspace binding
21
21
  apex login Sign in to Apex
22
22
  apex logout Sign out locally
@@ -29,7 +29,7 @@ export const CLI_HELP_TEXT = `Usage:
29
29
  Flags:
30
30
  --company <id-or-handle> Choose the Apex company to use
31
31
  --workspace-name <name> Set the Apex workspace name for this directory
32
- --scan <scan-id> Select a specific scan for findings or export
32
+ --scan <scan-id> Select a specific scan for status, findings, or export
33
33
  --status valid|invalid Set the finding feedback status explicitly
34
34
  --content <markdown> Supply comment content for apex findings comment
35
35
  --comment <markdown> Supply rationale for apex findings feedback
@@ -61,7 +61,7 @@ Tips:
61
61
  PR scans require --mode pr and at least one --pr selection for a GitHub repository.
62
62
  audit is the current scan mode for audit scans; ultra remains accepted as a legacy alias.
63
63
  apex workspace use accepts a workspace name, prefix, or ID.
64
- Finding comments, feedback, and fix review scans currently require CANTINA_AUTH_TOKEN from a logged-in Cantina/Apex browser session.
64
+ Finding comments, feedback, and fix review scans use the same Apex login credentials as other CLI commands.
65
65
  Invalid finding feedback requires --dismissal-reason.
66
66
  Fix review scans require valid feedback with --label fixed and at least one --fix-pr-url, then apex findings fix-review.
67
67
  Finding identifiers such as KERN2-25 resolve against the selected scan; pass --scan or use the finding UUID directly when needed.
@@ -71,7 +71,7 @@ Tips:
71
71
  export const SHELL_HELP_TEXT = `Press Tab to autocomplete commands and common arguments.
72
72
 
73
73
  Commands:
74
- /credits Show scan credits and audit scan entitlements for the active company
74
+ /credits Show scan credits plus audit and fix review scan entitlements for the active company
75
75
  /scan [standard|audit] Start a new Apex scan for this workspace
76
76
  /scan pr <pr-number> Start a PR scan for this workspace
77
77
  /scans List scans for this workspace
@@ -87,7 +87,7 @@ Commands:
87
87
  /export [scan-id] Export findings for the latest or selected scan
88
88
  /workspaces List accessible workspaces for the active company
89
89
  /cancel-scan [scan-id] Cancel a running or most recent scan
90
- /status Show progress for the most recent scan
90
+ /status [scan-id] Show progress for the most recent or selected scan
91
91
  /doctor Validate auth, repos, connections, and workspace binding
92
92
  /update Update the local Apex CLI install and exit the shell
93
93
  /logout Sign out locally and exit the shell
@@ -107,7 +107,7 @@ Tips:
107
107
  PR scans require a GitHub PR number and provider-backed repository access.
108
108
  audit is the current scan mode for audit scans; ultra remains accepted as a legacy alias.
109
109
  /workspace use accepts a workspace name, prefix, or ID.
110
- /findings comment, /findings feedback, and /findings fix-review require CANTINA_AUTH_TOKEN in the shell environment.
110
+ /findings comment, /findings feedback, and /findings fix-review use the current Apex login.
111
111
  Invalid finding feedback requires a dismissal reason.
112
112
  Fix review scans require fixed valid feedback with a Fix PR URL. Use scripted CLI or MCP for attaching Fix PR URLs.
113
113
  Use scripted CLI flags for advanced feedback options such as suggested severity or dismissal reason.
package/dist/mcp.js CHANGED
@@ -22,6 +22,7 @@ Suggested workflow:
22
22
  Notes:
23
23
  - Pass cwd explicitly for repository-specific operations.
24
24
  - Tool calls are always non-interactive and never auto-open a browser.
25
+ - Finding write tools use the same Apex device-login credentials as read tools.
25
26
  - Use mode "audit" for audit scans; "ultra" remains accepted as a legacy alias. Use mode "pr" for GitHub pull request scans.
26
27
  - Doctor reports whether Apex will use remote materialization or a local snapshot upload for each source.`;
27
28
  const WORKSPACE_BINDING_ERROR = "This directory is not bound to an Apex workspace yet. Run `apex workspaces` to list workspace names, prefixes, and IDs, then bind one with `apex workspace use \"<workspace name>\"`, or run `apex scan` to create or resolve one automatically.";
@@ -270,7 +271,7 @@ function registerTools(server) {
270
271
  }, (value) => `Apex doctor completed for ${String(value.cwd)}.`));
271
272
  server.registerTool("apex-credits", {
272
273
  title: "Get Apex Credits",
273
- description: "Show scan credits and audit scan entitlements for the active or selected company.",
274
+ description: "Show scan credits plus audit and fix review scan entitlements for the active or selected company.",
274
275
  inputSchema: {
275
276
  cwd: z.string().optional(),
276
277
  company: z.string().optional(),
@@ -380,15 +381,16 @@ function registerTools(server) {
380
381
  }, (value) => `Started an Apex scan for ${String(value.cwd)}.`));
381
382
  server.registerTool("apex-status", {
382
383
  title: "Get Apex Scan Status",
383
- description: "Show progress for the most recent Apex scan in a directory.",
384
+ description: "Show progress for the most recent or selected Apex scan in a directory.",
384
385
  inputSchema: {
385
386
  cwd: z.string().optional(),
387
+ scanId: z.string().optional(),
386
388
  },
387
- }, async ({ cwd }) => runTool("apex-status", async () => {
389
+ }, async ({ cwd, scanId }) => runTool("apex-status", async () => {
388
390
  const client = new ApexApiClient();
389
391
  await requireAuthenticated(client);
390
392
  const targetCwd = resolveCwd(cwd);
391
- const payload = await commandStatus(client, targetCwd, buildFlags({}));
393
+ const payload = await commandStatus(client, targetCwd, buildFlags({ scanId }));
392
394
  return {
393
395
  cwd: targetCwd,
394
396
  ...payload,
@@ -450,7 +452,7 @@ function registerTools(server) {
450
452
  }, (value) => `Fetched Apex findings for ${String(value.cwd)}.`));
451
453
  server.registerTool("apex-finding-comment", {
452
454
  title: "Add Apex Finding Comment",
453
- description: "Add a comment or note to an Apex finding. Requires CANTINA_AUTH_TOKEN in the MCP server environment.",
455
+ description: "Add a comment or note to an Apex finding using the current Apex login.",
454
456
  inputSchema: {
455
457
  cwd: z.string().optional(),
456
458
  findingRef: z.string(),
@@ -471,7 +473,7 @@ function registerTools(server) {
471
473
  }, (value) => `Added a finding comment for ${String(value.findingRef)}.`));
472
474
  server.registerTool("apex-finding-feedback", {
473
475
  title: "Leave Apex Finding Feedback",
474
- description: "Leave valid or invalid feedback on an Apex finding. To attach a fix PR, send status valid with label fixed and fixPrUrls. Requires CANTINA_AUTH_TOKEN in the MCP server environment.",
476
+ description: "Leave valid or invalid feedback on an Apex finding using the current Apex login. To attach a fix PR, send status valid with label fixed and fixPrUrls.",
475
477
  inputSchema: {
476
478
  cwd: z.string().optional(),
477
479
  findingRef: z.string(),
@@ -509,7 +511,7 @@ function registerTools(server) {
509
511
  "finding"))} feedback for ${String(value.findingRef)}.`));
510
512
  server.registerTool("apex-finding-fix-review", {
511
513
  title: "Start Apex Finding Fix Review Scan",
512
- description: "Start a fix review scan for a finding after fixed feedback with one or more Fix PR URLs has been saved. Requires CANTINA_AUTH_TOKEN in the MCP server environment.",
514
+ description: "Start a fix review scan for a finding after fixed feedback with one or more Fix PR URLs has been saved.",
513
515
  inputSchema: {
514
516
  cwd: z.string().optional(),
515
517
  findingRef: z.string(),
package/dist/scan.js CHANGED
@@ -121,6 +121,33 @@ export function normalizeWorkspaceScans(payload) {
121
121
  export function isActiveScanStatus(status) {
122
122
  return status ? ACTIVE_SCAN_STATUSES.has(status.trim().toLowerCase()) : false;
123
123
  }
124
+ function mergeScanProgress(scan, progress) {
125
+ const status = readString(progress.progress?.status) ?? scan.status;
126
+ const startedAt = progress.progress?.startedAt ?? scan.startedAt;
127
+ const finishedAt = !isActiveScanStatus(status) && !scan.finishedAt
128
+ ? scan.updatedAt
129
+ : scan.finishedAt;
130
+ return {
131
+ ...scan,
132
+ kernelScanId: progress.kernelScanId ?? scan.kernelScanId,
133
+ status,
134
+ startedAt,
135
+ finishedAt,
136
+ };
137
+ }
138
+ async function reconcileActiveScanStatus(client, scan) {
139
+ if (!isActiveScanStatus(scan.status)) {
140
+ return scan;
141
+ }
142
+ let progress;
143
+ try {
144
+ progress = await fetchScanProgress(client, getScanDisplayId(scan));
145
+ }
146
+ catch {
147
+ return scan;
148
+ }
149
+ return mergeScanProgress(scan, progress);
150
+ }
124
151
  export function findMostRelevantActiveScan(scans) {
125
152
  return scans.find((scan) => isActiveScanStatus(scan.status)) ?? null;
126
153
  }
@@ -148,7 +175,11 @@ export async function fetchWorkspaceScans(client, workspaceId) {
148
175
  { path: `/api/workspaces/${encodedWorkspaceId}/scans` },
149
176
  { path: `/api/scans?workspaceId=${encodedWorkspaceId}` },
150
177
  ]);
151
- return normalizeWorkspaceScans(payload);
178
+ const scans = normalizeWorkspaceScans(payload);
179
+ return Promise.all(scans.map((scan) => reconcileActiveScanStatus(client, scan)));
180
+ }
181
+ export async function fetchScanProgress(client, scanId) {
182
+ return client.request(`/api/cli/v1/scans/${encodeURIComponent(scanId)}/progress`);
152
183
  }
153
184
  export async function cancelScan(client, scanId) {
154
185
  const encodedScanId = encodeURIComponent(scanId);
package/dist/shell.js CHANGED
@@ -208,7 +208,13 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
208
208
  };
209
209
  }
210
210
  case "status":
211
- await commandStatus(client, cwd, shellFlags);
211
+ if (parsed.args.length > 1) {
212
+ process.stderr.write("Usage: /status [scan-id]\n");
213
+ return {};
214
+ }
215
+ await commandStatus(client, cwd, parsed.args[0]
216
+ ? withFlag(shellFlags, "scan", parsed.args[0])
217
+ : shellFlags);
212
218
  return {};
213
219
  case "scans":
214
220
  await commandScans(client, cwd, shellFlags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cantinasecurity/apex-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.9",
4
4
  "description": "Standalone CLI and MCP server for Apex.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -24,7 +24,7 @@ Workflow:
24
24
  Guidelines:
25
25
 
26
26
  - Do not rely on interactive CLI prompts. The MCP tools are intentionally non-interactive.
27
- - `apex-credits` reports standard credits and audit scan entitlements when Bedrock returns both balances.
27
+ - `apex-credits` reports standard credits plus audit and fix review scan entitlements when Bedrock returns those balances.
28
28
  - `apex-scan` scans the provided `cwd` by default; pass `repoPaths` only when the user asks to scan explicit alternate roots.
29
29
  - `apex-doctor` reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
30
30
  - Apex can scan plain local directories and dirty git worktrees without provider connections by using local snapshot uploads.
@@ -33,8 +33,9 @@ Guidelines:
33
33
  - `apex-workspace-use` accepts a workspace name, prefix, or ID.
34
34
  - Use `sourceMode: "remote"` only when the user explicitly wants to forbid local snapshot fallbacks.
35
35
  - Use `force: true` on `apex-scan` only when the user explicitly wants to replace or overlap an active scan.
36
+ - When checking a scan that is not the workspace binding's latest scan, pass `scanId` to `apex-status`; use `apex-scans` first if you need to discover scan IDs.
36
37
  - Prefer `apex-findings` for quick inspection and `apex-export-findings` when the user needs a file artifact.
37
- - Finding comments, feedback, and fix review scan starts currently require `CANTINA_AUTH_TOKEN` in the MCP server environment because those writes go through the Cantina web-app routes instead of the Apex CLI bearer-token routes.
38
+ - Finding comments, feedback, and fix review scan starts use the same Apex device-login credentials as read tools. If a write tool reports missing auth, re-run `apex-auth-status` and complete `apex-auth-start` / `apex-auth-wait` instead of asking for browser cookies or auth tokens.
38
39
  - Invalid finding feedback requires `dismissalReason`; valid feedback can include `suggestedSeverity`, including `extreme`.
39
40
  - Fix PR callback feedback requires valid feedback with `labels: ["fixed"]` and `fixPrUrls`; start the fix review scan with `apex-finding-fix-review` after saving that feedback.
40
41
  - Finding identifiers such as `KERN2-25` resolve against the selected or latest scan for the current workspace binding. Pass an explicit scan when needed, or use the finding UUID directly.