Skip to content

Commit 6af8c0e

Browse files
committed
COD-1178: expose app APIs and Hotlist via CLI
1 parent 2e29b80 commit 6af8c0e

15 files changed

Lines changed: 818 additions & 19 deletions

File tree

‎README.md‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,26 @@ Show the current API base URL and its source.
131131
codeant get-base-url
132132
```
133133

134+
#### `hotlist`
135+
136+
Query the same organization-wide prioritized findings shown in the CodeAnt Hotlist, or fetch one finding by its stable ID.
137+
138+
```bash
139+
codeant hotlist list --org CodeAnt-AI --service github --severity critical,high
140+
codeant hotlist get 0123456789abcdef0123456789abcdef --org CodeAnt-AI --service github
141+
```
142+
143+
#### `api request`
144+
145+
Call any CodeAnt application API using the saved bearer token. Only relative paths on the configured CodeAnt API host are accepted.
146+
147+
```bash
148+
codeant api request GET /some/read/endpoint --query '{"page":1}'
149+
codeant api request POST /some/app/endpoint --body '{"org":"CodeAnt-AI"}'
150+
```
151+
152+
See [cli-api.md](cli-api.md) for the complete Hotlist, raw API, authentication, self-hosted provider, and agent/MCP manual.
153+
134154
### Global Options
135155

136156
```bash
@@ -240,11 +260,11 @@ node src/index.js secrets --all
240260

241261
This package also ships an MCP (Model Context Protocol) server that exposes CodeAnt's scan, review, and PR data as tools to Claude and other MCP clients. The same source tree is packaged as a Desktop Extension (`.mcpb`) for one-click install in Claude Desktop.
242262

243-
See [mcp.md](mcp.md) for the tools listing, install paths (Claude Code CLI, Claude Desktop manual config, MCPB double-click), and bundling/submission instructions.
263+
See [mcp.md](mcp.md) for the tools listing, install paths (Claude Code CLI, Claude Desktop manual config, MCPB double-click), and bundling/submission instructions. See [cli-api.md](cli-api.md) for Hotlist and generic authenticated API usage.
244264

245265
## Privacy Policy
246266

247-
Full policy: **https://codeant.ai/privacy**
267+
Full policy: **https://www.codeant.ai/privacy-policy**
248268

249269
Summary of what this CLI / MCP server sends and stores:
250270

‎cli-api.md‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# CodeAnt application APIs from the CLI
2+
3+
The CLI uses the same authenticated CodeAnt API and organization/provider context as the web app. Sign in once, discover the connections available to that token, then use a first-class command or the generic API request command.
4+
5+
```bash
6+
codeant login
7+
codeant scans orgs
8+
```
9+
10+
`CODEANT_API_TOKEN` and `CODEANT_API_URL` can be used instead of the saved login for agents, CI, and self-hosted installations.
11+
12+
## Hotlist findings
13+
14+
Hotlist commands use the same organization-wide snapshot, ranking, filters, stable finding IDs, and cursor pagination as the app.
15+
16+
```bash
17+
# First page; org/service are auto-selected when unambiguous
18+
codeant hotlist list
19+
20+
# Highest-priority production findings for one authenticated connection
21+
codeant hotlist list \
22+
--org CodeAnt-AI \
23+
--service github \
24+
--severity critical,high \
25+
--validation exploit_confirmed \
26+
--limit 50
27+
28+
# Fetch every SCA finding across the organization
29+
codeant hotlist list --org CodeAnt-AI --service github --type SCA --all
30+
31+
# Continue a page using next_cursor from the previous response
32+
codeant hotlist list --org CodeAnt-AI --service github --cursor '<cursor>'
33+
34+
# Fetch exactly one finding using the stable ID shown in the app
35+
codeant hotlist get 0123456789abcdef0123456789abcdef \
36+
--org CodeAnt-AI \
37+
--service github
38+
```
39+
40+
Supported `hotlist list` filters:
41+
42+
| Option | Values |
43+
|---|---|
44+
| `--search` | title, repository/account, path, package, CVE, or check ID |
45+
| `--type` | `AI Exploitation`, `SCA`, `SAST`, `Secrets`, `IaC`, `Infrastructure` |
46+
| `--location` | repository full names or cloud accounts |
47+
| `--severity` | `critical`, `high`, `medium`, `low`, `unknown` |
48+
| `--ticket-status` | `created`, `not_created` |
49+
| `--compliance` | framework keys such as `soc2` |
50+
| `--validation` | `exploit_confirmed` |
51+
52+
Comma-separated values are accepted. The default page size is 30 and the maximum is 100. `--all` follows every cursor. If the first organization snapshot is still being built, the command waits up to 60 seconds; change that with `--max-wait <seconds>`.
53+
54+
For self-hosted GitHub, GitLab, Bitbucket, or Azure DevOps, the CLI normally discovers the provider base URL from the authenticated connection. Use `--provider-base-url` only to override it.
55+
56+
## Any app API
57+
58+
Use the generic request command when a first-class command does not exist yet:
59+
60+
```bash
61+
codeant api request GET /some/read/endpoint --query '{"page":1}'
62+
63+
codeant api request POST /some/app/endpoint \
64+
--body '{"org":"CodeAnt-AI","service":"github"}'
65+
66+
codeant api request PATCH /some/app/endpoint \
67+
--body-file ./request.json \
68+
--header 'If-Match: revision-123'
69+
```
70+
71+
The output is JSON:
72+
73+
```json
74+
{
75+
"ok": true,
76+
"status": 200,
77+
"data": {}
78+
}
79+
```
80+
81+
Security properties:
82+
83+
- The path must start with `/` and is always resolved against the configured CodeAnt API host. Absolute and protocol-relative URLs are rejected, so the bearer token cannot be forwarded to another host.
84+
- Authentication is supplied from `CODEANT_API_TOKEN` or the token saved by `codeant login`.
85+
- `Authorization`, `Cookie`, `Host`, and `Content-Length` headers cannot be overridden.
86+
- The backend remains authoritative for account access, organization membership, RBAC, and endpoint authorization.
87+
88+
The generic command can call write endpoints. Review the method, path, and body before running it.
89+
90+
## Agent and MCP access
91+
92+
Run `codeant mcp` or install the CodeAnt MCP bundle. Agents receive dedicated read-only tools:
93+
94+
- `codeant_hotlist_list` — filter and page through organization-wide findings.
95+
- `codeant_hotlist_get` — fetch one finding by stable ID.
96+
- `codeant_api_get` — authenticated GET access for newly-added read APIs.
97+
98+
Set `CODEANT_READ_ONLY=0` to opt in to write tools, including `codeant_api_request` for POST/PUT/PATCH/DELETE. Read-only mode is the default. The MCP server never opens a browser during startup; the agent must explicitly call `codeant_login` when no token is configured.
99+
100+
## Troubleshooting
101+
102+
| Error | Resolution |
103+
|---|---|
104+
| No matching organization | Run `codeant scans orgs`, then pass its exact `organizationName` and `service`. |
105+
| Multiple organizations match | Pass both `--org` and `--service`. |
106+
| Access denied (403) | Run `codeant logout`, then `codeant login`, or replace `CODEANT_API_TOKEN`. |
107+
| Hotlist is still building | Retry, or increase `--max-wait`. |
108+
| Finding not found | Refresh the app/Hotlist and copy the current stable finding ID and tenant context. |

‎mcp.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,22 @@ The CodeAnt CLI ships an MCP (Model Context Protocol) server that exposes CodeAn
1616
| `codeant_scans_get` | read | Severity/category summary for one scan. |
1717
| `codeant_scans_results` | read | Full findings (SAST, SCA, secrets, IaC, …) for one scan. |
1818
| `codeant_scans_dismissed` | read | Dismissed alerts for a repo. |
19+
| `codeant_hotlist_list` | read | Prioritized organization-wide Hotlist findings with stable IDs. |
20+
| `codeant_hotlist_get` | read | One complete Hotlist finding by stable ID. |
21+
| `codeant_api_get` | read | Authenticated GET request to any relative CodeAnt API path. |
1922
| `codeant_pr_list` | read | List PRs/MRs across GitHub, GitLab, Bitbucket, Azure DevOps. |
2023
| `codeant_pr_get` | read | Detail for a PR/MR. |
2124
| `codeant_pr_comments` | read | Comments on a PR, filtered. |
2225
| `codeant_comments_search` | read | Free-text search across CodeAnt review comments. |
2326
| `codeant_review_local` | read | Run a CodeAnt review on local working-copy changes. |
2427
| `codeant_scans_start` | **write** | Trigger a new scan. Gated. |
2528
| `codeant_pr_resolve` | **write** | Resolve a PR conversation thread. Gated. |
29+
| `codeant_api_request` | **write** | Authenticated POST/PUT/PATCH/DELETE request to a relative CodeAnt API path. Gated. |
2630

2731
Write tools are only registered when `CODEANT_READ_ONLY=0`. Default = read-only.
2832

33+
For Hotlist examples, raw API syntax, tenant/provider selection, and response details, see [cli-api.md](cli-api.md).
34+
2935
Every tool carries MCP annotations (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) so the client can decide whether to auto-approve calls.
3036

3137
## Configuration (env vars)
@@ -148,7 +154,7 @@ cd dist/mcpb-stage
148154
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}'; sleep 1) | node server/index.js
149155
```
150156

151-
Expect 11 tools in the `tools/list` response (or 13 if `CODEANT_READ_ONLY=0`).
157+
Expect 16 tools in the `tools/list` response (or 19 if `CODEANT_READ_ONLY=0`).
152158

153159
### Bumping the version
154160

@@ -173,7 +179,7 @@ CodeAnt's MCP server uses stdio + a packaged bundle, so the submission route is
173179
- **Submission URL:** https://claude.com/docs/connectors/building/submission
174180
- **Bundle:** upload `dist/codeant.mcpb`
175181
- **Required metadata:** already in [mcpb/manifest.json](mcpb/manifest.json) — display name, description, author, homepage, documentation, repository, license, keywords, `privacy_policies`, `tools` static listing, `user_config` schema.
176-
- **Privacy policy.** Linked from both [README.md](README.md#privacy-policy) and the manifest's `privacy_policies` field (`https://codeant.ai/privacy`).
182+
- **Privacy policy.** Linked from both [README.md](README.md#privacy-policy) and the manifest's `privacy_policies` field (`https://www.codeant.ai/privacy-policy`).
177183

178184
Reviewer notes worth preparing:
179185

‎mcpb/manifest.json‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
"manifest_version": "0.3",
33
"name": "codeant",
44
"display_name": "CodeAnt AI",
5-
"version": "0.5.1",
5+
"version": "0.5.3",
66
"description": "Drive CodeAnt AI security scans and code review from Claude — org-wide secret triage, cross-repo SAST/SCA findings, on-demand scans, and local PR review.",
7-
"long_description": "CodeAnt AI inside Claude. Ask things like \"how many critical SAST findings do I have across my org?\", \"show every exposed secret in payments-service\", or \"review my staged changes\" — Claude calls the CodeAnt API directly via this MCP server.\n\nIncludes 11 read-only tools (orgs, repos, scan history, scan metadata, findings, dismissed alerts, PRs, comments, comment search, local review) and 2 opt-in write tools (trigger a scan, resolve a PR conversation) gated behind a setting.\n\nRequires a CodeAnt account. Sign up at https://codeant.ai. To authenticate, call the `codeant_login` tool — it opens the CodeAnt sign-in page in your browser and saves the token automatically.\n\nCollects anonymous usage telemetry via PostHog by default; set CODEANT_TELEMETRY_DISABLED=1 to opt out.",
7+
"long_description": "CodeAnt AI inside Claude. Ask things like \"show my highest-priority Hotlist findings\", \"how many critical SAST findings do I have across my org?\", or \"review my staged changes\" — Claude calls the CodeAnt API directly via this MCP server.\n\nIncludes 16 read-only tools, including organization Hotlist list/get and an authenticated GET escape hatch for new APIs, plus 3 opt-in write tools gated behind a setting.\n\nRequires a CodeAnt account. Sign up at https://codeant.ai. To authenticate, call the `codeant_login` tool — it opens the CodeAnt sign-in page in your browser and saves the token automatically.\n\nCollects anonymous usage telemetry via PostHog by default; set CODEANT_TELEMETRY_DISABLED=1 to opt out.",
88
"author": {
99
"name": "CodeAnt AI",
1010
"email": "support@codeant.ai",
1111
"url": "https://codeant.ai"
1212
},
1313
"homepage": "https://codeant.ai",
14-
"documentation": "https://docs.codeant.ai/cli/claude-code-plugin",
15-
"support": "https://docs.codeant.ai/support",
14+
"documentation": "https://github.com/CodeAnt-AI/codeant-cli#readme",
15+
"support": "https://github.com/CodeAnt-AI/codeant-cli/issues",
1616
"repository": {
1717
"type": "git",
1818
"url": "https://github.com/CodeAnt-AI/codeant-cli"
@@ -65,6 +65,9 @@
6565
{ "name": "codeant_scans_get", "description": "Get summary metadata for a single scan (no findings)." },
6666
{ "name": "codeant_scans_results", "description": "Fetch full findings (SAST, SCA, secrets, IaC, etc.) for a scan." },
6767
{ "name": "codeant_scans_dismissed", "description": "List dismissed alerts for a repository." },
68+
{ "name": "codeant_hotlist_list", "description": "List prioritized organization-wide Hotlist findings with stable IDs." },
69+
{ "name": "codeant_hotlist_get", "description": "Fetch one complete Hotlist finding by its stable ID." },
70+
{ "name": "codeant_api_get", "description": "Call any authenticated GET endpoint on the configured CodeAnt API host." },
6871
{ "name": "codeant_pr_list", "description": "List pull requests / merge requests across GitHub, GitLab, Bitbucket, Azure DevOps." },
6972
{ "name": "codeant_pr_get", "description": "Fetch detailed information for a single PR/MR." },
7073
{ "name": "codeant_pr_comments", "description": "List comments on a PR/MR with optional filters." },
@@ -73,7 +76,8 @@
7376
{ "name": "codeant_login", "description": "Open app.codeant.ai in the browser and poll until the user completes sign-in; saves the resulting API token." },
7477
{ "name": "codeant_logout", "description": "Clear the saved API token and sign out of CodeAnt AI." },
7578
{ "name": "codeant_scans_start", "description": "Trigger a new scan run (write — gated behind read_only=false)." },
76-
{ "name": "codeant_pr_resolve", "description": "Resolve a PR conversation thread (write — gated behind read_only=false)." }
79+
{ "name": "codeant_pr_resolve", "description": "Resolve a PR conversation thread (write — gated behind read_only=false)." },
80+
{ "name": "codeant_api_request", "description": "Call an authenticated POST, PUT, PATCH, or DELETE API (write — gated behind read_only=false)." }
7781
],
7882
"user_config": {
7983
"api_token": {

‎package.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@
3434
"./scans/scan-history": "./src/scans/getScanHistory.js",
3535
"./scans/fetch-results": "./src/scans/fetchScanResults.js",
3636
"./scans/fetch-advanced-results": "./src/scans/fetchAdvancedScanResults.js",
37-
"./scans/dismissed-alerts": "./src/scans/fetchDismissedAlerts.js"
37+
"./scans/dismissed-alerts": "./src/scans/fetchDismissedAlerts.js",
38+
"./hotlist": "./src/hotlist/client.js",
39+
"./api": "./src/commands/api/request.js"
3840
},
3941
"files": [
4042
"src"

‎src/commands/api/index.js‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { runApiRequest } from './request.js';
2+
3+
function collect(value, previous) {
4+
return [...previous, value];
5+
}
6+
7+
export default function registerApiCommands(program, { runCmd }) {
8+
const api = program
9+
.command('api')
10+
.description('Call any authenticated CodeAnt application API');
11+
12+
api
13+
.command('request <method> <path>')
14+
.description('Send an authenticated request to a relative CodeAnt API path')
15+
.option('--query <json>', 'Query parameters as a JSON object')
16+
.option('--body <json>', 'JSON request body')
17+
.option('--body-file <path>', 'Read the JSON request body from a file')
18+
.option('-H, --header <header>', 'Additional header (repeatable, "Name: value")', collect, [])
19+
.action((method, path, options) => runCmd(() => runApiRequest({
20+
method,
21+
path,
22+
query: options.query,
23+
body: options.body,
24+
bodyFile: options.bodyFile,
25+
headers: options.header,
26+
})));
27+
}

‎src/commands/api/request.js‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { readFile } from 'node:fs/promises';
2+
3+
import { fetchApiResponse } from '../../utils/fetchApi.js';
4+
5+
const METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
6+
const BLOCKED_HEADERS = new Set(['authorization', 'cookie', 'host', 'content-length']);
7+
8+
export function normalizeApiPath(path) {
9+
const value = String(path || '').trim();
10+
if (!value.startsWith('/') || value.startsWith('//')) {
11+
throw new Error('API path must be relative to the configured CodeAnt API URL and start with `/`.');
12+
}
13+
const resolved = new URL(value, 'https://codeant.invalid');
14+
if (resolved.origin !== 'https://codeant.invalid') {
15+
throw new Error('API path must stay on the configured CodeAnt API host.');
16+
}
17+
return `${resolved.pathname}${resolved.search}`;
18+
}
19+
20+
export function parseJsonObject(value, label) {
21+
const parsed = parseJsonValue(value, label);
22+
if (parsed === undefined) return undefined;
23+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
24+
throw new Error(`${label} must be a JSON object.`);
25+
}
26+
return parsed;
27+
}
28+
29+
export function parseJsonValue(value, label) {
30+
if (value === undefined || value === null || value === '') return undefined;
31+
let parsed;
32+
try {
33+
parsed = typeof value === 'string' ? JSON.parse(value) : value;
34+
} catch (error) {
35+
throw new Error(`Invalid ${label} JSON: ${error.message}`);
36+
}
37+
return parsed;
38+
}
39+
40+
export function parseHeaders(values = []) {
41+
const headers = {};
42+
for (const entry of values || []) {
43+
const separator = String(entry).indexOf(':');
44+
if (separator < 1) throw new Error(`Invalid header ${JSON.stringify(entry)}; expected "Name: value".`);
45+
const name = String(entry).slice(0, separator).trim();
46+
const value = String(entry).slice(separator + 1).trim();
47+
if (BLOCKED_HEADERS.has(name.toLowerCase())) {
48+
throw new Error(`Header ${name} is managed by CodeAnt CLI and cannot be overridden.`);
49+
}
50+
headers[name] = value;
51+
}
52+
return headers;
53+
}
54+
55+
export async function runApiRequest({ path, method, query, body, bodyFile, headers } = {}) {
56+
const normalizedMethod = String(method || 'GET').toUpperCase();
57+
if (!METHODS.has(normalizedMethod)) {
58+
throw new Error(`Unsupported method ${normalizedMethod}. Use ${[...METHODS].join(', ')}.`);
59+
}
60+
if (body !== undefined && bodyFile) {
61+
throw new Error('Use either --body or --body-file, not both.');
62+
}
63+
64+
let requestBody = parseJsonValue(body, 'body');
65+
if (bodyFile) {
66+
requestBody = parseJsonValue(await readFile(bodyFile, 'utf8'), 'body file');
67+
}
68+
69+
const response = await fetchApiResponse(normalizeApiPath(path), {
70+
method: normalizedMethod,
71+
query: parseJsonObject(query, 'query'),
72+
body: requestBody,
73+
headers: parseHeaders(headers),
74+
allowHttpError: true,
75+
});
76+
return { ok: response.ok, status: response.status, data: response.data };
77+
}

0 commit comments

Comments
 (0)