@cantinasecurity/apex-cli 0.1.17 → 0.1.19

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.
@@ -0,0 +1,34 @@
1
+ const APEX_SECRET_PATTERN = /(apex_(?:ct|at|rt|dc)_)[A-Za-z0-9._~+/=-]+/gi;
2
+ const ONBOARDING_CODE_PATTERN = /SPBT-[A-Z0-9]{2,24}-[A-Z0-9]{8}/gi;
3
+ // Hex-only boundaries avoid redacting a 64-character window from a longer hex
4
+ // value while still catching fingerprints next to punctuation or prose.
5
+ const ONBOARDING_FINGERPRINT_PATTERN = /(?<![A-Fa-f0-9])[A-Fa-f0-9]{64}(?![A-Fa-f0-9])/g;
6
+ export function redactSensitiveText(value) {
7
+ return value
8
+ .replace(APEX_SECRET_PATTERN, (_match, prefix) => `${prefix}[REDACTED]`)
9
+ .replace(ONBOARDING_CODE_PATTERN, "SPBT-[REDACTED]")
10
+ .replace(ONBOARDING_FINGERPRINT_PATTERN, "[REDACTED]");
11
+ }
12
+ export function containsSensitiveText(value) {
13
+ return redactSensitiveText(value) !== value;
14
+ }
15
+ /** Redacts every string key and value in an API error body without mutating it. */
16
+ export function redactSensitiveValue(value) {
17
+ const seen = new WeakSet();
18
+ const visit = (current) => {
19
+ if (typeof current === "string")
20
+ return redactSensitiveText(current);
21
+ if (!current || typeof current !== "object")
22
+ return current;
23
+ if (seen.has(current))
24
+ return "[CIRCULAR]";
25
+ seen.add(current);
26
+ if (Array.isArray(current))
27
+ return current.map(visit);
28
+ return Object.fromEntries(Object.entries(current).map(([key, nested]) => [
29
+ redactSensitiveText(key),
30
+ visit(nested),
31
+ ]));
32
+ };
33
+ return visit(value);
34
+ }
package/dist/session.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import path from "node:path";
2
2
  import { canPromptInteractively, getFlagList, getFlagString, isJsonMode, } from "./args.js";
3
- import { login } from "./auth.js";
3
+ import { getMe, login } from "./auth.js";
4
4
  import { ApiError } from "./api-client.js";
5
+ import { isClaimProofAvailable } from "./claim-proof.js";
5
6
  import { openInBrowser } from "./browser.js";
6
7
  import { loadConfig } from "./config.js";
7
8
  import { chooseOne, confirm, promptText, waitForEnter } from "./prompt.js";
@@ -132,6 +133,14 @@ function ensurePlannedSources(selection, resolve) {
132
133
  };
133
134
  }
134
135
  export async function ensureAuthenticated(client, flags) {
136
+ const existing = await getMe(client);
137
+ if (existing) {
138
+ return existing;
139
+ }
140
+ const credentials = await client.getCredentials();
141
+ if (isClaimProofAvailable(credentials)) {
142
+ throw new Error("Your guest trial session is no longer available, but its claim proof is stored on this machine. Run `apex claim` to keep the trial results.");
143
+ }
135
144
  return login(client, {
136
145
  noOpen: flags["no-open"] === true,
137
146
  quiet: isJsonMode(flags),
package/dist/setup.js CHANGED
@@ -300,12 +300,12 @@ async function configureCopilot(launch) {
300
300
  if (existing) {
301
301
  await execText("copilot", ["mcp", "remove", MCP_SERVER_NAME]);
302
302
  }
303
+ // A command after `--` selects local stdio without relying on Copilot's renamed
304
+ // transport flag (`--type` in older releases, `--transport` in newer ones).
303
305
  await execText("copilot", [
304
306
  "mcp",
305
307
  "add",
306
308
  MCP_SERVER_NAME,
307
- "--type",
308
- "stdio",
309
309
  "--tools",
310
310
  "*",
311
311
  "--",
package/dist/shell.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { SHELL_HELP_TEXT } from "./help.js";
2
- import { commandCancelScan, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandLogout, commandScan, commandScans, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, initializeInteractiveSession, openCurrentApexView, } from "./commands.js";
2
+ import { commandCancelScan, commandClaim, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandLogout, commandScan, commandScans, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, initializeInteractiveSession, openCurrentApexView, } from "./commands.js";
3
3
  import { withFlag } from "./args.js";
4
4
  import { printCompanyDetails, printInteractiveSessionSummary, printSourceList, printWorkspaceDetails, } from "./output.js";
5
5
  import { readLine } from "./prompt.js";
6
6
  import { formatApiError } from "./api-client.js";
7
7
  import { createShellTelemetryInvocation, emitInvocationCompleted, emitInvocationStarted, withTelemetryContext, } from "./telemetry.js";
8
8
  const SHELL_COMPLETIONS = [
9
+ { command: "claim" },
9
10
  { command: "credits" },
10
11
  { command: "scan", args: ["standard", "audit", "ultra", "lite", "pr"] },
11
12
  { command: "scans" },
@@ -193,6 +194,13 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
193
194
  case "credits":
194
195
  await commandCredits(client, cwd, shellFlags);
195
196
  return {};
197
+ case "claim":
198
+ if (parsed.args.length > 0) {
199
+ process.stderr.write("Usage: /claim\n");
200
+ return {};
201
+ }
202
+ await commandClaim(client, cwd, shellFlags);
203
+ return false;
196
204
  case "scan": {
197
205
  const mode = parsed.args[0];
198
206
  if (mode && !["standard", "audit", "ultra", "lite", "pr"].includes(mode)) {
@@ -329,8 +337,7 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
329
337
  await commandUpdate(shellFlags);
330
338
  return false;
331
339
  case "logout":
332
- await commandLogout(client, shellFlags);
333
- return false;
340
+ return (await commandLogout(client, shellFlags)).loggedOut ? false : {};
334
341
  case "repos":
335
342
  printSourceList(session.sources);
336
343
  return {};
package/dist/telemetry.js CHANGED
@@ -2,7 +2,8 @@ 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 { loadConfig, saveConfig } from "./config.js";
5
+ import { loadConfig, updateConfig } from "./config.js";
6
+ import { containsSensitiveText } from "./redaction.js";
6
7
  import { APEX_CLI_VERSION } from "./version.js";
7
8
  const TELEMETRY_ENDPOINT_PATH = "/api/cli/v1/telemetry/events";
8
9
  const TELEMETRY_SESSION_ID = randomUUID();
@@ -29,6 +30,7 @@ const KNOWN_CLI_FLAG_NAMES = [
29
30
  "parent-comment",
30
31
  "pr",
31
32
  "pr-path",
33
+ "replace",
32
34
  "repo",
33
35
  "review-target",
34
36
  "scan",
@@ -41,6 +43,7 @@ const KNOWN_CLI_FLAG_NAMES = [
41
43
  const KNOWN_CLI_COMMANDS = [
42
44
  "cancel-scan",
43
45
  "connect",
46
+ "claim",
44
47
  "credits",
45
48
  "doctor",
46
49
  "export",
@@ -59,6 +62,50 @@ const KNOWN_CLI_COMMANDS = [
59
62
  "update",
60
63
  "workspace",
61
64
  "workspaces",
65
+ "redeem",
66
+ ];
67
+ const KNOWN_SHELL_COMMANDS = [
68
+ "cancel",
69
+ "cancel-scan",
70
+ "claim",
71
+ "clear",
72
+ "company",
73
+ "connect",
74
+ "credits",
75
+ "doctor",
76
+ "exit",
77
+ "export",
78
+ "findings",
79
+ "help",
80
+ "logout",
81
+ "open",
82
+ "quit",
83
+ "repos",
84
+ "scan",
85
+ "scans",
86
+ "status",
87
+ "telemetry",
88
+ "update",
89
+ "workspace",
90
+ "workspaces",
91
+ ];
92
+ const ONBOARDING_TELEMETRY_OUTCOMES = [
93
+ "redeemed",
94
+ "idempotent",
95
+ "already_authenticated",
96
+ "invalid",
97
+ "expired",
98
+ "exhausted",
99
+ "rate_limited",
100
+ "capacity",
101
+ "active_guest_session",
102
+ "claimed",
103
+ "email_unverified",
104
+ "claim_expired",
105
+ "already_claimed",
106
+ "scan_in_progress",
107
+ "fingerprint_mismatch",
108
+ "error",
62
109
  ];
63
110
  let cachedParentProcesses = null;
64
111
  let volatileTelemetryInstallId = null;
@@ -83,6 +130,8 @@ function safeToken(value, fallback = "unknown") {
83
130
  const trimmed = value?.trim();
84
131
  if (!trimmed)
85
132
  return fallback;
133
+ if (containsSensitiveText(trimmed))
134
+ return fallback;
86
135
  const normalized = trimmed
87
136
  .slice(0, 80)
88
137
  .replace(/[^A-Za-z0-9_.:-]+/g, "_")
@@ -173,6 +222,7 @@ function sanitizeCliFlags(flags) {
173
222
  unknownFlagCount: unknownFlagCount(flags),
174
223
  json: boolFlag(flags, "json"),
175
224
  force: boolFlag(flags, "force"),
225
+ replace: boolFlag(flags, "replace"),
176
226
  nonInteractive: boolFlag(flags, "non-interactive"),
177
227
  noOpen: boolFlag(flags, "no-open"),
178
228
  companyProvided: stringFlag(flags, "company") !== null,
@@ -323,6 +373,11 @@ function safeCliCommand(parsed) {
323
373
  const command = parsed.command ?? "interactive-shell";
324
374
  return KNOWN_CLI_COMMANDS.includes(command) ? command : "unknown";
325
375
  }
376
+ function safeShellCommand(command) {
377
+ return KNOWN_SHELL_COMMANDS.includes(command)
378
+ ? command
379
+ : "unknown";
380
+ }
326
381
  function getInputKeys(input) {
327
382
  return Object.entries(input)
328
383
  .filter(([, value]) => value !== undefined)
@@ -556,7 +611,7 @@ export function createShellTelemetryInvocation(parsed, flags) {
556
611
  : null;
557
612
  return createInvocation({
558
613
  surface: "interactive_shell",
559
- command: parsed.command,
614
+ command: safeShellCommand(parsed.command),
560
615
  metadata: {
561
616
  argCount: parsed.args.length,
562
617
  ...sanitizeCliFlags(flags),
@@ -587,6 +642,19 @@ export async function withTelemetryContext(invocation, action) {
587
642
  function currentInvocation() {
588
643
  return telemetryContext.getStore() ?? null;
589
644
  }
645
+ export function setTelemetryAuthKind(authKind) {
646
+ const invocation = currentInvocation();
647
+ if (!invocation)
648
+ return;
649
+ invocation.metadata.authKind = authKind;
650
+ }
651
+ export function setTelemetryOnboardingOutcome(outcome) {
652
+ const invocation = currentInvocation();
653
+ if (!invocation)
654
+ return;
655
+ invocation.metadata.onboardingOutcome =
656
+ safeEnum(outcome, ONBOARDING_TELEMETRY_OUTCOMES) ?? "error";
657
+ }
590
658
  function getTelemetryDisabled(config) {
591
659
  const disabledEnv = getDisabledEnvName();
592
660
  if (disabledEnv)
@@ -607,10 +675,7 @@ async function getTelemetryStatusInternal(options) {
607
675
  installId = randomUUID();
608
676
  volatileTelemetryInstallId = installId;
609
677
  try {
610
- await saveConfig({
611
- ...config,
612
- telemetryInstallId: installId,
613
- });
678
+ await updateConfig({ telemetryInstallId: installId });
614
679
  }
615
680
  catch {
616
681
  // Telemetry-only ID creation must not break normal CLI/API behavior.
@@ -635,10 +700,7 @@ export async function getTelemetryStatus(surface = "cli", options = {}) {
635
700
  }
636
701
  export async function setTelemetryEnabled(enabled) {
637
702
  const config = await loadConfig();
638
- await saveConfig({
639
- ...config,
640
- telemetryDisabled: enabled ? false : true,
641
- });
703
+ await updateConfig({ telemetryDisabled: enabled ? false : true });
642
704
  return getTelemetryStatus("cli", { createInstallId: enabled });
643
705
  }
644
706
  function runtimePayload() {
@@ -662,10 +724,19 @@ function errorPayload(error) {
662
724
  }
663
725
  const maybeError = error;
664
726
  const status = typeof maybeError.status === "number" ? maybeError.status : null;
727
+ const name = typeof maybeError.name === "string" &&
728
+ ["AbortError", "ApiError", "Error", "TypeError"].includes(maybeError.name)
729
+ ? maybeError.name
730
+ : "Error";
731
+ const code = typeof maybeError.code === "string" &&
732
+ !containsSensitiveText(maybeError.code) &&
733
+ /^[A-Z][A-Z0-9_]{1,63}$/.test(maybeError.code)
734
+ ? maybeError.code
735
+ : null;
665
736
  return {
666
- name: typeof maybeError.name === "string" ? safeToken(maybeError.name) : "Error",
737
+ name,
667
738
  status,
668
- code: typeof maybeError.code === "string" ? safeToken(maybeError.code) : null,
739
+ code,
669
740
  category: status ? `http_${status}` : "error",
670
741
  };
671
742
  }
@@ -1,4 +1,5 @@
1
- import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  function getWorkspaceDir(cwd) {
4
5
  return path.join(cwd, ".apex");
@@ -18,12 +19,21 @@ export async function loadWorkspaceBinding(cwd) {
18
19
  export async function saveWorkspaceBinding(cwd, binding) {
19
20
  const workspaceDir = getWorkspaceDir(cwd);
20
21
  const bindingPath = getWorkspaceBindingPath(cwd);
22
+ const temporaryPath = path.join(workspaceDir, `.workspace.json.${process.pid}.${randomUUID()}.tmp`);
21
23
  await mkdir(workspaceDir, { recursive: true, mode: 0o700 });
22
- await writeFile(bindingPath, `${JSON.stringify(binding, null, 2)}\n`, {
23
- encoding: "utf8",
24
- mode: 0o600,
25
- });
26
- await chmod(bindingPath, 0o600);
24
+ try {
25
+ await writeFile(temporaryPath, `${JSON.stringify(binding, null, 2)}\n`, {
26
+ encoding: "utf8",
27
+ mode: 0o600,
28
+ });
29
+ await chmod(temporaryPath, 0o600);
30
+ await rename(temporaryPath, bindingPath);
31
+ await chmod(bindingPath, 0o600);
32
+ }
33
+ catch (error) {
34
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
35
+ throw error;
36
+ }
27
37
  }
28
38
  export async function clearWorkspaceBinding(cwd) {
29
39
  await rm(getWorkspaceBindingPath(cwd), { force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cantinasecurity/apex-cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Standalone CLI and MCP server for Apex.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -29,11 +29,13 @@
29
29
  "dependencies": {
30
30
  "@modelcontextprotocol/sdk": "^1.29.0",
31
31
  "ignore": "^7.0.5",
32
+ "proper-lockfile": "^4.1.2",
32
33
  "tar": "^7.4.3",
33
34
  "zod": "^4.3.6"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/node": "^22.18.6",
38
+ "@types/proper-lockfile": "^4.1.4",
37
39
  "tsx": "^4.21.0",
38
40
  "typescript": "^5.9.3",
39
41
  "vitest": "^2.1.9"
@@ -15,22 +15,48 @@ For serverless, cron, or other HTTP-only integrations, use the hosted Apex REST
15
15
 
16
16
  For on-prem or other non-default Apex hosts, make sure the MCP server runs with `APEX_BASE_URL` set to the external HTTPS Bedrock origin before auth or provider connection flows.
17
17
 
18
+ Trial-code exception: if the user supplied a Cantina trial code, do not call
19
+ `apex-auth-start` or open a login URL. Run `apex redeem <code> --json` in a
20
+ terminal from the repository root, then use the normal MCP scan tools with the
21
+ shared guest credentials. Only when the first attempt has an ambiguous
22
+ transport/connection failure or interrupted/missing response with no typed
23
+ server error may you retry that exact same command once on the same machine,
24
+ HOME, and repository root. Never retry a typed server rejection or make more
25
+ than one recovery retry. Treat the code as a secret: pass it only to
26
+ `apex redeem`; never echo it, write it to a file, or include it in an MCP call
27
+ or tool result. Trial redemption, claim, and revocation are hosted-only; stop
28
+ before passing the code when `APEX_BASE_URL` has any origin other than
29
+ `https://ai.cantina.xyz`. Stop if the redeem outcome is not `redeemed` or
30
+ `idempotent`.
31
+
18
32
  Workflow:
19
33
 
20
34
  1. Start with `apex-auth-status`.
21
- 2. If Apex is unauthenticated, call `apex-auth-start`, ask the user to approve the device-login URL, then call `apex-auth-wait` with the returned `deviceCode`.
22
- 3. For repository work, pass `cwd` explicitly.
23
- 4. Call `apex-doctor` before starting a scan if workspace binding or source planning might be unclear.
24
- 5. If the directory is not bound to the right workspace, call `apex-workspaces` and `apex-workspace-use`.
25
- 6. Use `apex-scan`, `apex-status`, `apex-scans`, `apex-findings`, and `apex-export-findings` for the scan lifecycle.
26
- 7. For Lite scans, call `apex-scan` with `mode: "lite"` only when the company has Lite Scan beta access and the sources are provider-backed.
27
- 8. For PR scans, call `apex-scan` with `mode: "pr"` and `pullRequests`.
28
- 9. Use `apex-finding-comment` and `apex-finding-feedback` when the user wants to leave review notes or valid/invalid feedback on a finding.
29
- 10. For fix reviews, call `apex-finding-fix-review`. Pass `fixPrUrls` for a PR-based review, or omit them to review the current repository state when Bedrock has original scan repository context.
35
+ 2. Always call `apex-trial-status` next, even when account authentication succeeded. When `claimAvailable` is true, do not call `apex-auth-start`; tell the user to run `apex claim` in a terminal first so the trial workspace, scans, and findings are kept. When the claim deadline has passed and `claimAvailable` is false, continue normal account authentication instead of steering the user to an expired claim.
36
+ 3. Only when Apex is unauthenticated, no unexpired claim proof is available, and no trial code was supplied, call `apex-auth-start`, ask the user to approve the device-login URL, then call `apex-auth-wait` with both the returned `deviceCode` and opaque `credentialGeneration`. If the wait is superseded, keep the current credential state and re-check `apex-auth-status` before starting another login.
37
+ 4. For repository work, pass `cwd` explicitly.
38
+ 5. Call `apex-doctor` before starting a scan if workspace binding or source planning might be unclear.
39
+ 6. If the directory is not bound to the right workspace, call `apex-workspaces` and `apex-workspace-use`.
40
+ 7. Use `apex-scan`, `apex-status`, `apex-scans`, `apex-findings`, and `apex-export-findings` for the scan lifecycle.
41
+ 8. For Lite scans, call `apex-scan` with `mode: "lite"` only when the company has Lite Scan beta access and the sources are provider-backed.
42
+ 9. For PR scans, call `apex-scan` with `mode: "pr"` and `pullRequests`.
43
+ 10. Use `apex-finding-comment` and `apex-finding-feedback` when the user wants to leave review notes or valid/invalid feedback on a finding.
44
+ 11. For fix reviews, call `apex-finding-fix-review`. Pass `fixPrUrls` for a PR-based review, or omit them to review the current repository state when Bedrock has original scan repository context.
45
+
46
+ If the Apex MCP server is not configured, fall back to the local CLI:
47
+
48
+ - Prefer scripted commands over the interactive shell.
49
+ - 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.
50
+ - 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
+ - `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
+ - 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
+ - Claim currently requires a Cantina account that does not already own a personal Apex tenant. If the user already completed Cantina Apex onboarding, explain that CLI linking is not available yet and tell them to contact Cantina support before the claim deadline.
30
54
 
31
55
  Guidelines:
32
56
 
33
57
  - Do not rely on interactive CLI prompts. The MCP tools are intentionally non-interactive.
58
+ - The MCP-only `apex-trial-status` tool may report local guest and claim deadlines, but it must never return a trial code or claim token. `claimAvailable` becomes false at a known passed deadline; missing or invalid local deadline metadata remains conservative and server-authoritative. There is intentionally no MCP redemption or claim tool.
59
+ - 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.
34
60
  - 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.
35
61
  - 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.
36
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.
@@ -48,9 +74,15 @@ Guidelines:
48
74
  - Use `force: true` on `apex-scan` only when the user explicitly wants to replace or overlap an active scan.
49
75
  - 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.
50
76
  - Prefer `apex-findings` for quick inspection and `apex-export-findings` when the user needs a file artifact.
51
- - 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.
52
- - Anonymous telemetry records only sanitized command/tool metadata such as command names, enum modes, counts, durations, success/failure categories, CLI version, and client integration. It must not include raw repository paths, scan IDs, finding IDs, comments, file paths, PR URLs, or tokens.
77
+ - 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`, check `apex-trial-status`, and complete `apex-auth-start` / `apex-auth-wait` only when no unexpired local claim proof is available, instead of asking for browser cookies or auth tokens.
78
+ - Anonymous telemetry records only sanitized command/tool metadata such as command names, enum modes, counts, durations, success/failure categories, CLI version, and client integration. It must not include raw repository paths, scan IDs, finding IDs, comments, file paths, PR URLs, trial codes, claim tokens, onboarding fingerprints, or bearer tokens.
53
79
  - Invalid finding feedback requires `dismissalReason`; valid feedback can include `suggestedSeverity`, including `extreme`.
54
80
  - PR-based fix reviews can pass `fixPrUrls` directly to `apex-finding-fix-review`; agents that also need to record fixed feedback can first call `apex-finding-feedback` with `status: "valid"`, `labels: ["fixed"]`, and `fixPrUrls`.
55
81
  - Current-repository fix reviews omit `fixPrUrls` and require Bedrock original scan repository context for the finding.
56
82
  - 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.
83
+
84
+ Example trial flow: redeem a user-supplied code with `apex redeem <code> --json`
85
+ in a terminal, use MCP for scan/status/findings after redemption, then check the
86
+ MCP-only `apex-trial-status` tool and remind the user to run `apex claim`
87
+ locally while `claimAvailable` remains true. In a CLI-only flow, use the
88
+ redeem response's `claimDeadline` instead.