Skip to content

Commit 1fc81c7

Browse files
authored
Merge pull request #5 from CodeAnt-AI/feat/set-telemetry
Graceful handling of review
2 parents 660d7d5 + 034761e commit 1fc81c7

File tree

10 files changed

+88
-109
lines changed

10 files changed

+88
-109
lines changed

changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## [0.3.8] - 03/04/2026
4+
- Graceful handling of review
5+
36
## [0.3.7] - 03/04/2026
47
- Pre-push hook
58

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codeant-cli",
3-
"version": "0.3.7",
3+
"version": "0.3.8",
44
"description": "Code review CLI tool",
55
"type": "module",
66
"bin": {

src/commands/secrets.js

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
renderDone,
1212
} from '../components/SecretsUI.js';
1313

14-
export default function Secrets({ scanType = 'all', failOn = 'CRITICAL', include = [], exclude = [], lastNCommits = 1, baseBranch = null, baseCommit = null }) {
14+
export default function Secrets({ scanType = 'all', include = [], exclude = [], lastNCommits = 1, baseBranch = null, baseCommit = null }) {
1515
const { exit } = useApp();
1616
const [status, setStatus] = useState('initializing');
1717
const [secrets, setSecrets] = useState([]);
@@ -20,14 +20,6 @@ export default function Secrets({ scanType = 'all', failOn = 'CRITICAL', include
2020
const [scanMeta, setScanMeta] = useState(null);
2121
const [startTime] = useState(() => Date.now());
2222

23-
const shouldFailOn = (confidenceScore) => {
24-
const score = confidenceScore?.toUpperCase();
25-
if (score === 'FALSE_POSITIVE') return false;
26-
if (failOn === 'HIGH') return score === 'HIGH';
27-
if (failOn === 'MEDIUM') return score === 'HIGH' || score === 'MEDIUM';
28-
return true;
29-
};
30-
3123
useEffect(() => {
3224
let cancelled = false;
3325

@@ -81,10 +73,10 @@ export default function Secrets({ scanType = 'all', failOn = 'CRITICAL', include
8173

8274
useEffect(() => {
8375
if (status === 'done') {
84-
const hasBlockingSecrets = secrets.some(file =>
85-
file.secrets.some(secret => shouldFailOn(secret.confidence_score))
76+
const hasSecrets = secrets.some(file =>
77+
file.secrets.length > 0
8678
);
87-
if (hasBlockingSecrets) {
79+
if (hasSecrets) {
8880
setTimeout(() => { process.exitCode = 1; exit(new Error('Secrets detected')); }, 100);
8981
} else {
9082
setTimeout(() => exit(), 100);
@@ -101,7 +93,7 @@ export default function Secrets({ scanType = 'all', failOn = 'CRITICAL', include
10193
if (status === 'scanning') return renderScanning(startTime, fileCount, scanMeta);
10294
if (status === 'no_files') return renderNoFiles(scanType, lastNCommits, baseBranch, baseCommit);
10395
if (status === 'error') return renderError(error);
104-
if (status === 'done') return renderDone(secrets, failOn, shouldFailOn, startTime, fileCount, scanMeta);
96+
if (status === 'done') return renderDone(secrets, startTime, fileCount, scanMeta);
10597

10698
return null;
10799
}

src/components/SecretsUI.js

Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,8 @@ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧',
77
const DIVIDER = '─'.repeat(55);
88
const STEPS = ['Init', 'Fetch', 'Scan'];
99

10-
const CONFIDENCE_COLORS = {
11-
HIGH: 'red',
12-
MEDIUM: 'yellow',
13-
LOW: 'cyan',
14-
FALSE_POSITIVE: 'gray',
15-
};
16-
1710
// ── Helpers ──────────────────────────────────────────────────────────────────
1811

19-
function getConfidenceColor(score) {
20-
return CONFIDENCE_COLORS[score?.toUpperCase()] || 'gray';
21-
}
22-
2312
function padEnd(str, len) {
2413
return str + ' '.repeat(Math.max(0, len - str.length));
2514
}
@@ -211,22 +200,13 @@ export function renderNotLoggedIn() {
211200
);
212201
}
213202

214-
export function renderDone(secrets, failOn, shouldFailOn, startTime, fileCount, meta) {
203+
export function renderDone(secrets, startTime, fileCount, meta) {
215204
const allSecrets = secrets.flatMap(file =>
216205
file.secrets.map(s => ({ ...s, file_path: file.file_path }))
217206
);
218207

219-
const blockingSecrets = allSecrets.filter(s => shouldFailOn(s.confidence_score));
220-
const hasBlocking = blockingSecrets.length > 0;
221208
const elapsed = startTime ? formatElapsed(startTime) : null;
222209

223-
// Count by confidence
224-
const counts = {};
225-
allSecrets.forEach(s => {
226-
const conf = s.confidence_score?.toUpperCase() || 'UNKNOWN';
227-
counts[conf] = (counts[conf] || 0) + 1;
228-
});
229-
230210
// Group by file
231211
const grouped = {};
232212
allSecrets.forEach(s => {
@@ -269,22 +249,15 @@ export function renderDone(secrets, failOn, shouldFailOn, startTime, fileCount,
269249
));
270250

271251
fileSecrets.forEach((secret, si) => {
272-
const conf = secret.confidence_score?.toUpperCase() || 'UNKNOWN';
273-
const confColor = getConfidenceColor(conf);
274-
const isBlocking = shouldFailOn(secret.confidence_score);
275252
const isLast = si === fileSecrets.length - 1;
276253
const branch = isLast ? '└─' : '├─';
277254
const lineStr = secret.line_number ? `L${secret.line_number}` : '';
278255

279256
els.push(React.createElement(
280257
Box, { key: `s-${fi}-${si}` },
281258
React.createElement(Text, { color: 'gray' }, ` ${branch} `),
282-
React.createElement(Text, {
283-
color: confColor,
284-
bold: isBlocking,
285-
}, padEnd(conf, 16)),
286259
React.createElement(Text, { color: 'gray' }, padEnd(lineStr, 6)),
287-
React.createElement(Text, { color: isBlocking ? 'white' : 'gray', dimColor: !isBlocking }, secret.type || ''),
260+
React.createElement(Text, { color: 'white' }, secret.type || ''),
288261
));
289262
});
290263

@@ -330,28 +303,17 @@ export function renderDone(secrets, failOn, shouldFailOn, startTime, fileCount,
330303

331304
// Secret stats line
332305
if (allSecrets.length > 0) {
333-
const stats = [`${allSecrets.length} secret${allSecrets.length !== 1 ? 's' : ''} found`];
334-
for (const conf of ['HIGH', 'MEDIUM', 'LOW', 'FALSE_POSITIVE']) {
335-
if (counts[conf]) {
336-
const label = conf === 'FALSE_POSITIVE' ? 'false positive' : conf.toLowerCase();
337-
stats.push(`${counts[conf]} ${label}`);
338-
}
339-
}
340306
els.push(React.createElement(
341-
Text, { key: 'stats', color: 'gray' }, stats.join(' · '),
307+
Text, { key: 'stats', color: 'gray' },
308+
`${allSecrets.length} secret${allSecrets.length !== 1 ? 's' : ''} found`,
342309
));
343310
}
344311

345312
// Status line
346-
if (hasBlocking) {
313+
if (allSecrets.length > 0) {
347314
els.push(React.createElement(
348315
Text, { key: 'status', color: 'red', bold: true },
349-
`✗ ${blockingSecrets.length} blocking secret${blockingSecrets.length !== 1 ? 's' : ''} — remove before committing`,
350-
));
351-
} else if (allSecrets.length > 0) {
352-
els.push(React.createElement(
353-
Text, { key: 'status', color: 'green', bold: true },
354-
'✓ No blocking secrets found',
316+
`✗ ${allSecrets.length} secret${allSecrets.length !== 1 ? 's' : ''} found — remove before committing`,
355317
));
356318
} else {
357319
els.push(React.createElement(

src/index.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import GetBaseUrl from './commands/getBaseUrl.js';
1010
import Login from './commands/login.js';
1111
import Logout from './commands/logout.js';
1212
import Review from './commands/review.js';
13+
import { runReviewHeadless } from './reviewHeadless.js';
1314
import Welcome from './components/Welcome.js';
1415
import * as scm from './scm/index.js';
1516
import { setConfigValue } from './utils/config.js';
@@ -58,7 +59,6 @@ program
5859
.option('--last-n-commits <n>', 'Scan last n commits (max 5)', parseInt)
5960
.option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
6061
.option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
61-
.option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
6262
.option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
6363
.option('--exclude <paths>', 'Comma-separated list of file paths glob patterns to exclude')
6464
.action((options) => {
@@ -95,9 +95,7 @@ program
9595
? (Array.isArray(options.exclude) ? options.exclude : splitGlobs(options.exclude))
9696
: [];
9797

98-
const failOn = options.failOn?.toUpperCase() || 'CRITICAL';
99-
100-
render(React.createElement(Secrets, { scanType, failOn, include, exclude, lastNCommits, baseBranch, baseCommit }));
98+
render(React.createElement(Secrets, { scanType, include, exclude, lastNCommits, baseBranch, baseCommit }));
10199
});
102100

103101
program
@@ -114,7 +112,8 @@ program
114112
.option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
115113
.option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
116114
.option('--exclude <paths>', 'Comma-separated list of file paths glob patterns to exclude')
117-
.action((options) => {
115+
.option('--headless', 'Output clean JSON with no spinners (for agents and CI)')
116+
.action(async (options) => {
118117
let scanType = 'all';
119118
let lastNCommits = 1;
120119
let baseBranch = null;
@@ -150,7 +149,23 @@ program
150149

151150
const failOn = options.failOn?.toUpperCase() || 'CRITICAL';
152151

153-
render(React.createElement(Review, { scanType, lastNCommits, failOn, include, exclude, baseBranch, baseCommit }));
152+
if (options.headless) {
153+
const result = await runReviewHeadless({
154+
workspacePath: process.cwd(),
155+
scanType,
156+
lastNCommits,
157+
include,
158+
exclude,
159+
baseBranch,
160+
baseCommit,
161+
onProgress: (msg) => console.error(`[progress] ${msg}`),
162+
onFilesReady: (files, meta) => console.error(`[files] Reviewing ${files.length} file(s)`),
163+
});
164+
console.log(JSON.stringify(result, null, 2));
165+
process.exit(result.error ? 1 : 0);
166+
} else {
167+
render(React.createElement(Review, { scanType, lastNCommits, failOn, include, exclude, baseBranch, baseCommit }));
168+
}
154169
});
155170

156171
program

src/reviewHeadless.js

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ export async function runReviewHeadless(options = {}) {
126126

127127
onProgress(`Analyzing ${perFileRequests.length} file${perFileRequests.length !== 1 ? 's' : ''} in parallel...`);
128128

129-
// ── Per-file agent turn loops (parallel) ────────────────────────────
129+
// ── Per-file agent turn loops (parallel, fault-tolerant) ─────────────
130130
const perFileResults = await Promise.all(
131-
perFileRequests.map((fileReq) => {
131+
perFileRequests.map(async (fileReq) => {
132132
const filename = fileReq._filename;
133133
delete fileReq._filename;
134134

@@ -138,7 +138,15 @@ export async function runReviewHeadless(options = {}) {
138138
}
139139
delete fileReq.file_contents;
140140

141-
return runTurnLoop(fileReq, gitRoot, false);
141+
try {
142+
onProgress(`Reviewing ${filename}...`);
143+
const result = await runTurnLoop(fileReq, gitRoot, false);
144+
onProgress(`Done reviewing ${filename}`);
145+
return result;
146+
} catch (err) {
147+
console.error(`[error] Failed to review ${filename}: ${err.message}`);
148+
return { finalMessage: null, finalOutput: null };
149+
}
142150
})
143151
);
144152

@@ -149,20 +157,26 @@ export async function runReviewHeadless(options = {}) {
149157
output: perFileResults[i].finalOutput,
150158
})).filter(r => r.output?.code_suggestions?.length > 0);
151159

152-
// ── Per-file reflector loops (parallel) ──────────────────────────────
153-
onProgress('Running reflector...');
160+
onProgress(`${perFileWithSuggestions.length} file(s) have suggestions, running reflector...`);
161+
162+
// ── Per-file reflector loops (parallel, fault-tolerant) ──────────────
154163
const reflectorResults = await Promise.all(
155-
perFileWithSuggestions.map(({ diff_content, suggestions }) =>
156-
runTurnLoop(
157-
{
158-
diff_content,
159-
prompt_template_name: 'reflector',
160-
extra_variables: { suggestion_str: suggestions },
161-
},
162-
gitRoot,
163-
true
164-
)
165-
)
164+
perFileWithSuggestions.map(async ({ diff_content, suggestions }, i) => {
165+
try {
166+
return await runTurnLoop(
167+
{
168+
diff_content,
169+
prompt_template_name: 'reflector',
170+
extra_variables: { suggestion_str: suggestions },
171+
},
172+
gitRoot,
173+
true
174+
);
175+
} catch (err) {
176+
console.error(`[error] Reflector failed for file ${i}: ${err.message}`);
177+
return { finalMessage: null, finalOutput: null };
178+
}
179+
})
166180
);
167181

168182
// ── Parse results ───────────────────────────────────────────────────

src/utils/fetchApi.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ const fetchApi = async (endpoint, method = 'GET', body = null) => {
2323

2424
try {
2525
const response = await fetch(url, options);
26+
27+
if (response.status === 403) {
28+
throw new Error('Access denied (403). Please run `codeant logout` and then `codeant login` to re-authenticate.');
29+
}
30+
2631
const data = await response.json();
2732

2833
if (!response.ok) {

0 commit comments

Comments
 (0)