codeant-cli 0.5.8 → 0.5.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/findings.md CHANGED
@@ -49,9 +49,9 @@ codeant findings repo \
49
49
  --output codeant.sarif
50
50
  ```
51
51
 
52
- Supported repo types are `sast`, `sca`, `secrets`, `iac`, `dead_code`, `sbom`, `anti_patterns`, `docstring`, and `complex_functions`. Use `--types all` for all of them. Formats are `json`, `sarif`, `csv`, `md`, and `table`; JSON is the default.
52
+ Supported repo types are `sast`, `sca`, `secrets`, `iac`, `dead_code`, `duplicate_code`, `sbom`, `anti_patterns`, `docstring`, and `complex_functions`. Use `--types all` for all of them. Formats are `json`, `sarif`, `csv`, `md`, and `table`; JSON is the default.
53
53
 
54
- Use `--filter-dismissed` to exclude dismissed findings and `--no-false-positives` to exclude false positives. `--path`, `--check`, `--limit`, and `--offset` support agent-friendly filtering and pagination.
54
+ Use `--filter-dismissed` to exclude dismissed findings and `--no-false-positives` to exclude false positives, including ones users marked in the app. Without those flags, such findings are returned with `metadata.dismissed` or `metadata.false_positive`. `--path`, `--check`, `--limit`, and `--offset` support agent-friendly filtering and pagination.
55
55
 
56
56
  ## Organization Hotlist findings
57
57
 
package/mcp.md CHANGED
@@ -11,18 +11,19 @@ The CodeAnt CLI ships an MCP (Model Context Protocol) server that exposes CodeAn
11
11
  | Name | Read/Write | What it does |
12
12
  |------|------------|--------------|
13
13
  | `codeant_scans_orgs` | read | List authenticated CodeAnt orgs. |
14
- | `codeant_scans_repos` | read | List repos in an org. |
14
+ | `codeant_scans_repos` | read | List repos in an org (slim records, paged: `search`, `limit`, `offset`, `full`). |
15
15
  | `codeant_scans_history` | read | Recent scan runs for a repo. |
16
16
  | `codeant_scans_get` | read | Severity/category summary for one scan. |
17
17
  | `codeant_scans_results` | read | Full findings (SAST, SCA, secrets, IaC, …) for one scan. |
18
- | `codeant_scans_dismissed` | read | Dismissed alerts for a repo. |
18
+ | `codeant_scans_dismissed` | read | Dismissed alerts for a repo (any analysis type; `sast` and `anti_patterns` are accepted as aliases). |
19
+ | `codeant_scans_overrides` | read | User false-positive, confidence, and severity overrides for a repo. |
19
20
  | `codeant_hotlist_list` | read | Prioritized organization-wide Hotlist findings with stable IDs. |
20
21
  | `codeant_hotlist_get` | read | One complete Hotlist finding by stable ID. |
21
22
  | `codeant_findings_antipatterns` | read | Anti-pattern findings across selected or all organization repos (paged: `limit`, `offset`, `all`). |
22
- | `codeant_cloud_scan_history` | read | AWS/Azure/GCP CSPM, VM, or container scan history. |
23
- | `codeant_cloud_findings_list` | read | Findings for one CSPM, VM, or container scan (VM/container paged: `limit`, `offset`, `all`). |
23
+ | `codeant_cloud_scan_history` | read | AWS/Azure/GCP CSPM, VM, or container scan history (newest first, 10 per provider by default and up to 200, paged with `limit`/`offset`, without per-service rollups unless `full`). |
24
+ | `codeant_cloud_findings_list` | read | Findings for one CSPM, VM, or container scan (paged: `limit`, `offset`, `all`; compliance mappings only with `full`). |
24
25
  | `codeant_cloud_finding_get` | read | Full detail for one cloud finding UID. |
25
- | `codeant_pentest_history` | read | Pentest engagement history. |
26
+ | `codeant_pentest_history` | read | Pentest engagement history (paged: `limit`, `offset`, `full`). |
26
27
  | `codeant_pentest_issues` | read | All available issues for a pentest engagement. |
27
28
  | `codeant_pentest_report` | read | Full pentest customer report. |
28
29
  | `codeant_api_get` | read | Authenticated GET request to any relative CodeAnt app API path, with exact org/provider context. |
@@ -30,7 +31,9 @@ The CodeAnt CLI ships an MCP (Model Context Protocol) server that exposes CodeAn
30
31
  | `codeant_pr_get` | read | Detail for a PR/MR. |
31
32
  | `codeant_pr_comments` | read | Comments on a PR, filtered. |
32
33
  | `codeant_comments_search` | read | Free-text search across CodeAnt review comments. |
33
- | `codeant_review_local` | read | Run a CodeAnt review on local working-copy changes. |
34
+ | `codeant_review_local` | read | Run a CodeAnt review on local working-copy changes in the server's working directory. |
35
+ | `codeant_login` | auth | Start browser sign-in; returns the sign-in link immediately. Call again to confirm. |
36
+ | `codeant_logout` | auth | Revoke and clear the saved token (shared with the CLI). |
34
37
  | `codeant_scans_start` | **write** | Trigger a new scan. Gated. |
35
38
  | `codeant_pr_resolve` | **write** | Resolve a PR conversation thread. Gated. |
36
39
  | `codeant_api_request` | **write** | Authenticated POST/PUT/PATCH/DELETE request to a relative CodeAnt app API path, with exact org/provider context. Gated. |
@@ -39,17 +42,24 @@ Write tools are only registered when `CODEANT_READ_ONLY=0`. Default = read-only.
39
42
 
40
43
  For complete finding coverage, examples, tenant/provider selection, and response details, see the [CodeAnt findings documentation](https://docs.codeant.ai/cli/findings).
41
44
 
42
- Every tool carries MCP annotations (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) so the client can decide whether to auto-approve calls.
45
+ Every tool carries MCP annotations (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) so the client can decide whether to auto-approve calls. The server also sends `instructions` (discovery order, paging, auth recovery) in its `initialize` result.
46
+
47
+ Results are compact JSON. Any result over `CODEANT_MCP_MAX_RESULT_CHARS` characters is refused with an error and a narrowing hint; [src/mcp/shape.js](src/mcp/shape.js) holds the MCP-only projections and page defaults. CLI commands keep their full output.
48
+
49
+ `codeant_login` runs sign-in in the background: the first call returns `{ status: "pending", loginUrl, browserOpened }`, and the next call returns `{ status: "success" }` once the user finishes. The token is read from `~/.codeant/config.json` on every request, so CLI logins and logouts apply to a running server.
43
50
 
44
51
  ## Configuration (env vars)
45
52
 
46
53
  | Var | Purpose | Default |
47
54
  |-----|---------|---------|
48
- | `CODEANT_API_TOKEN` | API token (CodeAnt account → settings → API). Required. | — |
49
- | `CODEANT_API_URL` | API base URL. Override for self-hosted. | `https://api.codeant.ai` |
50
- | `CODEANT_READ_ONLY` | `1` to hide write tools, `0` to expose them. | `1` |
55
+ | `CODEANT_API_TOKEN` | API token. Optional: without it the server uses the token saved by `codeant login` or `codeant_login`. | — |
56
+ | `CODEANT_API_URL` | API base URL. Override for self-hosted. | `https://service.codeant.ai` |
57
+ | `CODEANT_DASHBOARD_URL` | Web app URL used by `codeant_login`. Needed on self-hosted base URLs. | detected |
58
+ | `CODEANT_READ_ONLY` | `0` or `false` exposes write tools; anything else hides them. | read-only |
59
+ | `CODEANT_TELEMETRY_DISABLED` | `1` or `true` stops usage events (PostHog, tied to the API token). | enabled |
60
+ | `CODEANT_MCP_MAX_RESULT_CHARS` | Largest tool result, in characters, before the server refuses it with a hint. | `80000` |
51
61
 
52
- The CLI also reads these from `~/.codeant/config.json` when not set in env. For MCP usage prefer env — it keeps per-client config explicit.
62
+ The token, base URL, and dashboard URL fall back to `~/.codeant/config.json` when not set in env. PR tools also need an SCM token (`GITHUB_TOKEN`, `GITLAB_TOKEN`, `BITBUCKET_TOKEN`, `AZURE_DEVOPS_TOKEN`) and, for Azure DevOps, `AZURE_DEVOPS_ORG_URL`.
53
63
 
54
64
  ---
55
65
 
@@ -108,10 +118,13 @@ npm run mcpb:build
108
118
  open -a Claude dist/codeant.mcpb
109
119
  ```
110
120
 
111
- Claude pops an install dialog asking for the `user_config` fields:
112
- - **CodeAnt API token** (sensitive, required)
113
- - **API base URL** (defaults to `https://api.codeant.ai`)
121
+ Claude pops an install dialog asking for the `user_config` fields (all optional):
122
+ - **CodeAnt API token** (sensitive; leave blank and call `codeant_login` instead)
123
+ - **API base URL** (defaults to `https://api.codeant.ai`, which the CLI maps to `https://service.codeant.ai`)
124
+ - **Dashboard URL** (self-hosted only; used by `codeant_login`)
114
125
  - **Read-only mode** (defaults to on)
126
+ - **Disable telemetry** (defaults to off)
127
+ - SCM tokens and self-hosted SCM URLs for the PR tools
115
128
 
116
129
  After install, manage it in **Settings → Connectors**.
117
130
 
@@ -161,7 +174,7 @@ cd dist/mcpb-stage
161
174
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'; sleep 1) | node server/index.js
162
175
  ```
163
176
 
164
- Expect 23 tools in the `tools/list` response (or 26 if `CODEANT_READ_ONLY=0`).
177
+ Expect 24 tools in the `tools/list` response (or 27 if `CODEANT_READ_ONLY=0`). `tests/mcp/server.test.js` checks that this list matches the manifest's `tools`.
165
178
 
166
179
  ### Bumping the version
167
180
 
@@ -190,9 +203,9 @@ CodeAnt's MCP server uses stdio + a packaged bundle, so the submission route is
190
203
 
191
204
  Reviewer notes worth preparing:
192
205
 
193
- - **Auth model.** CodeAnt uses a user-scoped API token entered into `user_config.api_token`. The token never leaves the user's machine — the bundle talks to `api.codeant.ai` (or the user's self-hosted URL) directly. No third-party OAuth flow needed.
206
+ - **Auth model.** Users sign in with the `codeant_login` tool (browser sign-in, token saved to `~/.codeant/config.json`) or paste a token into `user_config.api_token`. The token is stored only on the user's machine (`~/.codeant/config.json`) and is sent only to the CodeAnt API (or the user's self-hosted URL) as a Bearer token — never to any third party. No third-party OAuth flow needed.
194
207
  - **Sandbox creds.** Email support@codeant.ai for a reviewer sandbox token; paste it into the submission form's reviewer-notes field along with an org slug that has scans + PRs to browse.
195
- - **Write tools.** Gated behind `user_config.read_only` (defaults to on). Reviewers can toggle off to test `codeant_scans_start` / `codeant_pr_resolve`.
208
+ - **Write tools.** Gated behind `user_config.read_only` (defaults to on). Reviewers can toggle off to test `codeant_scans_start` / `codeant_pr_resolve` / `codeant_api_request`.
196
209
 
197
210
  ---
198
211
 
@@ -203,5 +216,8 @@ Reviewer notes worth preparing:
203
216
  | Server fails to start in Claude Desktop | `~/Library/Logs/Claude/mcp-server-codeant.log` |
204
217
  | Tool calls return errors | Same log — server stderr is captured there. |
205
218
  | Tool not listed at all | Check `CODEANT_READ_ONLY` — write tools are hidden when set to `1`. |
206
- | Auth errors on every tool | Confirm `CODEANT_API_TOKEN` is set in the MCP env (not just in `~/.codeant/config.json`). |
219
+ | Auth errors on every tool | Call `codeant_login`, or run `codeant login` in a terminal; the server reads `~/.codeant/config.json` on every request. An explicit `CODEANT_API_TOKEN` in the MCP env takes precedence, so check it is current. |
220
+ | `codeant_login` fails on a self-hosted instance | Set `CODEANT_DASHBOARD_URL` (or the MCPB **Dashboard URL** setting, or `codeant set-dashboard-url`). |
221
+ | A tool returns "Result too large" | Follow the hint (smaller `limit`, filters, `fields`), or raise `CODEANT_MCP_MAX_RESULT_CHARS` if your client accepts larger results. |
222
+ | `codeant_review_local` returns "Could not find a .git directory" | The client started the server outside a git repository (Claude Desktop does). Use a project-scoped client such as Claude Code. |
207
223
  | MCPB install dialog never appears | Open `.mcpb` with `open -a Claude dist/codeant.mcpb` to force the desktop app to handle it. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeant-cli",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "repository": {
@@ -36,6 +36,7 @@
36
36
  "./scans/fetch-results": "./src/scans/fetchScanResults.js",
37
37
  "./scans/fetch-advanced-results": "./src/scans/fetchAdvancedScanResults.js",
38
38
  "./scans/dismissed-alerts": "./src/scans/fetchDismissedAlerts.js",
39
+ "./scans/issue-overrides": "./src/scans/fetchIssueOverrides.js",
39
40
  "./hotlist": "./src/hotlist/client.js",
40
41
  "./findings/cloud": "./src/findings/cloud.js",
41
42
  "./findings/pentest": "./src/findings/pentest.js",
package/scans.md CHANGED
@@ -135,11 +135,12 @@ codeant scans results --repo <owner/repo> [options]
135
135
  | `--repo <repo>` | **(required)** Repository in `owner/repo` format |
136
136
  | `--scan <sha>` | Specific commit SHA to use |
137
137
  | `--branch <name>` | Resolve latest scan on this branch |
138
- | `--types <list>` | Comma-separated types: `sast`, `sca`, `secrets`, `iac`, `dead_code`, `sbom`, `anti_patterns`, `docstring`, `complex_functions`, `all` (default: `all`) |
138
+ | `--types <list>` | Comma-separated types: `sast`, `sca`, `secrets`, `iac`, `dead_code`, `duplicate_code`, `sbom`, `anti_patterns`, `docstring`, `complex_functions`, `all` (default: `all`) |
139
139
  | `--severity <list>` | Filter by severity (e.g. `critical,high`) |
140
140
  | `--path <glob>` | Filter by file path glob |
141
141
  | `--check <regex>` | Filter by check ID or name (regex) |
142
- | `--include-dismissed` | Include dismissed findings (excluded by default) |
142
+ | `--filter-dismissed` | Exclude dismissed findings (included by default, marked `metadata.dismissed`) |
143
+ | `--no-false-positives` | Exclude false positives, including ones users marked in the app (included by default, marked `metadata.false_positive`) |
143
144
  | `--format <fmt>` | Output format: `json`, `sarif`, `csv`, `md`, `table` (default: `json`) |
144
145
  | `--output <path>` | Write output to file instead of stdout |
145
146
  | `--fields <list>` | Project findings to a subset of fields (comma-separated) |
@@ -173,8 +174,8 @@ codeant scans results --repo acme/backend --format md
173
174
  # Output as SARIF to a file
174
175
  codeant scans results --repo acme/backend --format sarif --output results.sarif
175
176
 
176
- # Include dismissed findings
177
- codeant scans results --repo acme/backend --include-dismissed
177
+ # Only open findings: drop dismissed findings and false positives
178
+ codeant scans results --repo acme/backend --filter-dismissed --no-false-positives
178
179
 
179
180
  # Paginate through results
180
181
  codeant scans results --repo acme/backend --limit 50 --offset 100
@@ -206,7 +207,7 @@ codeant scans dismissed --repo <owner/repo> [options]
206
207
  | Option | Description |
207
208
  |--------|-------------|
208
209
  | `--repo <repo>` | **(required)** Repository in `owner/repo` format |
209
- | `--analysis-type <type>` | Analysis type: `security` or `secrets` (default: `security`) |
210
+ | `--analysis-type <type>` | `security`, `secrets`, `sca`, `iac`, `antipatterns`, `docstring`, `complex_functions`, `dead_code`, or `duplicate_code` (default: `security`). Result-type names `sast` and `anti_patterns` are accepted too. |
210
211
 
211
212
  **Examples:**
212
213
 
@@ -220,6 +221,16 @@ codeant scans dismissed --repo acme/backend --analysis-type secrets
220
221
 
221
222
  ---
222
223
 
224
+ ### `scans overrides`
225
+
226
+ List the per-finding overrides users set in the CodeAnt app: Mark/Unmark false positive (`security`, `iac`), secrets confidence (`secrets`), and Change Severity (`security`, `sca`). `scans results` already applies them.
227
+
228
+ ```bash
229
+ codeant scans overrides --repo <owner/repo> [--analysis-type security|secrets|iac|sca]
230
+ ```
231
+
232
+ ---
233
+
223
234
  ### `scans start-scan`
224
235
 
225
236
  Trigger a new analysis run for a repository.
@@ -1,16 +1,34 @@
1
1
  import { fetchDismissedAlerts } from '../../scans/fetchDismissedAlerts.js';
2
+ import { fetchIssueOverrides } from '../../scans/fetchIssueOverrides.js';
2
3
 
3
- /**
4
- * codeant scans dismissed --repo <repo> [--analysis-type security|secrets]
5
- */
6
- export async function runDismissed({ repo, analysisType = 'security' } = {}) {
4
+ // Result-type names (`scans results --types`) → the names dismissals and
5
+ // overrides are stored under.
6
+ const STORE_ANALYSIS_TYPES = {
7
+ sast: 'security',
8
+ security_issues: 'security',
9
+ anti_patterns: 'antipatterns',
10
+ };
11
+
12
+ export function storeAnalysisType(analysisType) {
13
+ return STORE_ANALYSIS_TYPES[analysisType] || analysisType;
14
+ }
15
+
16
+ function requireRepo(repo) {
7
17
  if (!repo) {
8
18
  const err = new Error('--repo is required');
9
19
  err.exitCode = 1;
10
20
  throw err;
11
21
  }
22
+ }
23
+
24
+ /**
25
+ * codeant scans dismissed --repo <repo> [--analysis-type <type>]
26
+ */
27
+ export async function runDismissed({ repo, analysisType = 'security' } = {}) {
28
+ requireRepo(repo);
29
+ const type = storeAnalysisType(analysisType);
12
30
 
13
- const result = await fetchDismissedAlerts(repo, analysisType);
31
+ const result = await fetchDismissedAlerts(repo, type);
14
32
  if (!result.success) {
15
33
  const err = new Error(result.error || 'Failed to fetch dismissed alerts');
16
34
  err.exitCode = 1;
@@ -19,8 +37,30 @@ export async function runDismissed({ repo, analysisType = 'security' } = {}) {
19
37
 
20
38
  return {
21
39
  repo,
22
- analysis_type: analysisType,
40
+ analysis_type: type,
23
41
  total: result.dismissedAlerts.length,
24
42
  dismissed_alerts: result.dismissedAlerts,
25
43
  };
26
44
  }
45
+
46
+ /**
47
+ * codeant scans overrides --repo <repo> [--analysis-type security|secrets|iac|sca]
48
+ */
49
+ export async function runOverrides({ repo, analysisType = 'security' } = {}) {
50
+ requireRepo(repo);
51
+ const type = storeAnalysisType(analysisType);
52
+
53
+ const result = await fetchIssueOverrides(repo, type);
54
+ if (!result.success) {
55
+ const err = new Error(result.error || 'Failed to fetch issue overrides');
56
+ err.exitCode = 1;
57
+ throw err;
58
+ }
59
+
60
+ return {
61
+ repo,
62
+ analysis_type: type,
63
+ total: Object.keys(result.overrides).length,
64
+ overrides: result.overrides,
65
+ };
66
+ }
@@ -3,10 +3,14 @@ import { runRepos } from './repos.js';
3
3
  import { runHistory } from './history.js';
4
4
  import { runGet } from './get.js';
5
5
  import { runResults } from './results.js';
6
- import { runDismissed } from './dismissed.js';
6
+ import { runDismissed, runOverrides } from './dismissed.js';
7
7
  import { runStartScan } from './start-scan.js';
8
8
  import { setQuiet, setNoColor } from './lib/log.js';
9
9
  import { setNoColor as tableSetNoColor } from './formatters/table.js';
10
+ import { intOption } from '../../utils/intOption.js';
11
+
12
+ const toCount = intOption({ min: 1 });
13
+ const toOffset = intOption();
10
14
 
11
15
  /**
12
16
  * Register all `codeant scans <verb>` subcommands.
@@ -37,7 +41,7 @@ export default function registerScansCommands(program, { runCmd }) {
37
41
  .requiredOption('--repo <repo>', 'Repository (owner/repo)')
38
42
  .option('--branch <name>', 'Filter by branch name')
39
43
  .option('--since <iso>', 'Show scans since ISO date')
40
- .option('--limit <n>', 'Max results (default: 20)', parseInt, 20)
44
+ .option('--limit <n>', 'Max results (default: 20)', toCount, 20)
41
45
  .action((opts) =>
42
46
  runCmd(() => runHistory({ repo: opts.repo, branch: opts.branch, since: opts.since, limit: opts.limit }))
43
47
  );
@@ -65,7 +69,7 @@ export default function registerScansCommands(program, { runCmd }) {
65
69
  .option('--branch <name>', 'Resolve latest scan on this branch')
66
70
  .option(
67
71
  '--types <list>',
68
- 'Comma-separated types: sast,sca,secrets,iac,dead_code,sbom,anti_patterns,docstring,complex_functions,all',
72
+ 'Comma-separated types: sast,sca,secrets,iac,dead_code,duplicate_code,sbom,anti_patterns,docstring,complex_functions,all',
69
73
  'all'
70
74
  )
71
75
  .option('--severity <list>', 'Filter by severity (e.g. critical,high)')
@@ -76,8 +80,8 @@ export default function registerScansCommands(program, { runCmd }) {
76
80
  .option('--format <fmt>', 'Output format: json|sarif|csv|md|table (default: json)', 'json')
77
81
  .option('--output <path>', 'Write output to file instead of stdout')
78
82
  .option('--fields <list>', 'Project findings to subset of fields (comma-separated)')
79
- .option('--limit <n>', 'Max findings per page (default: 100)', parseInt, 100)
80
- .option('--offset <n>', 'Pagination offset (default: 0)', parseInt, 0)
83
+ .option('--limit <n>', 'Max findings per page (default: 100)', toCount, 100)
84
+ .option('--offset <n>', 'Pagination offset (default: 0)', toOffset, 0)
81
85
  .option('--fail-fast', 'Exit 3 on first category fetch failure')
82
86
  .option('--no-color', 'Disable ANSI color (auto-disabled when not a TTY)')
83
87
  .option('--quiet', 'Suppress progress output on stderr')
@@ -117,11 +121,25 @@ export default function registerScansCommands(program, { runCmd }) {
117
121
  .command('dismissed')
118
122
  .description('List dismissed alerts for a repository')
119
123
  .requiredOption('--repo <repo>', 'Repository (owner/repo)')
120
- .option('--analysis-type <type>', 'Analysis type: security|secrets (default: security)', 'security')
124
+ .option(
125
+ '--analysis-type <type>',
126
+ 'Analysis type: security|secrets|sca|iac|antipatterns|docstring|complex_functions|dead_code|duplicate_code (default: security)',
127
+ 'security'
128
+ )
121
129
  .action((opts) =>
122
130
  runCmd(() => runDismissed({ repo: opts.repo, analysisType: opts.analysisType }))
123
131
  );
124
132
 
133
+ // ── overrides ──────────────────────────────────────────────────────────────
134
+ scans
135
+ .command('overrides')
136
+ .description('List user overrides (false positive, confidence, severity) for a repository')
137
+ .requiredOption('--repo <repo>', 'Repository (owner/repo)')
138
+ .option('--analysis-type <type>', 'Analysis type: security|secrets|iac|sca (default: security)', 'security')
139
+ .action((opts) =>
140
+ runCmd(() => runOverrides({ repo: opts.repo, analysisType: opts.analysisType }))
141
+ );
142
+
125
143
  // ── start-scan ─────────────────────────────────────────────────────────────
126
144
  scans
127
145
  .command('start-scan')
@@ -10,6 +10,7 @@ import {
10
10
  fetchSecretsResults,
11
11
  fetchIacResults,
12
12
  fetchDeadCodeResults,
13
+ fetchDuplicateCodeResults,
13
14
  } from '../../../scans/fetchAdvancedScanResults.js';
14
15
 
15
16
  /**
@@ -26,6 +27,7 @@ export const CATEGORIES = {
26
27
  secrets: { fetcher: fetchSecretsResults, kind: 'secret' },
27
28
  iac: { fetcher: fetchIacResults, kind: 'config' },
28
29
  dead_code: { fetcher: fetchDeadCodeResults, kind: 'code' },
30
+ duplicate_code: { fetcher: fetchDuplicateCodeResults, kind: 'code' },
29
31
  };
30
32
 
31
33
  /**
@@ -87,6 +87,20 @@ export function normalizeIssue(issue, category) {
87
87
  } else if (category === 'sca') {
88
88
  if (issue.cvss_score !== undefined) metadata.cvss_score = issue.cvss_score;
89
89
  if (issue.fix_version) metadata.fix_version = issue.fix_version;
90
+ } else if (category === 'duplicate_code') {
91
+ if (issue.duplicate_group !== undefined) metadata.duplicate_group = issue.duplicate_group;
92
+ if (issue.duplicate_locations) metadata.duplicate_locations = issue.duplicate_locations;
93
+ }
94
+
95
+ // Backend dispositions, present when dismissed / false positives are not filtered out.
96
+ if (issue.is_dismissed === true) {
97
+ metadata.dismissed = true;
98
+ if (issue.reason_for_dismiss) metadata.reason_for_dismiss = issue.reason_for_dismiss;
99
+ if (issue.comment_for_dismiss) metadata.comment_for_dismiss = issue.comment_for_dismiss;
100
+ }
101
+ // Secrets from older backends only carry the FALSE_POSITIVE confidence.
102
+ if (issue.is_false_positive === true || String(issue.confidence_score || '').toUpperCase() === 'FALSE_POSITIVE') {
103
+ metadata.false_positive = true;
90
104
  }
91
105
 
92
106
  const id = buildId(category, filePath, lineNumber, checkId, message);
@@ -30,6 +30,7 @@ export async function runResults(opts = {}) {
30
30
  limit = 100,
31
31
  offset = 0,
32
32
  failFast = false,
33
+ returnEnvelope = false,
33
34
  } = opts;
34
35
 
35
36
  if (!repo) {
@@ -145,7 +146,8 @@ export async function runResults(opts = {}) {
145
146
  });
146
147
  }
147
148
 
148
- // 10. Render + emit
149
+ // 10. Render + emit (MCP callers take the envelope instead of stdout)
150
+ if (returnEnvelope) return envelope;
149
151
  const rendered = formatter.render(envelope);
150
152
  emit(rendered, outputPath, envelope.findings.length);
151
153
  }
package/src/index.js CHANGED
@@ -20,6 +20,7 @@ import ScanCenter from './components/ScanCenter.js';
20
20
  import Welcome from './components/Welcome.js';
21
21
  import * as scm from './scm/index.js';
22
22
  import { setConfigValue } from './utils/config.js';
23
+ import { intOption } from './utils/intOption.js';
23
24
  import { track, shutdown as analyticsShutdown, isTelemetryDisabled } from './utils/analytics.js';
24
25
  import registerScansCommands from './commands/scans/index.js';
25
26
  import registerSettingsCommands from './commands/settings/index.js';
@@ -31,6 +32,9 @@ import registerFindingsCommands from './commands/findings/index.js';
31
32
  const require = createRequire(import.meta.url);
32
33
  const pkg = require('../package.json');
33
34
 
35
+ const toCount = intOption({ min: 1 });
36
+ const toOffset = intOption();
37
+
34
38
  // Split comma-separated globs while preserving commas inside {} brace expansions (e.g. "*.{js,ts}")
35
39
  function splitGlobs(input) {
36
40
  const parts = [];
@@ -67,7 +71,7 @@ program
67
71
  .option('--uncommitted', 'Scan only uncommitted changes (staged + unstaged + untracked)')
68
72
  .option('--staged', 'Scan only staged files')
69
73
  .option('--last-commit', 'Scan last commit')
70
- .option('--last-n-commits <n>', 'Scan last n commits (max 5)', parseInt)
74
+ .option('--last-n-commits <n>', 'Scan last n commits (max 5)', toCount)
71
75
  .option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
72
76
  .option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
73
77
  .option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
@@ -118,7 +122,7 @@ program
118
122
  .option('--uncommitted', 'Review only uncommitted changes (staged + unstaged + untracked)')
119
123
  .option('--staged', 'Review only staged files')
120
124
  .option('--last-commit', 'Review last commit')
121
- .option('--last-n-commits <n>', 'Review last n commits (max 5)', parseInt)
125
+ .option('--last-n-commits <n>', 'Review last n commits (max 5)', toCount)
122
126
  .option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
123
127
  .option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
124
128
  .option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
@@ -299,8 +303,8 @@ program
299
303
  .option('--source-branch <branch>', 'Filter by source branch (partial match)')
300
304
  .option('--author <login>', 'Filter by author (fuzzy match)')
301
305
  .option('--state <state>', 'open or closed (default: open)', 'open')
302
- .option('--limit <n>', 'Max results (default: 20, max: 100)', parseInt, 20)
303
- .option('--offset <n>', 'Pagination offset', parseInt, 0)
306
+ .option('--limit <n>', 'Max results (default: 20, max: 100)', intOption({ min: 1, max: 100 }), 20)
307
+ .option('--offset <n>', 'Pagination offset', toOffset, 0)
304
308
  .action((options) => {
305
309
  const opts = resolveRepoOpts(options);
306
310
  runCmd(() => scm.listPullRequests({
@@ -315,7 +319,7 @@ program
315
319
  .option('--name <repo>', 'Repository (owner/repo)')
316
320
  .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
317
321
  .option('--default-branch <branch>', 'Default branch name')
318
- .requiredOption('--pr-number <n>', 'PR number', parseInt)
322
+ .requiredOption('--pr-number <n>', 'PR number', toCount)
319
323
  .action((options) => {
320
324
  const opts = resolveRepoOpts(options);
321
325
  runCmd(() => scm.getPullRequest({
@@ -329,7 +333,7 @@ program
329
333
  .option('--name <repo>', 'Repository (owner/repo)')
330
334
  .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
331
335
  .option('--default-branch <branch>', 'Default branch name')
332
- .requiredOption('--pr-number <n>', 'PR number', parseInt)
336
+ .requiredOption('--pr-number <n>', 'PR number', toCount)
333
337
  .option('--codeant-generated <bool>', 'Filter by CodeAnt authorship (true/false)', (v) => v === 'true')
334
338
  .option('--addressed', 'Filter by addressed/resolved status')
335
339
  .option('--created-after <date>', 'ISO 8601 date filter')
@@ -347,8 +351,8 @@ program
347
351
  .description('Resolve a conversation/comment thread on a PR')
348
352
  .option('--name <repo>', 'Repository (owner/repo)')
349
353
  .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
350
- .requiredOption('--pr-number <n>', 'PR number', parseInt)
351
- .option('--comment-id <id>', 'Comment ID (GitHub, Bitbucket)', parseInt)
354
+ .requiredOption('--pr-number <n>', 'PR number', toCount)
355
+ .option('--comment-id <id>', 'Comment ID (GitHub, Bitbucket)', toCount)
352
356
  .option('--thread-id <id>', 'Thread/node ID (GitHub GraphQL, Azure)')
353
357
  .option('--discussion-id <id>', 'Discussion ID (GitLab)')
354
358
  .action((options) => {
@@ -370,10 +374,10 @@ program
370
374
  .option('--name <repo>', 'Repository (owner/repo)')
371
375
  .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
372
376
  .option('--default-branch <branch>', 'Default branch name')
373
- .option('--pr-number <n>', 'Filter by PR number', parseInt)
377
+ .option('--pr-number <n>', 'Filter by PR number', toCount)
374
378
  .option('--status <status>', 'Filter by status (PENDING, COMPLETED, FAILED, etc.)')
375
- .option('--limit <n>', 'Max results (default: 20)', parseInt, 20)
376
- .option('--offset <n>', 'Pagination offset', parseInt, 0)
379
+ .option('--limit <n>', 'Max results (default: 20)', toCount, 20)
380
+ .option('--offset <n>', 'Pagination offset', toOffset, 0)
377
381
  .action((options) => {
378
382
  const opts = resolveRepoOpts(options);
379
383
  runCmd(() => scm.listCodeReviews({
@@ -387,7 +391,7 @@ program
387
391
  .description('Get detailed information for a specific code review')
388
392
  .option('--name <repo>', 'Repository (owner/repo)')
389
393
  .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
390
- .requiredOption('--pr-number <n>', 'PR number', parseInt)
394
+ .requiredOption('--pr-number <n>', 'PR number', toCount)
391
395
  .requiredOption('--review-id <id>', 'Code review ID')
392
396
  .action((options) => {
393
397
  const opts = resolveRepoOpts(options);
@@ -405,7 +409,7 @@ program
405
409
  .option('--name <repo>', 'Repository (owner/repo)')
406
410
  .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
407
411
  .requiredOption('--query <term>', 'Search term')
408
- .option('--limit <n>', 'Max results (default: 10, max: 50)', parseInt, 10)
412
+ .option('--limit <n>', 'Max results (default: 10, max: 50)', intOption({ min: 1, max: 50 }), 10)
409
413
  .option('--include-addressed', 'Include resolved comments (default: false)')
410
414
  .option('--created-after <date>', 'ISO 8601 date filter')
411
415
  .action((options) => {