@cantinasecurity/apex-cli 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/apex-cli/SKILL.md +5 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.claude.json +1 -1
- package/.mcp.codex.json +1 -1
- package/MARKETPLACE.md +1 -1
- package/README.md +24 -15
- package/dist/api-client.js +1 -1
- package/dist/commands.js +108 -25
- package/dist/help.js +2 -0
- package/dist/local-source-scan.js +66 -24
- package/dist/mcp.js +3 -1
- package/dist/session.js +38 -5
- package/dist/setup.js +53 -22
- package/package.json +1 -1
- package/skills/apex-cli/SKILL.md +5 -2
|
@@ -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,6 +50,7 @@ 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.
|
|
@@ -61,7 +65,7 @@ If the Apex MCP server is not configured, fall back to the local CLI:
|
|
|
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.
|
|
9
|
+
"version": "0.1.25"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
"source": {
|
|
15
15
|
"source": "npm",
|
|
16
16
|
"package": "@cantinasecurity/apex-cli",
|
|
17
|
-
"version": "0.1.
|
|
17
|
+
"version": "0.1.25"
|
|
18
18
|
},
|
|
19
19
|
"description": "Run Apex security scans and review findings from Claude Code.",
|
|
20
|
-
"version": "0.1.
|
|
20
|
+
"version": "0.1.25",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "Cantina",
|
|
23
23
|
"email": "support@cantina.xyz"
|
package/.mcp.claude.json
CHANGED
package/.mcp.codex.json
CHANGED
package/MARKETPLACE.md
CHANGED
|
@@ -13,7 +13,7 @@ This package is prepared as both a Claude Code plugin and a Codex plugin. The pu
|
|
|
13
13
|
The plugin MCP configs launch the pinned npm CLI package with:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
16
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.25 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.
|
|
155
|
+
Install and run Cantina Apex CLI 0.1.25 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.
|
|
162
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.25 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.
|
|
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.25 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.
|
|
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.25 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.
|
|
172
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.25 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.
|
|
175
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.25 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.
|
|
178
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.25 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.
|
|
@@ -421,14 +428,16 @@ codex mcp add apex \
|
|
|
421
428
|
For Claude Code:
|
|
422
429
|
|
|
423
430
|
```bash
|
|
424
|
-
claude mcp add
|
|
431
|
+
claude mcp add apex \
|
|
432
|
+
--scope user \
|
|
425
433
|
-e APEX_MCP_CLIENT=claude \
|
|
426
434
|
-e APEX_CLIENT_INTEGRATION=claude \
|
|
427
|
-
-- apex
|
|
435
|
+
-- apex-mcp
|
|
428
436
|
```
|
|
429
437
|
|
|
430
|
-
`apex setup claude`
|
|
431
|
-
|
|
438
|
+
`apex setup claude` uses Claude's name-first stdio command syntax and checks
|
|
439
|
+
whether the installed Claude CLI supports `--scope user`. If a Claude build
|
|
440
|
+
omits that option or advertises it but rejects it at runtime, setup safely
|
|
432
441
|
merges the same user-level `mcpServers.apex` entry into `~/.claude.json`
|
|
433
442
|
without replacing other settings or servers.
|
|
434
443
|
|
|
@@ -567,7 +576,7 @@ Telemetry event posts do not include bearer tokens. The telemetry endpoint is `P
|
|
|
567
576
|
{
|
|
568
577
|
"schemaVersion": 1,
|
|
569
578
|
"event": "apex.invocation.completed",
|
|
570
|
-
"cliVersion": "0.1.
|
|
579
|
+
"cliVersion": "0.1.25",
|
|
571
580
|
"invocation": {
|
|
572
581
|
"surface": "mcp",
|
|
573
582
|
"command": "apex-scan",
|
|
@@ -598,7 +607,7 @@ The npm package also includes marketplace-ready plugin artifacts:
|
|
|
598
607
|
- `.claude-plugin/plugin.json` and `.mcp.claude.json` for Claude Code plugin installs
|
|
599
608
|
- `.claude-plugin/marketplace.json` for a Claude marketplace entry backed by the public npm package
|
|
600
609
|
|
|
601
|
-
These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
610
|
+
These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.25 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
|
|
602
611
|
|
|
603
612
|
The repository also includes `.agents/plugins/marketplace.json` for local Codex marketplace testing from a checkout.
|
|
604
613
|
|
package/dist/api-client.js
CHANGED
|
@@ -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
|
|
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/commands.js
CHANGED
|
@@ -5,13 +5,13 @@ 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 { chooseCompany, createWorkspaceBinding, ensureAuthenticated, planGuestWorkspaceSelection, remediateMissingConnections, resolveWorkspaceSelection, resolveWorkspaceSelectionLegacy, resolveWorkspaceAllowingMissingConnections, selectWorkspaceTarget, } from "./session.js";
|
|
15
15
|
import { createWorkspaceBindingFromSummary, fetchCompanyCredits, fetchCompanyWorkspaces, findWorkspaceByRef, } from "./workspaces.js";
|
|
16
16
|
import { loadWorkspaceBinding, saveWorkspaceBinding } from "./workspace-binding.js";
|
|
17
17
|
import { commandSetup as runCliSetup } from "./setup.js";
|
|
@@ -116,6 +116,47 @@ async function getCommandAuthState(client, me) {
|
|
|
116
116
|
: null,
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
+
async function resolveWorkspaceSelectionForScan(args) {
|
|
120
|
+
try {
|
|
121
|
+
return {
|
|
122
|
+
selection: args.selection,
|
|
123
|
+
result: await resolveWorkspaceSelection(args.client, args.selection),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (args.authKind !== "guest" ||
|
|
128
|
+
args.selection.workspaceNameWasExplicitlyChosen ||
|
|
129
|
+
args.selection.binding ||
|
|
130
|
+
getApiErrorCode(error) !== "GUEST_WORKSPACE_LIMIT") {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
let response;
|
|
134
|
+
try {
|
|
135
|
+
response = await fetchCompanyWorkspaces(args.client, args.selection.company.id);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
const recoverable = response.workspaces.filter((workspace) => workspace.scanCount === 0 && workspace.findingsCount === 0);
|
|
141
|
+
if (recoverable.length !== 1) {
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
const workspace = recoverable[0];
|
|
145
|
+
const recoveredSelection = {
|
|
146
|
+
...args.selection,
|
|
147
|
+
workspaceName: workspace.name,
|
|
148
|
+
binding: createWorkspaceBindingFromSummary({
|
|
149
|
+
companyId: args.selection.company.id,
|
|
150
|
+
workspace,
|
|
151
|
+
currentBinding: args.selection.binding,
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
selection: recoveredSelection,
|
|
156
|
+
result: await resolveWorkspaceSelection(args.client, recoveredSelection),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
119
160
|
function logGuestScanFooter(authState, flags) {
|
|
120
161
|
if (isJsonMode(flags) || !authState.guestCredentials)
|
|
121
162
|
return;
|
|
@@ -945,8 +986,16 @@ export async function commandTelemetry(flags, target) {
|
|
|
945
986
|
}
|
|
946
987
|
export async function commandDoctor(client, cwd, flags) {
|
|
947
988
|
const me = await ensureAuthenticated(client, flags);
|
|
989
|
+
const authState = await getCommandAuthState(client, me);
|
|
990
|
+
setTelemetryAuthKind(authState.authKind);
|
|
948
991
|
try {
|
|
949
|
-
const result = await withLoadingIndicator("Inspecting this directory and checking Apex setup...", flags, () =>
|
|
992
|
+
const result = await withLoadingIndicator("Inspecting this directory and checking Apex setup...", flags, async () => {
|
|
993
|
+
if (authState.authKind === "guest") {
|
|
994
|
+
const selection = await selectWorkspaceTarget(cwd, me, flags);
|
|
995
|
+
return planGuestWorkspaceSelection(selection);
|
|
996
|
+
}
|
|
997
|
+
return resolveWorkspaceAllowingMissingConnections(client, cwd, me, flags);
|
|
998
|
+
});
|
|
950
999
|
const localSnapshotCount = result.resolve.plannedSources.filter((source) => source.sourceKind === "local_archive").length;
|
|
951
1000
|
const payload = {
|
|
952
1001
|
authenticatedAs: me.user.username ?? me.user.id,
|
|
@@ -1040,14 +1089,42 @@ export async function commandScan(client, cwd, flags) {
|
|
|
1040
1089
|
const me = await ensureAuthenticated(client, flags);
|
|
1041
1090
|
const authState = await getCommandAuthState(client, me);
|
|
1042
1091
|
setTelemetryAuthKind(authState.authKind);
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
if (
|
|
1046
|
-
|
|
1092
|
+
let selection = await selectWorkspaceTarget(cwd, me, flags);
|
|
1093
|
+
let preparedArchives = null;
|
|
1094
|
+
if (authState.authKind === "guest" && requestedMode === "standard") {
|
|
1095
|
+
preparedArchives = await withLoadingIndicator("Preparing local source archive...", flags, () => prepareLocalSourceArchives({
|
|
1096
|
+
client,
|
|
1097
|
+
detectedSources: selection.sources,
|
|
1098
|
+
}));
|
|
1047
1099
|
}
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1100
|
+
const resolvedContext = await (async () => {
|
|
1101
|
+
try {
|
|
1102
|
+
const resolved = await withLoadingIndicator("Resolving workspace and scan plan...", flags, () => resolveWorkspaceSelectionForScan({
|
|
1103
|
+
client,
|
|
1104
|
+
selection,
|
|
1105
|
+
authKind: authState.authKind,
|
|
1106
|
+
}));
|
|
1107
|
+
const result = resolved.result;
|
|
1108
|
+
if (!result.resolve.workspaceId) {
|
|
1109
|
+
throw new Error("Workspace resolution did not return a workspaceId.");
|
|
1110
|
+
}
|
|
1111
|
+
const resolvedWorkspaceId = result.resolve.workspaceId;
|
|
1112
|
+
const activeScan = await withLoadingIndicator("Checking for existing scans...", flags, () => findActiveWorkspaceScan(client, resolvedWorkspaceId, result.binding));
|
|
1113
|
+
const forceRestart = await ensureScanRestartConfirmed(flags, activeScan);
|
|
1114
|
+
return {
|
|
1115
|
+
selection: resolved.selection,
|
|
1116
|
+
result,
|
|
1117
|
+
resolvedWorkspaceId,
|
|
1118
|
+
forceRestart,
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
catch (error) {
|
|
1122
|
+
await preparedArchives?.cleanup();
|
|
1123
|
+
throw error;
|
|
1124
|
+
}
|
|
1125
|
+
})();
|
|
1126
|
+
selection = resolvedContext.selection;
|
|
1127
|
+
const { result, resolvedWorkspaceId, forceRestart } = resolvedContext;
|
|
1051
1128
|
let workspaceId = resolvedWorkspaceId;
|
|
1052
1129
|
let scan;
|
|
1053
1130
|
let prScanSource = null;
|
|
@@ -1105,22 +1182,28 @@ export async function commandScan(client, cwd, flags) {
|
|
|
1105
1182
|
}));
|
|
1106
1183
|
}
|
|
1107
1184
|
else {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1185
|
+
try {
|
|
1186
|
+
scan = await withLoadingIndicator("Starting scan...", flags, async () => {
|
|
1187
|
+
const scanSources = await prepareExplicitScanSources({
|
|
1188
|
+
client,
|
|
1189
|
+
workspaceId: resolvedWorkspaceId,
|
|
1190
|
+
plannedSources: result.resolve.plannedSources,
|
|
1191
|
+
detectedSources: result.sources,
|
|
1192
|
+
preparedArchives,
|
|
1193
|
+
});
|
|
1194
|
+
return client.request(`/api/cli/v2/local-workspaces/${encodeURIComponent(resolvedWorkspaceId)}/scan`, {
|
|
1195
|
+
method: "POST",
|
|
1196
|
+
json: {
|
|
1197
|
+
mode: requestedMode,
|
|
1198
|
+
force: forceRestart || flags.force === true,
|
|
1199
|
+
scanSources,
|
|
1200
|
+
},
|
|
1201
|
+
});
|
|
1122
1202
|
});
|
|
1123
|
-
}
|
|
1203
|
+
}
|
|
1204
|
+
finally {
|
|
1205
|
+
await preparedArchives?.cleanup();
|
|
1206
|
+
}
|
|
1124
1207
|
}
|
|
1125
1208
|
const trackedScanId = getTrackedScanId(scan);
|
|
1126
1209
|
const binding = {
|
package/dist/help.js
CHANGED
|
@@ -85,6 +85,8 @@ 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.
|
|
@@ -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
|
-
|
|
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
|
|
394
|
-
const
|
|
395
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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 ";
|
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
|
|
64
|
+
return {
|
|
65
|
+
workspaceName: explicit,
|
|
66
|
+
wasExplicitlyChosen: true,
|
|
67
|
+
};
|
|
65
68
|
}
|
|
66
69
|
if (binding?.workspaceName) {
|
|
67
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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/setup.js
CHANGED
|
@@ -55,6 +55,20 @@ async function tryExecText(command, args, cwd) {
|
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
function isUnknownClaudeScopeOption(error) {
|
|
59
|
+
if (!error || typeof error !== "object") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const processError = error;
|
|
63
|
+
const output = [
|
|
64
|
+
processError.message,
|
|
65
|
+
processError.stdout,
|
|
66
|
+
processError.stderr,
|
|
67
|
+
]
|
|
68
|
+
.filter((value) => typeof value === "string")
|
|
69
|
+
.join("\n");
|
|
70
|
+
return /(?:unknown|unrecognized) option(?::)?\s+['"]?--scope['"]?/i.test(output);
|
|
71
|
+
}
|
|
58
72
|
async function resolveMcpLaunchSpec(packageRoot = PACKAGE_ROOT) {
|
|
59
73
|
const repoLocalBinary = path.join(packageRoot, "bin", "apex-mcp");
|
|
60
74
|
if ((await pathExists(path.join(packageRoot, ".git"))) &&
|
|
@@ -315,35 +329,52 @@ async function configureClaude(cwd, launch) {
|
|
|
315
329
|
const existing = await readClaudeUserMcpConfig();
|
|
316
330
|
const env = mcpClientEnv("claude");
|
|
317
331
|
const supportsUserScope = await claudeSupportsUserScope();
|
|
332
|
+
let usedConfigFallback = supportsUserScope === false;
|
|
318
333
|
const mcpStatus = existing === null
|
|
319
334
|
? "installed"
|
|
320
335
|
: claudeConfigMatches(existing, launch)
|
|
321
336
|
? "unchanged"
|
|
322
337
|
: "updated";
|
|
323
|
-
if (
|
|
324
|
-
// Older Claude Code releases reject --scope. Merge the same stdio
|
|
325
|
-
// registration into the user config while preserving all other settings.
|
|
338
|
+
if (usedConfigFallback) {
|
|
326
339
|
await writeClaudeUserMcpConfig(launch);
|
|
327
340
|
}
|
|
328
341
|
else {
|
|
329
|
-
|
|
330
|
-
|
|
342
|
+
try {
|
|
343
|
+
if (existing) {
|
|
344
|
+
await execText("claude", [
|
|
345
|
+
"mcp",
|
|
346
|
+
"remove",
|
|
347
|
+
MCP_SERVER_NAME,
|
|
348
|
+
"--scope",
|
|
349
|
+
"user",
|
|
350
|
+
]);
|
|
351
|
+
}
|
|
352
|
+
// Keep the server name before `--`, which separates Claude options from
|
|
353
|
+
// the stdio command and its arguments.
|
|
354
|
+
await execText("claude", [
|
|
355
|
+
"mcp",
|
|
356
|
+
"add",
|
|
357
|
+
MCP_SERVER_NAME,
|
|
358
|
+
"--scope",
|
|
359
|
+
"user",
|
|
360
|
+
"-e",
|
|
361
|
+
`${MCP_CLIENT_ENV_NAME}=${env[MCP_CLIENT_ENV_NAME]}`,
|
|
362
|
+
"-e",
|
|
363
|
+
`${MCP_CLIENT_INTEGRATION_ENV_NAME}=${env[MCP_CLIENT_INTEGRATION_ENV_NAME]}`,
|
|
364
|
+
"--",
|
|
365
|
+
launch.command,
|
|
366
|
+
...launch.args,
|
|
367
|
+
]);
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
if (!isUnknownClaudeScopeOption(error)) {
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
// Some Claude Code builds advertise --scope in help but reject it when
|
|
374
|
+
// the command runs. Preserve the user-level registration directly.
|
|
375
|
+
await writeClaudeUserMcpConfig(launch);
|
|
376
|
+
usedConfigFallback = true;
|
|
331
377
|
}
|
|
332
|
-
// Claude's variadic --env option consumes later tokens until `--`.
|
|
333
|
-
await execText("claude", [
|
|
334
|
-
"mcp",
|
|
335
|
-
"add",
|
|
336
|
-
"--scope",
|
|
337
|
-
"user",
|
|
338
|
-
"-e",
|
|
339
|
-
`${MCP_CLIENT_ENV_NAME}=${env[MCP_CLIENT_ENV_NAME]}`,
|
|
340
|
-
"-e",
|
|
341
|
-
`${MCP_CLIENT_INTEGRATION_ENV_NAME}=${env[MCP_CLIENT_INTEGRATION_ENV_NAME]}`,
|
|
342
|
-
"--",
|
|
343
|
-
MCP_SERVER_NAME,
|
|
344
|
-
launch.command,
|
|
345
|
-
...launch.args,
|
|
346
|
-
]);
|
|
347
378
|
}
|
|
348
379
|
const skillSource = path.join(PACKAGE_ROOT, ".claude", "skills", CODEX_SKILL_NAME, "SKILL.md");
|
|
349
380
|
const skillTarget = path.join(cwd, ".claude", "skills", CODEX_SKILL_NAME, "SKILL.md");
|
|
@@ -355,8 +386,8 @@ async function configureClaude(cwd, launch) {
|
|
|
355
386
|
kind: "mcp",
|
|
356
387
|
status: mcpStatus,
|
|
357
388
|
path: path.join(os.homedir(), ".claude.json"),
|
|
358
|
-
detail:
|
|
359
|
-
? `Registered ${MCP_SERVER_NAME} -> ${launch.label} in Claude Code user config (
|
|
389
|
+
detail: usedConfigFallback
|
|
390
|
+
? `Registered ${MCP_SERVER_NAME} -> ${launch.label} in Claude Code user config (CLI compatibility fallback)`
|
|
360
391
|
: `Registered ${MCP_SERVER_NAME} -> ${launch.label} in Claude Code user config`,
|
|
361
392
|
},
|
|
362
393
|
{
|
package/package.json
CHANGED
package/skills/apex-cli/SKILL.md
CHANGED
|
@@ -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.
|
|
@@ -64,7 +67,7 @@ Guidelines:
|
|
|
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.
|