codeant-cli 0.1.5 → 0.1.6

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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "codeant-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
7
- "codeant": "./src/index.js"
7
+ "codeant": "./dist/index.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node src/index.js"
10
+ "start": "node src/index.js",
11
+ "build": "node build.js",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest"
11
14
  },
12
15
  "keywords": [
13
16
  "cli",
@@ -21,7 +24,7 @@
21
24
  "url": ""
22
25
  },
23
26
  "files": [
24
- "src"
27
+ "dist"
25
28
  ],
26
29
  "dependencies": {
27
30
  "commander": "^12.1.0",
@@ -29,5 +32,10 @@
29
32
  "minimatch": "^10.1.1",
30
33
  "open": "^10.1.0",
31
34
  "react": "^18.3.1"
35
+ },
36
+ "devDependencies": {
37
+ "@vercel/ncc": "^0.38.4",
38
+ "javascript-obfuscator": "^4.1.1",
39
+ "vitest": "^1.6.1"
32
40
  }
33
41
  }
@@ -1,25 +0,0 @@
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
- }
@@ -1,124 +0,0 @@
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
- }
@@ -1,30 +0,0 @@
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
- }
@@ -1,239 +0,0 @@
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 = 'staged-only', lastNCommits = 1, failOn = 'CRITICAL' }) {
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
-
30
- const apiKey = getConfigValue('apiKey');
31
-
32
- useEffect(() => {
33
- if (!apiKey) {
34
- if (process.stdin.isTTY) {
35
- // Interactive session: don't exit immediately, let component render the error
36
- return;
37
- }
38
- const id = setTimeout(() => exit(new Error('Not logged in')), 0);
39
- return () => clearTimeout(id);
40
- }
41
- }, [apiKey, exit]);
42
-
43
- if (!apiKey) {
44
- return renderNotLoggedIn();
45
- }
46
-
47
- useEffect(() => {
48
- let cancelled = false;
49
-
50
- async function runReview() {
51
- try {
52
- // ── Fetch diff ─────────────────────────────────────────────────────
53
- setStatus('fetching_diff');
54
-
55
- const helper = new ReviewApiHelper(process.cwd());
56
- await helper.init();
57
- const gitRoot = helper.getGitRoot() || process.cwd();
58
- const requestBody = await helper.buildReviewApiRequest(scanType, [], [], { lastNCommits });
59
-
60
- if (!requestBody?.diff_content?.length) {
61
- if (!cancelled) setStatus('no_files');
62
- return;
63
- }
64
-
65
- if (cancelled) return;
66
- setStatus('reviewing');
67
-
68
- // ── Normal agent turn loop ─────────────────────────────────────────
69
- setCurrentMessage('Analyzing code...');
70
- const { finalMessage: reviewFinal } = await runTurnLoop(
71
- requestBody,
72
- gitRoot,
73
- () => cancelled,
74
- (msg) => { if (!cancelled) setCurrentMessage(msg); },
75
- false
76
- );
77
-
78
- if (cancelled) return;
79
-
80
- // ── Reflector loop ─────────────────────────────────────────────────
81
- setCurrentMessage('Running reflector...');
82
- const reflectorPayload = {
83
- diff_content: requestBody.diff_content,
84
- prompt_template_name: 'reflector',
85
- extra_variables: {
86
- suggestion_str: reviewFinal?.content || '',
87
- },
88
- };
89
-
90
- const { finalOutput: reflectorOutput } = await runTurnLoop(
91
- reflectorPayload,
92
- gitRoot,
93
- () => cancelled,
94
- (msg) => { if (!cancelled) setCurrentMessage(msg); },
95
- true
96
- );
97
-
98
- if (cancelled) return;
99
-
100
- const reviewIssues = (reflectorOutput?.code_suggestions || []).map(issue => ({
101
- issue_content: issue.issue_content || '',
102
- relevant_file: issue.relevant_file || 'Unknown',
103
- start_line: issue.start_line || 0,
104
- label: issue.label || 'Code Quality',
105
- }));
106
- setIssues(reviewIssues);
107
- setSummary(null);
108
- setStatus('done');
109
-
110
- } catch (err) {
111
- if (!cancelled) {
112
- setError(err.message);
113
- setStatus('error');
114
- }
115
- }
116
- }
117
-
118
- runReview();
119
- return () => { cancelled = true; };
120
- }, [scanType, lastNCommits, apiKey]);
121
-
122
- // Handle exit on terminal states
123
- useEffect(() => {
124
- if (status === 'done') {
125
- const severityOrder = ['INFO', 'MINOR', 'MAJOR', 'CRITICAL', 'BLOCKER'];
126
- const failOnIndex = severityOrder.indexOf(failOn);
127
- if (failOnIndex === -1) {
128
- setTimeout(() => { process.exitCode = 1; exit(new Error(`Invalid failOn value: ${failOn}`)); }, 100);
129
- return;
130
- }
131
- const hasBlocking = issues.some(issue =>
132
- severityOrder.indexOf(mapLabelToSeverity(issue.label)) >= failOnIndex
133
- );
134
- if (hasBlocking) {
135
- setTimeout(() => { process.exitCode = 1; exit(new Error('Issues detected')); }, 100);
136
- } else {
137
- setTimeout(() => exit(), 100);
138
- }
139
- } else if (status === 'no_files') {
140
- setTimeout(() => exit(), 100);
141
- } else if (status === 'error') {
142
- setTimeout(() => exit(new Error(error)), 100);
143
- }
144
- }, [status, issues, failOn, exit, error]);
145
-
146
- // ── Renders ──────────────────────────────────────────────────────────────
147
-
148
- if (status === 'initializing') return renderInitializing();
149
- if (status === 'fetching_diff') return renderFetchingDiff();
150
- if (status === 'reviewing') return renderReviewing(currentMessage);
151
- if (status === 'no_files') return renderNoFiles(scanType, lastNCommits);
152
- if (status === 'error') return renderError(error);
153
- if (status === 'done') return renderDone(issues, summary, failOn);
154
-
155
- return null;
156
- }
157
-
158
- // ── Turn loop ──────────────────────────────────────────────────────────────
159
-
160
- async function runTurnLoop(initialPayload, gitRoot, isCancelled, onMessage, isReflectorLoop) {
161
- let nextPayload = initialPayload;
162
- let finalMessage = null;
163
- let finalOutput = null;
164
-
165
- for (let turn = 0; turn < MAX_TURNS; turn+=1) {
166
- if (isCancelled()) break;
167
-
168
- onMessage('Analyzing code...');
169
-
170
- const resp = await fetchApi('/extension/pr-review/agent/turn', 'POST', nextPayload);
171
-
172
- if (!resp || typeof resp !== 'object') {
173
- throw new Error('Invalid response from agent API');
174
- }
175
-
176
- const sessionId = resp.session_id;
177
- const assistantMsg = resp.response;
178
- const done = resp.done;
179
-
180
- if (!sessionId) {
181
- throw new Error('Missing session_id in agent response');
182
- }
183
-
184
- finalMessage = assistantMsg;
185
- if (resp.output) finalOutput = resp.output;
186
-
187
- if (done) {
188
- // Handle parsing errors in final message by dumping response content (only on reflector loop)
189
- if (isReflectorLoop && resp.parsing_error) {
190
- const dumpDir = path.join(gitRoot, '.codeant', 'codeSuggestions');
191
- const dumpFile = path.join(dumpDir, `code_suggestions_${Date.now()}.txt`);
192
- let dumpContent = resp.response?.content || JSON.stringify(resp, null, 2);
193
-
194
- // Ensure dumpContent is a string
195
- if (typeof dumpContent !== 'string') {
196
- dumpContent = JSON.stringify(dumpContent, null, 2);
197
- }
198
-
199
- // Unescape all serialization escape sequences
200
- dumpContent = dumpContent
201
- .replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
202
- .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
203
- .replace(/\\n/g, '\n')
204
- .replace(/\\r/g, '\r')
205
- .replace(/\\t/g, '\t')
206
- .replace(/\\"/g, '"')
207
- .replace(/\\'/g, "'")
208
- .replace(/\\\\/g, '\\');
209
-
210
- try {
211
- // Create directories if they don't exist
212
- await fs.promises.mkdir(dumpDir, { recursive: true });
213
- await fs.promises.writeFile(dumpFile, dumpContent, 'utf8');
214
- console.error(`⚠ Parsing error encountered. Full response dumped to: ${dumpFile}`);
215
- } catch (writeErr) {
216
- console.error(`⚠ Parsing error encountered (could not dump to file: ${writeErr.message})`);
217
- }
218
- }
219
- break;
220
- }
221
-
222
- const toolCalls = assistantMsg?.tool_calls || [];
223
- if (toolCalls.length === 0) break;
224
-
225
- const toolResults = [];
226
- for (const tc of toolCalls) {
227
- const content = await executeTool(tc, gitRoot);
228
- toolResults.push({
229
- tool_call_id: tc.id,
230
- content,
231
- });
232
- }
233
-
234
- nextPayload = { session_id: sessionId, tool_results: toolResults };
235
- }
236
-
237
- return { finalMessage, finalOutput };
238
- }
239
-