@cantinasecurity/apex-cli 0.1.6 → 0.1.10
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 +4 -3
- 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 +20 -15
- package/dist/api-client.js +4 -0
- package/dist/commands.js +42 -12
- package/dist/findings.js +62 -19
- package/dist/help.js +9 -8
- package/dist/mcp.js +9 -7
- package/dist/scan.js +32 -1
- package/dist/setup.js +126 -13
- package/dist/shell.js +7 -1
- package/dist/update.js +1 -1
- package/package.json +1 -1
- package/skills/apex-cli/SKILL.md +4 -3
|
@@ -28,7 +28,7 @@ If the Apex MCP server is not configured, fall back to the local CLI:
|
|
|
28
28
|
- Prefer scripted commands over the interactive shell.
|
|
29
29
|
- Use `--non-interactive` and `--no-open` for automation-friendly CLI calls.
|
|
30
30
|
- Use `--json` whenever structured output is helpful.
|
|
31
|
-
- `apex credits` reports standard credits
|
|
31
|
+
- `apex credits` reports standard credits plus audit and fix review scan entitlements when Bedrock returns those balances.
|
|
32
32
|
- Work from the target repository directory so Apex can resolve `.apex/workspace.json`.
|
|
33
33
|
- `apex scan` scans the current working directory by default; pass `--repo` only when the user asks to scan explicit alternate roots.
|
|
34
34
|
- `apex-doctor` reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
|
|
@@ -37,7 +37,8 @@ If the Apex MCP server is not configured, fall back to the local CLI:
|
|
|
37
37
|
- PR scans use `--mode pr --pr <number>` in CLI calls, or `mode: "pr"` with `pullRequests` in MCP. They require one provider-backed GitHub repository.
|
|
38
38
|
- `apex-workspace-use` accepts a workspace name, prefix, or ID.
|
|
39
39
|
- Use `sourceMode: "remote"` only when the user explicitly wants to forbid local snapshot fallbacks.
|
|
40
|
-
-
|
|
40
|
+
- When checking a scan that is not the workspace binding's latest scan, pass `scanId` to `apex-status`; use `apex scans` or `apex-scans` first if you need to discover scan IDs.
|
|
41
|
+
- Finding comments, feedback, and fix review scan starts use the same Apex device-login credentials as read tools. If a write tool reports missing auth, re-run `apex-auth-status` and complete `apex-auth-start` / `apex-auth-wait` instead of asking for browser cookies or auth tokens.
|
|
41
42
|
- Invalid finding feedback requires `dismissalReason`; valid feedback can include `suggestedSeverity`, including `extreme`.
|
|
42
43
|
- Fix PR callback feedback requires valid feedback with `labels: ["fixed"]` and `fixPrUrls`; start the fix review scan with `apex-finding-fix-review` after saving that feedback.
|
|
43
44
|
- Finding identifiers such as `KERN2-25` resolve against the selected or latest scan for the current workspace binding. Pass an explicit `scanId` when needed, or use the finding UUID directly.
|
|
@@ -46,7 +47,7 @@ If the Apex MCP server is not configured, fall back to the local CLI:
|
|
|
46
47
|
|
|
47
48
|
- Start a scan for the current repository or directory: run `apex-doctor`, bind a workspace if needed, then call `apex-scan`.
|
|
48
49
|
- Start a PR scan: call `apex-scan` with `mode: "pr"` and `pullRequests`.
|
|
49
|
-
- Check an active scan: call `apex-status`, then `apex-findings` when the scan completes.
|
|
50
|
+
- Check an active scan: call `apex-status`, passing `scanId` when the scan is not the latest binding, then `apex-findings` when the scan completes.
|
|
50
51
|
- Export findings for review: call `apex-export-findings` with `format` set to `markdown`, `json`, or `gitlab-sast`.
|
|
51
52
|
- Leave review feedback: call `apex-finding-comment` to add a note, or `apex-finding-feedback` with `status` set to `valid` or `invalid`.
|
|
52
53
|
- Add a Fix PR and run fix review: call `apex-finding-feedback` with `status: "valid"`, `labels: ["fixed"]`, and `fixPrUrls`, then call `apex-finding-fix-review`.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Cantina agent plugins for security review workflows.",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.10"
|
|
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.10"
|
|
18
18
|
},
|
|
19
19
|
"description": "Run Apex security scans and review findings from Claude Code.",
|
|
20
|
-
"version": "0.1.
|
|
20
|
+
"version": "0.1.10",
|
|
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.10 apex-mcp
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
That keeps marketplace installs independent of a user's global `apex` install.
|
package/README.md
CHANGED
|
@@ -19,15 +19,17 @@ apex setup
|
|
|
19
19
|
|
|
20
20
|
`apex setup` is the lowest-friction path for agent clients. It:
|
|
21
21
|
|
|
22
|
-
- registers Apex as an MCP server in any installed Codex
|
|
22
|
+
- registers Apex as an MCP server in any installed Codex CLI, Claude Code, and GitHub Copilot CLI clients
|
|
23
23
|
- installs the Codex skill into `$CODEX_HOME/skills/apex-cli`
|
|
24
24
|
- installs the Claude project skill into `.claude/skills/apex-cli` in the current repository
|
|
25
|
+
- installs the GitHub Copilot CLI skill into `$COPILOT_HOME/skills/apex-cli` or `~/.copilot/skills/apex-cli`
|
|
25
26
|
|
|
26
27
|
If you only want one client, run:
|
|
27
28
|
|
|
28
29
|
```bash
|
|
29
30
|
apex setup codex
|
|
30
31
|
apex setup claude
|
|
32
|
+
apex setup copilot
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
If one client is not installed yet, `apex setup` skips it automatically. If you target a client explicitly, its CLI must already be installed.
|
|
@@ -109,7 +111,7 @@ Supported shell commands:
|
|
|
109
111
|
- `/export [scan-id]`
|
|
110
112
|
- `/workspaces`
|
|
111
113
|
- `/cancel-scan [scan-id]`
|
|
112
|
-
- `/status`
|
|
114
|
+
- `/status [scan-id]`
|
|
113
115
|
- `/doctor`
|
|
114
116
|
- `/update`
|
|
115
117
|
- `/logout`
|
|
@@ -142,11 +144,11 @@ Supported shell commands:
|
|
|
142
144
|
- `apex workspace`
|
|
143
145
|
- `apex workspace use <workspace-name|workspace-prefix|workspace-id>`
|
|
144
146
|
- `apex cancel-scan [scan-id]`
|
|
145
|
-
- `apex status`
|
|
147
|
+
- `apex status [--scan <scan-id>]`
|
|
146
148
|
- `apex doctor`
|
|
147
149
|
- `apex login`
|
|
148
150
|
- `apex logout`
|
|
149
|
-
- `apex setup [all|codex|claude]`
|
|
151
|
+
- `apex setup [all|codex|claude|copilot]`
|
|
150
152
|
- `apex update`
|
|
151
153
|
- `apex connect github`
|
|
152
154
|
- `apex connect gitlab`
|
|
@@ -156,7 +158,7 @@ Helpful workspace flags:
|
|
|
156
158
|
- `--company <id-or-handle>` to choose the Apex company when more than one is available
|
|
157
159
|
- `--workspace-name <name>` to set the Apex workspace name for this directory
|
|
158
160
|
|
|
159
|
-
`apex credits` shows standard scan credits plus audit scan entitlements when the server returns them.
|
|
161
|
+
`apex credits` shows standard scan credits plus audit and fix review scan entitlements when the server returns them.
|
|
160
162
|
|
|
161
163
|
## Finding Review Feedback
|
|
162
164
|
|
|
@@ -174,7 +176,7 @@ Finding review collaboration now has explicit write commands:
|
|
|
174
176
|
|
|
175
177
|
Identifiers such as `KERN2-25` are resolved against the selected or latest scan for the current workspace binding. Pass `--scan <scan-id>` when you need a specific scan, or pass the finding UUID directly to skip workspace-based resolution.
|
|
176
178
|
|
|
177
|
-
Finding comments, valid/invalid feedback, and fix review scan starts
|
|
179
|
+
Finding comments, valid/invalid feedback, and fix review scan starts use the same Apex login credentials as read commands. In MCP clients, `apex-auth-start` followed by `apex-auth-wait` is enough to authenticate these write tools.
|
|
178
180
|
|
|
179
181
|
Invalid feedback requires a dismissal reason. Valid feedback can include `--suggested-severity extreme|critical|high|medium|low|informational`.
|
|
180
182
|
|
|
@@ -185,10 +187,7 @@ Fix review scans use a two-step callback flow for agents that create a PR outsid
|
|
|
185
187
|
|
|
186
188
|
The matching MCP flow is `apex-finding-feedback` with `status: "valid"`, `labels: ["fixed"]`, and `fixPrUrls`, followed by `apex-finding-fix-review`.
|
|
187
189
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
- read operations such as `apex findings`, `apex export findings`, and `apex-findings` use the Apex CLI device-login bearer token
|
|
191
|
-
- finding comments and feedback currently go through the Cantina web-app routes and require `CANTINA_AUTH_TOKEN`
|
|
190
|
+
The matching CLI and MCP flows intentionally use the same device-login session so agents should not ask users to paste browser cookies or auth tokens.
|
|
192
191
|
|
|
193
192
|
## Local Source Scans
|
|
194
193
|
|
|
@@ -222,7 +221,7 @@ If Apex is installed globally, prefer:
|
|
|
222
221
|
apex setup
|
|
223
222
|
```
|
|
224
223
|
|
|
225
|
-
That registers Apex for installed Codex
|
|
224
|
+
That registers Apex for installed Codex CLI, Claude Code, and GitHub Copilot CLI clients automatically.
|
|
226
225
|
|
|
227
226
|
If you want to wire clients manually instead, Apex ships a stable `apex-mcp` binary. For Codex:
|
|
228
227
|
|
|
@@ -236,6 +235,12 @@ For Claude Code:
|
|
|
236
235
|
claude mcp add --scope user apex -- apex-mcp
|
|
237
236
|
```
|
|
238
237
|
|
|
238
|
+
For GitHub Copilot CLI:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
copilot mcp add apex --type stdio --tools "*" -- apex-mcp
|
|
242
|
+
```
|
|
243
|
+
|
|
239
244
|
For any other MCP client, configure it to launch:
|
|
240
245
|
|
|
241
246
|
```json
|
|
@@ -288,7 +293,7 @@ The MCP server exposes Apex-specific tools for:
|
|
|
288
293
|
|
|
289
294
|
For repository-scoped operations, pass `cwd` explicitly so the server can resolve the right `.apex/workspace.json` binding and repository roots.
|
|
290
295
|
|
|
291
|
-
For Codex-style clients, the packaged skill can be installed with `apex setup codex`. The repo-local source lives at `skills/apex-cli/SKILL.md`.
|
|
296
|
+
For Codex-style clients, the packaged skill can be installed with `apex setup codex`. For GitHub Copilot CLI, the same skill is installed into `~/.copilot/skills/apex-cli` with `apex setup copilot`. The repo-local source lives at `skills/apex-cli/SKILL.md`.
|
|
292
297
|
|
|
293
298
|
For Claude Code, the packaged project skill can be installed into the current repository with `apex setup claude`. The repo-local source lives at `.claude/skills/apex-cli/SKILL.md`. Anthropic documents project skills as filesystem directories under `.claude/skills/<name>/SKILL.md`, and the Claude Agent SDK uses the same location when the `Skill` tool is enabled.
|
|
294
299
|
|
|
@@ -300,7 +305,7 @@ The npm package also includes marketplace-ready plugin artifacts:
|
|
|
300
305
|
- `.claude-plugin/plugin.json` and `.mcp.claude.json` for Claude Code plugin installs
|
|
301
306
|
- `.claude-plugin/marketplace.json` for a Claude marketplace entry backed by the public npm package
|
|
302
307
|
|
|
303
|
-
These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
308
|
+
These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.10 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
|
|
304
309
|
|
|
305
310
|
The repository also includes `.agents/plugins/marketplace.json` for local Codex marketplace testing from a checkout.
|
|
306
311
|
|
|
@@ -328,11 +333,11 @@ The CLI uses the Apex `/api/cli/v2/**` local-source routes for scan planning and
|
|
|
328
333
|
- `~/.config/apex/credentials.json`
|
|
329
334
|
- `.apex/workspace.json`
|
|
330
335
|
|
|
331
|
-
If a scan is already running in the current workspace, `apex scan` and `/scan` now require confirmation before starting another one. Scripted usage can opt in explicitly with `--force`.
|
|
336
|
+
If a scan is already running in the current workspace, `apex scan` and `/scan` now require confirmation before starting another one. Scripted usage can opt in explicitly with `--force`. Active-looking workspace scan rows are checked against the scan progress endpoint before the CLI treats them as blockers, so stale list entries do not hide terminal states such as `cancelled`.
|
|
332
337
|
|
|
333
338
|
To move between existing Apex workspaces from the CLI:
|
|
334
339
|
|
|
335
340
|
1. Run `apex workspaces` to list the workspaces available to your active company.
|
|
336
341
|
2. Run `apex workspace use <workspace-name|workspace-prefix|workspace-id>` to bind the current directory.
|
|
337
342
|
3. If the workspace name contains spaces, quote it, for example `apex workspace use "Core Platform"`.
|
|
338
|
-
4. Use `apex scans`, `apex findings`, and `apex export findings` against that binding.
|
|
343
|
+
4. Use `apex scans`, `apex status --scan <scan-id>`, `apex findings`, and `apex export findings` against that binding.
|
package/dist/api-client.js
CHANGED
|
@@ -45,6 +45,10 @@ export class ApexApiClient {
|
|
|
45
45
|
async getCredentials() {
|
|
46
46
|
return loadCredentials();
|
|
47
47
|
}
|
|
48
|
+
async getAccessToken() {
|
|
49
|
+
const credentials = await this.refreshIfNeeded().catch(() => null);
|
|
50
|
+
return credentials?.accessToken ?? null;
|
|
51
|
+
}
|
|
48
52
|
async clearCredentials() {
|
|
49
53
|
await clearCredentials();
|
|
50
54
|
}
|
package/dist/commands.js
CHANGED
|
@@ -3,11 +3,11 @@ import { logout } from "./auth.js";
|
|
|
3
3
|
import { ApiError } from "./api-client.js";
|
|
4
4
|
import { openInBrowser } from "./browser.js";
|
|
5
5
|
import { loadConfig, saveConfig } from "./config.js";
|
|
6
|
-
import { createFindingComment, fetchScanExport, fetchScanFindings, isFindingUuid, normalizeFindingRef,
|
|
6
|
+
import { createFindingComment, fetchScanExport, fetchScanFindings, isFindingUuid, normalizeFindingRef, resolveFindingSelection, resolveScanSelection, startFindingFixReviewScan, submitFindingFeedback, } from "./findings.js";
|
|
7
7
|
import { prepareExplicitScanSources, supportsLegacyRemoteFlow, } from "./local-source-scan.js";
|
|
8
8
|
import { logLine, printJson, withLoadingIndicator } from "./output.js";
|
|
9
9
|
import { confirm } from "./prompt.js";
|
|
10
|
-
import { cancelScan, fetchWorkspaceScans, findMostRelevantActiveScan, getScanDisplayLabel, getScanDisplayId, getTrackedScanId, isActiveScanStatus, matchesScanId, selectDefaultScanToCancel, } from "./scan.js";
|
|
10
|
+
import { cancelScan, fetchScanProgress, fetchWorkspaceScans, findMostRelevantActiveScan, getScanDisplayLabel, getScanDisplayId, getTrackedScanId, isActiveScanStatus, matchesScanId, selectDefaultScanToCancel, } from "./scan.js";
|
|
11
11
|
import { chooseCompany, createWorkspaceBinding, ensureAuthenticated, remediateMissingConnections, resolveWorkspaceSelection, resolveWorkspaceSelectionLegacy, resolveWorkspaceAllowingMissingConnections, selectWorkspaceTarget, } from "./session.js";
|
|
12
12
|
import { createWorkspaceBindingFromSummary, fetchCompanyCredits, fetchCompanyWorkspaces, findWorkspaceByRef, } from "./workspaces.js";
|
|
13
13
|
import { loadWorkspaceBinding, saveWorkspaceBinding } from "./workspace-binding.js";
|
|
@@ -81,6 +81,29 @@ function formatAuditScanBalance(scanBalance) {
|
|
|
81
81
|
}
|
|
82
82
|
return `Audit scans: ${detailParts.join(", ")}`;
|
|
83
83
|
}
|
|
84
|
+
function formatFixReviewScanBalance(scanBalance) {
|
|
85
|
+
const purchased = getOptionalCount(scanBalance.fixReviewPurchased);
|
|
86
|
+
const used = getOptionalCount(scanBalance.fixReviewUsed);
|
|
87
|
+
const remaining = getOptionalCount(scanBalance.fixReviewRemaining);
|
|
88
|
+
const available = getOptionalCount(scanBalance.fixReviewAvailable) ??
|
|
89
|
+
remaining ??
|
|
90
|
+
(purchased !== null && used !== null ? Math.max(0, purchased - used) : null);
|
|
91
|
+
if (purchased === null && used === null && available === null) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const detailParts = [];
|
|
95
|
+
if (purchased !== null) {
|
|
96
|
+
detailParts.push(`${purchased} purchased`);
|
|
97
|
+
}
|
|
98
|
+
if (used !== null) {
|
|
99
|
+
detailParts.push(`${used} used`);
|
|
100
|
+
}
|
|
101
|
+
if (available !== null) {
|
|
102
|
+
const detailSuffix = detailParts.length ? ` (${detailParts.join(", ")})` : "";
|
|
103
|
+
return `Fix review scans: ${available} available${detailSuffix}`;
|
|
104
|
+
}
|
|
105
|
+
return `Fix review scans: ${detailParts.join(", ")}`;
|
|
106
|
+
}
|
|
84
107
|
function normalizeRequestedScanMode(value) {
|
|
85
108
|
if (value === "ultra" || value === "audit") {
|
|
86
109
|
return "audit";
|
|
@@ -450,6 +473,14 @@ export async function commandCredits(client, cwd, flags) {
|
|
|
450
473
|
if (redeemableAuditScans && redeemableAuditScans > 0) {
|
|
451
474
|
logLine(`Redeemable audit scans from standard credits: ${redeemableAuditScans}`, flags);
|
|
452
475
|
}
|
|
476
|
+
const fixReviewBalance = formatFixReviewScanBalance(payload.scanBalance);
|
|
477
|
+
if (fixReviewBalance) {
|
|
478
|
+
logLine(fixReviewBalance, flags);
|
|
479
|
+
}
|
|
480
|
+
const redeemableFixReviewScans = getOptionalCount(payload.scanBalance.fixReviewRedeemableFromCredits);
|
|
481
|
+
if (redeemableFixReviewScans && redeemableFixReviewScans > 0) {
|
|
482
|
+
logLine(`Redeemable fix review scans from standard credits: ${redeemableFixReviewScans}`, flags);
|
|
483
|
+
}
|
|
453
484
|
logLine(`Scans enabled: ${payload.scansEnabled ? "yes" : "no"}`, flags);
|
|
454
485
|
return payload;
|
|
455
486
|
}
|
|
@@ -507,24 +538,26 @@ export async function commandDoctor(client, cwd, flags) {
|
|
|
507
538
|
export async function commandStatus(client, cwd, flags) {
|
|
508
539
|
await ensureAuthenticated(client, flags);
|
|
509
540
|
const binding = await requireWorkspaceBinding(cwd);
|
|
510
|
-
|
|
541
|
+
const requestedScanId = getFlagString(flags, "scan");
|
|
542
|
+
const scanId = requestedScanId ?? binding.lastScanId;
|
|
543
|
+
if (!scanId) {
|
|
511
544
|
throw new Error("No scan has been started from this directory yet. Run `apex scan` first.");
|
|
512
545
|
}
|
|
513
|
-
const progress = await withLoadingIndicator("Loading scan status...", flags, () => client
|
|
546
|
+
const progress = await withLoadingIndicator("Loading scan status...", flags, () => fetchScanProgress(client, scanId));
|
|
514
547
|
if (isJsonMode(flags)) {
|
|
515
|
-
printJson({ binding, progress });
|
|
516
|
-
return { binding, progress };
|
|
548
|
+
printJson({ binding, scanId, progress });
|
|
549
|
+
return { binding, scanId, progress };
|
|
517
550
|
}
|
|
518
551
|
logLine(`Workspace: ${binding.workspaceName}`, flags);
|
|
519
|
-
logLine(`Scan: ${
|
|
552
|
+
logLine(`Scan: ${scanId}`, flags);
|
|
520
553
|
logLine(`Status: ${progress.progress?.status ?? "unknown"}`, flags);
|
|
521
554
|
if (typeof progress.progress?.progressPct === "number") {
|
|
522
555
|
logLine(`Progress: ${progress.progress.progressPct}%`, flags);
|
|
523
556
|
}
|
|
524
|
-
if (binding.lastScanUrl) {
|
|
557
|
+
if (binding.lastScanUrl && (!requestedScanId || requestedScanId === binding.lastScanId)) {
|
|
525
558
|
logLine(`View: ${binding.lastScanUrl}`, flags);
|
|
526
559
|
}
|
|
527
|
-
return { binding, progress };
|
|
560
|
+
return { binding, scanId, progress };
|
|
528
561
|
}
|
|
529
562
|
export async function commandConnect(client, cwd, flags, provider) {
|
|
530
563
|
const me = await ensureAuthenticated(client, flags);
|
|
@@ -923,7 +956,6 @@ export async function commandFindingComment(client, cwd, flags, requestedFinding
|
|
|
923
956
|
if (!trimmedContent) {
|
|
924
957
|
throw new Error("Usage: apex findings comment <finding-id|finding-identifier> --content \"<markdown>\"");
|
|
925
958
|
}
|
|
926
|
-
requireCantinaAuthToken();
|
|
927
959
|
const currentBinding = await loadWorkspaceBinding(cwd);
|
|
928
960
|
const needsWorkspaceResolution = !isFindingUuid(findingRef);
|
|
929
961
|
const binding = needsWorkspaceResolution
|
|
@@ -998,7 +1030,6 @@ export async function commandFindingFeedback(client, cwd, flags, params) {
|
|
|
998
1030
|
if (fixPrUrls.length > 0 && labels?.[0] !== "fixed") {
|
|
999
1031
|
throw new Error("Fix PR URLs require the fixed feedback label.");
|
|
1000
1032
|
}
|
|
1001
|
-
requireCantinaAuthToken();
|
|
1002
1033
|
const currentBinding = await loadWorkspaceBinding(cwd);
|
|
1003
1034
|
const needsWorkspaceResolution = !isFindingUuid(findingRef);
|
|
1004
1035
|
const binding = needsWorkspaceResolution
|
|
@@ -1063,7 +1094,6 @@ export async function commandFindingFeedback(client, cwd, flags, params) {
|
|
|
1063
1094
|
}
|
|
1064
1095
|
export async function commandFindingFixReview(client, cwd, flags, requestedFindingRef) {
|
|
1065
1096
|
const findingRef = normalizeFindingRefInput(requestedFindingRef);
|
|
1066
|
-
requireCantinaAuthToken();
|
|
1067
1097
|
const currentBinding = await loadWorkspaceBinding(cwd);
|
|
1068
1098
|
const needsWorkspaceResolution = !isFindingUuid(findingRef);
|
|
1069
1099
|
const binding = needsWorkspaceResolution
|
package/dist/findings.js
CHANGED
|
@@ -2,7 +2,7 @@ import { fetchWorkspaceScans, getScanDisplayId, matchesScanId, } from "./scan.js
|
|
|
2
2
|
import { ApiError } from "./api-client.js";
|
|
3
3
|
const FINDING_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4
4
|
const CANTINA_AUTH_TOKEN_ENV = "CANTINA_AUTH_TOKEN";
|
|
5
|
-
const
|
|
5
|
+
const FINDING_WRITE_AUTH_ERROR = "Finding comments, feedback, and fix review scans require Apex authentication. Run `apex login` or use MCP `apex-auth-start` followed by `apex-auth-wait`, then retry.";
|
|
6
6
|
function parsePositiveInt(value) {
|
|
7
7
|
if (!value)
|
|
8
8
|
return null;
|
|
@@ -32,11 +32,8 @@ function normalizeCantinaAuthToken(value) {
|
|
|
32
32
|
}
|
|
33
33
|
return token.length > 0 ? token : null;
|
|
34
34
|
}
|
|
35
|
-
export function
|
|
35
|
+
export function getCantinaAuthToken() {
|
|
36
36
|
const token = normalizeCantinaAuthToken(process.env[CANTINA_AUTH_TOKEN_ENV]);
|
|
37
|
-
if (!token) {
|
|
38
|
-
throw new Error(CANTINA_AUTH_TOKEN_ERROR);
|
|
39
|
-
}
|
|
40
37
|
return token;
|
|
41
38
|
}
|
|
42
39
|
export function isFindingUuid(value) {
|
|
@@ -63,22 +60,68 @@ function matchesFindingRef(finding, findingRef) {
|
|
|
63
60
|
.some((value) => value.trim().toLowerCase() === normalizedRef);
|
|
64
61
|
}
|
|
65
62
|
async function requestFindingAppRoute(client, path, options = {}) {
|
|
66
|
-
const token = requireCantinaAuthToken();
|
|
67
63
|
const baseUrl = await client.getBaseUrl();
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
const accessToken = await client.getAccessToken();
|
|
65
|
+
const legacyCookieToken = getCantinaAuthToken();
|
|
66
|
+
let attemptedBearerAuth = false;
|
|
67
|
+
if (!accessToken && !legacyCookieToken) {
|
|
68
|
+
throw new Error(FINDING_WRITE_AUTH_ERROR);
|
|
69
|
+
}
|
|
70
|
+
const send = async (auth) => {
|
|
71
|
+
const headers = new Headers(options.headers ?? {});
|
|
72
|
+
headers.set("Accept", "application/json");
|
|
73
|
+
if (options.json !== undefined) {
|
|
74
|
+
headers.set("Content-Type", "application/json");
|
|
75
|
+
}
|
|
76
|
+
if (auth.type === "bearer") {
|
|
77
|
+
headers.set("Authorization", `Bearer ${auth.token}`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
headers.set("Cookie", `auth_token=${auth.token}`);
|
|
81
|
+
}
|
|
82
|
+
return fetch(new URL(path, baseUrl), {
|
|
83
|
+
...options,
|
|
84
|
+
headers,
|
|
85
|
+
body: options.json !== undefined ? JSON.stringify(options.json) : options.body,
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const sendAndParse = async (auth) => {
|
|
89
|
+
if (auth.type === "bearer") {
|
|
90
|
+
attemptedBearerAuth = true;
|
|
91
|
+
}
|
|
92
|
+
const response = await send(auth);
|
|
93
|
+
const body = await parseWebRouteResponse(response);
|
|
94
|
+
return { response, body };
|
|
95
|
+
};
|
|
96
|
+
const primaryAuth = accessToken
|
|
97
|
+
? { type: "bearer", token: accessToken }
|
|
98
|
+
: { type: "cookie", token: legacyCookieToken };
|
|
99
|
+
let { response, body } = await sendAndParse(primaryAuth);
|
|
100
|
+
if (!response.ok && response.status === 401 && primaryAuth.type === "bearer") {
|
|
101
|
+
const refreshedToken = await client
|
|
102
|
+
.refreshSession()
|
|
103
|
+
.then((credentials) => credentials?.accessToken ?? null)
|
|
104
|
+
.catch(() => null);
|
|
105
|
+
if (refreshedToken) {
|
|
106
|
+
({ response, body } = await sendAndParse({
|
|
107
|
+
type: "bearer",
|
|
108
|
+
token: refreshedToken,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!response.ok && response.status === 401 && primaryAuth.type === "bearer" && legacyCookieToken) {
|
|
113
|
+
({ response, body } = await sendAndParse({
|
|
114
|
+
type: "cookie",
|
|
115
|
+
token: legacyCookieToken,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
81
118
|
if (!response.ok) {
|
|
119
|
+
if (response.status === 401) {
|
|
120
|
+
if (attemptedBearerAuth) {
|
|
121
|
+
await client.clearCredentials();
|
|
122
|
+
}
|
|
123
|
+
throw new Error(FINDING_WRITE_AUTH_ERROR);
|
|
124
|
+
}
|
|
82
125
|
throw new ApiError(`Request failed with ${response.status}`, response.status, body);
|
|
83
126
|
}
|
|
84
127
|
return body;
|
package/dist/help.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const CLI_HELP_TEXT = `Usage:
|
|
2
2
|
apex Open the interactive Apex shell
|
|
3
|
-
apex credits Show scan credits
|
|
3
|
+
apex credits Show scan credits plus audit and fix review scan entitlements for the active company
|
|
4
4
|
apex scan Create or resolve a workspace for this directory and start a scan
|
|
5
5
|
apex scans List scans for the current workspace binding
|
|
6
6
|
apex findings List findings for the latest or selected scan
|
|
@@ -16,12 +16,13 @@ export const CLI_HELP_TEXT = `Usage:
|
|
|
16
16
|
apex workspace use <workspace-name|workspace-prefix|workspace-id>
|
|
17
17
|
Bind this directory to an existing Apex workspace
|
|
18
18
|
apex cancel-scan [scan-id] Cancel a running scan
|
|
19
|
-
apex status Show the latest
|
|
19
|
+
apex status Show progress for the latest or selected scan
|
|
20
20
|
apex doctor Validate auth, repos, connections, and workspace binding
|
|
21
21
|
apex login Sign in to Apex
|
|
22
22
|
apex logout Sign out locally
|
|
23
23
|
apex mcp Start the Apex MCP server over stdio
|
|
24
|
-
apex setup [all|codex|claude]
|
|
24
|
+
apex setup [all|codex|claude|copilot]
|
|
25
|
+
Configure Apex for Codex, Claude Code, and GitHub Copilot CLI
|
|
25
26
|
apex update Update the local Apex CLI install
|
|
26
27
|
apex connect github Open the GitHub connection flow
|
|
27
28
|
apex connect gitlab Open the GitLab connection flow
|
|
@@ -29,7 +30,7 @@ export const CLI_HELP_TEXT = `Usage:
|
|
|
29
30
|
Flags:
|
|
30
31
|
--company <id-or-handle> Choose the Apex company to use
|
|
31
32
|
--workspace-name <name> Set the Apex workspace name for this directory
|
|
32
|
-
--scan <scan-id> Select a specific scan for findings or export
|
|
33
|
+
--scan <scan-id> Select a specific scan for status, findings, or export
|
|
33
34
|
--status valid|invalid Set the finding feedback status explicitly
|
|
34
35
|
--content <markdown> Supply comment content for apex findings comment
|
|
35
36
|
--comment <markdown> Supply rationale for apex findings feedback
|
|
@@ -61,7 +62,7 @@ Tips:
|
|
|
61
62
|
PR scans require --mode pr and at least one --pr selection for a GitHub repository.
|
|
62
63
|
audit is the current scan mode for audit scans; ultra remains accepted as a legacy alias.
|
|
63
64
|
apex workspace use accepts a workspace name, prefix, or ID.
|
|
64
|
-
Finding comments, feedback, and fix review scans
|
|
65
|
+
Finding comments, feedback, and fix review scans use the same Apex login credentials as other CLI commands.
|
|
65
66
|
Invalid finding feedback requires --dismissal-reason.
|
|
66
67
|
Fix review scans require valid feedback with --label fixed and at least one --fix-pr-url, then apex findings fix-review.
|
|
67
68
|
Finding identifiers such as KERN2-25 resolve against the selected scan; pass --scan or use the finding UUID directly when needed.
|
|
@@ -71,7 +72,7 @@ Tips:
|
|
|
71
72
|
export const SHELL_HELP_TEXT = `Press Tab to autocomplete commands and common arguments.
|
|
72
73
|
|
|
73
74
|
Commands:
|
|
74
|
-
/credits Show scan credits
|
|
75
|
+
/credits Show scan credits plus audit and fix review scan entitlements for the active company
|
|
75
76
|
/scan [standard|audit] Start a new Apex scan for this workspace
|
|
76
77
|
/scan pr <pr-number> Start a PR scan for this workspace
|
|
77
78
|
/scans List scans for this workspace
|
|
@@ -87,7 +88,7 @@ Commands:
|
|
|
87
88
|
/export [scan-id] Export findings for the latest or selected scan
|
|
88
89
|
/workspaces List accessible workspaces for the active company
|
|
89
90
|
/cancel-scan [scan-id] Cancel a running or most recent scan
|
|
90
|
-
/status
|
|
91
|
+
/status [scan-id] Show progress for the most recent or selected scan
|
|
91
92
|
/doctor Validate auth, repos, connections, and workspace binding
|
|
92
93
|
/update Update the local Apex CLI install and exit the shell
|
|
93
94
|
/logout Sign out locally and exit the shell
|
|
@@ -107,7 +108,7 @@ Tips:
|
|
|
107
108
|
PR scans require a GitHub PR number and provider-backed repository access.
|
|
108
109
|
audit is the current scan mode for audit scans; ultra remains accepted as a legacy alias.
|
|
109
110
|
/workspace use accepts a workspace name, prefix, or ID.
|
|
110
|
-
/findings comment, /findings feedback, and /findings fix-review
|
|
111
|
+
/findings comment, /findings feedback, and /findings fix-review use the current Apex login.
|
|
111
112
|
Invalid finding feedback requires a dismissal reason.
|
|
112
113
|
Fix review scans require fixed valid feedback with a Fix PR URL. Use scripted CLI or MCP for attaching Fix PR URLs.
|
|
113
114
|
Use scripted CLI flags for advanced feedback options such as suggested severity or dismissal reason.
|
package/dist/mcp.js
CHANGED
|
@@ -22,6 +22,7 @@ Suggested workflow:
|
|
|
22
22
|
Notes:
|
|
23
23
|
- Pass cwd explicitly for repository-specific operations.
|
|
24
24
|
- Tool calls are always non-interactive and never auto-open a browser.
|
|
25
|
+
- Finding write tools use the same Apex device-login credentials as read tools.
|
|
25
26
|
- Use mode "audit" for audit scans; "ultra" remains accepted as a legacy alias. Use mode "pr" for GitHub pull request scans.
|
|
26
27
|
- Doctor reports whether Apex will use remote materialization or a local snapshot upload for each source.`;
|
|
27
28
|
const WORKSPACE_BINDING_ERROR = "This directory is not bound to an Apex workspace yet. Run `apex workspaces` to list workspace names, prefixes, and IDs, then bind one with `apex workspace use \"<workspace name>\"`, or run `apex scan` to create or resolve one automatically.";
|
|
@@ -270,7 +271,7 @@ function registerTools(server) {
|
|
|
270
271
|
}, (value) => `Apex doctor completed for ${String(value.cwd)}.`));
|
|
271
272
|
server.registerTool("apex-credits", {
|
|
272
273
|
title: "Get Apex Credits",
|
|
273
|
-
description: "Show scan credits
|
|
274
|
+
description: "Show scan credits plus audit and fix review scan entitlements for the active or selected company.",
|
|
274
275
|
inputSchema: {
|
|
275
276
|
cwd: z.string().optional(),
|
|
276
277
|
company: z.string().optional(),
|
|
@@ -380,15 +381,16 @@ function registerTools(server) {
|
|
|
380
381
|
}, (value) => `Started an Apex scan for ${String(value.cwd)}.`));
|
|
381
382
|
server.registerTool("apex-status", {
|
|
382
383
|
title: "Get Apex Scan Status",
|
|
383
|
-
description: "Show progress for the most recent Apex scan in a directory.",
|
|
384
|
+
description: "Show progress for the most recent or selected Apex scan in a directory.",
|
|
384
385
|
inputSchema: {
|
|
385
386
|
cwd: z.string().optional(),
|
|
387
|
+
scanId: z.string().optional(),
|
|
386
388
|
},
|
|
387
|
-
}, async ({ cwd }) => runTool("apex-status", async () => {
|
|
389
|
+
}, async ({ cwd, scanId }) => runTool("apex-status", async () => {
|
|
388
390
|
const client = new ApexApiClient();
|
|
389
391
|
await requireAuthenticated(client);
|
|
390
392
|
const targetCwd = resolveCwd(cwd);
|
|
391
|
-
const payload = await commandStatus(client, targetCwd, buildFlags({}));
|
|
393
|
+
const payload = await commandStatus(client, targetCwd, buildFlags({ scanId }));
|
|
392
394
|
return {
|
|
393
395
|
cwd: targetCwd,
|
|
394
396
|
...payload,
|
|
@@ -450,7 +452,7 @@ function registerTools(server) {
|
|
|
450
452
|
}, (value) => `Fetched Apex findings for ${String(value.cwd)}.`));
|
|
451
453
|
server.registerTool("apex-finding-comment", {
|
|
452
454
|
title: "Add Apex Finding Comment",
|
|
453
|
-
description: "Add a comment or note to an Apex finding
|
|
455
|
+
description: "Add a comment or note to an Apex finding using the current Apex login.",
|
|
454
456
|
inputSchema: {
|
|
455
457
|
cwd: z.string().optional(),
|
|
456
458
|
findingRef: z.string(),
|
|
@@ -471,7 +473,7 @@ function registerTools(server) {
|
|
|
471
473
|
}, (value) => `Added a finding comment for ${String(value.findingRef)}.`));
|
|
472
474
|
server.registerTool("apex-finding-feedback", {
|
|
473
475
|
title: "Leave Apex Finding Feedback",
|
|
474
|
-
description: "Leave valid or invalid feedback on an Apex finding. To attach a fix PR, send status valid with label fixed and fixPrUrls.
|
|
476
|
+
description: "Leave valid or invalid feedback on an Apex finding using the current Apex login. To attach a fix PR, send status valid with label fixed and fixPrUrls.",
|
|
475
477
|
inputSchema: {
|
|
476
478
|
cwd: z.string().optional(),
|
|
477
479
|
findingRef: z.string(),
|
|
@@ -509,7 +511,7 @@ function registerTools(server) {
|
|
|
509
511
|
"finding"))} feedback for ${String(value.findingRef)}.`));
|
|
510
512
|
server.registerTool("apex-finding-fix-review", {
|
|
511
513
|
title: "Start Apex Finding Fix Review Scan",
|
|
512
|
-
description: "Start a fix review scan for a finding after fixed feedback with one or more Fix PR URLs has been saved.
|
|
514
|
+
description: "Start a fix review scan for a finding after fixed feedback with one or more Fix PR URLs has been saved.",
|
|
513
515
|
inputSchema: {
|
|
514
516
|
cwd: z.string().optional(),
|
|
515
517
|
findingRef: z.string(),
|
package/dist/scan.js
CHANGED
|
@@ -121,6 +121,33 @@ export function normalizeWorkspaceScans(payload) {
|
|
|
121
121
|
export function isActiveScanStatus(status) {
|
|
122
122
|
return status ? ACTIVE_SCAN_STATUSES.has(status.trim().toLowerCase()) : false;
|
|
123
123
|
}
|
|
124
|
+
function mergeScanProgress(scan, progress) {
|
|
125
|
+
const status = readString(progress.progress?.status) ?? scan.status;
|
|
126
|
+
const startedAt = progress.progress?.startedAt ?? scan.startedAt;
|
|
127
|
+
const finishedAt = !isActiveScanStatus(status) && !scan.finishedAt
|
|
128
|
+
? scan.updatedAt
|
|
129
|
+
: scan.finishedAt;
|
|
130
|
+
return {
|
|
131
|
+
...scan,
|
|
132
|
+
kernelScanId: progress.kernelScanId ?? scan.kernelScanId,
|
|
133
|
+
status,
|
|
134
|
+
startedAt,
|
|
135
|
+
finishedAt,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function reconcileActiveScanStatus(client, scan) {
|
|
139
|
+
if (!isActiveScanStatus(scan.status)) {
|
|
140
|
+
return scan;
|
|
141
|
+
}
|
|
142
|
+
let progress;
|
|
143
|
+
try {
|
|
144
|
+
progress = await fetchScanProgress(client, getScanDisplayId(scan));
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return scan;
|
|
148
|
+
}
|
|
149
|
+
return mergeScanProgress(scan, progress);
|
|
150
|
+
}
|
|
124
151
|
export function findMostRelevantActiveScan(scans) {
|
|
125
152
|
return scans.find((scan) => isActiveScanStatus(scan.status)) ?? null;
|
|
126
153
|
}
|
|
@@ -148,7 +175,11 @@ export async function fetchWorkspaceScans(client, workspaceId) {
|
|
|
148
175
|
{ path: `/api/workspaces/${encodedWorkspaceId}/scans` },
|
|
149
176
|
{ path: `/api/scans?workspaceId=${encodedWorkspaceId}` },
|
|
150
177
|
]);
|
|
151
|
-
|
|
178
|
+
const scans = normalizeWorkspaceScans(payload);
|
|
179
|
+
return Promise.all(scans.map((scan) => reconcileActiveScanStatus(client, scan)));
|
|
180
|
+
}
|
|
181
|
+
export async function fetchScanProgress(client, scanId) {
|
|
182
|
+
return client.request(`/api/cli/v1/scans/${encodeURIComponent(scanId)}/progress`);
|
|
152
183
|
}
|
|
153
184
|
export async function cancelScan(client, scanId) {
|
|
154
185
|
const encodedScanId = encodeURIComponent(scanId);
|
package/dist/setup.js
CHANGED
|
@@ -10,6 +10,7 @@ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
|
|
|
10
10
|
const CODEX_SKILL_NAME = "apex-cli";
|
|
11
11
|
const MCP_SERVER_NAME = "apex";
|
|
12
12
|
const execFile = promisify(execFileCallback);
|
|
13
|
+
const SETUP_CLIENTS = ["codex", "claude", "copilot"];
|
|
13
14
|
function quoteShellArg(value) {
|
|
14
15
|
return /[^A-Za-z0-9_./:-]/.test(value)
|
|
15
16
|
? `'${value.replace(/'/g, `'\\''`)}'`
|
|
@@ -73,9 +74,28 @@ function getCodexHome() {
|
|
|
73
74
|
}
|
|
74
75
|
return path.join(os.homedir(), ".codex");
|
|
75
76
|
}
|
|
77
|
+
function getCopilotHome() {
|
|
78
|
+
const configured = process.env.COPILOT_HOME?.trim();
|
|
79
|
+
if (configured) {
|
|
80
|
+
return configured;
|
|
81
|
+
}
|
|
82
|
+
return path.join(os.homedir(), ".copilot");
|
|
83
|
+
}
|
|
76
84
|
function normalizeArgs(value) {
|
|
77
85
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
78
86
|
}
|
|
87
|
+
function normalizeTools(value) {
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
return value.filter((item) => typeof item === "string");
|
|
90
|
+
}
|
|
91
|
+
if (typeof value === "string") {
|
|
92
|
+
return value
|
|
93
|
+
.split(",")
|
|
94
|
+
.map((item) => item.trim())
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
79
99
|
function codexConfigMatches(existing, launch) {
|
|
80
100
|
if (!existing?.transport) {
|
|
81
101
|
return false;
|
|
@@ -92,6 +112,17 @@ function claudeConfigMatches(existing, launch) {
|
|
|
92
112
|
JSON.stringify(normalizeArgs(existing.args)) === JSON.stringify(launch.args) &&
|
|
93
113
|
normalizedEnv === 0);
|
|
94
114
|
}
|
|
115
|
+
function copilotConfigMatches(existing, launch) {
|
|
116
|
+
const env = existing?.env;
|
|
117
|
+
const normalizedEnv = env && typeof env === "object" && !Array.isArray(env) ? Object.keys(env).length : 0;
|
|
118
|
+
const normalizedTools = normalizeTools(existing?.tools);
|
|
119
|
+
return ((existing?.type === "stdio" || existing?.type === "local") &&
|
|
120
|
+
existing.command === launch.command &&
|
|
121
|
+
JSON.stringify(normalizeArgs(existing.args)) === JSON.stringify(launch.args) &&
|
|
122
|
+
normalizedEnv === 0 &&
|
|
123
|
+
normalizedTools.length === 1 &&
|
|
124
|
+
normalizedTools[0] === "*");
|
|
125
|
+
}
|
|
95
126
|
async function writeManagedFile(filePath, content) {
|
|
96
127
|
const current = await readTextFile(filePath);
|
|
97
128
|
if (current === content) {
|
|
@@ -127,6 +158,20 @@ async function readClaudeUserMcpConfig() {
|
|
|
127
158
|
return null;
|
|
128
159
|
}
|
|
129
160
|
}
|
|
161
|
+
async function readCopilotUserMcpConfig() {
|
|
162
|
+
const configPath = path.join(getCopilotHome(), "mcp-config.json");
|
|
163
|
+
const raw = await readTextFile(configPath);
|
|
164
|
+
if (!raw) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const parsed = JSON.parse(raw);
|
|
169
|
+
return parsed.mcpServers?.[MCP_SERVER_NAME] ?? null;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
130
175
|
async function configureCodex(launch) {
|
|
131
176
|
const existing = await readCodexMcpConfig();
|
|
132
177
|
const mcpStatus = existing === null
|
|
@@ -197,6 +242,49 @@ async function configureClaude(cwd, launch) {
|
|
|
197
242
|
},
|
|
198
243
|
];
|
|
199
244
|
}
|
|
245
|
+
async function configureCopilot(launch) {
|
|
246
|
+
const existing = await readCopilotUserMcpConfig();
|
|
247
|
+
const mcpStatus = existing === null
|
|
248
|
+
? "installed"
|
|
249
|
+
: copilotConfigMatches(existing, launch)
|
|
250
|
+
? "unchanged"
|
|
251
|
+
: "updated";
|
|
252
|
+
if (existing) {
|
|
253
|
+
await execText("copilot", ["mcp", "remove", MCP_SERVER_NAME]);
|
|
254
|
+
}
|
|
255
|
+
await execText("copilot", [
|
|
256
|
+
"mcp",
|
|
257
|
+
"add",
|
|
258
|
+
MCP_SERVER_NAME,
|
|
259
|
+
"--type",
|
|
260
|
+
"stdio",
|
|
261
|
+
"--tools",
|
|
262
|
+
"*",
|
|
263
|
+
"--",
|
|
264
|
+
launch.command,
|
|
265
|
+
...launch.args,
|
|
266
|
+
]);
|
|
267
|
+
const skillSource = path.join(PACKAGE_ROOT, "skills", CODEX_SKILL_NAME, "SKILL.md");
|
|
268
|
+
const skillTarget = path.join(getCopilotHome(), "skills", CODEX_SKILL_NAME, "SKILL.md");
|
|
269
|
+
const skillContent = await readFile(skillSource, "utf8");
|
|
270
|
+
const skillStatus = await writeManagedFile(skillTarget, skillContent);
|
|
271
|
+
return [
|
|
272
|
+
{
|
|
273
|
+
client: "copilot",
|
|
274
|
+
kind: "mcp",
|
|
275
|
+
status: mcpStatus,
|
|
276
|
+
path: path.join(getCopilotHome(), "mcp-config.json"),
|
|
277
|
+
detail: `Registered ${MCP_SERVER_NAME} -> ${launch.label} in GitHub Copilot CLI user config`,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
client: "copilot",
|
|
281
|
+
kind: "skill",
|
|
282
|
+
status: skillStatus,
|
|
283
|
+
path: skillTarget,
|
|
284
|
+
detail: "Installed the Apex GitHub Copilot CLI skill",
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
}
|
|
200
288
|
function summarizeStep(step) {
|
|
201
289
|
const prefix = `${step.client} ${step.kind}`;
|
|
202
290
|
if (step.path) {
|
|
@@ -204,29 +292,51 @@ function summarizeStep(step) {
|
|
|
204
292
|
}
|
|
205
293
|
return `${prefix}: ${step.status}`;
|
|
206
294
|
}
|
|
295
|
+
function isSetupClient(value) {
|
|
296
|
+
return SETUP_CLIENTS.includes(value);
|
|
297
|
+
}
|
|
298
|
+
function displayClientName(client) {
|
|
299
|
+
switch (client) {
|
|
300
|
+
case "codex":
|
|
301
|
+
return "Codex";
|
|
302
|
+
case "claude":
|
|
303
|
+
return "Claude Code";
|
|
304
|
+
case "copilot":
|
|
305
|
+
return "GitHub Copilot CLI";
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function isMissingClientError(error) {
|
|
309
|
+
return error instanceof Error && /spawn (codex|claude|copilot) ENOENT/i.test(error.message);
|
|
310
|
+
}
|
|
311
|
+
async function configureClient(client, cwd, launch) {
|
|
312
|
+
switch (client) {
|
|
313
|
+
case "codex":
|
|
314
|
+
return configureCodex(launch);
|
|
315
|
+
case "claude":
|
|
316
|
+
return configureClaude(cwd, launch);
|
|
317
|
+
case "copilot":
|
|
318
|
+
return configureCopilot(launch);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
207
321
|
export async function commandSetup(cwd, flags, requestedTarget, packageRoot = PACKAGE_ROOT) {
|
|
208
|
-
if (requestedTarget !== null &&
|
|
209
|
-
|
|
210
|
-
requestedTarget !== "codex" &&
|
|
211
|
-
requestedTarget !== "claude") {
|
|
212
|
-
throw new Error("Usage: apex setup [all|codex|claude]");
|
|
322
|
+
if (requestedTarget !== null && requestedTarget !== "all" && !isSetupClient(requestedTarget)) {
|
|
323
|
+
throw new Error("Usage: apex setup [all|codex|claude|copilot]");
|
|
213
324
|
}
|
|
214
325
|
const target = requestedTarget ?? "all";
|
|
215
326
|
const launch = await resolveMcpLaunchSpec(packageRoot);
|
|
216
327
|
const steps = [];
|
|
217
|
-
const requestedClients = target === "all" ? [
|
|
328
|
+
const requestedClients = target === "all" ? [...SETUP_CLIENTS] : [target];
|
|
218
329
|
let configuredClients = 0;
|
|
219
330
|
for (const client of requestedClients) {
|
|
220
331
|
try {
|
|
221
|
-
const clientSteps =
|
|
332
|
+
const clientSteps = await configureClient(client, cwd, launch);
|
|
222
333
|
steps.push(...clientSteps);
|
|
223
334
|
configuredClients += 1;
|
|
224
335
|
}
|
|
225
336
|
catch (error) {
|
|
226
|
-
const missingClient = error
|
|
227
|
-
/spawn (codex|claude) ENOENT/i.test(error.message);
|
|
337
|
+
const missingClient = isMissingClientError(error);
|
|
228
338
|
if (missingClient && target !== "all") {
|
|
229
|
-
throw new Error(`${client
|
|
339
|
+
throw new Error(`${displayClientName(client)} is not installed on this machine. Install it first, then re-run \`apex setup ${client}\`.`);
|
|
230
340
|
}
|
|
231
341
|
if (!missingClient) {
|
|
232
342
|
throw error;
|
|
@@ -236,19 +346,19 @@ export async function commandSetup(cwd, flags, requestedTarget, packageRoot = PA
|
|
|
236
346
|
kind: "mcp",
|
|
237
347
|
status: "skipped",
|
|
238
348
|
path: null,
|
|
239
|
-
detail: `${client} is not installed on this machine`,
|
|
349
|
+
detail: `${displayClientName(client)} is not installed on this machine`,
|
|
240
350
|
});
|
|
241
351
|
steps.push({
|
|
242
352
|
client,
|
|
243
353
|
kind: "skill",
|
|
244
354
|
status: "skipped",
|
|
245
355
|
path: null,
|
|
246
|
-
detail: `${client} is not installed on this machine`,
|
|
356
|
+
detail: `${displayClientName(client)} is not installed on this machine`,
|
|
247
357
|
});
|
|
248
358
|
}
|
|
249
359
|
}
|
|
250
360
|
if (configuredClients === 0) {
|
|
251
|
-
throw new Error("Neither Codex
|
|
361
|
+
throw new Error("Neither Codex, Claude Code, nor GitHub Copilot CLI is installed. Install one of them first, then re-run `apex setup`.");
|
|
252
362
|
}
|
|
253
363
|
const payload = {
|
|
254
364
|
target,
|
|
@@ -271,5 +381,8 @@ export async function commandSetup(cwd, flags, requestedTarget, packageRoot = PA
|
|
|
271
381
|
if (requestedClients.includes("claude")) {
|
|
272
382
|
logLine("Re-run `apex setup claude` in each repository where you want the Claude project skill.", flags);
|
|
273
383
|
}
|
|
384
|
+
if (requestedClients.includes("copilot")) {
|
|
385
|
+
logLine("Restart GitHub Copilot CLI or run `/skills reload` to pick up a newly installed or updated skill.", flags);
|
|
386
|
+
}
|
|
274
387
|
return payload;
|
|
275
388
|
}
|
package/dist/shell.js
CHANGED
|
@@ -208,7 +208,13 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
|
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
210
|
case "status":
|
|
211
|
-
|
|
211
|
+
if (parsed.args.length > 1) {
|
|
212
|
+
process.stderr.write("Usage: /status [scan-id]\n");
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
await commandStatus(client, cwd, parsed.args[0]
|
|
216
|
+
? withFlag(shellFlags, "scan", parsed.args[0])
|
|
217
|
+
: shellFlags);
|
|
212
218
|
return {};
|
|
213
219
|
case "scans":
|
|
214
220
|
await commandScans(client, cwd, shellFlags);
|
package/dist/update.js
CHANGED
|
@@ -456,7 +456,7 @@ export async function commandUpdate(flags, packageRoot = PACKAGE_ROOT) {
|
|
|
456
456
|
printJson(payload);
|
|
457
457
|
}
|
|
458
458
|
else {
|
|
459
|
-
logLine("Apex CLI updated. Re-run `apex` to use the latest version. Re-run `apex setup` to refresh copied Codex or
|
|
459
|
+
logLine("Apex CLI updated. Re-run `apex` to use the latest version. Re-run `apex setup` to refresh copied Codex, Claude Code, or GitHub Copilot CLI skill files.", flags);
|
|
460
460
|
}
|
|
461
461
|
return payload;
|
|
462
462
|
}
|
package/package.json
CHANGED
package/skills/apex-cli/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Use when a user wants to start Apex scans, inspect findings, bind w
|
|
|
5
5
|
|
|
6
6
|
# Apex CLI
|
|
7
7
|
|
|
8
|
-
This skill is bundled with Apex CLI and can be installed into Codex with `apex setup codex`.
|
|
8
|
+
This skill is bundled with Apex CLI and can be installed into Codex with `apex setup codex` or GitHub Copilot CLI with `apex setup copilot`.
|
|
9
9
|
|
|
10
10
|
Prefer the Apex MCP tools over running `apex` in the shell when the server is available.
|
|
11
11
|
|
|
@@ -24,7 +24,7 @@ Workflow:
|
|
|
24
24
|
Guidelines:
|
|
25
25
|
|
|
26
26
|
- Do not rely on interactive CLI prompts. The MCP tools are intentionally non-interactive.
|
|
27
|
-
- `apex-credits` reports standard credits
|
|
27
|
+
- `apex-credits` reports standard credits plus audit and fix review scan entitlements when Bedrock returns those balances.
|
|
28
28
|
- `apex-scan` scans the provided `cwd` by default; pass `repoPaths` only when the user asks to scan explicit alternate roots.
|
|
29
29
|
- `apex-doctor` reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
|
|
30
30
|
- Apex can scan plain local directories and dirty git worktrees without provider connections by using local snapshot uploads.
|
|
@@ -33,8 +33,9 @@ Guidelines:
|
|
|
33
33
|
- `apex-workspace-use` accepts a workspace name, prefix, or ID.
|
|
34
34
|
- Use `sourceMode: "remote"` only when the user explicitly wants to forbid local snapshot fallbacks.
|
|
35
35
|
- Use `force: true` on `apex-scan` only when the user explicitly wants to replace or overlap an active scan.
|
|
36
|
+
- When checking a scan that is not the workspace binding's latest scan, pass `scanId` to `apex-status`; use `apex-scans` first if you need to discover scan IDs.
|
|
36
37
|
- Prefer `apex-findings` for quick inspection and `apex-export-findings` when the user needs a file artifact.
|
|
37
|
-
- Finding comments, feedback, and fix review scan starts
|
|
38
|
+
- Finding comments, feedback, and fix review scan starts use the same Apex device-login credentials as read tools. If a write tool reports missing auth, re-run `apex-auth-status` and complete `apex-auth-start` / `apex-auth-wait` instead of asking for browser cookies or auth tokens.
|
|
38
39
|
- Invalid finding feedback requires `dismissalReason`; valid feedback can include `suggestedSeverity`, including `extreme`.
|
|
39
40
|
- Fix PR callback feedback requires valid feedback with `labels: ["fixed"]` and `fixPrUrls`; start the fix review scan with `apex-finding-fix-review` after saving that feedback.
|
|
40
41
|
- Finding identifiers such as `KERN2-25` resolve against the selected or latest scan for the current workspace binding. Pass an explicit scan when needed, or use the finding UUID directly.
|