codeant-cli 0.3.1 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeant-cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -269,6 +269,25 @@ program
269
269
  }));
270
270
  });
271
271
 
272
+ pr.command('resolve')
273
+ .description('Resolve a conversation/comment thread on a PR')
274
+ .option('--name <repo>', 'Repository (owner/repo)')
275
+ .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
276
+ .requiredOption('--pr-number <n>', 'PR number', parseInt)
277
+ .option('--comment-id <id>', 'Comment ID (GitHub, Bitbucket)', parseInt)
278
+ .option('--thread-id <id>', 'Thread/node ID (GitHub GraphQL, Azure)')
279
+ .option('--discussion-id <id>', 'Discussion ID (GitLab)')
280
+ .action((options) => {
281
+ const opts = resolveRepoOpts(options);
282
+ runCmd(() => scm.resolveConversation({
283
+ name: opts.name, remote: opts.remote,
284
+ prNumber: opts.prNumber,
285
+ commentId: opts.commentId,
286
+ threadId: opts.threadId,
287
+ discussionId: opts.discussionId,
288
+ }));
289
+ });
290
+
272
291
  // ─── Code review commands ───
273
292
  const codeReview = program.command('code-review').description('Code review tools');
274
293
 
@@ -4,3 +4,4 @@ export { listPullRequestComments } from './listPullRequestComments.js';
4
4
  export { listCodeReviews } from './listCodeReviews.js';
5
5
  export { getCodeReview } from './getCodeReview.js';
6
6
  export { searchComments } from './searchComments.js';
7
+ export { resolveConversation } from './resolveConversation.js';
@@ -0,0 +1,22 @@
1
+ import { getConnection, splitRepo } from './client.js';
2
+
3
+ /**
4
+ * Resolve a comment thread on an Azure DevOps pull request.
5
+ * Azure uses thread status: 1 = Active, 2 = Fixed (resolved), 3 = WontFix, 4 = Closed, 5 = ByDesign, 6 = Pending
6
+ */
7
+ export async function resolveConversation({ name, prNumber, threadId }) {
8
+ if (!threadId) throw new Error('threadId is required to resolve an Azure DevOps thread');
9
+
10
+ const parsedThreadId = Number(threadId);
11
+ if (!Number.isInteger(parsedThreadId) || parsedThreadId <= 0) {
12
+ throw new Error('threadId must be a positive integer for Azure DevOps');
13
+ }
14
+
15
+ const connection = getConnection();
16
+ const gitApi = await connection.getGitApi();
17
+ const { project, repo } = splitRepo(name);
18
+
19
+ await gitApi.updateThread({ status: 2 }, repo, prNumber, parsedThreadId, project);
20
+
21
+ return { success: true };
22
+ }
@@ -4,3 +4,4 @@ export { listPullRequestComments } from './listPullRequestComments.js';
4
4
  export { listCodeReviews } from './listCodeReviews.js';
5
5
  export { getCodeReview } from './getCodeReview.js';
6
6
  export { searchComments } from './searchComments.js';
7
+ export { resolveConversation } from './resolveConversation.js';
@@ -0,0 +1,21 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ /**
4
+ * Resolve a comment (conversation) on a Bitbucket pull request.
5
+ */
6
+ export async function resolveConversation({ name, prNumber, commentId }) {
7
+ if (!commentId) throw new Error('commentId is required to resolve a Bitbucket comment');
8
+
9
+ const client = getClient();
10
+ const { workspace, repo_slug } = splitRepo(name);
11
+
12
+ await client.repositories.updatePullRequestComment({
13
+ workspace,
14
+ repo_slug,
15
+ pull_request_id: prNumber,
16
+ comment_id: commentId,
17
+ _body: { resolved: true },
18
+ });
19
+
20
+ return { success: true };
21
+ }
@@ -4,3 +4,4 @@ export { listPullRequestComments } from './listPullRequestComments.js';
4
4
  export { listCodeReviews } from './listCodeReviews.js';
5
5
  export { getCodeReview } from './getCodeReview.js';
6
6
  export { searchComments } from './searchComments.js';
7
+ export { resolveConversation } from './resolveConversation.js';
@@ -0,0 +1,85 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ /**
4
+ * Resolve a review conversation on GitHub.
5
+ * GitHub doesn't have a native "resolve conversation" API for individual comments.
6
+ * We use the GraphQL API to minimize (hide) the comment, which is the closest equivalent.
7
+ * For review comments that are part of a PR review thread, we resolve via GraphQL resolveReviewThread.
8
+ */
9
+ export async function resolveConversation({ name, prNumber, commentId, threadId }) {
10
+ const octokit = getClient();
11
+ const { owner, repo } = splitRepo(name);
12
+
13
+ // If a threadId (graphql node_id of the review thread) is provided, resolve the thread
14
+ if (threadId) {
15
+ await octokit.graphql(
16
+ `mutation($threadId: ID!) {
17
+ resolveReviewThread(input: { threadId: $threadId }) {
18
+ thread { isResolved }
19
+ }
20
+ }`,
21
+ { threadId }
22
+ );
23
+ return { success: true };
24
+ }
25
+
26
+ // Otherwise, try to find the review comment and resolve its thread via node_id
27
+ if (commentId) {
28
+ // Fetch the review comment to get its node_id for the thread
29
+ const { data: comment } = await octokit.pulls.getReviewComment({
30
+ owner, repo, comment_id: commentId,
31
+ });
32
+
33
+ if (comment.node_id) {
34
+ // Attempt to resolve the thread this comment belongs to
35
+ // The comment's node_id is not the thread node_id, so we need to query for the thread
36
+ const { repository } = await octokit.graphql(
37
+ `query($owner: String!, $repo: String!, $prNumber: Int!) {
38
+ repository(owner: $owner, name: $repo) {
39
+ pullRequest(number: $prNumber) {
40
+ reviewThreads(first: 100) {
41
+ nodes {
42
+ id
43
+ isResolved
44
+ comments(first: 1) {
45
+ nodes { databaseId }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }`,
52
+ { owner, repo, prNumber }
53
+ );
54
+
55
+ const threads = repository?.pullRequest?.reviewThreads?.nodes;
56
+ if (!threads) {
57
+ throw new Error(`Pull request ${prNumber} was not found or is not accessible`);
58
+ }
59
+
60
+ const thread = threads.find(
61
+ t => t.comments.nodes.some(c => c.databaseId === commentId)
62
+ );
63
+
64
+ if (thread && !thread.isResolved) {
65
+ await octokit.graphql(
66
+ `mutation($threadId: ID!) {
67
+ resolveReviewThread(input: { threadId: $threadId }) {
68
+ thread { isResolved }
69
+ }
70
+ }`,
71
+ { threadId: thread.id }
72
+ );
73
+ return { success: true };
74
+ }
75
+
76
+ if (thread?.isResolved) {
77
+ return { success: true, message: 'Thread already resolved' };
78
+ }
79
+ }
80
+
81
+ throw new Error(`Could not find a review thread for comment ${commentId}`);
82
+ }
83
+
84
+ throw new Error('Either commentId or threadId is required');
85
+ }
@@ -4,3 +4,4 @@ export { listPullRequestComments } from './listPullRequestComments.js';
4
4
  export { listCodeReviews } from './listCodeReviews.js';
5
5
  export { getCodeReview } from './getCodeReview.js';
6
6
  export { searchComments } from './searchComments.js';
7
+ export { resolveConversation } from './resolveConversation.js';
@@ -0,0 +1,14 @@
1
+ import { getClient, projectId } from './client.js';
2
+
3
+ /**
4
+ * Resolve a discussion (conversation) on a GitLab merge request.
5
+ */
6
+ export async function resolveConversation({ name, prNumber, discussionId }) {
7
+ if (!discussionId) throw new Error('discussionId is required to resolve a GitLab discussion');
8
+
9
+ const api = getClient();
10
+
11
+ await api.MergeRequestDiscussions.edit(projectId(name), prNumber, discussionId, { resolved: true });
12
+
13
+ return { success: true };
14
+ }
package/src/scm/index.js CHANGED
@@ -94,3 +94,4 @@ export function listPullRequestComments(opts) { return getProvider(opts.remote).
94
94
  export function listCodeReviews(opts) { return getProvider(opts.remote).listCodeReviews(opts); }
95
95
  export function getCodeReview(opts) { return getProvider(opts.remote).getCodeReview(opts); }
96
96
  export function searchComments(opts) { return getProvider(opts.remote).searchComments(opts); }
97
+ export function resolveConversation(opts) { return getProvider(opts.remote).resolveConversation(opts); }
@@ -5,6 +5,7 @@ import { minimatch } from 'minimatch';
5
5
 
6
6
  const MAX_REVIEW_FILES = 10;
7
7
  const MAX_FILE_LINES = 5000;
8
+ const MAX_FILE_SIZE_BYTES = 200 * 1024; // 200 KB
8
9
 
9
10
  const EXCLUDED_FILENAMES = new Set([
10
11
  'package-lock.json',
@@ -37,6 +38,10 @@ const EXCLUDED_EXTENSIONS = new Set([
37
38
  '.pb', '.proto.bin',
38
39
  // PDF / docs
39
40
  '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
41
+ // Data / config (often large, auto-generated)
42
+ '.csv', '.tsv', '.parquet', '.avro', '.ndjson',
43
+ // Database
44
+ '.sqlite', '.db',
40
45
  ]);
41
46
 
42
47
  function matchesPatterns(filePath, includePatterns, excludePatterns) {
@@ -111,10 +116,15 @@ class ReviewApiHelper extends CommonApiHelper {
111
116
  continue;
112
117
  }
113
118
 
114
- // Skip files exceeding line limit to avoid blowing up the AI context window
119
+ // Skip files exceeding size or line limits to avoid blowing up the AI context window
115
120
  try {
116
121
  const fullPath = path.resolve(gitRoot, f);
117
122
  if (fs.existsSync(fullPath)) {
123
+ const stats = fs.statSync(fullPath);
124
+ if (stats.size > MAX_FILE_SIZE_BYTES) {
125
+ skipped.push({ file: f, reason: `too large (${(stats.size / 1024).toFixed(0)} KB, max ${MAX_FILE_SIZE_BYTES / 1024} KB)` });
126
+ continue;
127
+ }
118
128
  const content = fs.readFileSync(fullPath, 'utf-8');
119
129
  const lineCount = content.split('\n').length;
120
130
  if (lineCount > MAX_FILE_LINES) {
@@ -214,6 +224,12 @@ class ReviewApiHelper extends CommonApiHelper {
214
224
  const hunks = fileHunks.get(f);
215
225
  const content = hunks[0]?.head_file_str || '';
216
226
  if (content) {
227
+ const contentSize = Buffer.byteLength(content, 'utf-8');
228
+ if (contentSize > MAX_FILE_SIZE_BYTES) {
229
+ skippedLargeFiles.add(f);
230
+ dedupedSkipped.push({ file: f, reason: `too large (${(contentSize / 1024).toFixed(0)} KB, max ${MAX_FILE_SIZE_BYTES / 1024} KB)` });
231
+ continue;
232
+ }
217
233
  const lineCount = content.split('\n').length;
218
234
  if (lineCount > MAX_FILE_LINES) {
219
235
  skippedLargeFiles.add(f);
@@ -48,9 +48,14 @@ const EXCLUDED_EXTENSIONS = new Set([
48
48
  '.pb', '.proto.bin',
49
49
  // PDF / docs
50
50
  '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
51
+ // Data / config (often large, auto-generated)
52
+ '.parquet', '.avro',
53
+ // Database
54
+ '.sqlite', '.db',
51
55
  ]);
52
56
 
53
57
  const MAX_FILE_LINES = 5000;
58
+ const MAX_FILE_SIZE_BYTES = 200 * 1024; // 200 KB
54
59
 
55
60
  function isScannableFile(filePath) {
56
61
  const basename = path.basename(filePath);
@@ -201,8 +206,13 @@ class SecretsApiHelper extends CommonApiHelper {
201
206
  return false;
202
207
  }
203
208
 
204
- // Skip files exceeding line limit
209
+ // Skip files exceeding size or line limits
205
210
  if (fileObj.code) {
211
+ const contentSize = Buffer.byteLength(fileObj.code, 'utf-8');
212
+ if (contentSize > MAX_FILE_SIZE_BYTES) {
213
+ skipped.push({ file: filePath, reason: `too large (${(contentSize / 1024).toFixed(0)} KB, max ${MAX_FILE_SIZE_BYTES / 1024} KB)` });
214
+ return false;
215
+ }
206
216
  const lineCount = fileObj.code.split('\n').length;
207
217
  if (lineCount > MAX_FILE_LINES) {
208
218
  skipped.push({ file: filePath, reason: `too large (${lineCount} lines, max ${MAX_FILE_LINES})` });