@cantinasecurity/apex-cli 0.1.24 → 0.1.26

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,6 +28,9 @@ transport/connection failure or interrupted/missing response with no typed
28
28
  server error may you retry that exact same command once on the same machine,
29
29
  HOME, and repository root. Never retry a typed server rejection or make more
30
30
  than one recovery retry. Continue only for `redeemed` or `idempotent` outcomes.
31
+ Parse the JSON from stdout only; do not merge stderr into stdout, because npm
32
+ may write warnings to stderr. `idempotent` means the existing guest session
33
+ resumed and it may already own a workspace.
31
34
 
32
35
  Recommended workflow:
33
36
 
@@ -47,21 +50,22 @@ If the Apex MCP server is not configured, fall back to the local CLI:
47
50
 
48
51
  - Prefer scripted commands over the interactive shell.
49
52
  - A trial redeem with an ambiguous transport or response-loss failure may be retried exactly once with the same code, machine, HOME, and repository root. Never retry a typed server rejection or use `--replace` for response-loss recovery.
53
+ - Guest standard scans validate the complete local archive before resolving or reserving a workspace. For an unbound renamed checkout, Apex may recover only the sole empty guest workspace when no explicit workspace name was supplied.
50
54
  - 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.
51
55
  - `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.
52
56
  - 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.
53
57
  - 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.
54
58
  - Service-key management has no MCP tools. Never ask a user to paste a service key into a prompt, and never place one in an MCP call, tool result, source file, or log.
55
59
  - 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 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.
56
- - For a headless HTTP adapter, read the service key from a secret-backed environment variable, send it as a bearer token to `/api/apex/v1`, and redact it from errors and logs. Do not shell out to `apex` or launch MCP from the adapter.
57
- - Hosted API v1 requires the GitHub repository to be linked to an active Apex workspace. It never creates or mutates workspaces; use optional `workspaceId` to disambiguate matches and treat zero or ambiguous matches as HTTP `409` setup errors.
60
+ - For a headless HTTP adapter, read the service key from the secret-backed `APEX_SERVICE_KEY` environment variable, send it as a bearer token to `/api/apex/v1`, and redact it from errors and logs. A service key does not authenticate CLI or MCP commands; those use device login. Do not shell out to `apex` or launch MCP from the adapter.
61
+ - Hosted API v1 remote-repository scans require the GitHub repository to be linked to an active Apex workspace and do not create or mutate workspaces; use optional `workspaceId` to disambiguate matches and treat zero or ambiguous matches as HTTP `409` setup errors. Local or non-Git scans use the documented source-upload flow, which can create or reuse a workspace; send `generateThreatModel: true` for the first scan of a newly created local workspace.
58
62
  - Hosted 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 scan, use a new key.
59
63
  - Use `--non-interactive` and `--no-open` for automation-friendly CLI calls.
60
64
  - Use `--json` whenever structured output is helpful.
61
65
  - `apex credits` reports standard credits plus audit, fix review, and Lite scan entitlements when Bedrock returns those balances.
62
66
  - Work from the target repository directory so Apex can resolve `.apex/workspace.json`.
63
67
  - `apex scan` scans the current working directory by default; pass `--repo` only when the user asks to scan explicit alternate roots.
64
- - `apex-doctor` reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
68
+ - `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.
65
69
  - Plain local directories and dirty git worktrees can scan through local snapshot uploads without provider access.
66
70
  - 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 or GitLab sources.
67
71
  - 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.24"
9
+ "version": "0.1.26"
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.24"
17
+ "version": "0.1.26"
18
18
  },
19
19
  "description": "Run Apex security scans and review findings from Claude Code.",
20
- "version": "0.1.24",
20
+ "version": "0.1.26",
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.24",
3
+ "version": "0.1.26",
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.24",
3
+ "version": "0.1.26",
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.24",
8
+ "@cantinasecurity/apex-cli@0.1.26",
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.24",
7
+ "@cantinasecurity/apex-cli@0.1.26",
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.24 apex-mcp
16
+ npx -y -p @cantinasecurity/apex-cli@0.1.26 apex-mcp
17
17
  ```
18
18
 
19
19
  That keeps marketplace installs independent of a user's global `apex` install.
package/README.md CHANGED
@@ -135,6 +135,13 @@ is idempotent while the guest session remains active. A different active trial
135
135
  requires the explicit `--replace` flag, which revokes the outgoing guest
136
136
  session before redeeming the new code.
137
137
 
138
+ Guest standard scans validate the complete local archive before resolving or
139
+ reserving a workspace. `apex doctor` is read-only for guest sessions. For an
140
+ unbound renamed checkout, Apex may recover only the sole empty guest workspace
141
+ when no explicit workspace name was supplied; it will not silently switch an
142
+ explicit workspace selection or reuse a workspace that already contains scan
143
+ history.
144
+
138
145
  Logging out of an active guest trial, or discarding a proof-only trial stub,
139
146
  requires explicit confirmation (`apex logout --force` in non-interactive
140
147
  flows). Apex first attempts a proof-bound server revoke; an explicitly
@@ -145,30 +152,30 @@ unclaimed local trial proof so `apex claim` can still use it.
145
152
  Agent-safe copy/paste prompt (substitute the issued code):
146
153
 
147
154
  ```text
148
- Install and run Cantina Apex CLI 0.1.24 to start one free standard security scan of this repository immediately.
155
+ Install and run Cantina Apex CLI 0.1.26 to start one free standard security scan of this repository immediately.
149
156
 
150
157
  Use this trial code only where <TRIAL_CODE> appears below: SPBT-DEVREL-XXXXXXXX
151
158
 
152
159
  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`.
153
160
 
154
161
  Use this exact command prefix for every Apex command:
155
- npx -y -p @cantinasecurity/apex-cli@0.1.24 apex
162
+ npx -y -p @cantinasecurity/apex-cli@0.1.26 apex
156
163
 
157
164
  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.
158
165
  2. Redeem, with at most one response-loss recovery retry:
159
- npx -y -p @cantinasecurity/apex-cli@0.1.24 apex redeem <TRIAL_CODE> --json
160
- 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. Continue when the command exits 0 and stdout is JSON with `redeemed: true` and `outcome` equal to `redeemed` or `idempotent`. 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.
166
+ npx -y -p @cantinasecurity/apex-cli@0.1.26 apex redeem <TRIAL_CODE> --json
167
+ 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.
161
168
  3. Immediately start exactly one scan; do not wait for another instruction:
162
- npx -y -p @cantinasecurity/apex-cli@0.1.24 apex scan --mode standard --non-interactive --no-open --json
163
- 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.
169
+ npx -y -p @cantinasecurity/apex-cli@0.1.26 apex scan --mode standard --non-interactive --no-open --json
170
+ 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.
164
171
  4. About every 60 seconds, poll that exact scan:
165
- npx -y -p @cantinasecurity/apex-cli@0.1.24 apex status --scan <SCAN_ID> --json
172
+ npx -y -p @cantinasecurity/apex-cli@0.1.26 apex status --scan <SCAN_ID> --json
166
173
  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.
167
174
  5. Fetch that exact scan's findings:
168
- npx -y -p @cantinasecurity/apex-cli@0.1.24 apex findings --scan <SCAN_ID> --json
175
+ npx -y -p @cantinasecurity/apex-cli@0.1.26 apex findings --scan <SCAN_ID> --json
169
176
  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.
170
177
  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:
171
- npx -y -p @cantinasecurity/apex-cli@0.1.24 apex claim
178
+ npx -y -p @cantinasecurity/apex-cli@0.1.26 apex claim
172
179
  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.
173
180
 
174
181
  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.
@@ -263,6 +270,8 @@ Supported shell commands:
263
270
  - `apex connect github`
264
271
  - `apex connect gitlab`
265
272
 
273
+ `--workspace-ref` is scoped to `apex findings --workspace`. For `apex scans` or `apex export findings`, bind the intended workspace first with `apex workspace use <name|prefix|id>`; unsupported or unknown flags fail instead of falling back to the local binding.
274
+
266
275
  Helpful workspace flags:
267
276
 
268
277
  - `--company <id-or-handle>` to choose the Apex company when more than one is available
@@ -300,6 +309,8 @@ apex service-key revoke <key-id>
300
309
 
301
310
  Service-key management is intentionally available only through the scripted CLI. It is not exposed as an MCP tool or interactive-shell command, which keeps raw secrets out of model tool results and transcripts. The CLI requires an existing device-login bearer session, never initiates login from these commands, and never stores a created service key locally.
302
311
 
312
+ `APEX_SERVICE_KEY` authenticates the hosted REST API only; it does not authenticate CLI commands or replace `apex login`. When `apex doctor` finds a service key but no active device-login session, it reports that split before starting a login flow. `APEX_API_KEY` is also recognized for this diagnostic so older integrations receive migration guidance, but `APEX_SERVICE_KEY` is the documented REST variable.
313
+
303
314
  Use the service key as a bearer token with the hosted scan lifecycle:
304
315
 
305
316
  ```bash
@@ -322,7 +333,9 @@ curl \
322
333
  https://ai.cantina.xyz/api/apex/v1/scans/$SCAN_ID/findings
323
334
  ```
324
335
 
325
- In v1, the GitHub repository must already be linked to an active Apex workspace. The hosted API never creates, binds, or mutates workspaces. Apex selects the workspace automatically when exactly one active workspace matches the repository; pass the optional `workspaceId` to disambiguate multiple matches. Zero matches, or multiple matches without a valid `workspaceId`, return HTTP `409`.
336
+ For a remote GitHub scan, the repository must already be linked to an active Apex workspace. This remote-repository flow never creates, binds, or mutates workspaces. Apex selects the workspace automatically when exactly one active workspace matches the repository; pass the optional `workspaceId` to disambiguate multiple matches. Zero matches, or multiple matches without a valid `workspaceId`, return HTTP `409`.
337
+
338
+ For a local or non-Git checkout, use the service API's source-upload flow instead. It can create or reuse a workspace, uploads a `tar.gz` archive, and starts the scan with a `local_archive` source. A newly created local workspace has no threat model to reuse, so its first scan must send `generateThreatModel: true`. See the [Apex service API guide](https://ai.cantina.xyz/docs/apex-api) for the current request schemas and limits.
326
339
 
327
340
  The optional `paths` array is a set of best-effort focus hints, not a hard repository boundary. Apex may inspect or report code outside those paths. Version one does not yet provide strict path isolation.
328
341
 
@@ -569,7 +582,7 @@ Telemetry event posts do not include bearer tokens. The telemetry endpoint is `P
569
582
  {
570
583
  "schemaVersion": 1,
571
584
  "event": "apex.invocation.completed",
572
- "cliVersion": "0.1.24",
585
+ "cliVersion": "0.1.26",
573
586
  "invocation": {
574
587
  "surface": "mcp",
575
588
  "command": "apex-scan",
@@ -600,7 +613,7 @@ The npm package also includes marketplace-ready plugin artifacts:
600
613
  - `.claude-plugin/plugin.json` and `.mcp.claude.json` for Claude Code plugin installs
601
614
  - `.claude-plugin/marketplace.json` for a Claude marketplace entry backed by the public npm package
602
615
 
603
- These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.24 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
616
+ These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.26 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
604
617
 
605
618
  The repository also includes `.agents/plugins/marketplace.json` for local Codex marketplace testing from a checkout.
606
619
 
package/dist/apex.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ApexApiClient, formatApiError } from "./api-client.js";
2
- import { parseArgs } from "./args.js";
2
+ import { parseArgs, validateCommandFlags } from "./args.js";
3
3
  import { commandCancelScan, commandClaim, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandWorkspaceFindings, commandLogin, commandLogout, commandRedeem, commandScan, commandScans, commandServiceKeyCreate, commandServiceKeyList, commandServiceKeyRevoke, commandSetup, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, getRedeemExitCode, } from "./commands.js";
4
4
  import { CLI_HELP_TEXT } from "./help.js";
5
5
  import { runMcpServer } from "./mcp.js";
@@ -154,6 +154,7 @@ async function dispatch(parsed) {
154
154
  }
155
155
  async function main() {
156
156
  const parsed = parseArgs(process.argv.slice(2));
157
+ validateCommandFlags(parsed);
157
158
  const invocation = createCliTelemetryInvocation(parsed);
158
159
  emitInvocationStarted(invocation);
159
160
  await withTelemetryContext(invocation, async () => {
@@ -83,7 +83,7 @@ function getFriendlyApiErrorMessage(error) {
83
83
  case "GUEST_MODE_UNSUPPORTED":
84
84
  return "Guest trials support standard scans only. Run `apex claim` before starting another scan mode.";
85
85
  case "GUEST_WORKSPACE_LIMIT":
86
- return "This guest trial has reached its workspace limit. Run `apex claim` to continue with your account.";
86
+ return "This guest trial already has a workspace. Run `apex workspaces` and `apex workspace use <workspace>` to reuse it, or run `apex claim` to continue with your Cantina account.";
87
87
  case "GUEST_CLAIM_REQUIRED":
88
88
  return "This action requires an account. Run `apex claim`, then retry it.";
89
89
  case "CLAIM_EXPIRED":
package/dist/args.js CHANGED
@@ -1,3 +1,39 @@
1
+ export const KNOWN_CLI_FLAG_NAMES = [
2
+ "comment",
3
+ "company",
4
+ "content",
5
+ "dismissal-reason",
6
+ "expires-at",
7
+ "fix-pr-url",
8
+ "force",
9
+ "format",
10
+ "filter",
11
+ "finding-state",
12
+ "help",
13
+ "json",
14
+ "label",
15
+ "limit",
16
+ "mode",
17
+ "name",
18
+ "no-open",
19
+ "non-interactive",
20
+ "output",
21
+ "parent-comment",
22
+ "pr",
23
+ "pr-path",
24
+ "replace",
25
+ "repo",
26
+ "review-target",
27
+ "scan",
28
+ "scope",
29
+ "source-mode",
30
+ "status",
31
+ "suggested-severity",
32
+ "workspace-name",
33
+ "workspace-ref",
34
+ "workspace",
35
+ ];
36
+ const KNOWN_CLI_FLAGS = new Set(KNOWN_CLI_FLAG_NAMES);
1
37
  const BOOLEAN_FLAGS = new Set([
2
38
  "force",
3
39
  "non-interactive",
@@ -25,6 +61,9 @@ export function parseArgs(argv) {
25
61
  continue;
26
62
  }
27
63
  const key = value.slice(2);
64
+ if (!KNOWN_CLI_FLAGS.has(key)) {
65
+ throw new Error(`Unknown flag: --${key}`);
66
+ }
28
67
  if (BOOLEAN_FLAGS.has(key)) {
29
68
  flags[key] = true;
30
69
  continue;
@@ -50,6 +89,17 @@ export function parseArgs(argv) {
50
89
  flags,
51
90
  };
52
91
  }
92
+ export function validateCommandFlags(parsed) {
93
+ if (parsed.flags["workspace-ref"] === undefined) {
94
+ return;
95
+ }
96
+ const isWorkspaceFindings = parsed.command === "findings" &&
97
+ parsed.subcommand === null &&
98
+ parsed.flags.workspace === true;
99
+ if (!isWorkspaceFindings) {
100
+ throw new Error("--workspace-ref is only supported with `apex findings --workspace`. To change the local workspace used by scans or exports, run `apex workspace use <workspace-ref>` first.");
101
+ }
102
+ }
53
103
  export function isJsonMode(flags) {
54
104
  return flags.json === true;
55
105
  }
package/dist/commands.js CHANGED
@@ -5,13 +5,14 @@ import { openInBrowser } from "./browser.js";
5
5
  import { getClaimProofState, isClaimProofAvailable, parseClaimDeadlineMs, } from "./claim-proof.js";
6
6
  import { loadConfig, updateConfig } from "./config.js";
7
7
  import { createFindingComment, fetchScanExport, fetchScanFindings, fetchWorkspaceFindings, isFindingUuid, normalizeFindingRef, resolveFindingSelection, resolveScanSelection, startFindingFixReviewScan, submitFindingFeedback, } from "./findings.js";
8
- import { prepareExplicitScanSources, supportsLegacyRemoteFlow, } from "./local-source-scan.js";
8
+ import { prepareLocalSourceArchives, prepareExplicitScanSources, supportsLegacyRemoteFlow, } from "./local-source-scan.js";
9
9
  import { logLine, printJson, withLoadingIndicator } from "./output.js";
10
10
  import { claimOnboardingSession, isProofOnlyOnboardingCredentials, redeemOnboardingCode, revokeGuestOnboardingSession, } from "./onboarding.js";
11
11
  import { confirm } from "./prompt.js";
12
12
  import { cancelScan, fetchScanProgress, fetchWorkspaceScans, findMostRelevantActiveScan, getScanDisplayLabel, getScanDisplayId, getTrackedScanId, isActiveScanStatus, matchesScanId, selectDefaultScanToCancel, } from "./scan.js";
13
13
  import { createServiceApiKey, isServiceApiScope, listServiceApiKeys, revokeServiceApiKey, SERVICE_API_SCOPES, } from "./service-keys.js";
14
- import { chooseCompany, createWorkspaceBinding, ensureAuthenticated, remediateMissingConnections, resolveWorkspaceSelection, resolveWorkspaceSelectionLegacy, resolveWorkspaceAllowingMissingConnections, selectWorkspaceTarget, } from "./session.js";
14
+ import { getServiceKeyCliAuthGuidance } from "./service-key-environment.js";
15
+ import { chooseCompany, createWorkspaceBinding, ensureAuthenticated, planGuestWorkspaceSelection, remediateMissingConnections, resolveWorkspaceSelection, resolveWorkspaceSelectionLegacy, resolveWorkspaceAllowingMissingConnections, selectWorkspaceTarget, } from "./session.js";
15
16
  import { createWorkspaceBindingFromSummary, fetchCompanyCredits, fetchCompanyWorkspaces, findWorkspaceByRef, } from "./workspaces.js";
16
17
  import { loadWorkspaceBinding, saveWorkspaceBinding } from "./workspace-binding.js";
17
18
  import { commandSetup as runCliSetup } from "./setup.js";
@@ -116,6 +117,47 @@ async function getCommandAuthState(client, me) {
116
117
  : null,
117
118
  };
118
119
  }
120
+ async function resolveWorkspaceSelectionForScan(args) {
121
+ try {
122
+ return {
123
+ selection: args.selection,
124
+ result: await resolveWorkspaceSelection(args.client, args.selection),
125
+ };
126
+ }
127
+ catch (error) {
128
+ if (args.authKind !== "guest" ||
129
+ args.selection.workspaceNameWasExplicitlyChosen ||
130
+ args.selection.binding ||
131
+ getApiErrorCode(error) !== "GUEST_WORKSPACE_LIMIT") {
132
+ throw error;
133
+ }
134
+ let response;
135
+ try {
136
+ response = await fetchCompanyWorkspaces(args.client, args.selection.company.id);
137
+ }
138
+ catch {
139
+ throw error;
140
+ }
141
+ const recoverable = response.workspaces.filter((workspace) => workspace.scanCount === 0 && workspace.findingsCount === 0);
142
+ if (recoverable.length !== 1) {
143
+ throw error;
144
+ }
145
+ const workspace = recoverable[0];
146
+ const recoveredSelection = {
147
+ ...args.selection,
148
+ workspaceName: workspace.name,
149
+ binding: createWorkspaceBindingFromSummary({
150
+ companyId: args.selection.company.id,
151
+ workspace,
152
+ currentBinding: args.selection.binding,
153
+ }),
154
+ };
155
+ return {
156
+ selection: recoveredSelection,
157
+ result: await resolveWorkspaceSelection(args.client, recoveredSelection),
158
+ };
159
+ }
160
+ }
119
161
  function logGuestScanFooter(authState, flags) {
120
162
  if (isJsonMode(flags) || !authState.guestCredentials)
121
163
  return;
@@ -944,9 +986,22 @@ export async function commandTelemetry(flags, target) {
944
986
  return payload;
945
987
  }
946
988
  export async function commandDoctor(client, cwd, flags) {
947
- const me = await ensureAuthenticated(client, flags);
948
989
  try {
949
- const result = await withLoadingIndicator("Inspecting this directory and checking Apex setup...", flags, () => resolveWorkspaceAllowingMissingConnections(client, cwd, me, flags));
990
+ const serviceKeyGuidance = getServiceKeyCliAuthGuidance();
991
+ const existingMe = serviceKeyGuidance ? await getMe(client) : null;
992
+ if (serviceKeyGuidance && !existingMe) {
993
+ throw new Error(serviceKeyGuidance);
994
+ }
995
+ const me = existingMe ?? (await ensureAuthenticated(client, flags));
996
+ const authState = await getCommandAuthState(client, me);
997
+ setTelemetryAuthKind(authState.authKind);
998
+ const result = await withLoadingIndicator("Inspecting this directory and checking Apex setup...", flags, async () => {
999
+ if (authState.authKind === "guest") {
1000
+ const selection = await selectWorkspaceTarget(cwd, me, flags);
1001
+ return planGuestWorkspaceSelection(selection);
1002
+ }
1003
+ return resolveWorkspaceAllowingMissingConnections(client, cwd, me, flags);
1004
+ });
950
1005
  const localSnapshotCount = result.resolve.plannedSources.filter((source) => source.sourceKind === "local_archive").length;
951
1006
  const payload = {
952
1007
  authenticatedAs: me.user.username ?? me.user.id,
@@ -1040,14 +1095,42 @@ export async function commandScan(client, cwd, flags) {
1040
1095
  const me = await ensureAuthenticated(client, flags);
1041
1096
  const authState = await getCommandAuthState(client, me);
1042
1097
  setTelemetryAuthKind(authState.authKind);
1043
- const selection = await selectWorkspaceTarget(cwd, me, flags);
1044
- const result = await withLoadingIndicator("Resolving workspace and scan plan...", flags, () => resolveWorkspaceSelection(client, selection));
1045
- if (!result.resolve.workspaceId) {
1046
- throw new Error("Workspace resolution did not return a workspaceId.");
1098
+ let selection = await selectWorkspaceTarget(cwd, me, flags);
1099
+ let preparedArchives = null;
1100
+ if (authState.authKind === "guest" && requestedMode === "standard") {
1101
+ preparedArchives = await withLoadingIndicator("Preparing local source archive...", flags, () => prepareLocalSourceArchives({
1102
+ client,
1103
+ detectedSources: selection.sources,
1104
+ }));
1047
1105
  }
1048
- const resolvedWorkspaceId = result.resolve.workspaceId;
1049
- const activeScan = await withLoadingIndicator("Checking for existing scans...", flags, () => findActiveWorkspaceScan(client, resolvedWorkspaceId, result.binding));
1050
- const forceRestart = await ensureScanRestartConfirmed(flags, activeScan);
1106
+ const resolvedContext = await (async () => {
1107
+ try {
1108
+ const resolved = await withLoadingIndicator("Resolving workspace and scan plan...", flags, () => resolveWorkspaceSelectionForScan({
1109
+ client,
1110
+ selection,
1111
+ authKind: authState.authKind,
1112
+ }));
1113
+ const result = resolved.result;
1114
+ if (!result.resolve.workspaceId) {
1115
+ throw new Error("Workspace resolution did not return a workspaceId.");
1116
+ }
1117
+ const resolvedWorkspaceId = result.resolve.workspaceId;
1118
+ const activeScan = await withLoadingIndicator("Checking for existing scans...", flags, () => findActiveWorkspaceScan(client, resolvedWorkspaceId, result.binding));
1119
+ const forceRestart = await ensureScanRestartConfirmed(flags, activeScan);
1120
+ return {
1121
+ selection: resolved.selection,
1122
+ result,
1123
+ resolvedWorkspaceId,
1124
+ forceRestart,
1125
+ };
1126
+ }
1127
+ catch (error) {
1128
+ await preparedArchives?.cleanup();
1129
+ throw error;
1130
+ }
1131
+ })();
1132
+ selection = resolvedContext.selection;
1133
+ const { result, resolvedWorkspaceId, forceRestart } = resolvedContext;
1051
1134
  let workspaceId = resolvedWorkspaceId;
1052
1135
  let scan;
1053
1136
  let prScanSource = null;
@@ -1105,22 +1188,28 @@ export async function commandScan(client, cwd, flags) {
1105
1188
  }));
1106
1189
  }
1107
1190
  else {
1108
- scan = await withLoadingIndicator("Starting scan...", flags, async () => {
1109
- const scanSources = await prepareExplicitScanSources({
1110
- client,
1111
- workspaceId: resolvedWorkspaceId,
1112
- plannedSources: result.resolve.plannedSources,
1113
- detectedSources: result.sources,
1114
- });
1115
- return client.request(`/api/cli/v2/local-workspaces/${encodeURIComponent(resolvedWorkspaceId)}/scan`, {
1116
- method: "POST",
1117
- json: {
1118
- mode: requestedMode,
1119
- force: forceRestart || flags.force === true,
1120
- scanSources,
1121
- },
1191
+ try {
1192
+ scan = await withLoadingIndicator("Starting scan...", flags, async () => {
1193
+ const scanSources = await prepareExplicitScanSources({
1194
+ client,
1195
+ workspaceId: resolvedWorkspaceId,
1196
+ plannedSources: result.resolve.plannedSources,
1197
+ detectedSources: result.sources,
1198
+ preparedArchives,
1199
+ });
1200
+ return client.request(`/api/cli/v2/local-workspaces/${encodeURIComponent(resolvedWorkspaceId)}/scan`, {
1201
+ method: "POST",
1202
+ json: {
1203
+ mode: requestedMode,
1204
+ force: forceRestart || flags.force === true,
1205
+ scanSources,
1206
+ },
1207
+ });
1122
1208
  });
1123
- });
1209
+ }
1210
+ finally {
1211
+ await preparedArchives?.cleanup();
1212
+ }
1124
1213
  }
1125
1214
  const trackedScanId = getTrackedScanId(scan);
1126
1215
  const binding = {
package/dist/help.js CHANGED
@@ -43,7 +43,7 @@ Flags:
43
43
  --workspace-name <name> Set the Apex workspace name for this directory
44
44
  --scan <scan-id> Select a specific scan for status, findings, or export
45
45
  --workspace Use the cross-scan workspace findings view
46
- --workspace-ref <name|prefix|id> Select a workspace without changing the local binding
46
+ --workspace-ref <name|prefix|id> Select a workspace for apex findings --workspace only
47
47
  --finding-state all|open Select all findings or only findings needing review
48
48
  --filter <[!]facet:value> Filter workspace findings; repeat for multiple filters
49
49
  --status valid|invalid Set the finding feedback status explicitly
@@ -85,9 +85,12 @@ Tips:
85
85
  Trial codes go only to apex redeem. Never echo, log, save, telemeter, or send them through MCP.
86
86
  Trial redemption, claim, and revocation require https://ai.cantina.xyz and reject other APEX_BASE_URL origins before sending secrets.
87
87
  apex redeem never starts device login or opens a browser; apex claim is the only onboarding command that may open one.
88
+ Guest standard scans validate the local archive before reserving a workspace; apex doctor is read-only for guests.
89
+ An unbound renamed checkout may recover only the sole empty guest workspace unless you pass --workspace-name.
88
90
  After a guest scan, run apex claim on the same machine within the displayed claim deadline to keep the results.
89
91
  Claim moves trial results into the selected Cantina workspace when you can run scans there, or into your existing personal workspace.
90
92
  Service-key commands require an existing login; run apex login first when needed.
93
+ APEX_SERVICE_KEY authenticates the hosted REST API only; it does not replace apex login for CLI commands.
91
94
  Apex automatically selects your company when only one is available. With multiple companies, choose one interactively or pass --company.
92
95
  Service-key creation also requires --name and reveals the secret only once; omit --scope to grant all hosted API scopes.
93
96
  Service-key management is available only in the scripted CLI, not the interactive shell or MCP server.
@@ -95,6 +98,7 @@ Tips:
95
98
  Fix review scans use saved or supplied Fix PR URLs when present; otherwise they review the current repository when Bedrock has original scan repository context.
96
99
  Finding identifiers such as KERN2-25 resolve against the selected scan; pass --scan or use the finding UUID directly when needed.
97
100
  Use apex findings --workspace --company <handle> --workspace-ref <ref> --finding-state open to retrieve findings needing review without changing the local binding.
101
+ To run scans or exports against a different workspace, bind it first with apex workspace use <ref>.
98
102
  Open excludes fixed, invalid, dismissed, and duplicate findings. It is a finding state, not a status filter.
99
103
  Workspace filters match the web UI facets: severity, scan, repo, review, impact, likelihood, status, validation, visibility, duplicate, and source.
100
104
  Anonymous usage telemetry helps Apex understand CLI, MCP, Codex, Claude, and Copilot usage. Disable it with apex telemetry disable or APEX_TELEMETRY_DISABLED=1.
@@ -317,7 +317,6 @@ async function createDeterministicArchive(params) {
317
317
  file: params.archivePath,
318
318
  gzip: true,
319
319
  portable: true,
320
- noPax: true,
321
320
  mtime: FIXED_MTIME,
322
321
  follow: false,
323
322
  preservePaths: false,
@@ -343,6 +342,50 @@ async function createDeterministicArchive(params) {
343
342
  entryCount: observedArchive.entryCount,
344
343
  };
345
344
  }
345
+ export async function prepareLocalSourceArchives(args) {
346
+ const capabilities = args.capabilities ?? (await fetchLocalSourceScanCapabilities(args.client));
347
+ if (!capabilities.localSourceScans) {
348
+ throw new Error("This Apex server does not support local source scans.");
349
+ }
350
+ if (args.detectedSources.length > capabilities.archive.maxSourcesPerScan) {
351
+ throw new Error(`This scan includes ${args.detectedSources.length} sources, but the server limit is ${capabilities.archive.maxSourcesPerScan}.`);
352
+ }
353
+ const tempDir = await mkdtemp(path.join(tmpdir(), "apex-local-source-"));
354
+ let cleaned = false;
355
+ const cleanup = async () => {
356
+ if (cleaned)
357
+ return;
358
+ cleaned = true;
359
+ await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
360
+ };
361
+ try {
362
+ const archivesByPath = new Map();
363
+ for (const [index, source] of args.detectedSources.entries()) {
364
+ const archivePath = path.join(tempDir, `source-${index + 1}.tar.gz`);
365
+ const manifest = await createDeterministicArchive({
366
+ source,
367
+ archivePath,
368
+ maxUncompressedBytes: capabilities.archive.maxUncompressedBytes,
369
+ });
370
+ if (manifest.compressedBytes > capabilities.archive.maxCompressedBytes) {
371
+ throw new Error(`Archive for ${source.displayName} exceeds the compressed upload limit.`);
372
+ }
373
+ if (manifest.uncompressedBytes > capabilities.archive.maxUncompressedBytes) {
374
+ throw new Error(`Archive for ${source.displayName} exceeds the uncompressed upload limit.`);
375
+ }
376
+ archivesByPath.set(source.path, { archivePath, manifest });
377
+ }
378
+ return {
379
+ capabilities,
380
+ archivesByPath,
381
+ cleanup,
382
+ };
383
+ }
384
+ catch (error) {
385
+ await cleanup();
386
+ throw error;
387
+ }
388
+ }
346
389
  export async function fetchLocalSourceScanCapabilities(client) {
347
390
  return client.request("/api/cli/v2/capabilities");
348
391
  }
@@ -369,7 +412,9 @@ async function uploadArchive(archivePath, uploadUrl, headers) {
369
412
  }
370
413
  }
371
414
  export async function prepareExplicitScanSources(args) {
372
- const capabilities = await fetchLocalSourceScanCapabilities(args.client);
415
+ let preparation = args.preparedArchives ?? null;
416
+ let ownsPreparation = false;
417
+ const capabilities = preparation?.capabilities ?? (await fetchLocalSourceScanCapabilities(args.client));
373
418
  const localSources = args.plannedSources.filter((source) => source.sourceKind === "local_archive");
374
419
  if (args.plannedSources.length > capabilities.archive.maxSourcesPerScan) {
375
420
  throw new Error(`This scan includes ${args.plannedSources.length} sources, but the server limit is ${capabilities.archive.maxSourcesPerScan}.`);
@@ -386,37 +431,32 @@ export async function prepareExplicitScanSources(args) {
386
431
  git: source.git,
387
432
  });
388
433
  }
389
- const detectedByPath = new Map(args.detectedSources.map((source) => [source.path, source]));
390
- const tempDir = await mkdtemp(path.join(tmpdir(), "apex-local-source-"));
391
434
  try {
435
+ if (!preparation) {
436
+ const localPaths = new Set(localSources.map((source) => source.path));
437
+ preparation = await prepareLocalSourceArchives({
438
+ client: args.client,
439
+ detectedSources: args.detectedSources.filter((source) => localPaths.has(source.path)),
440
+ capabilities,
441
+ });
442
+ ownsPreparation = true;
443
+ }
392
444
  const preparedUploads = [];
393
- for (const [index, source] of localSources.entries()) {
394
- const detectedSource = detectedByPath.get(source.path);
395
- if (!detectedSource) {
445
+ for (const source of localSources) {
446
+ const preparedArchive = preparation.archivesByPath.get(source.path);
447
+ if (!preparedArchive) {
396
448
  throw new Error(`Could not resolve local source path: ${source.path}`);
397
449
  }
398
- const archivePath = path.join(tempDir, `source-${index + 1}.tar.gz`);
399
- const manifest = await createDeterministicArchive({
400
- source: detectedSource,
401
- archivePath,
402
- maxUncompressedBytes: capabilities.archive.maxUncompressedBytes,
403
- });
404
- if (manifest.compressedBytes > capabilities.archive.maxCompressedBytes) {
405
- throw new Error(`Archive for ${source.displayName} exceeds the compressed upload limit.`);
406
- }
407
- if (manifest.uncompressedBytes > capabilities.archive.maxUncompressedBytes) {
408
- throw new Error(`Archive for ${source.displayName} exceeds the uncompressed upload limit.`);
409
- }
410
450
  preparedUploads.push({
411
451
  plannedSource: source,
412
- archivePath,
452
+ archivePath: preparedArchive.archivePath,
413
453
  upload: {
414
454
  displayName: source.displayName,
415
455
  relativePath: source.relativePath,
416
456
  archiveFormat: "tar.gz",
417
- sha256: manifest.sha256,
418
- compressedBytes: manifest.compressedBytes,
419
- uncompressedBytes: manifest.uncompressedBytes,
457
+ sha256: preparedArchive.manifest.sha256,
458
+ compressedBytes: preparedArchive.manifest.compressedBytes,
459
+ uncompressedBytes: preparedArchive.manifest.uncompressedBytes,
420
460
  git: source.git,
421
461
  },
422
462
  });
@@ -475,6 +515,8 @@ export async function prepareExplicitScanSources(args) {
475
515
  });
476
516
  }
477
517
  finally {
478
- await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
518
+ if (ownsPreparation) {
519
+ await preparation?.cleanup();
520
+ }
479
521
  }
480
522
  }
package/dist/mcp.js CHANGED
@@ -31,7 +31,9 @@ Notes:
31
31
  - 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 an MCP prompt or tool call.
32
32
  - HTTP-only serverless and cron integrations should use the hosted API documented at https://ai.cantina.xyz/docs/apex-api instead of launching this stdio server.
33
33
  - Use mode "audit" for audit scans; "ultra" remains accepted as a legacy alias. Use mode "lite" only for beta-enabled provider-backed Lite scans, and mode "pr" for GitHub pull request scans.
34
- - Doctor reports whether Apex will use remote materialization or a local snapshot upload for each source.`;
34
+ - Doctor is read-only for guest sessions and reports the local snapshot plan without reserving a workspace.
35
+ - Guest standard scans validate the complete local archive before resolving or reserving a workspace. For an unbound renamed checkout, Apex may recover only the sole empty guest workspace when no explicit workspace name was supplied.
36
+ - When terminal redemption returns JSON, parse stdout only and keep stderr warnings separate. An idempotent outcome resumes the existing guest session, which may already own a workspace.`;
35
37
  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.";
36
38
  const MISSING_PROVIDER_CONNECTIONS_ERROR = "Missing provider connections. Re-run interactively or pre-connect providers.";
37
39
  const PR_MISSING_PROVIDER_CONNECTIONS_ERROR_PREFIX = "PR scans require connected provider access for ";
@@ -0,0 +1,22 @@
1
+ const APEX_SERVICE_KEY_PREFIX = "apex_sk_";
2
+ export function detectApexServiceKeyEnvironment(env = process.env) {
3
+ for (const variableName of ["APEX_SERVICE_KEY", "APEX_API_KEY"]) {
4
+ const value = env[variableName]?.trim();
5
+ if (value?.startsWith(APEX_SERVICE_KEY_PREFIX)) {
6
+ return variableName;
7
+ }
8
+ }
9
+ return null;
10
+ }
11
+ export function getServiceKeyCliAuthGuidance(env = process.env) {
12
+ const variableName = detectApexServiceKeyEnvironment(env);
13
+ if (!variableName) {
14
+ return null;
15
+ }
16
+ const legacyVariableGuidance = variableName === "APEX_API_KEY"
17
+ ? " For REST API examples, rename APEX_API_KEY to APEX_SERVICE_KEY."
18
+ : "";
19
+ return (`Apex service key detected in ${variableName}, but service keys authenticate the hosted REST API, not the Apex CLI. ` +
20
+ "Use `apex login` for CLI device authentication, or use the REST API for a headless/service-account integration: https://ai.cantina.xyz/docs/apex-api." +
21
+ legacyVariableGuidance);
22
+ }
package/dist/session.js CHANGED
@@ -61,17 +61,31 @@ export async function chooseCompany(me, flags, binding, options = {}) {
61
61
  async function chooseWorkspaceName(cwd, flags, binding) {
62
62
  const explicit = getFlagString(flags, "workspace-name");
63
63
  if (explicit) {
64
- return explicit;
64
+ return {
65
+ workspaceName: explicit,
66
+ wasExplicitlyChosen: true,
67
+ };
65
68
  }
66
69
  if (binding?.workspaceName) {
67
- return binding.workspaceName;
70
+ return {
71
+ workspaceName: binding.workspaceName,
72
+ wasExplicitlyChosen: false,
73
+ };
68
74
  }
69
75
  const suggested = path.basename(cwd);
70
76
  if (!canPromptInteractively(flags)) {
71
- return suggested;
77
+ return {
78
+ workspaceName: suggested,
79
+ wasExplicitlyChosen: false,
80
+ };
72
81
  }
73
82
  const chosen = await promptText("Workspace name to use in Apex for this directory (press Enter to use the current folder name)", suggested);
74
- return chosen.trim().length > 0 ? chosen.trim() : suggested;
83
+ return {
84
+ workspaceName: chosen.trim().length > 0 ? chosen.trim() : suggested,
85
+ // Accepting a value through the prompt is an explicit choice even when the
86
+ // user keeps the suggested folder name.
87
+ wasExplicitlyChosen: true,
88
+ };
75
89
  }
76
90
  function selectBindingForWorkspaceName(flags, binding, workspaceName) {
77
91
  const explicit = getFlagString(flags, "workspace-name");
@@ -132,6 +146,23 @@ function ensurePlannedSources(selection, resolve) {
132
146
  plannedSources: selection.sources.map((source) => buildFallbackLocalArchivePlan(source, selection.sourceMode)),
133
147
  };
134
148
  }
149
+ export function planGuestWorkspaceSelection(selection) {
150
+ if (selection.sourceMode === "remote") {
151
+ throw new Error("Guest trials require local snapshot uploads. Remove `--source-mode remote` and retry.");
152
+ }
153
+ return {
154
+ company: selection.company,
155
+ workspaceName: selection.workspaceName,
156
+ binding: selection.binding,
157
+ sources: selection.sources,
158
+ legacyRepositories: selection.legacyRepositories,
159
+ resolve: {
160
+ workspaceId: selection.binding?.workspaceId ?? null,
161
+ workspaceCreated: false,
162
+ plannedSources: selection.sources.map((source) => buildFallbackLocalArchivePlan(source, selection.sourceMode)),
163
+ },
164
+ };
165
+ }
135
166
  export async function ensureAuthenticated(client, flags) {
136
167
  const existing = await getMe(client);
137
168
  if (existing) {
@@ -153,7 +184,8 @@ export async function resolveWorkspace(client, cwd, me, flags) {
153
184
  export async function selectWorkspaceTarget(cwd, me, flags) {
154
185
  const loadedBinding = await loadWorkspaceBinding(cwd);
155
186
  const company = await chooseCompany(me, flags, loadedBinding);
156
- const workspaceName = await chooseWorkspaceName(cwd, flags, loadedBinding);
187
+ const workspaceNameSelection = await chooseWorkspaceName(cwd, flags, loadedBinding);
188
+ const workspaceName = workspaceNameSelection.workspaceName;
157
189
  const binding = selectBindingForWorkspaceName(flags, loadedBinding, workspaceName);
158
190
  const sourceSelection = getFlagList(flags, "repo");
159
191
  const discovered = await discoverSources(cwd, sourceSelection);
@@ -163,6 +195,7 @@ export async function selectWorkspaceTarget(cwd, me, flags) {
163
195
  return {
164
196
  company,
165
197
  workspaceName,
198
+ workspaceNameWasExplicitlyChosen: workspaceNameSelection.wasExplicitlyChosen,
166
199
  binding,
167
200
  sources: discovered.sources,
168
201
  legacyRepositories: extractLegacyRepositories(discovered.sources),
package/dist/telemetry.js CHANGED
@@ -2,6 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import path from "node:path";
5
+ import { KNOWN_CLI_FLAG_NAMES, } from "./args.js";
5
6
  import { loadConfig, updateConfig } from "./config.js";
6
7
  import { containsSensitiveText } from "./redaction.js";
7
8
  import { APEX_CLI_VERSION } from "./version.js";
@@ -9,41 +10,6 @@ const TELEMETRY_ENDPOINT_PATH = "/api/cli/v1/telemetry/events";
9
10
  const TELEMETRY_SESSION_ID = randomUUID();
10
11
  const TELEMETRY_TIMEOUT_MS = 750;
11
12
  const telemetryContext = new AsyncLocalStorage();
12
- const KNOWN_CLI_FLAG_NAMES = [
13
- "comment",
14
- "company",
15
- "content",
16
- "dismissal-reason",
17
- "expires-at",
18
- "fix-pr-url",
19
- "force",
20
- "format",
21
- "filter",
22
- "finding-state",
23
- "help",
24
- "json",
25
- "label",
26
- "limit",
27
- "mode",
28
- "name",
29
- "no-open",
30
- "non-interactive",
31
- "output",
32
- "parent-comment",
33
- "pr",
34
- "pr-path",
35
- "replace",
36
- "repo",
37
- "review-target",
38
- "scan",
39
- "scope",
40
- "source-mode",
41
- "status",
42
- "suggested-severity",
43
- "workspace-name",
44
- "workspace-ref",
45
- "workspace",
46
- ];
47
13
  const KNOWN_CLI_COMMANDS = [
48
14
  "cancel-scan",
49
15
  "connect",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cantinasecurity/apex-cli",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Standalone CLI and MCP server for Apex.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -27,7 +27,9 @@ than one recovery retry. Treat the code as a secret: pass it only to
27
27
  or tool result. Trial redemption, claim, and revocation are hosted-only; stop
28
28
  before passing the code when `APEX_BASE_URL` has any origin other than
29
29
  `https://ai.cantina.xyz`. Stop if the redeem outcome is not `redeemed` or
30
- `idempotent`.
30
+ `idempotent`. Parse the JSON from stdout only; do not merge stderr into stdout,
31
+ because npm may write warnings to stderr. `idempotent` means the existing guest
32
+ session resumed and it may already own a workspace.
31
33
 
32
34
  Workflow:
33
35
 
@@ -47,6 +49,7 @@ If the Apex MCP server is not configured, fall back to the local CLI:
47
49
 
48
50
  - Prefer scripted commands over the interactive shell.
49
51
  - A trial redeem with an ambiguous transport or response-loss failure may be retried exactly once with the same code, machine, HOME, and repository root. Never retry a typed server rejection or use `--replace` for response-loss recovery.
52
+ - Guest standard scans validate the complete local archive before resolving or reserving a workspace. For an unbound renamed checkout, Apex may recover only the sole empty guest workspace when no explicit workspace name was supplied.
50
53
  - 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.
51
54
  - `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.
52
55
  - 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.
@@ -59,12 +62,12 @@ Guidelines:
59
62
  - 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.
60
63
  - Service-key management has no MCP tools. Never ask a user to paste a service key into a prompt, and never place one in an MCP call, tool result, source file, or log.
61
64
  - 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 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.
62
- - For a headless HTTP adapter, read the service key from a secret-backed environment variable, send it as a bearer token to `/api/apex/v1`, and redact it from errors and logs. Do not shell out to `apex` or launch MCP from the adapter.
63
- - Hosted API v1 requires the GitHub repository to be linked to an active Apex workspace. It never creates or mutates workspaces; use optional `workspaceId` to disambiguate matches and treat zero or ambiguous matches as HTTP `409` setup errors.
65
+ - For a headless HTTP adapter, read the service key from the secret-backed `APEX_SERVICE_KEY` environment variable, send it as a bearer token to `/api/apex/v1`, and redact it from errors and logs. A service key does not authenticate CLI or MCP commands; those use device login. Do not shell out to `apex` or launch MCP from the adapter.
66
+ - Hosted API v1 remote-repository scans require the GitHub repository to be linked to an active Apex workspace and do not create or mutate workspaces; use optional `workspaceId` to disambiguate matches and treat zero or ambiguous matches as HTTP `409` setup errors. Local or non-Git scans use the documented source-upload flow, which can create or reuse a workspace; send `generateThreatModel: true` for the first scan of a newly created local workspace.
64
67
  - Hosted 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 scan, use a new key.
65
68
  - `apex-credits` reports standard credits plus audit, fix review, and Lite scan entitlements when Bedrock returns those balances.
66
69
  - `apex-scan` scans the provided `cwd` by default; pass `repoPaths` only when the user asks to scan explicit alternate roots.
67
- - `apex-doctor` reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
70
+ - `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.
68
71
  - Apex can scan plain local directories and dirty git worktrees without provider connections by using local snapshot uploads.
69
72
  - 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 or GitLab sources.
70
73
  - Lite scans use `mode: "lite"`, require Lite Scan beta access, and require provider-backed GitHub or GitLab sources without local snapshot fallback.