codeant-cli 0.2.0 → 0.2.2

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.
Files changed (67) hide show
  1. package/dist/178.index.js +1 -1
  2. package/dist/239.index.js +1 -1
  3. package/dist/701.index.js +1 -1
  4. package/dist/880.index.js +1 -1
  5. package/dist/935.index.js +1 -1
  6. package/dist/966.index.js +1 -1
  7. package/dist/index.js +4 -4
  8. package/package.json +8 -2
  9. package/src/commands/getBaseUrl.js +25 -0
  10. package/src/commands/login.js +124 -0
  11. package/src/commands/logout.js +30 -0
  12. package/src/commands/review.js +271 -0
  13. package/src/commands/secrets.js +268 -0
  14. package/src/commands/securityAnalysis.js +284 -0
  15. package/src/commands/setBaseUrl.js +37 -0
  16. package/src/commands/staticAnalysis.js +405 -0
  17. package/src/components/ReviewUI.js +410 -0
  18. package/src/components/Welcome.js +144 -0
  19. package/src/index.js +398 -0
  20. package/src/reviewHeadless.js +170 -0
  21. package/src/scm/azure/client.js +21 -0
  22. package/src/scm/azure/getCodeReview.js +20 -0
  23. package/src/scm/azure/getPullRequest.js +30 -0
  24. package/src/scm/azure/index.js +6 -0
  25. package/src/scm/azure/listCodeReviews.js +17 -0
  26. package/src/scm/azure/listPullRequestComments.js +41 -0
  27. package/src/scm/azure/listPullRequests.js +26 -0
  28. package/src/scm/azure/searchComments.js +35 -0
  29. package/src/scm/bitbucket/client.js +18 -0
  30. package/src/scm/bitbucket/getCodeReview.js +3 -0
  31. package/src/scm/bitbucket/getPullRequest.js +26 -0
  32. package/src/scm/bitbucket/index.js +6 -0
  33. package/src/scm/bitbucket/listCodeReviews.js +17 -0
  34. package/src/scm/bitbucket/listPullRequestComments.js +33 -0
  35. package/src/scm/bitbucket/listPullRequests.js +33 -0
  36. package/src/scm/bitbucket/searchComments.js +29 -0
  37. package/src/scm/github/client.js +17 -0
  38. package/src/scm/github/getCodeReview.js +26 -0
  39. package/src/scm/github/getPullRequest.js +27 -0
  40. package/src/scm/github/index.js +6 -0
  41. package/src/scm/github/listCodeReviews.js +17 -0
  42. package/src/scm/github/listPullRequestComments.js +51 -0
  43. package/src/scm/github/listPullRequests.js +32 -0
  44. package/src/scm/github/searchComments.js +30 -0
  45. package/src/scm/gitlab/client.js +16 -0
  46. package/src/scm/gitlab/getCodeReview.js +18 -0
  47. package/src/scm/gitlab/getPullRequest.js +24 -0
  48. package/src/scm/gitlab/index.js +6 -0
  49. package/src/scm/gitlab/listCodeReviews.js +21 -0
  50. package/src/scm/gitlab/listPullRequestComments.js +44 -0
  51. package/src/scm/gitlab/listPullRequests.js +26 -0
  52. package/src/scm/gitlab/searchComments.js +28 -0
  53. package/src/scm/index.js +96 -0
  54. package/src/tools/executeTool.js +31 -0
  55. package/src/tools/globTool.js +9 -0
  56. package/src/tools/grepTool.js +18 -0
  57. package/src/tools/lsTool.js +7 -0
  58. package/src/tools/readTool.js +12 -0
  59. package/src/utils/commonApiHelper.js +143 -0
  60. package/src/utils/config.js +42 -0
  61. package/src/utils/fetchApi.js +42 -0
  62. package/src/utils/gitDiffHelper.js +1203 -0
  63. package/src/utils/reviewApiHelper.js +275 -0
  64. package/src/utils/scmAuth.js +136 -0
  65. package/src/utils/secretsApiHelper.js +134 -0
  66. package/src/utils/securityAnalysisApiHelper.js +119 -0
  67. package/src/utils/staticAnalysisApiHelper.js +119 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeant-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,8 +23,14 @@
23
23
  "type": "git",
24
24
  "url": ""
25
25
  },
26
+ "main": "./src/reviewHeadless.js",
27
+ "exports": {
28
+ ".": "./src/reviewHeadless.js",
29
+ "./review": "./src/reviewHeadless.js"
30
+ },
26
31
  "files": [
27
- "dist"
32
+ "dist",
33
+ "src"
28
34
  ],
29
35
  "dependencies": {
30
36
  "@gitbeaker/rest": "^43.8.0",
@@ -0,0 +1,25 @@
1
+ import React, { useEffect } from 'react';
2
+ import { Text, Box, useApp } from 'ink';
3
+ import { getConfigValue } from '../utils/config.js';
4
+
5
+ export default function GetBaseUrl() {
6
+ const { exit } = useApp();
7
+
8
+ const envUrl = process.env.CODEANT_API_URL;
9
+ const configUrl = getConfigValue('baseUrl');
10
+ const defaultUrl = 'https://api.codeant.ai';
11
+
12
+ const activeUrl = envUrl || configUrl || defaultUrl;
13
+ const source = envUrl ? 'env' : configUrl ? 'config' : 'default';
14
+
15
+ useEffect(() => {
16
+ exit();
17
+ }, []);
18
+
19
+ return React.createElement(
20
+ Box,
21
+ { flexDirection: 'column', padding: 1 },
22
+ React.createElement(Text, { bold: true }, 'Base URL: ', activeUrl),
23
+ React.createElement(Text, { color: 'gray' }, 'Source: ', source)
24
+ );
25
+ }
@@ -0,0 +1,124 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Text, Box, useApp } from 'ink';
3
+ import { getConfigValue, setConfigValue } from '../utils/config.js';
4
+ import { randomUUID } from 'crypto';
5
+
6
+ const POLL_INTERVAL = 10000; // 10 seconds
7
+ const TIMEOUT = 10 * 60 * 1000; // 10 minutes
8
+
9
+ export default function Login() {
10
+ const { exit } = useApp();
11
+ const [status, setStatus] = useState('opening');
12
+ const [error, setError] = useState(null);
13
+
14
+ useEffect(() => {
15
+ // Check if already logged in
16
+ const existingToken = getConfigValue('apiKey');
17
+ if (existingToken) {
18
+ setStatus('already_logged_in');
19
+ setTimeout(() => exit(), 100);
20
+ return;
21
+ }
22
+
23
+ const token = randomUUID();
24
+ const baseUrl = getConfigValue('baseUrl') || 'https://api.codeant.ai';
25
+ const loginUrl = `https://app.codeant.ai?ideLoginToken=${token}`;
26
+ const pollUrl = `${baseUrl}/extension/login/status?apiKey=${token}`;
27
+
28
+ // Open browser
29
+ import('open').then(({ default: open }) => {
30
+ open(loginUrl);
31
+ setStatus('waiting');
32
+ }).catch(() => {
33
+ // Fallback: just show the URL
34
+ console.log(`\nOpen this URL in your browser:\n${loginUrl}\n`);
35
+ setStatus('waiting');
36
+ });
37
+
38
+ // Poll for login status
39
+ let timeoutId;
40
+ const intervalId = setInterval(async () => {
41
+ try {
42
+ const response = await fetch(pollUrl);
43
+ const data = await response.json();
44
+
45
+ if (data.status === 'yes') {
46
+ clearInterval(intervalId);
47
+ clearTimeout(timeoutId);
48
+
49
+ // Save the API key
50
+ setConfigValue('apiKey', token);
51
+
52
+ // Check for custom API URL based on email domain
53
+ if (data.item?.email?.includes('commvault.com')) {
54
+ setConfigValue('baseUrl', 'https://codeant-api.commvault.com');
55
+ }
56
+
57
+ setStatus('success');
58
+ setTimeout(() => exit(), 100);
59
+ }
60
+ } catch (err) {
61
+ // Silently continue polling
62
+ }
63
+ }, POLL_INTERVAL);
64
+
65
+ // Timeout after 10 minutes
66
+ timeoutId = setTimeout(() => {
67
+ clearInterval(intervalId);
68
+ setError('Login timed out. Please try again.');
69
+ setStatus('error');
70
+ setTimeout(() => exit(new Error('Login timed out')), 100);
71
+ }, TIMEOUT);
72
+
73
+ // Cleanup
74
+ return () => {
75
+ clearInterval(intervalId);
76
+ clearTimeout(timeoutId);
77
+ };
78
+ }, []);
79
+
80
+ if (status === 'already_logged_in') {
81
+ return React.createElement(
82
+ Box,
83
+ { flexDirection: 'column', padding: 1 },
84
+ React.createElement(Text, { color: 'yellow' }, 'Already logged in.'),
85
+ React.createElement(Text, { color: 'gray' }, 'Run "codeant logout" first to re-authenticate.')
86
+ );
87
+ }
88
+
89
+ if (status === 'opening') {
90
+ return React.createElement(
91
+ Box,
92
+ { flexDirection: 'column', padding: 1 },
93
+ React.createElement(Text, null, 'Opening browser...')
94
+ );
95
+ }
96
+
97
+ if (status === 'waiting') {
98
+ return React.createElement(
99
+ Box,
100
+ { flexDirection: 'column', padding: 1 },
101
+ React.createElement(Text, { color: 'cyan' }, 'Waiting for login...'),
102
+ React.createElement(Text, { color: 'gray' }, 'Complete the login in your browser.'),
103
+ React.createElement(Text, { color: 'gray' }, 'Checking every 10 seconds. Timeout in 10 minutes.')
104
+ );
105
+ }
106
+
107
+ if (status === 'success') {
108
+ return React.createElement(
109
+ Box,
110
+ { flexDirection: 'column', padding: 1 },
111
+ React.createElement(Text, { color: 'green' }, '✓ Login successful!')
112
+ );
113
+ }
114
+
115
+ if (status === 'error') {
116
+ return React.createElement(
117
+ Box,
118
+ { flexDirection: 'column', padding: 1 },
119
+ React.createElement(Text, { color: 'red' }, '✗ ', error)
120
+ );
121
+ }
122
+
123
+ return null;
124
+ }
@@ -0,0 +1,30 @@
1
+ import React, { useEffect } from 'react';
2
+ import { Text, Box, useApp } from 'ink';
3
+ import { getConfigValue, setConfigValue } from '../utils/config.js';
4
+
5
+ export default function Logout() {
6
+ const { exit } = useApp();
7
+
8
+ const wasLoggedIn = !!getConfigValue('apiKey');
9
+
10
+ useEffect(() => {
11
+ if (wasLoggedIn) {
12
+ setConfigValue('apiKey', null);
13
+ }
14
+ exit();
15
+ }, []);
16
+
17
+ if (!wasLoggedIn) {
18
+ return React.createElement(
19
+ Box,
20
+ { flexDirection: 'column', padding: 1 },
21
+ React.createElement(Text, { color: 'yellow' }, 'Not logged in.')
22
+ );
23
+ }
24
+
25
+ return React.createElement(
26
+ Box,
27
+ { flexDirection: 'column', padding: 1 },
28
+ React.createElement(Text, { color: 'green' }, '✓ Logged out successfully.')
29
+ );
30
+ }
@@ -0,0 +1,271 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { useApp } from 'ink';
3
+ import { getConfigValue } from '../utils/config.js';
4
+ import ReviewApiHelper from '../utils/reviewApiHelper.js';
5
+ import { fetchApi } from '../utils/fetchApi.js';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { executeTool } from '../tools/executeTool.js';
9
+ import {
10
+ renderInitializing,
11
+ renderFetchingDiff,
12
+ renderReviewing,
13
+ renderNoFiles,
14
+ renderError,
15
+ renderNotLoggedIn,
16
+ renderDone,
17
+ mapLabelToSeverity,
18
+ } from '../components/ReviewUI.js';
19
+
20
+ const MAX_TURNS = 5;
21
+
22
+ export default function Review({ scanType = 'all', lastNCommits = 1, failOn = 'CRITICAL', include = [], exclude = [], baseBranch = null, baseCommit = null }) {
23
+ const { exit } = useApp();
24
+ const [status, setStatus] = useState('initializing');
25
+ const [error, setError] = useState(null);
26
+ const [currentMessage, setCurrentMessage] = useState('');
27
+ const [issues, setIssues] = useState([]);
28
+ const [summary, setSummary] = useState(null);
29
+ const [startTime] = useState(() => Date.now());
30
+ const [fileCount, setFileCount] = useState(0);
31
+ const [reviewMeta, setReviewMeta] = useState(null);
32
+
33
+ const apiKey = getConfigValue('apiKey');
34
+
35
+ useEffect(() => {
36
+ if (!apiKey) {
37
+ if (process.stdin.isTTY) {
38
+ // Interactive session: don't exit immediately, let component render the error
39
+ return;
40
+ }
41
+ const id = setTimeout(() => exit(new Error('Not logged in')), 0);
42
+ return () => clearTimeout(id);
43
+ }
44
+ }, [apiKey, exit]);
45
+
46
+ if (!apiKey) {
47
+ return renderNotLoggedIn();
48
+ }
49
+
50
+ useEffect(() => {
51
+ let cancelled = false;
52
+
53
+ async function runReview() {
54
+ try {
55
+ // ── Fetch diff ─────────────────────────────────────────────────────
56
+ setStatus('fetching_diff');
57
+
58
+ const helper = new ReviewApiHelper(process.cwd());
59
+ await helper.init();
60
+ const gitRoot = helper.getGitRoot() || process.cwd();
61
+ const requestBody = await helper.buildReviewApiRequest(scanType, include, exclude, { lastNCommits, baseBranch, baseCommit });
62
+
63
+ const meta = requestBody?._meta || null;
64
+ setFileCount(meta?.reviewed_files?.length || Object.keys(requestBody?.file_contents || {}).length);
65
+ setReviewMeta(meta);
66
+
67
+ // Strip _meta before sending to API
68
+ delete requestBody._meta;
69
+
70
+ if (!requestBody?.diff_content?.length) {
71
+ if (!cancelled) setStatus('no_files');
72
+ return;
73
+ }
74
+
75
+ if (cancelled) return;
76
+ setStatus('reviewing');
77
+
78
+ // ── Split into per-file requests ───────────────────────────────────
79
+ const perFileRequests = ReviewApiHelper.splitIntoPerFileRequests(requestBody);
80
+
81
+ // ── Per-file agent turn loops (parallel) ──────────────────────────
82
+ setCurrentMessage(`Analyzing ${perFileRequests.length} file${perFileRequests.length !== 1 ? 's' : ''} in parallel...`);
83
+
84
+ const perFileResults = await Promise.all(
85
+ perFileRequests.map((fileReq) => {
86
+ const filename = fileReq._filename;
87
+ delete fileReq._filename;
88
+
89
+ // Convert file_contents dict → file_content/file_path for API
90
+ if (fileReq.file_contents?.[filename]) {
91
+ fileReq.file_content = fileReq.file_contents[filename];
92
+ fileReq.file_path = filename;
93
+ }
94
+ delete fileReq.file_contents;
95
+
96
+ return runTurnLoop(
97
+ fileReq,
98
+ gitRoot,
99
+ () => cancelled,
100
+ () => {},
101
+ false
102
+ );
103
+ })
104
+ );
105
+
106
+ const allSuggestions = perFileResults
107
+ .map(r => r.finalMessage?.content)
108
+ .filter(Boolean);
109
+
110
+ if (cancelled) return;
111
+
112
+ // ── Reflector loop (on combined suggestions) ───────────────────────
113
+ const combinedSuggestions = allSuggestions.join('\n\n');
114
+ setCurrentMessage('Running reflector...');
115
+ const reflectorPayload = {
116
+ diff_content: requestBody.diff_content,
117
+ prompt_template_name: 'reflector',
118
+ extra_variables: {
119
+ suggestion_str: combinedSuggestions,
120
+ },
121
+ };
122
+
123
+ const { finalOutput: reflectorOutput } = await runTurnLoop(
124
+ reflectorPayload,
125
+ gitRoot,
126
+ () => cancelled,
127
+ (msg) => { if (!cancelled) setCurrentMessage(msg); },
128
+ true
129
+ );
130
+
131
+ if (cancelled) return;
132
+
133
+ const reviewIssues = (reflectorOutput?.code_suggestions || []).map(issue => ({
134
+ issue_content: issue.issue_content || '',
135
+ relevant_file: issue.relevant_file || 'Unknown',
136
+ start_line: issue.start_line || 0,
137
+ label: issue.label || 'Code Quality',
138
+ }));
139
+ setIssues(reviewIssues);
140
+ setSummary(null);
141
+ setStatus('done');
142
+
143
+ } catch (err) {
144
+ if (!cancelled) {
145
+ setError(err.message);
146
+ setStatus('error');
147
+ }
148
+ }
149
+ }
150
+
151
+ runReview();
152
+ return () => { cancelled = true; };
153
+ }, [scanType, lastNCommits, apiKey]);
154
+
155
+ // Handle exit on terminal states
156
+ useEffect(() => {
157
+ if (status === 'done') {
158
+ const severityOrder = ['INFO', 'MINOR', 'MAJOR', 'CRITICAL', 'BLOCKER'];
159
+ const failOnIndex = severityOrder.indexOf(failOn);
160
+ if (failOnIndex === -1) {
161
+ setTimeout(() => { process.exitCode = 1; exit(new Error(`Invalid failOn value: ${failOn}`)); }, 100);
162
+ return;
163
+ }
164
+ const hasBlocking = issues.some(issue =>
165
+ severityOrder.indexOf(mapLabelToSeverity(issue.label)) >= failOnIndex
166
+ );
167
+ if (hasBlocking) {
168
+ setTimeout(() => { process.exitCode = 1; exit(new Error('Issues detected')); }, 100);
169
+ } else {
170
+ setTimeout(() => exit(), 100);
171
+ }
172
+ } else if (status === 'no_files') {
173
+ setTimeout(() => exit(), 100);
174
+ } else if (status === 'error') {
175
+ setTimeout(() => exit(new Error(error)), 100);
176
+ }
177
+ }, [status, issues, failOn, exit, error]);
178
+
179
+ // ── Renders ──────────────────────────────────────────────────────────────
180
+
181
+ if (status === 'initializing') return renderInitializing(startTime);
182
+ if (status === 'fetching_diff') return renderFetchingDiff(startTime);
183
+ if (status === 'reviewing') return renderReviewing(currentMessage, startTime, reviewMeta);
184
+ if (status === 'no_files') return renderNoFiles(scanType, lastNCommits);
185
+ if (status === 'error') return renderError(error);
186
+ if (status === 'done') return renderDone(issues, summary, failOn, startTime, fileCount, reviewMeta);
187
+
188
+ return null;
189
+ }
190
+
191
+ // ── Turn loop ──────────────────────────────────────────────────────────────
192
+
193
+ async function runTurnLoop(initialPayload, gitRoot, isCancelled, onMessage, isReflectorLoop) {
194
+ let nextPayload = initialPayload;
195
+ let finalMessage = null;
196
+ let finalOutput = null;
197
+
198
+ for (let turn = 0; turn < MAX_TURNS; turn+=1) {
199
+ if (isCancelled()) break;
200
+
201
+ onMessage('Analyzing code...');
202
+
203
+ const resp = await fetchApi('/extension/pr-review/agent/turn', 'POST', nextPayload);
204
+
205
+ if (!resp || typeof resp !== 'object') {
206
+ throw new Error('Invalid response from agent API');
207
+ }
208
+
209
+ const sessionId = resp.session_id;
210
+ const assistantMsg = resp.response;
211
+ const done = resp.done;
212
+
213
+ if (!sessionId) {
214
+ throw new Error('Missing session_id in agent response');
215
+ }
216
+
217
+ finalMessage = assistantMsg;
218
+ if (resp.output) finalOutput = resp.output;
219
+
220
+ if (done) {
221
+ // Handle parsing errors in final message by dumping response content (only on reflector loop)
222
+ if (isReflectorLoop && resp.parsing_error) {
223
+ const dumpDir = path.join(gitRoot, '.codeant', 'codeSuggestions');
224
+ const dumpFile = path.join(dumpDir, `code_suggestions_${Date.now()}.txt`);
225
+ let dumpContent = resp.response?.content || JSON.stringify(resp, null, 2);
226
+
227
+ // Ensure dumpContent is a string
228
+ if (typeof dumpContent !== 'string') {
229
+ dumpContent = JSON.stringify(dumpContent, null, 2);
230
+ }
231
+
232
+ // Unescape all serialization escape sequences
233
+ dumpContent = dumpContent
234
+ .replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
235
+ .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
236
+ .replace(/\\n/g, '\n')
237
+ .replace(/\\r/g, '\r')
238
+ .replace(/\\t/g, '\t')
239
+ .replace(/\\"/g, '"')
240
+ .replace(/\\'/g, "'")
241
+ .replace(/\\\\/g, '\\');
242
+
243
+ try {
244
+ // Create directories if they don't exist
245
+ await fs.promises.mkdir(dumpDir, { recursive: true });
246
+ await fs.promises.writeFile(dumpFile, dumpContent, 'utf8');
247
+ console.error(`⚠ Parsing error encountered. Full response dumped to: ${dumpFile}`);
248
+ } catch (writeErr) {
249
+ console.error(`⚠ Parsing error encountered (could not dump to file: ${writeErr.message})`);
250
+ }
251
+ }
252
+ break;
253
+ }
254
+
255
+ const toolCalls = assistantMsg?.tool_calls || [];
256
+ if (toolCalls.length === 0) break;
257
+
258
+ // Run tool calls in parallel to avoid serial latency when agent requests multiple reads
259
+ const toolResults = await Promise.all(
260
+ toolCalls.map(async (tc) => ({
261
+ tool_call_id: tc.id,
262
+ content: await executeTool(tc, gitRoot),
263
+ }))
264
+ );
265
+
266
+ nextPayload = { session_id: sessionId, tool_results: toolResults };
267
+ }
268
+
269
+ return { finalMessage, finalOutput };
270
+ }
271
+