codeant-cli 0.3.3 → 0.3.5

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 (76) hide show
  1. package/package.json +8 -10
  2. package/src/commands/getBaseUrl.js +21 -0
  3. package/src/commands/login.js +120 -0
  4. package/src/commands/logout.js +30 -0
  5. package/src/commands/review.js +294 -0
  6. package/src/commands/secrets.js +107 -0
  7. package/src/commands/setBaseUrl.js +37 -0
  8. package/src/components/ReviewUI.js +410 -0
  9. package/src/components/SecretsUI.js +366 -0
  10. package/src/components/Welcome.js +144 -0
  11. package/src/index.js +390 -0
  12. package/src/reviewHeadless.js +198 -0
  13. package/src/rules/secrets.toml +4994 -0
  14. package/src/scm/azure/client.js +21 -0
  15. package/src/scm/azure/getCodeReview.js +20 -0
  16. package/src/scm/azure/getPullRequest.js +30 -0
  17. package/src/scm/azure/index.js +7 -0
  18. package/src/scm/azure/listCodeReviews.js +17 -0
  19. package/src/scm/azure/listPullRequestComments.js +41 -0
  20. package/src/scm/azure/listPullRequests.js +26 -0
  21. package/src/scm/azure/resolveConversation.js +22 -0
  22. package/src/scm/azure/searchComments.js +35 -0
  23. package/src/scm/bitbucket/client.js +18 -0
  24. package/src/scm/bitbucket/getCodeReview.js +3 -0
  25. package/src/scm/bitbucket/getPullRequest.js +26 -0
  26. package/src/scm/bitbucket/index.js +7 -0
  27. package/src/scm/bitbucket/listCodeReviews.js +17 -0
  28. package/src/scm/bitbucket/listPullRequestComments.js +33 -0
  29. package/src/scm/bitbucket/listPullRequests.js +33 -0
  30. package/src/scm/bitbucket/resolveConversation.js +21 -0
  31. package/src/scm/bitbucket/searchComments.js +29 -0
  32. package/src/scm/github/client.js +17 -0
  33. package/src/scm/github/getCodeReview.js +26 -0
  34. package/src/scm/github/getPullRequest.js +27 -0
  35. package/src/scm/github/index.js +7 -0
  36. package/src/scm/github/listCodeReviews.js +17 -0
  37. package/src/scm/github/listPullRequestComments.js +51 -0
  38. package/src/scm/github/listPullRequests.js +32 -0
  39. package/src/scm/github/resolveConversation.js +85 -0
  40. package/src/scm/github/searchComments.js +30 -0
  41. package/src/scm/gitlab/client.js +16 -0
  42. package/src/scm/gitlab/getCodeReview.js +18 -0
  43. package/src/scm/gitlab/getPullRequest.js +24 -0
  44. package/src/scm/gitlab/index.js +7 -0
  45. package/src/scm/gitlab/listCodeReviews.js +21 -0
  46. package/src/scm/gitlab/listPullRequestComments.js +44 -0
  47. package/src/scm/gitlab/listPullRequests.js +26 -0
  48. package/src/scm/gitlab/resolveConversation.js +14 -0
  49. package/src/scm/gitlab/searchComments.js +28 -0
  50. package/src/scm/index.js +97 -0
  51. package/src/tools/executeTool.js +31 -0
  52. package/src/tools/globTool.js +9 -0
  53. package/src/tools/grepTool.js +18 -0
  54. package/src/tools/lsTool.js +7 -0
  55. package/src/tools/readTool.js +12 -0
  56. package/src/utils/analytics.js +56 -0
  57. package/src/utils/baseUrl.js +11 -0
  58. package/src/utils/commonApiHelper.js +143 -0
  59. package/src/utils/config.js +42 -0
  60. package/src/utils/fetchApi.js +39 -0
  61. package/src/utils/gitDiffHelper.js +1203 -0
  62. package/src/utils/reviewApiHelper.js +291 -0
  63. package/src/utils/scmAuth.js +136 -0
  64. package/src/utils/secretsApiHelper.js +252 -0
  65. package/src/utils/secretsDetector.js +210 -0
  66. package/dist/178.index.js +0 -1
  67. package/dist/239.index.js +0 -1
  68. package/dist/701.index.js +0 -1
  69. package/dist/880.index.js +0 -1
  70. package/dist/935.index.js +0 -1
  71. package/dist/966.index.js +0 -1
  72. package/dist/index.js +0 -28
  73. package/dist/package.json +0 -1
  74. package/dist/review/239.index.js +0 -1
  75. package/dist/review/index.js +0 -8
  76. package/dist/review/package.json +0 -3
@@ -0,0 +1,21 @@
1
+ import * as azdev from 'azure-devops-node-api';
2
+ import { getAzureDevOpsToken } from '../../utils/scmAuth.js';
3
+ import { getConfigValue } from '../../utils/config.js';
4
+
5
+ export function getConnection() {
6
+ const token = getAzureDevOpsToken();
7
+ if (!token) throw new Error('Azure DevOps token not found. Set AZURE_DEVOPS_TOKEN env var or run `codeant set-token azure <token>`.');
8
+
9
+ const orgUrl = process.env.AZURE_DEVOPS_ORG_URL || getConfigValue('azureDevOpsOrgUrl');
10
+ if (!orgUrl) throw new Error('Azure DevOps org URL not found. Set AZURE_DEVOPS_ORG_URL env var or run `codeant set-azure-org <url>`.');
11
+
12
+ const authHandler = azdev.getPersonalAccessTokenHandler(token);
13
+ return new azdev.WebApi(orgUrl, authHandler);
14
+ }
15
+
16
+ // Azure repos use "project/repo" format
17
+ export function splitRepo(name) {
18
+ const [project, repo] = name.split('/');
19
+ if (!project || !repo) throw new Error(`Invalid repo name "${name}". Use project/repo format.`);
20
+ return { project, repo };
21
+ }
@@ -0,0 +1,20 @@
1
+ import { getConnection, splitRepo } from './client.js';
2
+
3
+ export async function getCodeReview({ name, prNumber, reviewId }) {
4
+ const connection = getConnection();
5
+ const gitApi = await connection.getGitApi();
6
+ const { project, repo } = splitRepo(name);
7
+
8
+ const pr = await gitApi.getPullRequest(repo, prNumber, project);
9
+ const reviewer = (pr.reviewers || []).find(r => String(r.id) === String(reviewId));
10
+
11
+ if (!reviewer) throw new Error(`Reviewer ${reviewId} not found on PR #${prNumber}`);
12
+
13
+ return {
14
+ id: reviewer.id,
15
+ user: reviewer.displayName || reviewer.uniqueName,
16
+ state: reviewer.vote > 0 ? 'APPROVED' : reviewer.vote < 0 ? 'REJECTED' : 'PENDING',
17
+ body: null,
18
+ comments: [],
19
+ };
20
+ }
@@ -0,0 +1,30 @@
1
+ import { getConnection, splitRepo } from './client.js';
2
+
3
+ export async function getPullRequest({ name, prNumber }) {
4
+ const connection = getConnection();
5
+ const gitApi = await connection.getGitApi();
6
+ const { project, repo } = splitRepo(name);
7
+
8
+ const pr = await gitApi.getPullRequest(repo, prNumber, project);
9
+ const threads = await gitApi.getThreads(repo, prNumber, project);
10
+
11
+ const reviewers = (pr.reviewers || []).map(r => ({
12
+ user: r.displayName || r.uniqueName,
13
+ state: r.vote > 0 ? 'APPROVED' : r.vote < 0 ? 'REJECTED' : 'PENDING',
14
+ }));
15
+
16
+ return {
17
+ number: pr.pullRequestId,
18
+ title: pr.title,
19
+ state: pr.status === 1 ? 'open' : 'closed',
20
+ merged: pr.status === 3,
21
+ author: pr.createdBy?.displayName || pr.createdBy?.uniqueName,
22
+ sourceBranch: pr.sourceRefName?.replace('refs/heads/', ''),
23
+ targetBranch: pr.targetRefName?.replace('refs/heads/', ''),
24
+ body: pr.description,
25
+ createdAt: pr.creationDate,
26
+ url: pr.url,
27
+ reviewSummary: reviewers,
28
+ threadCount: threads?.length || 0,
29
+ };
30
+ }
@@ -0,0 +1,7 @@
1
+ export { listPullRequests } from './listPullRequests.js';
2
+ export { getPullRequest } from './getPullRequest.js';
3
+ export { listPullRequestComments } from './listPullRequestComments.js';
4
+ export { listCodeReviews } from './listCodeReviews.js';
5
+ export { getCodeReview } from './getCodeReview.js';
6
+ export { searchComments } from './searchComments.js';
7
+ export { resolveConversation } from './resolveConversation.js';
@@ -0,0 +1,17 @@
1
+ import { getConnection, splitRepo } from './client.js';
2
+
3
+ export async function listCodeReviews({ name, prNumber, limit = 20 }) {
4
+ const connection = getConnection();
5
+ const gitApi = await connection.getGitApi();
6
+ const { project, repo } = splitRepo(name);
7
+
8
+ const pr = await gitApi.getPullRequest(repo, prNumber, project);
9
+
10
+ return (pr.reviewers || []).slice(0, limit).map(r => ({
11
+ id: r.id,
12
+ user: r.displayName || r.uniqueName,
13
+ state: r.vote > 0 ? 'APPROVED' : r.vote < 0 ? 'REJECTED' : 'PENDING',
14
+ body: null,
15
+ submittedAt: null,
16
+ }));
17
+ }
@@ -0,0 +1,41 @@
1
+ import { getConnection, splitRepo } from './client.js';
2
+
3
+ export async function listPullRequestComments({ name, prNumber, codeantGenerated, createdAfter, createdBefore }) {
4
+ const connection = getConnection();
5
+ const gitApi = await connection.getGitApi();
6
+ const { project, repo } = splitRepo(name);
7
+
8
+ const threads = await gitApi.getThreads(repo, prNumber, project);
9
+
10
+ let comments = [];
11
+ for (const thread of (threads || [])) {
12
+ for (const comment of (thread.comments || [])) {
13
+ if (comment.commentType === 'system') continue;
14
+ comments.push({
15
+ id: comment.id,
16
+ threadId: thread.id,
17
+ type: thread.threadContext?.filePath ? 'review' : 'issue',
18
+ author: comment.author?.displayName || comment.author?.uniqueName,
19
+ body: comment.content,
20
+ path: thread.threadContext?.filePath || null,
21
+ line: thread.threadContext?.rightFileStart?.line || null,
22
+ createdAt: comment.publishedDate,
23
+ updatedAt: comment.lastUpdatedDate,
24
+ isCodeantComment: (comment.author?.displayName || '').toLowerCase().includes('codeant') || comment.content?.includes('Suggestion'),
25
+ resolved: thread.status === 2, // Fixed
26
+ });
27
+ }
28
+ }
29
+
30
+ if (codeantGenerated !== undefined) {
31
+ comments = comments.filter(c => c.isCodeantComment === codeantGenerated);
32
+ }
33
+ if (createdAfter) {
34
+ comments = comments.filter(c => new Date(c.createdAt) >= new Date(createdAfter));
35
+ }
36
+ if (createdBefore) {
37
+ comments = comments.filter(c => new Date(c.createdAt) <= new Date(createdBefore));
38
+ }
39
+
40
+ return comments;
41
+ }
@@ -0,0 +1,26 @@
1
+ import { getConnection, splitRepo } from './client.js';
2
+
3
+ export async function listPullRequests({ name, sourceBranch, authorLogin, state = 'open', limit = 20 }) {
4
+ const connection = getConnection();
5
+ const gitApi = await connection.getGitApi();
6
+ const { project, repo } = splitRepo(name);
7
+
8
+ const statusMap = { open: 1 /* Active */, closed: 3 /* Completed */ };
9
+ const searchCriteria = { status: statusMap[state] || 1 };
10
+ if (sourceBranch) searchCriteria.sourceRefName = `refs/heads/${sourceBranch}`;
11
+ if (authorLogin) searchCriteria.creatorId = authorLogin;
12
+
13
+ const prs = await gitApi.getPullRequests(repo, searchCriteria, project, undefined, undefined, limit);
14
+
15
+ return (prs || []).map(pr => ({
16
+ number: pr.pullRequestId,
17
+ title: pr.title,
18
+ state: pr.status === 1 ? 'open' : pr.status === 3 ? 'closed' : 'unknown',
19
+ merged: pr.status === 3 && pr.closedBy != null,
20
+ author: pr.createdBy?.displayName || pr.createdBy?.uniqueName,
21
+ sourceBranch: pr.sourceRefName?.replace('refs/heads/', ''),
22
+ targetBranch: pr.targetRefName?.replace('refs/heads/', ''),
23
+ createdAt: pr.creationDate,
24
+ url: pr.url,
25
+ }));
26
+ }
@@ -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
+ }
@@ -0,0 +1,35 @@
1
+ import { getConnection, splitRepo } from './client.js';
2
+
3
+ export async function searchComments({ name, query, limit = 10 }) {
4
+ const connection = getConnection();
5
+ const gitApi = await connection.getGitApi();
6
+ const { project, repo } = splitRepo(name);
7
+
8
+ const prs = await gitApi.getPullRequests(repo, { status: 1 }, project, undefined, undefined, 10);
9
+
10
+ let allComments = [];
11
+ for (const pr of (prs || [])) {
12
+ const threads = await gitApi.getThreads(repo, pr.pullRequestId, project);
13
+ for (const thread of (threads || [])) {
14
+ for (const comment of (thread.comments || [])) {
15
+ if (comment.commentType === 'system') continue;
16
+ allComments.push({
17
+ prNumber: pr.pullRequestId,
18
+ prTitle: pr.title,
19
+ id: comment.id,
20
+ author: comment.author?.displayName,
21
+ body: comment.content,
22
+ path: thread.threadContext?.filePath || null,
23
+ line: thread.threadContext?.rightFileStart?.line || null,
24
+ createdAt: comment.publishedDate,
25
+ isCodeantComment: (comment.author?.displayName || '').toLowerCase().includes('codeant'),
26
+ });
27
+ }
28
+ }
29
+ }
30
+
31
+ const q = query.toLowerCase();
32
+ allComments = allComments.filter(c => c.body?.toLowerCase().includes(q));
33
+
34
+ return allComments.slice(0, limit);
35
+ }
@@ -0,0 +1,18 @@
1
+ import pkg from 'bitbucket';
2
+ const { Bitbucket } = pkg;
3
+ import { getBitbucketToken, getBitbucketBaseUrl } from '../../utils/scmAuth.js';
4
+
5
+ export function getClient() {
6
+ const token = getBitbucketToken();
7
+ if (!token) throw new Error('Bitbucket token not found. Set BITBUCKET_TOKEN env var or run `codeant set-token bitbucket <token>`.');
8
+ const opts = { auth: { token } };
9
+ const baseUrl = getBitbucketBaseUrl();
10
+ if (baseUrl) opts.baseUrl = baseUrl.replace(/\/+$/, '');
11
+ return new Bitbucket(opts);
12
+ }
13
+
14
+ export function splitRepo(name) {
15
+ const [workspace, repo_slug] = name.split('/');
16
+ if (!workspace || !repo_slug) throw new Error(`Invalid repo name "${name}". Use workspace/repo format.`);
17
+ return { workspace, repo_slug };
18
+ }
@@ -0,0 +1,3 @@
1
+ export async function getCodeReview() {
2
+ throw new Error('Bitbucket does not support individual code review retrieval. Use `pr comments` instead.');
3
+ }
@@ -0,0 +1,26 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function getPullRequest({ name, prNumber }) {
4
+ const client = getClient();
5
+ const { workspace, repo_slug } = splitRepo(name);
6
+
7
+ const { data: pr } = await client.repositories.getPullRequest({ workspace, repo_slug, pull_request_id: prNumber });
8
+
9
+ return {
10
+ number: pr.id,
11
+ title: pr.title,
12
+ state: pr.state?.toLowerCase(),
13
+ merged: pr.state === 'MERGED',
14
+ author: pr.author?.display_name || pr.author?.nickname,
15
+ sourceBranch: pr.source?.branch?.name,
16
+ targetBranch: pr.destination?.branch?.name,
17
+ body: pr.description,
18
+ createdAt: pr.created_on,
19
+ updatedAt: pr.updated_on,
20
+ url: pr.links?.html?.href,
21
+ reviewSummary: pr.participants?.filter(p => p.role === 'REVIEWER').map(p => ({
22
+ user: p.user?.display_name || p.user?.nickname,
23
+ state: p.approved ? 'APPROVED' : 'PENDING',
24
+ })) || [],
25
+ };
26
+ }
@@ -0,0 +1,7 @@
1
+ export { listPullRequests } from './listPullRequests.js';
2
+ export { getPullRequest } from './getPullRequest.js';
3
+ export { listPullRequestComments } from './listPullRequestComments.js';
4
+ export { listCodeReviews } from './listCodeReviews.js';
5
+ export { getCodeReview } from './getCodeReview.js';
6
+ export { searchComments } from './searchComments.js';
7
+ export { resolveConversation } from './resolveConversation.js';
@@ -0,0 +1,17 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function listCodeReviews({ name, prNumber, limit = 20 }) {
4
+ // Bitbucket doesn't have discrete "reviews" — participants with reviewer role are the equivalent
5
+ const client = getClient();
6
+ const { workspace, repo_slug } = splitRepo(name);
7
+
8
+ const { data: pr } = await client.repositories.getPullRequest({ workspace, repo_slug, pull_request_id: prNumber });
9
+
10
+ return (pr.participants || []).filter(p => p.role === 'REVIEWER').slice(0, limit).map(p => ({
11
+ id: null,
12
+ user: p.user?.display_name || p.user?.nickname,
13
+ state: p.approved ? 'APPROVED' : 'PENDING',
14
+ body: null,
15
+ submittedAt: null,
16
+ }));
17
+ }
@@ -0,0 +1,33 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function listPullRequestComments({ name, prNumber, codeantGenerated, createdAfter, createdBefore }) {
4
+ const client = getClient();
5
+ const { workspace, repo_slug } = splitRepo(name);
6
+
7
+ const { data } = await client.repositories.listPullRequestComments({ workspace, repo_slug, pull_request_id: prNumber, pagelen: 100 });
8
+
9
+ let comments = (data.values || []).map(c => ({
10
+ id: c.id,
11
+ type: c.inline ? 'review' : 'issue',
12
+ author: c.user?.display_name || c.user?.nickname,
13
+ body: c.content?.raw || c.content?.markup || '',
14
+ path: c.inline?.path || null,
15
+ line: c.inline?.to || null,
16
+ createdAt: c.created_on,
17
+ updatedAt: c.updated_on,
18
+ isCodeantComment: (c.user?.display_name || c.user?.nickname || '').toLowerCase().includes('codeant') || (c.content?.raw || c.content?.markup || '').includes('Suggestion'),
19
+ resolved: c.resolved || false,
20
+ }));
21
+
22
+ if (codeantGenerated !== undefined) {
23
+ comments = comments.filter(c => c.isCodeantComment === codeantGenerated);
24
+ }
25
+ if (createdAfter) {
26
+ comments = comments.filter(c => new Date(c.createdAt) >= new Date(createdAfter));
27
+ }
28
+ if (createdBefore) {
29
+ comments = comments.filter(c => new Date(c.createdAt) <= new Date(createdBefore));
30
+ }
31
+
32
+ return comments;
33
+ }
@@ -0,0 +1,33 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function listPullRequests({ name, sourceBranch, authorLogin, state = 'open', limit = 20 }) {
4
+ const client = getClient();
5
+ const { workspace, repo_slug } = splitRepo(name);
6
+
7
+ const stateMap = { open: 'OPEN', closed: 'MERGED,DECLINED,SUPERSEDED' };
8
+ const { data } = await client.repositories.listPullRequests({ workspace, repo_slug, state: stateMap[state] || 'OPEN', pagelen: limit });
9
+
10
+ let prs = (data.values || []).map(pr => ({
11
+ number: pr.id,
12
+ title: pr.title,
13
+ state: pr.state?.toLowerCase(),
14
+ merged: pr.state === 'MERGED',
15
+ author: pr.author?.display_name || pr.author?.nickname,
16
+ sourceBranch: pr.source?.branch?.name,
17
+ targetBranch: pr.destination?.branch?.name,
18
+ createdAt: pr.created_on,
19
+ updatedAt: pr.updated_on,
20
+ url: pr.links?.html?.href,
21
+ }));
22
+
23
+ if (sourceBranch) {
24
+ const sb = sourceBranch.toLowerCase();
25
+ prs = prs.filter(pr => pr.sourceBranch?.toLowerCase().includes(sb));
26
+ }
27
+ if (authorLogin) {
28
+ const author = authorLogin.toLowerCase();
29
+ prs = prs.filter(pr => pr.author?.toLowerCase().includes(author));
30
+ }
31
+
32
+ return prs;
33
+ }
@@ -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
+ }
@@ -0,0 +1,29 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function searchComments({ name, query, limit = 10 }) {
4
+ const client = getClient();
5
+ const { workspace, repo_slug } = splitRepo(name);
6
+
7
+ const { data: prsData } = await client.repositories.listPullRequests({ workspace, repo_slug, state: 'OPEN', pagelen: 10 });
8
+
9
+ let allComments = [];
10
+ for (const pr of (prsData.values || [])) {
11
+ const { data: commentsData } = await client.repositories.listPullRequestComments({ workspace, repo_slug, pull_request_id: pr.id, pagelen: 50 });
12
+ allComments.push(...(commentsData.values || []).map(c => ({
13
+ prNumber: pr.id,
14
+ prTitle: pr.title,
15
+ id: c.id,
16
+ author: c.user?.display_name || c.user?.nickname,
17
+ body: c.content?.raw || '',
18
+ path: c.inline?.path || null,
19
+ line: c.inline?.to || null,
20
+ createdAt: c.created_on,
21
+ isCodeantComment: (c.user?.display_name || '').toLowerCase().includes('codeant'),
22
+ })));
23
+ }
24
+
25
+ const q = query.toLowerCase();
26
+ allComments = allComments.filter(c => c.body?.toLowerCase().includes(q));
27
+
28
+ return allComments.slice(0, limit);
29
+ }
@@ -0,0 +1,17 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ import { getGitHubToken, getGitHubBaseUrl } from '../../utils/scmAuth.js';
3
+
4
+ export function getClient() {
5
+ const token = getGitHubToken();
6
+ if (!token) throw new Error('GitHub token not found. Set GITHUB_TOKEN env var, login via `gh auth login`, or run `codeant set-token github <token>`.');
7
+ const opts = { auth: token };
8
+ const baseUrl = getGitHubBaseUrl();
9
+ if (baseUrl) opts.baseUrl = baseUrl.replace(/\/+$/, '') + '/api/v3';
10
+ return new Octokit(opts);
11
+ }
12
+
13
+ export function splitRepo(name) {
14
+ const [owner, repo] = name.split('/');
15
+ if (!owner || !repo) throw new Error(`Invalid repo name "${name}". Use owner/repo format.`);
16
+ return { owner, repo };
17
+ }
@@ -0,0 +1,26 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function getCodeReview({ name, prNumber, reviewId }) {
4
+ const octokit = getClient();
5
+ const { owner, repo } = splitRepo(name);
6
+
7
+ const [{ data: review }, { data: reviewComments }] = await Promise.all([
8
+ octokit.pulls.getReview({ owner, repo, pull_number: prNumber, review_id: reviewId }),
9
+ octokit.pulls.listCommentsForReview({ owner, repo, pull_number: prNumber, review_id: reviewId, per_page: 100 }),
10
+ ]);
11
+
12
+ return {
13
+ id: review.id,
14
+ user: review.user?.login,
15
+ state: review.state,
16
+ body: review.body,
17
+ submittedAt: review.submitted_at,
18
+ comments: reviewComments.map(c => ({
19
+ id: c.id,
20
+ path: c.path,
21
+ line: c.line || c.original_line,
22
+ body: c.body,
23
+ createdAt: c.created_at,
24
+ })),
25
+ };
26
+ }
@@ -0,0 +1,27 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function getPullRequest({ name, prNumber }) {
4
+ const octokit = getClient();
5
+ const { owner, repo } = splitRepo(name);
6
+
7
+ const { data: pr } = await octokit.pulls.get({ owner, repo, pull_number: prNumber });
8
+ const { data: reviews } = await octokit.pulls.listReviews({ owner, repo, pull_number: prNumber });
9
+
10
+ return {
11
+ number: pr.number,
12
+ title: pr.title,
13
+ state: pr.state,
14
+ merged: pr.merged_at !== null,
15
+ author: pr.user?.login,
16
+ sourceBranch: pr.head?.ref,
17
+ targetBranch: pr.base?.ref,
18
+ body: pr.body,
19
+ createdAt: pr.created_at,
20
+ updatedAt: pr.updated_at,
21
+ url: pr.html_url,
22
+ additions: pr.additions,
23
+ deletions: pr.deletions,
24
+ changedFiles: pr.changed_files,
25
+ reviewSummary: reviews.map(r => ({ user: r.user?.login, state: r.state, submittedAt: r.submitted_at })),
26
+ };
27
+ }
@@ -0,0 +1,7 @@
1
+ export { listPullRequests } from './listPullRequests.js';
2
+ export { getPullRequest } from './getPullRequest.js';
3
+ export { listPullRequestComments } from './listPullRequestComments.js';
4
+ export { listCodeReviews } from './listCodeReviews.js';
5
+ export { getCodeReview } from './getCodeReview.js';
6
+ export { searchComments } from './searchComments.js';
7
+ export { resolveConversation } from './resolveConversation.js';
@@ -0,0 +1,17 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function listCodeReviews({ name, prNumber, limit = 20, offset = 0 }) {
4
+ const octokit = getClient();
5
+ const { owner, repo } = splitRepo(name);
6
+
7
+ const { data } = await octokit.pulls.listReviews({ owner, repo, pull_number: prNumber, per_page: limit, page: Math.floor(offset / limit) + 1 });
8
+
9
+ return data.map(r => ({
10
+ id: r.id,
11
+ user: r.user?.login,
12
+ state: r.state,
13
+ body: r.body,
14
+ submittedAt: r.submitted_at,
15
+ url: r.html_url,
16
+ }));
17
+ }
@@ -0,0 +1,51 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function listPullRequestComments({ name, prNumber, codeantGenerated, addressed, createdAfter, createdBefore }) {
4
+ const octokit = getClient();
5
+ const { owner, repo } = splitRepo(name);
6
+
7
+ // Get both review comments (inline) and issue comments (general)
8
+ const [{ data: reviewComments }, { data: issueComments }] = await Promise.all([
9
+ octokit.pulls.listReviewComments({ owner, repo, pull_number: prNumber, per_page: 100 }),
10
+ octokit.issues.listComments({ owner, repo, issue_number: prNumber, per_page: 100 }),
11
+ ]);
12
+
13
+ let comments = [
14
+ ...reviewComments.map(c => ({
15
+ id: c.id,
16
+ type: 'review',
17
+ author: c.user?.login,
18
+ body: c.body,
19
+ path: c.path,
20
+ line: c.line || c.original_line,
21
+ createdAt: c.created_at,
22
+ updatedAt: c.updated_at,
23
+ isCodeantComment: c.user?.login?.includes('codeant') || c.body?.includes('Suggestion'),
24
+ inReplyToId: c.in_reply_to_id || null,
25
+ })),
26
+ ...issueComments.map(c => ({
27
+ id: c.id,
28
+ type: 'issue',
29
+ author: c.user?.login,
30
+ body: c.body,
31
+ path: null,
32
+ line: null,
33
+ createdAt: c.created_at,
34
+ updatedAt: c.updated_at,
35
+ isCodeantComment: c.user?.login?.includes('codeant') || c.body?.includes('Suggestion'),
36
+ inReplyToId: null,
37
+ })),
38
+ ];
39
+
40
+ if (codeantGenerated !== undefined) {
41
+ comments = comments.filter(c => c.isCodeantComment === codeantGenerated);
42
+ }
43
+ if (createdAfter) {
44
+ comments = comments.filter(c => new Date(c.createdAt) >= new Date(createdAfter));
45
+ }
46
+ if (createdBefore) {
47
+ comments = comments.filter(c => new Date(c.createdAt) <= new Date(createdBefore));
48
+ }
49
+
50
+ return comments;
51
+ }
@@ -0,0 +1,32 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function listPullRequests({ name, sourceBranch, authorLogin, state = 'open', limit = 20, offset = 0 }) {
4
+ const octokit = getClient();
5
+ const { owner, repo } = splitRepo(name);
6
+
7
+ const perPage = Math.max(limit, 1);
8
+ const params = { owner, repo, state: state === 'closed' ? 'closed' : 'open', per_page: perPage, page: Math.floor(offset / perPage) + 1, sort: 'updated', direction: 'desc' };
9
+ if (sourceBranch) params.head = sourceBranch.includes(':') ? sourceBranch : `${owner}:${sourceBranch}`;
10
+
11
+ const { data } = await octokit.pulls.list(params);
12
+
13
+ let prs = data.map(pr => ({
14
+ number: pr.number,
15
+ title: pr.title,
16
+ state: pr.state,
17
+ merged: pr.merged_at !== null,
18
+ author: pr.user?.login,
19
+ sourceBranch: pr.head?.ref,
20
+ targetBranch: pr.base?.ref,
21
+ createdAt: pr.created_at,
22
+ updatedAt: pr.updated_at,
23
+ url: pr.html_url,
24
+ }));
25
+
26
+ if (authorLogin) {
27
+ const author = authorLogin.toLowerCase();
28
+ prs = prs.filter(pr => pr.author?.toLowerCase().includes(author));
29
+ }
30
+
31
+ return prs;
32
+ }