@cantinasecurity/apex-cli 0.1.38 → 0.1.39

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.
@@ -76,7 +76,7 @@ If the Apex MCP server is not configured, fall back to the local CLI:
76
76
  - `apex scan` scans the current working directory by default; pass `--repo` only when the user asks to scan explicit alternate roots.
77
77
  - `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.
78
78
  - Plain local directories and dirty git worktrees can scan through local snapshot uploads without provider access.
79
- - 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.
79
+ - 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, GitLab or company-enabled Forgejo sources.
80
80
  - 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.
81
81
  - PR scans use `--mode pr --pr <number>` in CLI calls, or `mode: "pr"` with `pullRequests` in MCP. They require one provider-backed GitHub repository.
82
82
  - Starting a PR scan may run alongside active workspace scans and does not require `--force` or `force: true`. Starting any non-PR scan still treats every active scan, including a PR scan, as a blocker; use force only when the user explicitly wants that overlap.
@@ -155,7 +155,7 @@ values. If Audit Setup or another workspace update is active, wait for it to
155
155
  finish before saving again.
156
156
 
157
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`.
158
+ - CLI `apex scan [target...]` and repeatable `--repo` accept exact files, directories, or HTTPS GitHub/GitLab or company-enabled Forgejo repository URLs; local MCP uses `repoPaths` for the same selections. No target means `cwd`.
159
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
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
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.
@@ -169,3 +169,9 @@ Finding lifecycle:
169
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
170
  - Use the finding updatedAt as expectedUpdatedAt when working from a prior snapshot. On FINDING_STATUS_CONFLICT, reload and review before retrying.
171
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.
172
+
173
+ ## Forgejo
174
+
175
+ Forgejo connections, remote repository scans, PR scans and Fix Review require the Forgejo company feature enabled by an administrator in Bedrock Settings. Run `apex connect forgejo` (or `apex-connect-provider` with `provider: "forgejo"`) to obtain the browser connection URL. A dedicated bot, HTTPS instance URL and repository-scoped read token are required; PR statuses, comments and webhook installation also require a write token.
176
+
177
+ After connecting, run `apex doctor --repo . --source-mode remote`, then `apex scan --mode standard --repo . --source-mode remote` or `apex scan --mode pr --pr 42 --repo .`. Same-repository Forgejo PR URLs use `/owner/repository/pulls/42`. Pass a Forgejo Fix PR URL through finding feedback and start Fix Review using the existing finding commands or MCP tools. Disabling the company flag stops native Forgejo access. Existing local Standard snapshots remain provider-independent.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Cantina agent plugins for security review workflows.",
9
- "version": "0.1.38"
9
+ "version": "0.1.39"
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.38"
17
+ "version": "0.1.39"
18
18
  },
19
19
  "description": "Run Apex security scans and review findings from Claude Code.",
20
- "version": "0.1.38",
20
+ "version": "0.1.39",
21
21
  "author": {
22
22
  "name": "Cantina",
23
23
  "email": "support@cantina.xyz"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apex-cli",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "Run Apex security scans and review findings from Claude Code.",
5
5
  "author": {
6
6
  "name": "Cantina",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apex-cli",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "Run Apex security scans and review findings from Codex.",
5
5
  "author": {
6
6
  "name": "Cantina",
package/.mcp.claude.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "args": [
6
6
  "-y",
7
7
  "-p",
8
- "@cantinasecurity/apex-cli@0.1.38",
8
+ "@cantinasecurity/apex-cli@0.1.39",
9
9
  "apex-mcp"
10
10
  ],
11
11
  "env": {
package/.mcp.codex.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "args": [
5
5
  "-y",
6
6
  "-p",
7
- "@cantinasecurity/apex-cli@0.1.38",
7
+ "@cantinasecurity/apex-cli@0.1.39",
8
8
  "apex-mcp"
9
9
  ],
10
10
  "env": {
package/MARKETPLACE.md CHANGED
@@ -13,7 +13,7 @@ This package is prepared as both a Claude Code plugin and a Codex plugin. The pu
13
13
  The plugin MCP configs launch the pinned npm CLI package with:
14
14
 
15
15
  ```bash
16
- npx -y -p @cantinasecurity/apex-cli@0.1.38 apex-mcp
16
+ npx -y -p @cantinasecurity/apex-cli@0.1.39 apex-mcp
17
17
  ```
18
18
 
19
19
  That keeps marketplace installs independent of a user's global `apex` install.
package/README.md CHANGED
@@ -158,30 +158,30 @@ unclaimed local trial proof so `apex claim` can still use it.
158
158
  Agent-safe copy/paste prompt (substitute the issued code):
159
159
 
160
160
  ```text
161
- Install and run Cantina Apex CLI 0.1.38 to start one free standard security scan of this repository immediately.
161
+ Install and run Cantina Apex CLI 0.1.39 to start one free standard security scan of this repository immediately.
162
162
 
163
163
  Use this trial code only where <TRIAL_CODE> appears below: SPBT-DEVREL-XXXXXXXX
164
164
 
165
165
  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`.
166
166
 
167
167
  Use this exact command prefix for every Apex command:
168
- npx -y -p @cantinasecurity/apex-cli@0.1.38 apex
168
+ npx -y -p @cantinasecurity/apex-cli@0.1.39 apex
169
169
 
170
170
  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.
171
171
  2. Redeem, with at most one response-loss recovery retry:
172
- npx -y -p @cantinasecurity/apex-cli@0.1.38 apex redeem <TRIAL_CODE> --json
172
+ npx -y -p @cantinasecurity/apex-cli@0.1.39 apex redeem <TRIAL_CODE> --json
173
173
  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.
174
174
  3. Immediately start exactly one scan; do not wait for another instruction:
175
- npx -y -p @cantinasecurity/apex-cli@0.1.38 apex scan --mode standard --non-interactive --no-open --json
175
+ npx -y -p @cantinasecurity/apex-cli@0.1.39 apex scan --mode standard --non-interactive --no-open --json
176
176
  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.
177
177
  4. About every 60 seconds, poll that exact scan:
178
- npx -y -p @cantinasecurity/apex-cli@0.1.38 apex status --scan <SCAN_ID> --json
178
+ npx -y -p @cantinasecurity/apex-cli@0.1.39 apex status --scan <SCAN_ID> --json
179
179
  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.
180
180
  5. Fetch that exact scan's findings:
181
- npx -y -p @cantinasecurity/apex-cli@0.1.38 apex findings --scan <SCAN_ID> --json
181
+ npx -y -p @cantinasecurity/apex-cli@0.1.39 apex findings --scan <SCAN_ID> --json
182
182
  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.
183
183
  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:
184
- npx -y -p @cantinasecurity/apex-cli@0.1.38 apex claim
184
+ npx -y -p @cantinasecurity/apex-cli@0.1.39 apex claim
185
185
  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.
186
186
 
187
187
  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.
@@ -262,8 +262,8 @@ Supported shell commands:
262
262
  - `apex findings link-ticket <finding-ref> <linear|jira> <issue-key|url> [--scan <scan-id>] [--json]`
263
263
  - `apex findings comment <finding-id|finding-identifier> --content <markdown> [--parent-comment <comment-id>] [--scan <scan-id>]`
264
264
  - `apex findings status <finding-ref> <open|fixed|acknowledged|in_review|pending_merge|false_positive|wont_fix|skipped> [--note <rationale>] [--expected-updated-at <timestamp>] [--scan <scan-id>]`
265
- - `apex findings feedback <finding-id|finding-identifier> <valid|invalid> [comment] [--comment <markdown>] [--scan <scan-id>] [--suggested-severity extreme|critical|high|medium|low|informational] [--dismissal-reason false-positive|by-design|not-relevant] [--label acknowledged|fixed] [--fix-pr-url <github-pr-url>]`
266
- - `apex findings fix-review <finding-id|finding-identifier> [--scan <scan-id>] [--review-target pull-request|repository-current] [--fix-pr-url <github-pr-url>]`
265
+ - `apex findings feedback <finding-id|finding-identifier> <valid|invalid> [comment] [--comment <markdown>] [--scan <scan-id>] [--suggested-severity extreme|critical|high|medium|low|informational] [--dismissal-reason false-positive|by-design|not-relevant] [--label acknowledged|fixed] [--fix-pr-url <fix-pr-url>]`
266
+ - `apex findings fix-review <finding-id|finding-identifier> [--scan <scan-id>] [--review-target pull-request|repository-current] [--fix-pr-url <fix-pr-url>]`
267
267
  - `apex export findings [--scan <scan-id>] [--format markdown|json|gitlab-sast] [--output <path>]`
268
268
  - `apex workspaces`
269
269
  - `apex workspace`
@@ -468,7 +468,7 @@ Local MCP exposes `apex-finding-link-ticket` with `findingRef`, `provider`, `iss
468
468
 
469
469
  ## Local Source Scans
470
470
 
471
- `apex scan [target...]` accepts files, directories, and HTTPS GitHub/GitLab repository URLs. With no targets, it scans the current working directory:
471
+ `apex scan [target...]` accepts files, directories, and HTTPS GitHub/GitLab or company-enabled Forgejo repository URLs. With no targets, it scans the current working directory:
472
472
 
473
473
  ```bash
474
474
  apex scan . --mode standard
@@ -481,7 +481,7 @@ A file target uploads only that file, never its siblings. Local snapshots includ
481
481
 
482
482
  Source planning:
483
483
 
484
- - clean GitHub or GitLab checkouts can stay on the remote-materialization path
484
+ - clean GitHub, GitLab or company-enabled Forgejo checkouts can stay on the remote-materialization path
485
485
  - dirty git worktrees fall back to a local snapshot upload by default
486
486
  - plain directories that are not git repositories are scanned through a local snapshot upload
487
487
 
@@ -490,20 +490,20 @@ Useful flags:
490
490
  - `--repo <path-or-url>` as a repeatable alternative to positional targets
491
491
  - `--source-mode auto|remote|local` to control remote-first fallback behavior
492
492
  - `--mode standard|audit|lite|pr` to choose the scan mode
493
- - `--pr <number>` to select one or more GitHub pull requests for `--mode pr`
493
+ - `--pr <number>` to select one or more GitHub or enabled Forgejo pull requests for `--mode pr`
494
494
  - `--pr <number:path,path>` or `--pr-path <path>` to limit a PR scan to changed paths
495
495
 
496
496
  Unauthenticated `--json`, `--non-interactive`, and non-TTY commands fail immediately with `apex login` guidance. Run login explicitly before automation. Temporary refresh/network failures preserve credentials; retry after the service recovers. Provider connection commands return URLs without opening browsers in automation.
497
497
 
498
498
  `auto` is the default. `remote` requires Apex to materialize from a remote repository. `local` forces a local snapshot upload even when a clean remote path is available.
499
499
 
500
- Audit scans use `audit` as the scan mode and still require provider-backed GitHub or GitLab repositories that Apex can materialize remotely without a local snapshot fallback. `ultra` remains accepted as a backwards-compatible alias.
500
+ Audit scans use `audit` as the scan mode and still require provider-backed GitHub, GitLab or company-enabled Forgejo repositories that Apex can materialize remotely without a local snapshot fallback. `ultra` remains accepted as a backwards-compatible alias.
501
501
 
502
502
  Lite scans use `lite` as the scan mode and require the Lite Scan beta feature plus provider-backed GitHub or GitLab repositories that Apex can materialize remotely. Local snapshot uploads are not supported for Lite scans.
503
503
 
504
504
  If a backend returns `AUDIT_SETUP_REQUIRED`, the error includes a setup command for the selected workspace. This explicit beta workflow uses the workspace's saved repositories and context; it does not preserve positional file or directory selections. Do not automatically substitute it for a local target scan. An explicit Standard source scan requires current CLI and backend versions. Eligible accounts receive setup commands in CLI help; draft operations use the ID returned by preparation. Lite and PR scans do not use `--setup`.
505
505
 
506
- PR scans require exactly one provider-backed GitHub repository. If the current directory resolves to multiple sources, pass `--repo <path>` to select the one that contains the pull request.
506
+ PR scans require exactly one provider-backed GitHub or company-enabled, connected Forgejo repository. If the current directory resolves to multiple sources, pass `--repo <path>` to select the one that contains the pull request.
507
507
 
508
508
  CLI and MCP scan results use the public Bedrock `scanId`. Kernel execution IDs are internal implementation details and are neither returned nor accepted as scan aliases.
509
509
 
@@ -687,7 +687,7 @@ Telemetry event posts do not include bearer tokens. The telemetry endpoint is `P
687
687
  {
688
688
  "schemaVersion": 1,
689
689
  "event": "apex.invocation.completed",
690
- "cliVersion": "0.1.38",
690
+ "cliVersion": "0.1.39",
691
691
  "invocation": {
692
692
  "surface": "mcp",
693
693
  "command": "apex-scan",
@@ -718,7 +718,7 @@ The npm package also includes marketplace-ready plugin artifacts:
718
718
  - `.claude-plugin/plugin.json` and `.mcp.claude.json` for Claude Code plugin installs
719
719
  - `.claude-plugin/marketplace.json` for a Claude marketplace entry backed by the public npm package
720
720
 
721
- These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.38 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
721
+ These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.39 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
722
722
 
723
723
  The repository also includes `.agents/plugins/marketplace.json` for local Codex marketplace testing from a checkout.
724
724
 
@@ -820,3 +820,9 @@ Reload and review the latest state before retrying a conflict.
820
820
  Linear sync currently runs from Linear to Apex: started tickets become acknowledged
821
821
  and completed tickets become fixed. Apex status changes do not update linked Linear
822
822
  or Jira tickets. Use the direct status operation to manage issues from MCP or REST.
823
+
824
+ ## Forgejo
825
+
826
+ Forgejo connections, remote repository scans, PR scans and Fix Review require the Forgejo company feature enabled by an administrator in Bedrock Settings. Run `apex connect forgejo` (or `apex-connect-provider` with `provider: "forgejo"`) to obtain the browser connection URL. A dedicated bot, HTTPS instance URL and repository-scoped read token are required; PR statuses, comments and webhook installation also require a write token.
827
+
828
+ After connecting, run `apex doctor --repo . --source-mode remote`, then `apex scan --mode standard --repo . --source-mode remote` or `apex scan --mode pr --pr 42 --repo .`. Same-repository Forgejo PR URLs use `/owner/repository/pulls/42`. Pass a Forgejo Fix PR URL through finding feedback and start Fix Review using the existing finding commands or MCP tools. Disabling the company flag stops native Forgejo access. Existing local Standard snapshots remain provider-independent.
package/dist/apex.js CHANGED
@@ -182,8 +182,8 @@ async function dispatch(parsed) {
182
182
  await commandStatus(client, cwd, parsed.flags);
183
183
  return;
184
184
  case "connect":
185
- if (parsed.subcommand !== "github" && parsed.subcommand !== "gitlab") {
186
- throw new Error("Usage: apex connect <github|gitlab>");
185
+ if (parsed.subcommand !== "github" && parsed.subcommand !== "gitlab" && parsed.subcommand !== "forgejo") {
186
+ throw new Error("Usage: apex connect <github|gitlab|forgejo>");
187
187
  }
188
188
  await commandConnect(client, cwd, parsed.flags, parsed.subcommand);
189
189
  return;
package/dist/commands.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { parseForgejoPullRequestUrl } from "./forgejo-url.js";
1
2
  import { canPromptInteractively, getFlagList, getFlagString, isJsonMode, } from "./args.js";
2
3
  import { getMe, login, logout } from "./auth.js";
3
4
  import { ApiError, formatApiError, getApiErrorCode, } from "./api-client.js";
@@ -371,6 +372,10 @@ function normalizeRepositoryUrl(value) {
371
372
  return value.replace(/\.git$/i, "").replace(/\/+$/, "").toLowerCase();
372
373
  }
373
374
  function parseGithubPullRequestUrl(value) {
375
+ const forgejoWithPaths = value.match(/^(https?:\/\/[^\s]+\/pulls\/\d+):(.+)$/);
376
+ const forgejo = parseForgejoPullRequestUrl(forgejoWithPaths?.[1] ?? value);
377
+ if (forgejo)
378
+ return { number: forgejo.prNumber, repoUrl: forgejo.baseUrl + "/" + forgejo.repoName + ".git", pathFilterValue: forgejoWithPaths?.[2] ?? null };
374
379
  const withPathFilter = value.match(/^(https?:\/\/github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+):(.+)$/i);
375
380
  const urlValue = withPathFilter?.[1] ?? value;
376
381
  const match = urlValue.match(/^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)(?:[/?#].*)?$/i);
@@ -457,10 +462,12 @@ function resolvePrScanSource(repositories) {
457
462
  throw new Error("PR scans require exactly one GitHub repository. Re-run with --repo to select a single source root.");
458
463
  }
459
464
  const source = repositories[0];
460
- if (source.provider !== "github") {
461
- throw new Error("PR scans require a provider-backed GitHub repository.");
465
+ if (source.provider !== "github" && source.provider !== "forgejo") {
466
+ throw new Error("PR scans require a provider-backed GitHub repository or an enabled Forgejo repository.");
462
467
  }
463
- if (typeof source.installationId !== "number") {
468
+ if (source.provider === "forgejo" && (!source.connectionId || !source.projectId || !source.baseUrl))
469
+ throw new Error("PR scans require a connected Forgejo repository.");
470
+ if (source.provider === "github" && typeof source.installationId !== "number") {
464
471
  throw new Error("PR scans require GitHub App installation access for the selected repository.");
465
472
  }
466
473
  return source;
@@ -481,9 +488,10 @@ function formatMissingProviderConnections(missing) {
481
488
  function buildPrScanRepositoriesPayload(params) {
482
489
  return [
483
490
  {
484
- provider: "github",
491
+ provider: params.source.provider,
485
492
  repoUrl: params.source.repoUrl,
486
493
  installationId: params.source.installationId,
494
+ ...(params.source.provider === "forgejo" ? { connectionId: params.source.connectionId, projectId: params.source.projectId, baseUrl: params.source.baseUrl } : {}),
487
495
  sourceType: "pr",
488
496
  pullRequests: params.pullRequests.map((pullRequest) => ({
489
497
  number: pullRequest.number,
@@ -508,7 +516,7 @@ function buildPrScanPlannedSource(source) {
508
516
  displayName: getPrScanSourceDisplayName(source),
509
517
  relativePath: source.path || ".",
510
518
  sourceKind: "remote_repo",
511
- provider: "github",
519
+ provider: source.provider,
512
520
  repoUrl: source.repoUrl,
513
521
  ref: {
514
522
  type: "default",
@@ -518,7 +526,7 @@ function buildPrScanPlannedSource(source) {
518
526
  connectionId: source.connectionId ?? null,
519
527
  projectId: source.projectId ?? null,
520
528
  baseUrl: source.baseUrl ?? null,
521
- materializationAuth: "installation",
529
+ materializationAuth: source.provider === "forgejo" ? "connection" : "installation",
522
530
  };
523
531
  }
524
532
  function getPrScanSelectedSources(sources, source) {
@@ -1070,9 +1078,7 @@ export async function commandConnect(client, cwd, flags, provider) {
1070
1078
  const me = await ensureAuthenticated(client, flags);
1071
1079
  const binding = await loadWorkspaceBinding(cwd);
1072
1080
  const company = await chooseCompany(me, flags, binding);
1073
- const route = provider === "github"
1074
- ? `/api/cli/v1/providers/github/connect?companyId=${encodeURIComponent(company.id)}`
1075
- : `/api/cli/v1/providers/gitlab/connect?companyId=${encodeURIComponent(company.id)}`;
1081
+ const route = `/api/cli/v1/providers/${provider}/connect?companyId=${encodeURIComponent(company.id)}`;
1076
1082
  const response = await withLoadingIndicator(`Preparing ${provider} connection link...`, flags, () => client.request(route));
1077
1083
  if (!isJsonMode(flags)) {
1078
1084
  logLine(`Connection URL for ${provider}: ${response.url}`, flags);
@@ -0,0 +1,53 @@
1
+ export function normalizeForgejoBaseUrl(value) {
2
+ try {
3
+ const url = new URL(value.trim());
4
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash)
5
+ return null;
6
+ if (!url.hostname || /^(localhost|127\.|0\.|\[::1\])/i.test(url.hostname))
7
+ return null;
8
+ return url.origin + url.pathname.replace(/\/+$/, "");
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export function parseForgejoRepository(value, baseUrl) {
15
+ const base = normalizeForgejoBaseUrl(baseUrl);
16
+ if (!base)
17
+ return null;
18
+ try {
19
+ const url = new URL(value.includes("://") ? value : base + "/" + value.replace(/^\/+/, ""));
20
+ if (url.username || url.password || url.search || url.hash || !url.href.startsWith(base + "/"))
21
+ return null;
22
+ const segments = url.pathname.slice(new URL(base).pathname.replace(/\/+$/, "").length).replace(/^\/+/, "").replace(/\.git$/, "").split("/");
23
+ if (segments.length !== 2 || segments.some(part => !/^[a-z0-9_.-]+$/i.test(part) || part === "." || part === ".."))
24
+ return null;
25
+ const [owner, repo] = segments;
26
+ return { provider: "forgejo", baseUrl: base, owner, repo, repoName: owner + "/" + repo, normalizedUrl: base + "/" + owner + "/" + repo };
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export function parseForgejoPullRequestUrl(value) {
33
+ try {
34
+ const url = new URL(value.trim());
35
+ const match = url.pathname.match(/^(.*)\/([^/]+)\/([^/]+)\/pulls\/(\d+)(?:\/.*)?$/);
36
+ if (!match || url.username || url.password)
37
+ return null;
38
+ const identity = parseForgejoRepository(match[2] + "/" + match[3], url.origin + match[1]);
39
+ const prNumber = Number(match[4]);
40
+ if (!identity || !Number.isSafeInteger(prNumber) || prNumber <= 0)
41
+ return null;
42
+ return { ...identity, prNumber, normalizedUrl: identity.normalizedUrl + "/pulls/" + prNumber };
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ export function forgejoRepositoryPath(repoName) {
49
+ const parts = repoName.split("/");
50
+ if (parts.length !== 2 || parts.some(part => !/^[a-z0-9_.-]+$/i.test(part) || part === "." || part === ".."))
51
+ throw new Error("Invalid Forgejo repository path");
52
+ return "/repos/" + parts.map(encodeURIComponent).join("/");
53
+ }
package/dist/help.js CHANGED
@@ -43,6 +43,7 @@ export const CLI_HELP_TEXT = `Usage:
43
43
  apex update Update the local Apex CLI install
44
44
  apex connect github Open the GitHub connection flow
45
45
  apex connect gitlab Open the GitLab connection flow
46
+ apex connect forgejo Connect company-enabled Forgejo access
46
47
 
47
48
  Flags:
48
49
  --company <id-or-handle> Override the automatically selected Apex company
@@ -66,7 +67,7 @@ Flags:
66
67
  --dismissal-reason false-positive|by-design|not-relevant
67
68
  Explain why invalid feedback is being left
68
69
  --label acknowledged|fixed Attach a valid-feedback label
69
- --fix-pr-url <github-pr-url> Attach one or more Fix PR URLs to feedback or fix-review start
70
+ --fix-pr-url <fix-pr-url> Attach one or more Fix PR URLs to feedback or fix-review start
70
71
  --review-target pull-request|repository-current
71
72
  Choose the fix review target when starting a fix review
72
73
  --format markdown|json|gitlab-sast
@@ -74,7 +75,7 @@ Flags:
74
75
  --output <path> Write exported findings to this file path
75
76
  --limit <count> Limit the number of findings returned
76
77
  --mode standard|audit|lite|pr Choose the scan mode
77
- --pr <number[:path,path]> Select a GitHub pull request for --mode pr
78
+ --pr <number[:path,path]> Select a GitHub or Forgejo PR for --mode pr
78
79
  --pr-path <path> Limit a single-PR scan to a changed file path
79
80
  --source-mode auto|remote|local Control remote-vs-local source materialization
80
81
  --force Start an overlapping non-PR scan, or confirm destructive guest logout
@@ -90,7 +91,7 @@ Tips:
90
91
  apex scan defaults to the current directory; positional targets or --repo select files, directories, or HTTPS GitHub/GitLab repositories.
91
92
  A file target scans only that file. Use --mode standard for local changes and public repositories without a provider connection.
92
93
  Headless and --json commands require an existing session; run apex login explicitly first.
93
- PR scans require --mode pr and at least one --pr selection for a GitHub repository.
94
+ PR scans require --mode pr and at least one --pr selection for a connected GitHub or company-enabled Forgejo repository.
94
95
  audit is the current scan mode for audit scans; ultra remains accepted as a legacy alias.
95
96
  Lite scans use --mode lite and require beta access plus provider-backed GitHub or GitLab sources.
96
97
  apex workspace use accepts a workspace name, prefix, or ID.
@@ -190,6 +191,7 @@ Commands:
190
191
  /company [id|handle] Show or switch the active company
191
192
  /connect github Connect GitHub access in the browser
192
193
  /connect gitlab Connect GitLab access in the browser
194
+ /connect forgejo Connect company-enabled Forgejo access
193
195
  /open Open the latest scan or Apex home in the browser
194
196
  /clear Clear the terminal
195
197
  /help Show this help
@@ -206,7 +208,7 @@ Scripted-only commands (run outside the interactive shell):
206
208
  Revoke a service key
207
209
 
208
210
  Tips:
209
- PR scans require a GitHub PR number and provider-backed repository access.
211
+ PR scans require a GitHub or enabled Forgejo PR number and provider-backed access.
210
212
  audit is the current scan mode for audit scans; ultra remains accepted as a legacy alias.
211
213
  Lite scans require beta access plus provider-backed GitHub or GitLab sources.
212
214
  /workspace use accepts a workspace name, prefix, or ID.
package/dist/mcp.js CHANGED
@@ -33,7 +33,7 @@ Notes:
33
33
  - Finding write tools use the same Apex device-login credentials as read tools. Never ask for browser cookies or CANTINA_AUTH_TOKEN. An error requiring CANTINA_AUTH_TOKEN proves the MCP server is outdated; update Apex CLI, re-run apex setup for this client, restart it, and retry.
34
34
  - 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 a prompt or tool argument. A hosted MCP client supplies it only as the HTTP Authorization header.
35
35
  - Production agents that do not need local filesystem access can connect to https://ai.cantina.xyz/mcp over Streamable HTTP with Authorization: Bearer <service-key>. Serverless and cron applications can instead use the REST API documented at https://ai.cantina.xyz/docs/apex-api. This local stdio server continues to use device login.
36
- - 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.
36
+ - 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 or company-enabled Forgejo pull request scans.
37
37
  - Use only the public Bedrock scanId returned by Apex for scan inputs. Kernel execution IDs are private and are neither returned nor accepted as aliases.
38
38
  - Starting a PR scan may run alongside active workspace scans and does not require force. Starting any non-PR scan still treats every active scan, including a PR scan, as a blocker; use force only when the user explicitly wants that overlap.
39
39
  - Doctor is read-only for guest sessions and reports the local snapshot plan without reserving a workspace.
@@ -543,7 +543,7 @@ function registerTools(server, auditSetupEnabled = false) {
543
543
  }, value => `Threat-model draft ${String(value.draftId)}: ${String(value.state)}.`, input));
544
544
  server.registerTool("apex-scan", {
545
545
  title: "Start Apex Scan",
546
- description: "Start a new Apex scan for the provided cwd by default. Pass repoPaths to select exact files, directories, or HTTPS GitHub/GitLab repository URLs. Remote URLs use the default branch; files never include siblings. Use mode audit for audit scans, mode lite for beta-gated provider-backed Lite scans, and mode pr for GitHub pull request scans.",
546
+ description: "Start a new Apex scan for the provided cwd by default. Pass repoPaths to select exact files, directories, or HTTPS GitHub/GitLab or company-enabled Forgejo repository URLs. Remote URLs use the default branch; files never include siblings. Use mode audit for audit scans, mode lite for beta-gated provider-backed Lite scans, and mode pr for GitHub or company-enabled Forgejo pull request scans.",
547
547
  inputSchema: {
548
548
  cwd: z.string().optional(),
549
549
  company: z.string().optional(),
@@ -885,11 +885,11 @@ function registerTools(server, auditSetupEnabled = false) {
885
885
  }));
886
886
  server.registerTool("apex-connect-provider", {
887
887
  title: "Get Apex Provider Connection URL",
888
- description: "Return the browser URL a user needs to connect GitHub or GitLab access for Apex. This tool never opens a browser.",
888
+ description: "Return the browser URL a user needs to connect GitHub, GitLab or company-enabled Forgejo access for Apex. This tool never opens a browser.",
889
889
  inputSchema: {
890
890
  cwd: z.string().optional(),
891
891
  company: z.string().optional(),
892
- provider: z.enum(["github", "gitlab"]),
892
+ provider: z.enum(["github", "gitlab", "forgejo"]),
893
893
  },
894
894
  }, async ({ cwd, company, provider }) => runTool("apex-connect-provider", async () => {
895
895
  const client = new ApexApiClient();
@@ -60,13 +60,13 @@ function inspectRemoteTarget(value) {
60
60
  url = new URL(target);
61
61
  }
62
62
  catch {
63
- throw new Error("Remote scan targets must be HTTPS GitHub or GitLab repository URLs.");
63
+ throw new Error("Remote scan targets must be HTTPS GitHub, GitLab or company-enabled Forgejo repository URLs.");
64
64
  }
65
65
  const segments = url.pathname.replace(/\/+$/, "").split("/").filter(Boolean);
66
- if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.port ||
67
- !["github.com", "gitlab.com"].includes(url.hostname) || segments.length < 2 ||
66
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash ||
67
+ segments.length < 2 ||
68
68
  (url.hostname === "github.com" && segments.length !== 2) || segments.includes("-")) {
69
- throw new Error("Remote scan targets must be HTTPS GitHub or GitLab repository URLs without credentials, query strings, or file/tree paths.");
69
+ throw new Error("Remote scan targets must be HTTPS GitHub, GitLab or company-enabled Forgejo repository URLs without credentials, query strings, or file/tree paths.");
70
70
  }
71
71
  const metadata = extractCanonicalRepoMetadata(url.href);
72
72
  if (!metadata)
@@ -183,5 +183,5 @@ export async function discoverSources(cwd, selectedPaths = []) {
183
183
  export function extractLegacyRepositories(sources) {
184
184
  return sources.filter((source) => source.kind === "git_candidate" &&
185
185
  typeof source.repoUrl === "string" &&
186
- (source.provider === "github" || source.provider === "gitlab"));
186
+ (source.provider === "github" || source.provider === "gitlab" || source.provider === "forgejo"));
187
187
  }
package/dist/repo-url.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { parseForgejoRepository } from "./forgejo-url.js";
1
2
  function normalizeGitlabBaseUrl(value) {
2
3
  try {
3
4
  const url = new URL(value.trim());
@@ -99,6 +100,15 @@ export function extractGitlabProjectPathFromRepoUrl(repoUrl) {
99
100
  }
100
101
  }
101
102
  export function extractCanonicalRepoMetadata(value) {
103
+ try {
104
+ const candidate = new URL(value.replace(/^git@([^:]+):/, "https://$1/"));
105
+ if (candidate.hostname === "codeberg.org" || candidate.hostname === "code.forgejo.org" || candidate.hostname.split(".").includes("forgejo")) {
106
+ const repo = parseForgejoRepository(candidate.href, candidate.origin);
107
+ if (repo)
108
+ return { provider: "forgejo", repoUrl: repo.normalizedUrl + ".git", repoName: repo.repoName, baseUrl: repo.baseUrl };
109
+ }
110
+ }
111
+ catch { /* Local owner/repository shorthand is handled below. */ }
102
112
  const githubRepoUrl = normalizeGithubRepoUrl(value);
103
113
  if (githubRepoUrl?.startsWith("https://github.com/")) {
104
114
  try {
package/dist/shell.js CHANGED
@@ -27,7 +27,7 @@ const SHELL_COMPLETIONS = [
27
27
  { command: "repos" },
28
28
  { command: "workspace", args: ["use", "name"] },
29
29
  { command: "company" },
30
- { command: "connect", args: ["github", "gitlab"] },
30
+ { command: "connect", args: ["github", "gitlab", "forgejo"] },
31
31
  { command: "open" },
32
32
  { command: "clear" },
33
33
  { command: "help" },
@@ -432,8 +432,8 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
432
432
  }
433
433
  case "connect": {
434
434
  const provider = parsed.args[0];
435
- if (provider !== "github" && provider !== "gitlab") {
436
- process.stderr.write("Usage: /connect <github|gitlab>\n");
435
+ if (provider !== "github" && provider !== "gitlab" && provider !== "forgejo") {
436
+ process.stderr.write("Usage: /connect <github|gitlab|forgejo>\n");
437
437
  return {};
438
438
  }
439
439
  await commandConnect(client, cwd, shellFlags, provider);
package/dist/telemetry.js CHANGED
@@ -256,7 +256,7 @@ function sanitizeShellArgs(parsed) {
256
256
  };
257
257
  case "connect":
258
258
  return {
259
- provider: safeEnum(parsed.args[0] ?? null, ["github", "gitlab"]),
259
+ provider: safeEnum(parsed.args[0] ?? null, ["github", "gitlab", "forgejo"]),
260
260
  };
261
261
  case "findings": {
262
262
  const action = parsed.args[0] ?? null;
@@ -327,7 +327,7 @@ function safeCliSubcommand(parsed) {
327
327
  return null;
328
328
  switch (parsed.command) {
329
329
  case "connect":
330
- return safeEnum(subcommand, ["github", "gitlab"]);
330
+ return safeEnum(subcommand, ["github", "gitlab", "forgejo"]);
331
331
  case "export":
332
332
  return safeEnum(subcommand, ["findings"]);
333
333
  case "findings":
@@ -404,6 +404,7 @@ export function sanitizeMcpInput(input = {}) {
404
404
  provider: safeEnum(typeof input.provider === "string" ? input.provider : null, [
405
405
  "github",
406
406
  "gitlab",
407
+ "forgejo",
407
408
  ]),
408
409
  ticketProvider: safeEnum(typeof input.provider === "string" ? input.provider : null, ["linear", "jira"]),
409
410
  feedbackStatus: safeEnum(typeof input.status === "string" ? input.status : null, [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cantinasecurity/apex-cli",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "Standalone CLI and MCP server for Apex.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -78,9 +78,9 @@ Guidelines:
78
78
  - `apex-scan` scans the provided `cwd` by default; pass `repoPaths` only when the user asks to scan explicit alternate roots.
79
79
  - `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.
80
80
  - Apex can scan plain local directories and dirty git worktrees without provider connections by using local snapshot uploads.
81
- - 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.
81
+ - 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, GitLab or company-enabled Forgejo sources.
82
82
  - Lite scans use `mode: "lite"`, require Lite Scan beta access, and require provider-backed GitHub or GitLab sources without local snapshot fallback.
83
- - PR scans use `mode: "pr"` and require one provider-backed GitHub repository plus one or more `pullRequests`.
83
+ - PR scans use `mode: "pr"` and require one provider-backed GitHub or company-enabled, connected Forgejo repository plus one or more `pullRequests`.
84
84
  - Starting a PR scan may run alongside active workspace scans and does not require `force`. Starting any non-PR scan still treats every active scan, including a PR scan, as a blocker; use `force: true` only when the user explicitly wants that overlap.
85
85
  - `apex-workspace-use` accepts a workspace name, prefix, or ID.
86
86
  - Use `sourceMode: "remote"` only when the user explicitly wants to forbid local snapshot fallbacks.
@@ -155,7 +155,7 @@ values. If Audit Setup or another workspace update is active, wait for it to
155
155
  finish before saving again.
156
156
 
157
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`.
158
+ - CLI `apex scan [target...]` and repeatable `--repo` accept exact files, directories, or HTTPS GitHub/GitLab or company-enabled Forgejo repository URLs; local MCP uses `repoPaths` for the same selections. No target means `cwd`.
159
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
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
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.
@@ -169,3 +169,9 @@ Finding lifecycle:
169
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
170
  - Use the finding updatedAt as expectedUpdatedAt when working from a prior snapshot. On FINDING_STATUS_CONFLICT, reload and review before retrying.
171
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.
172
+
173
+ ## Forgejo
174
+
175
+ Forgejo connections, remote repository scans, PR scans and Fix Review require the Forgejo company feature enabled by an administrator in Bedrock Settings. Run `apex connect forgejo` (or `apex-connect-provider` with `provider: "forgejo"`) to obtain the browser connection URL. A dedicated bot, HTTPS instance URL and repository-scoped read token are required; PR statuses, comments and webhook installation also require a write token.
176
+
177
+ After connecting, run `apex doctor --repo . --source-mode remote`, then `apex scan --mode standard --repo . --source-mode remote` or `apex scan --mode pr --pr 42 --repo .`. Same-repository Forgejo PR URLs use `/owner/repository/pulls/42`. Pass a Forgejo Fix PR URL through finding feedback and start Fix Review using the existing finding commands or MCP tools. Disabling the company flag stops native Forgejo access. Existing local Standard snapshots remain provider-independent.