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,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
+ }
@@ -0,0 +1,30 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function searchComments({ name, query, limit = 10, includeAddressed = false }) {
4
+ const octokit = getClient();
5
+ const { owner, repo } = splitRepo(name);
6
+
7
+ // GitHub doesn't have a native comment search API, so we fetch recent PR comments and filter
8
+ const { data: prs } = await octokit.pulls.list({ owner, repo, state: 'all', per_page: 10, sort: 'updated' });
9
+
10
+ let allComments = [];
11
+ for (const pr of prs) {
12
+ const { data: comments } = await octokit.pulls.listReviewComments({ owner, repo, pull_number: pr.number, per_page: 50 });
13
+ allComments.push(...comments.map(c => ({
14
+ prNumber: pr.number,
15
+ prTitle: pr.title,
16
+ id: c.id,
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
+ isCodeantComment: c.user?.login?.includes('codeant'),
23
+ })));
24
+ }
25
+
26
+ const q = query.toLowerCase();
27
+ allComments = allComments.filter(c => c.body?.toLowerCase().includes(q));
28
+
29
+ return allComments.slice(0, limit);
30
+ }
@@ -0,0 +1,16 @@
1
+ import { Gitlab } from '@gitbeaker/rest';
2
+ import { getGitLabToken, getGitLabBaseUrl } from '../../utils/scmAuth.js';
3
+
4
+ export function getClient() {
5
+ const token = getGitLabToken();
6
+ if (!token) throw new Error('GitLab token not found. Set GITLAB_TOKEN env var, login via `glab auth login`, or run `codeant set-token gitlab <token>`.');
7
+ const opts = { token };
8
+ const host = getGitLabBaseUrl();
9
+ if (host) opts.host = host.replace(/\/+$/, '');
10
+ return new Gitlab(opts);
11
+ }
12
+
13
+ // GitLab uses project ID or "owner/repo" encoded as "owner%2Frepo"
14
+ export function projectId(name) {
15
+ return encodeURIComponent(name);
16
+ }
@@ -0,0 +1,18 @@
1
+ import { getClient, projectId } from './client.js';
2
+
3
+ export async function getCodeReview({ name, prNumber, reviewId }) {
4
+ const api = getClient();
5
+
6
+ const note = await api.MergeRequestNotes.show(projectId(name), prNumber, reviewId);
7
+
8
+ return {
9
+ id: note.id,
10
+ user: note.author?.username,
11
+ state: note.resolved ? 'APPROVED' : 'COMMENTED',
12
+ body: note.body,
13
+ submittedAt: note.created_at,
14
+ path: note.position?.new_path || null,
15
+ line: note.position?.new_line || null,
16
+ comments: [],
17
+ };
18
+ }
@@ -0,0 +1,24 @@
1
+ import { getClient, projectId } from './client.js';
2
+
3
+ export async function getPullRequest({ name, prNumber }) {
4
+ const api = getClient();
5
+
6
+ const mr = await api.MergeRequests.show(projectId(name), prNumber);
7
+ const approvals = await api.MergeRequestApprovals.configuration(projectId(name), { mergerequestIid: prNumber }).catch(() => null);
8
+
9
+ return {
10
+ number: mr.iid,
11
+ title: mr.title,
12
+ state: mr.state === 'opened' ? 'open' : mr.state,
13
+ merged: mr.state === 'merged',
14
+ author: mr.author?.username,
15
+ sourceBranch: mr.source_branch,
16
+ targetBranch: mr.target_branch,
17
+ body: mr.description,
18
+ createdAt: mr.created_at,
19
+ updatedAt: mr.updated_at,
20
+ url: mr.web_url,
21
+ additions: mr.changes_count,
22
+ reviewSummary: approvals?.approved_by?.map(a => ({ user: a.user?.username, state: 'APPROVED' })) || [],
23
+ };
24
+ }
@@ -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,21 @@
1
+ import { getClient, projectId } from './client.js';
2
+
3
+ export async function listCodeReviews({ name, prNumber, limit = 20, offset = 0 }) {
4
+ const api = getClient();
5
+
6
+ // GitLab doesn't have "reviews" like GitHub — use notes + approvals
7
+ const notes = await api.MergeRequestNotes.all(projectId(name), prNumber, {
8
+ perPage: limit,
9
+ page: Math.floor(offset / Math.max(limit, 1)) + 1,
10
+ });
11
+
12
+ const reviewNotes = notes.filter(n => !n.system);
13
+
14
+ return reviewNotes.map(n => ({
15
+ id: n.id,
16
+ user: n.author?.username,
17
+ state: n.resolved ? 'APPROVED' : 'COMMENTED',
18
+ body: n.body,
19
+ submittedAt: n.created_at,
20
+ }));
21
+ }
@@ -0,0 +1,44 @@
1
+ import { getClient, projectId } from './client.js';
2
+
3
+ export async function listPullRequestComments({ name, prNumber, codeantGenerated, createdAfter, createdBefore }) {
4
+ const api = getClient();
5
+
6
+ const [notes, discussions] = await Promise.all([
7
+ api.MergeRequestNotes.all(projectId(name), prNumber, { perPage: 100 }),
8
+ api.MergeRequestDiscussions.all(projectId(name), prNumber, { perPage: 100 }),
9
+ ]);
10
+
11
+ let comments = notes.map(n => ({
12
+ id: n.id,
13
+ type: n.type === 'DiffNote' ? 'review' : 'issue',
14
+ author: n.author?.username,
15
+ body: n.body,
16
+ path: n.position?.new_path || null,
17
+ line: n.position?.new_line || null,
18
+ createdAt: n.created_at,
19
+ updatedAt: n.updated_at,
20
+ isCodeantComment: n.author?.username?.includes('codeant') || n.body?.includes('Suggestion'),
21
+ resolved: n.resolved || false,
22
+ discussionId: null,
23
+ }));
24
+
25
+ // Enrich with discussion context
26
+ for (const disc of discussions) {
27
+ for (const note of disc.notes || []) {
28
+ const match = comments.find(c => c.id === note.id);
29
+ if (match) match.discussionId = disc.id;
30
+ }
31
+ }
32
+
33
+ if (codeantGenerated !== undefined) {
34
+ comments = comments.filter(c => c.isCodeantComment === codeantGenerated);
35
+ }
36
+ if (createdAfter) {
37
+ comments = comments.filter(c => new Date(c.createdAt) >= new Date(createdAfter));
38
+ }
39
+ if (createdBefore) {
40
+ comments = comments.filter(c => new Date(c.createdAt) <= new Date(createdBefore));
41
+ }
42
+
43
+ return comments;
44
+ }
@@ -0,0 +1,26 @@
1
+ import { getClient, projectId } from './client.js';
2
+
3
+ export async function listPullRequests({ name, sourceBranch, authorLogin, state = 'open', limit = 20, offset = 0 }) {
4
+ const api = getClient();
5
+
6
+ const params = { perPage: limit, page: Math.floor(offset / limit) + 1, orderBy: 'updated_at', sort: 'desc' };
7
+ if (state === 'open') params.state = 'opened';
8
+ else if (state === 'closed') params.state = 'closed';
9
+ if (sourceBranch) params.sourceBranch = sourceBranch;
10
+ if (authorLogin) params.authorUsername = authorLogin;
11
+
12
+ const mrs = await api.MergeRequests.all({ projectId: projectId(name), ...params });
13
+
14
+ return mrs.map(mr => ({
15
+ number: mr.iid,
16
+ title: mr.title,
17
+ state: mr.state === 'opened' ? 'open' : mr.state,
18
+ merged: mr.state === 'merged',
19
+ author: mr.author?.username,
20
+ sourceBranch: mr.source_branch,
21
+ targetBranch: mr.target_branch,
22
+ createdAt: mr.created_at,
23
+ updatedAt: mr.updated_at,
24
+ url: mr.web_url,
25
+ }));
26
+ }
@@ -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
+ }
@@ -0,0 +1,28 @@
1
+ import { getClient, projectId } from './client.js';
2
+
3
+ export async function searchComments({ name, query, limit = 10 }) {
4
+ const api = getClient();
5
+
6
+ const mrs = await api.MergeRequests.all({ projectId: projectId(name), perPage: 10, orderBy: 'updated_at', sort: 'desc', state: 'all' });
7
+
8
+ let allComments = [];
9
+ for (const mr of mrs) {
10
+ const notes = await api.MergeRequestNotes.all(projectId(name), mr.iid, { perPage: 50 });
11
+ allComments.push(...notes.filter(n => !n.system).map(n => ({
12
+ prNumber: mr.iid,
13
+ prTitle: mr.title,
14
+ id: n.id,
15
+ author: n.author?.username,
16
+ body: n.body,
17
+ path: n.position?.new_path || null,
18
+ line: n.position?.new_line || null,
19
+ createdAt: n.created_at,
20
+ isCodeantComment: n.author?.username?.includes('codeant'),
21
+ })));
22
+ }
23
+
24
+ const q = query.toLowerCase();
25
+ allComments = allComments.filter(c => c.body?.toLowerCase().includes(q));
26
+
27
+ return allComments.slice(0, limit);
28
+ }
@@ -0,0 +1,97 @@
1
+ import { execSync } from 'child_process';
2
+ import * as github from './github/index.js';
3
+ import * as gitlab from './gitlab/index.js';
4
+ import * as bitbucket from './bitbucket/index.js';
5
+ import * as azure from './azure/index.js';
6
+
7
+ const platforms = { github, gitlab, bitbucket, azure };
8
+
9
+ function getProvider(remote) {
10
+ const provider = platforms[remote];
11
+ if (!provider) throw new Error(`Unknown remote "${remote}". Supported: github, gitlab, bitbucket, azure`);
12
+ return provider;
13
+ }
14
+
15
+ // ─── Shared origin parsing (single source of truth) ───
16
+
17
+ function parseOriginUrl(origin) {
18
+ // Parse host from SSH (git@host:owner/repo) or HTTPS (https://host/owner/repo)
19
+ let host = null;
20
+ const sshMatch = origin.match(/^[^@]+@([^:]+):/);
21
+ const httpsMatch = origin.match(/^https?:\/\/([^/]+)/);
22
+ if (sshMatch) host = sshMatch[1];
23
+ else if (httpsMatch) host = httpsMatch[1];
24
+ return host;
25
+ }
26
+
27
+ const HOST_TO_PLATFORM = {
28
+ 'github.com': 'github',
29
+ 'gitlab.com': 'gitlab',
30
+ 'bitbucket.org': 'bitbucket',
31
+ 'dev.azure.com': 'azure',
32
+ 'visualstudio.com': 'azure',
33
+ };
34
+
35
+ function detectPlatformFromHost(host) {
36
+ if (!host) return null;
37
+ // Exact match or subdomain match against known hosts
38
+ for (const [publicHost, platform] of Object.entries(HOST_TO_PLATFORM)) {
39
+ if (host === publicHost || host.endsWith('.' + publicHost)) return platform;
40
+ }
41
+ // Heuristic for self-hosted: check if hostname contains platform name
42
+ if (host.includes('github')) return 'github';
43
+ if (host.includes('gitlab')) return 'gitlab';
44
+ if (host.includes('bitbucket')) return 'bitbucket';
45
+ if (host.includes('azure') || host.includes('visualstudio')) return 'azure';
46
+ return null;
47
+ }
48
+
49
+ function getOrigin() {
50
+ try {
51
+ return execSync('git remote get-url origin', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
52
+ } catch {}
53
+ return null;
54
+ }
55
+
56
+ // Auto-detect remote from git origin URL
57
+ export function detectRemote() {
58
+ const origin = getOrigin();
59
+ if (!origin) return null;
60
+ const host = parseOriginUrl(origin);
61
+ return detectPlatformFromHost(host);
62
+ }
63
+
64
+ // Auto-detect repo name from git origin URL
65
+ export function detectRepoName() {
66
+ const origin = getOrigin();
67
+ if (!origin) return null;
68
+ // Handle SSH: git@github.com:owner/repo.git
69
+ // Handle HTTPS: https://github.com/owner/repo.git
70
+ const match = origin.match(/[/:]([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
71
+ return match ? match[1] : null;
72
+ }
73
+
74
+ // Auto-detect default branch
75
+ export function detectDefaultBranch() {
76
+ try {
77
+ const ref = execSync('git symbolic-ref refs/remotes/origin/HEAD', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
78
+ return ref.replace('refs/remotes/origin/', '');
79
+ } catch {}
80
+ try {
81
+ execSync('git show-ref --verify refs/heads/main', { stdio: ['pipe', 'pipe', 'pipe'] });
82
+ return 'main';
83
+ } catch {}
84
+ try {
85
+ execSync('git show-ref --verify refs/heads/master', { stdio: ['pipe', 'pipe', 'pipe'] });
86
+ return 'master';
87
+ } catch {}
88
+ return null;
89
+ }
90
+
91
+ export function listPullRequests(opts) { return getProvider(opts.remote).listPullRequests(opts); }
92
+ export function getPullRequest(opts) { return getProvider(opts.remote).getPullRequest(opts); }
93
+ export function listPullRequestComments(opts) { return getProvider(opts.remote).listPullRequestComments(opts); }
94
+ export function listCodeReviews(opts) { return getProvider(opts.remote).listCodeReviews(opts); }
95
+ export function getCodeReview(opts) { return getProvider(opts.remote).getCodeReview(opts); }
96
+ export function searchComments(opts) { return getProvider(opts.remote).searchComments(opts); }
97
+ export function resolveConversation(opts) { return getProvider(opts.remote).resolveConversation(opts); }
@@ -0,0 +1,31 @@
1
+ import { readTool } from './readTool.js';
2
+ import { globTool } from './globTool.js';
3
+ import { grepTool } from './grepTool.js';
4
+ import { lsTool } from './lsTool.js';
5
+
6
+ export async function executeTool(toolCall, cwd) {
7
+ const { name, args } = toolCall;
8
+
9
+ try {
10
+ if (name === 'Read') {
11
+ return await readTool(args, cwd);
12
+ }
13
+
14
+ if (name === 'Glob') {
15
+ return await globTool(args, cwd);
16
+ }
17
+
18
+ if (name === 'Grep') {
19
+ return await grepTool(args, cwd);
20
+ }
21
+
22
+ if (name === 'LS') {
23
+ return await lsTool(args, cwd);
24
+ }
25
+
26
+ // Bash tool intentionally removed to prevent arbitrary command execution
27
+ return `Unknown tool: ${name}`;
28
+ } catch (err) {
29
+ return `Error: ${err.message}`;
30
+ }
31
+ }
@@ -0,0 +1,9 @@
1
+ import path from 'path';
2
+
3
+ export async function globTool(args, cwd) {
4
+ const { globSync } = await import('glob');
5
+ const pattern = path.resolve(cwd, args.pattern);
6
+ const matches = globSync(pattern);
7
+ if (!matches.length) return 'No files found';
8
+ return matches.map(m => path.relative(cwd, m)).join('\n');
9
+ }
@@ -0,0 +1,18 @@
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+
4
+ export async function grepTool(args, cwd) {
5
+ const target = args.path ? path.resolve(cwd, args.path) : cwd;
6
+ const result = await new Promise((resolve) => {
7
+ const proc = spawn('grep', ['-rn', args.pattern, target], {
8
+ cwd,
9
+ stdio: ['ignore', 'pipe', 'ignore'],
10
+ timeout: 30000,
11
+ });
12
+ let stdout = '';
13
+ proc.stdout.on('data', (data) => { stdout += data; });
14
+ proc.on('close', () => resolve(stdout.trim() || 'No matches found'));
15
+ proc.on('error', () => resolve('No matches found'));
16
+ });
17
+ return result;
18
+ }
@@ -0,0 +1,7 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export async function lsTool(args, cwd) {
5
+ const dirPath = args.path ? path.resolve(cwd, args.path) : cwd;
6
+ return fs.readdirSync(dirPath).sort().join('\n');
7
+ }
@@ -0,0 +1,12 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export async function readTool(args, cwd) {
5
+ const filePath = path.resolve(cwd, args.file_path);
6
+ const content = await fs.promises.readFile(filePath, 'utf8');
7
+ const lines = content.split('\n');
8
+ const offset = args.offset || 1;
9
+ const limit = args.limit || lines.length;
10
+ const selected = lines.slice(offset - 1, offset - 1 + limit);
11
+ return selected.map((line, i) => ` ${offset + i}\t${line}`).join('\n');
12
+ }
@@ -0,0 +1,56 @@
1
+ import { PostHog } from 'posthog-node';
2
+ import { getConfigValue } from './config.js';
3
+
4
+ const POSTHOG_API_KEY = 'phc_TpsLE5AMwHYsrSQIYgI7uqvorzlDMsin3lDvTEv2DxO';
5
+ const POSTHOG_HOST = 'https://r.codeant.ai';
6
+
7
+ let client = null;
8
+
9
+ /** Check if telemetry is disabled via env var or config. */
10
+ export function isTelemetryDisabled() {
11
+ const envVal = process.env.CODEANT_TELEMETRY_DISABLED;
12
+ if (envVal !== undefined) {
13
+ return envVal === '1' || envVal.toLowerCase() === 'true';
14
+ }
15
+ return getConfigValue('telemetryEnabled') === false;
16
+ }
17
+
18
+ function getClient() {
19
+ if (!client) {
20
+ client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST, flushAt: 1, flushInterval: 0 });
21
+ }
22
+ return client;
23
+ }
24
+
25
+ /** Use the API key directly as distinct ID */
26
+ function getDistinctId() {
27
+ return process.env.CODEANT_API_TOKEN || getConfigValue('apiKey') || 'anonymous';
28
+ }
29
+
30
+ /**
31
+ * Track an analytics event. Fire-and-forget — never throws or blocks the CLI.
32
+ * Respects telemetry opt-out via config or CODEANT_TELEMETRY_DISABLED env var.
33
+ * @param {string} event - Event name (e.g. 'review_triggered')
34
+ * @param {Object} [properties={}] - Event properties
35
+ */
36
+ export function track(event, properties = {}) {
37
+ try {
38
+ if (isTelemetryDisabled()) return;
39
+ getClient().capture({
40
+ distinctId: getDistinctId(),
41
+ event,
42
+ properties: { source: 'cli', cli_version: process.env.npm_package_version || 'unknown', ...properties },
43
+ });
44
+ } catch (_) {
45
+ // Analytics should never break the CLI
46
+ }
47
+ }
48
+
49
+ /** Flush pending events. Call before process exits. */
50
+ export async function shutdown() {
51
+ try {
52
+ if (client) await client.shutdown();
53
+ } catch (_) {
54
+ // noop
55
+ }
56
+ }
@@ -0,0 +1,11 @@
1
+ import { getConfigValue } from './config.js';
2
+
3
+ const NEW_BASE_URL = 'https://service.codeant.ai';
4
+
5
+ const getBaseUrl = () => {
6
+ const url = process.env.CODEANT_API_URL || getConfigValue('baseUrl') || NEW_BASE_URL;
7
+ if (url === 'https://api.codeant.ai') return NEW_BASE_URL;
8
+ return url;
9
+ };
10
+
11
+ export { getBaseUrl };