@cantinasecurity/apex-cli 0.1.36 → 0.1.38

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/dist/session.js CHANGED
@@ -98,9 +98,16 @@ function selectBindingForWorkspaceName(flags, binding, workspaceName) {
98
98
  }
99
99
  function getSourceMode(flags) {
100
100
  const value = getFlagString(flags, "source-mode");
101
- return value === "remote" || value === "local" ? value : "auto";
101
+ if (value === null || value === "auto")
102
+ return "auto";
103
+ if (value === "remote" || value === "local")
104
+ return value;
105
+ throw new Error("--source-mode must be auto, remote, or local.");
102
106
  }
103
107
  function buildFallbackLocalArchivePlan(source, sourceMode) {
108
+ if (!source.absolutePath) {
109
+ throw new Error("A remote repository URL cannot use a local snapshot. Connect its provider or clone it locally, then retry.");
110
+ }
104
111
  const reason = sourceMode === "local"
105
112
  ? "user_requested_local_snapshot"
106
113
  : source.kind === "directory_candidate"
@@ -136,6 +143,11 @@ function buildFallbackLocalArchivePlan(source, sourceMode) {
136
143
  }
137
144
  function ensurePlannedSources(selection, resolve) {
138
145
  if (resolve.plannedSources.length > 0) {
146
+ for (const source of selection.sources) {
147
+ if (!source.absolutePath && !resolve.plannedSources.some((plan) => plan.path === source.path && plan.sourceKind === "remote_repo")) {
148
+ throw new Error("The remote repository is not accessible. Connect its provider or clone it locally, then retry.");
149
+ }
150
+ }
139
151
  return resolve;
140
152
  }
141
153
  if (selection.sourceMode === "remote") {
@@ -172,6 +184,9 @@ export async function ensureAuthenticated(client, flags) {
172
184
  if (isClaimProofAvailable(credentials)) {
173
185
  throw new Error("Your guest trial session is no longer available, but its claim proof is stored on this machine. Run `apex claim` to keep the trial results.");
174
186
  }
187
+ if (!canPromptInteractively(flags)) {
188
+ throw new Error("Apex is not authenticated. Run `apex login` explicitly, then retry this command.");
189
+ }
175
190
  return login(client, {
176
191
  noOpen: flags["no-open"] === true,
177
192
  quiet: isJsonMode(flags),
@@ -182,16 +197,21 @@ export async function resolveWorkspace(client, cwd, me, flags) {
182
197
  return resolveWorkspaceSelection(client, selection);
183
198
  }
184
199
  export async function selectWorkspaceTarget(cwd, me, flags) {
200
+ const sourceMode = getSourceMode(flags);
185
201
  const loadedBinding = await loadWorkspaceBinding(cwd);
186
202
  const company = await chooseCompany(me, flags, loadedBinding);
187
- const workspaceNameSelection = await chooseWorkspaceName(cwd, flags, loadedBinding);
203
+ const companyBinding = loadedBinding?.companyId === company.id ? loadedBinding : null;
204
+ const workspaceNameSelection = await chooseWorkspaceName(cwd, flags, companyBinding);
188
205
  const workspaceName = workspaceNameSelection.workspaceName;
189
- const binding = selectBindingForWorkspaceName(flags, loadedBinding, workspaceName);
206
+ const binding = selectBindingForWorkspaceName(flags, companyBinding, workspaceName);
190
207
  const sourceSelection = getFlagList(flags, "repo");
191
208
  const discovered = await discoverSources(cwd, sourceSelection);
192
209
  if (discovered.sources.length === 0) {
193
210
  throw new Error("No local sources were found.");
194
211
  }
212
+ if (sourceMode === "local" && discovered.sources.some((source) => source.absolutePath === null)) {
213
+ throw new Error("Remote repository URLs cannot use --source-mode local. Clone the repository locally first.");
214
+ }
195
215
  return {
196
216
  company,
197
217
  workspaceName,
@@ -199,7 +219,7 @@ export async function selectWorkspaceTarget(cwd, me, flags) {
199
219
  binding,
200
220
  sources: discovered.sources,
201
221
  legacyRepositories: extractLegacyRepositories(discovered.sources),
202
- sourceMode: getSourceMode(flags),
222
+ sourceMode,
203
223
  };
204
224
  }
205
225
  export async function resolveWorkspaceSelection(client, selection) {
package/dist/shell.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { commandStartThreatModel, hasAuditSetupBeta } from "./threat-model.js";
2
2
  import { shellHelpText } from "./help.js";
3
- import { commandCancelScan, commandClaim, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingTickets, commandFindingLinkTicket, commandFindingSend, commandFindingFeedback, commandFindingFixReview, commandFindings, commandWorkspaceFindings, commandLogout, commandScan, commandScans, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, initializeInteractiveSession, openCurrentApexView, } from "./commands.js";
3
+ import { commandCancelScan, commandClaim, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingStatus, commandFindingTickets, commandFindingLinkTicket, commandFindingSend, commandFindingFeedback, commandFindingFixReview, commandFindings, commandWorkspaceFindings, commandLogout, commandScan, commandScans, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, initializeInteractiveSession, openCurrentApexView, } from "./commands.js";
4
4
  import { withFlag } from "./args.js";
5
5
  import { printCompanyDetails, printInteractiveSessionSummary, printSourceList, printWorkspaceDetails, } from "./output.js";
6
6
  import { readLine } from "./prompt.js";
@@ -13,7 +13,7 @@ const SHELL_COMPLETIONS = [
13
13
  { command: "scans" },
14
14
  {
15
15
  command: "findings",
16
- args: ["workspace", "comment", "feedback", "fix-review", "tickets", "link-ticket", "send"],
16
+ args: ["workspace", "status", "comment", "feedback", "fix-review", "tickets", "link-ticket", "send"],
17
17
  },
18
18
  { command: "export" },
19
19
  { command: "workspaces" },
@@ -206,11 +206,8 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
206
206
  await commandClaim(client, cwd, shellFlags);
207
207
  return false;
208
208
  case "scan": {
209
- const mode = parsed.args[0];
210
- if (mode && !["standard", "audit", "ultra", "lite", "pr"].includes(mode)) {
211
- process.stderr.write("Usage: /scan [standard|audit|ultra|lite|pr] [pr-number]\n");
212
- return {};
213
- }
209
+ const first = parsed.args[0];
210
+ const mode = first && ["standard", "audit", "ultra", "lite", "pr"].includes(first) ? first : null;
214
211
  const prNumber = mode === "pr" ? parsed.args[1] : null;
215
212
  if (mode === "pr" && !prNumber) {
216
213
  process.stderr.write("Usage: /scan pr <pr-number>\n");
@@ -220,11 +217,15 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
220
217
  process.stderr.write("Usage: /scan pr <pr-number>\n");
221
218
  return {};
222
219
  }
223
- const commandFlags = mode === "pr" && prNumber
220
+ let commandFlags = mode === "pr" && prNumber
224
221
  ? withFlag(withFlag(shellFlags, "mode", mode), "pr", [prNumber])
225
222
  : mode
226
223
  ? withFlag(shellFlags, "mode", mode)
227
224
  : shellFlags;
225
+ const targets = parsed.args.slice(mode ? 1 : 0);
226
+ if (mode !== "pr" && targets.length > 0) {
227
+ commandFlags = withFlag(commandFlags, "repo", targets);
228
+ }
228
229
  if (commandFlags.setup !== undefined) {
229
230
  await commandStartThreatModel(client, cwd, commandFlags);
230
231
  return {};
@@ -253,6 +254,12 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
253
254
  await commandScans(client, cwd, shellFlags);
254
255
  return {};
255
256
  case "findings": {
257
+ if (parsed.args[0] === "status") {
258
+ if (parsed.args.length < 3)
259
+ throw new Error("Usage: /findings status <finding-ref> <open|fixed|acknowledged|in_review|pending_merge|false_positive|wont_fix|skipped> [note]");
260
+ await commandFindingStatus(client, cwd, shellFlags, parsed.args[1], parsed.args[2], parsed.args.slice(3).join(" ") || undefined);
261
+ return {};
262
+ }
256
263
  if (parsed.args[0] === "send") {
257
264
  if (parsed.args.length !== 3)
258
265
  throw new Error("Usage: /findings send <finding-ref> <linear|jira>");
package/dist/telemetry.js CHANGED
@@ -283,7 +283,7 @@ function sanitizeShellArgs(parsed) {
283
283
  commentLengthBucket: bucketLength(parsed.args.slice(feedbackStatus === "invalid" ? 4 : 3).join(" ")),
284
284
  };
285
285
  }
286
- if (["fix-review", "tickets", "link-ticket", "send"].includes(action)) {
286
+ if (["status", "fix-review", "tickets", "link-ticket", "send"].includes(action)) {
287
287
  return {
288
288
  findingRefProvided: Boolean(parsed.args[1]),
289
289
  };
@@ -331,7 +331,7 @@ function safeCliSubcommand(parsed) {
331
331
  case "export":
332
332
  return safeEnum(subcommand, ["findings"]);
333
333
  case "findings":
334
- return safeEnum(subcommand, ["comment", "feedback", "fix-review", "tickets", "link-ticket", "send"]);
334
+ return safeEnum(subcommand, ["comment", "feedback", "status", "fix-review", "tickets", "link-ticket", "send"]);
335
335
  case "setup":
336
336
  return safeEnum(subcommand, ["all", "codex", "claude", "copilot"]);
337
337
  case "service-key":
@@ -314,7 +314,18 @@ async function interactiveAnswers(state) {
314
314
  };
315
315
  }
316
316
  export async function commandThreatModel(client, cwd, flags, action, supplied, emit = true) {
317
- const draftId = uuid.parse(required(flags, "draft"));
317
+ const selectedDraft = getFlagString(flags, "draft");
318
+ if (!selectedDraft) {
319
+ const binding = await loadWorkspaceBinding(cwd);
320
+ const setupCommand = binding
321
+ ? "apex scan --mode standard --setup guided"
322
+ : "apex workspaces";
323
+ throw new ThreatModelUsageError(`Select --draft <id> from the setup response. To create a draft, run ${setupCommand}${binding ? "" : ", bind the intended workspace with apex workspace use <ref>, then run apex scan --mode standard --setup guided"}. Setup uses the workspace's saved repositories and context, not local file selections.`);
324
+ }
325
+ const parsedDraft = uuid.safeParse(selectedDraft);
326
+ if (!parsedDraft.success)
327
+ throw new ThreatModelUsageError("--draft must be a valid draft ID from the setup response.");
328
+ const draftId = parsedDraft.data;
318
329
  requireAuditSetupBeta(await requireAccount(client));
319
330
  let state;
320
331
  if (action === "status" || action === "next") {
@@ -368,8 +379,15 @@ export async function commandThreatModel(client, cwd, flags, action, supplied, e
368
379
  return state;
369
380
  }
370
381
  export async function commandStartThreatModel(client, cwd, flags, emit = true) {
371
- const setup = z.enum(["guided", "auto"]).parse(required(flags, "setup"));
372
- const mode = z.enum(["standard", "audit"]).parse(required(flags, "mode"));
382
+ const setup = getFlagString(flags, "setup");
383
+ if (setup !== "guided" && setup !== "auto") {
384
+ throw new ThreatModelUsageError("--setup must be guided or auto.");
385
+ }
386
+ const requestedMode = getFlagString(flags, "mode");
387
+ const mode = requestedMode === "ultra" ? "audit" : requestedMode;
388
+ if (mode !== "standard" && mode !== "audit") {
389
+ throw new ThreatModelUsageError("Audit Setup supports --mode standard or --mode audit. Lite and PR scans do not use --setup; remove it and retry the intended scan mode.");
390
+ }
373
391
  if (["repo", "pr", "pr-path", "source-mode", "force", "workspace-name"].some((key) => flags[key] !== undefined))
374
392
  throw new ThreatModelUsageError("Audit Setup uses the configured workspace repositories and context; local source and force flags are not supported");
375
393
  const me = await requireAccount(client);
package/dist/types.js CHANGED
@@ -1 +1,4 @@
1
- export {};
1
+ export const FINDING_LIFECYCLE_STATUSES = [
2
+ "open", "fixed", "acknowledged", "in_review", "pending_merge",
3
+ "false_positive", "wont_fix", "skipped",
4
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cantinasecurity/apex-cli",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "Standalone CLI and MCP server for Apex.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -27,18 +27,19 @@
27
27
  "node": ">=20"
28
28
  },
29
29
  "dependencies": {
30
- "@modelcontextprotocol/sdk": "^1.29.0",
31
- "ignore": "^7.0.5",
30
+ "@modelcontextprotocol/sdk": "^1.32.1",
31
+ "ignore": "^7.0.12",
32
32
  "proper-lockfile": "^4.1.2",
33
- "tar": "^7.4.3",
34
- "zod": "^4.3.6"
33
+ "tar": "^7.5.22",
34
+ "zod": "^4.6.5"
35
35
  },
36
36
  "devDependencies": {
37
- "@types/node": "^22.18.6",
37
+ "@types/node": "^22.20.5",
38
38
  "@types/proper-lockfile": "^4.1.4",
39
- "tsx": "^4.21.0",
39
+ "tsx": "^4.23.15",
40
40
  "typescript": "^5.9.3",
41
- "vitest": "^2.1.9"
41
+ "vite": "^6.4.4",
42
+ "vitest": "^4.1.11"
42
43
  },
43
44
  "scripts": {
44
45
  "apex": "tsx src/apex.ts",
@@ -9,7 +9,7 @@ This skill is bundled with Apex CLI and can be installed into Codex with `apex s
9
9
 
10
10
  Prefer the Apex MCP tools over running `apex` in the shell when the server is available. The tools installed by `apex setup` use the local stdio server and device login.
11
11
 
12
- For a production MCP client that does not need local filesystem access, use the hosted Streamable HTTP endpoint at `https://ai.cantina.xyz/mcp`. Read the organization service key from `APEX_SERVICE_KEY` and supply it only as `Authorization: Bearer <TOKEN>`; never put the raw key in a tool argument, result, prompt, source file, or log. The hosted tools are `apex-credits`, `apex-workspaces`, `apex-scan`, `apex-black-start`, `apex-status`, `apex-cancel-scan`, `apex-findings`, `apex-workspace-findings`, `apex-finding-tickets`, `apex-finding-link-ticket`, and `apex-finding-send`.
12
+ For a production MCP client that does not need local filesystem access, use the hosted Streamable HTTP endpoint at `https://ai.cantina.xyz/mcp`. Read the organization service key from `APEX_SERVICE_KEY` and supply it only as `Authorization: Bearer <TOKEN>`; never put the raw key in a tool argument, result, prompt, source file, or log. The hosted tools are `apex-credits`, `apex-workspaces`, `apex-scan`, `apex-black-start`, `apex-status`, `apex-cancel-scan`, `apex-findings`, `apex-workspace-findings`, `apex-finding-tickets`, `apex-finding-link-ticket`, `apex-finding-send`, and `apex-finding-status`.
13
13
 
14
14
  OAuth-capable clients may instead use the hosted endpoint’s OAuth discovery and Cantina manager-consent flow. Let the client manage OAuth credentials; never pass them to tools, the REST API, or local CLI. Local stdio MCP continues to use device login.
15
15
 
@@ -153,3 +153,19 @@ Keep `policy.version` at `3.1`. Supply a name, instructions, and all eight metri
153
153
  allowed value and nonempty guidance for each allowed value. Omit disallowed
154
154
  values. If Audit Setup or another workspace update is active, wait for it to
155
155
  finish before saving again.
156
+
157
+ Scan target and recovery rules:
158
+ - CLI `apex scan [target...]` and repeatable `--repo` accept exact files, directories, or HTTPS GitHub/GitLab repository URLs; local MCP uses `repoPaths` for the same selections. No target means `cwd`.
159
+ - A file selection scans only that file, respecting ignore rules. Local snapshots exclude `.apex/` state. Remote URLs use the default branch and cannot fall back to uploading `cwd`; clone locally to choose a branch or commit.
160
+ - Use Standard for local changes, files, plain directories, and public repositories without provider access. Lite and Audit still require connected provider sources; do not silently substitute another mode.
161
+ - Unauthenticated headless CLI commands fail with explicit login guidance; they never start device login. Use the MCP auth tools for explicit MCP login. Retry transient refresh failures without clearing credentials.
162
+ - Audit Setup is an explicit Standard/Audit beta flow using saved workspace repositories and context. It does not preserve local targets. Never automatically substitute setup after `AUDIT_SETUP_REQUIRED`; use the error's selected-workspace command only when that saved scope is intended. Draft operations require the ID returned by preparation; eligible accounts receive setup commands in CLI help. Lite and PR do not use `--setup`.
163
+
164
+ Finding lifecycle:
165
+ - Close resolved issues with `apex-finding-status` and `status: "fixed"`; reopen with `status: "open"`.
166
+ - Local MCP takes `findingRef`, optional `cwd`/`scanId`, `note`, and `expectedUpdatedAt`, using device login. Hosted MCP takes `findingId` (UUID), no local paths, and requires explicit `findings:write`.
167
+ - The scripted equivalent is `apex findings status <finding-ref> <open|fixed> --note "rationale" --json`. Other statuses: acknowledged, in_review, pending_merge, false_positive, wont_fix, skipped.
168
+ - Status changes preserve scanner validity, severity, feedback, and linked ticket state. Invalid or duplicate findings cannot be reopened into the open queue until their classification is reviewed.
169
+ - Read `remediationStatus` in workspace finding lists for the current lifecycle; `status` remains the scanner verdict. `open` and `acknowledged` stay distinct even though both use the `accepted` review state.
170
+ - Use the finding updatedAt as expectedUpdatedAt when working from a prior snapshot. On FINDING_STATUS_CONFLICT, reload and review before retrying.
171
+ - Linear sync is Linear-to-Apex: started becomes acknowledged, completed becomes fixed. Do not promise Apex-to-Linear status updates or automatic Linear reopening.