From 9a4a2ba41436cc3f4036623977c9a01fd67217ff Mon Sep 17 00:00:00 2001 From: Wellington Santana Date: Tue, 17 Mar 2026 19:30:23 -0300 Subject: [PATCH 1/4] fix(cli): send git context for personal token auth flow - analyzeWithMetrics now handles both team key and JWT auth - Personal token path now collects gitInfo and sends gitRemote/branch - Fixes repo-scoped kody rules not being applied when using personal token Co-Authored-By: Claude Sonnet 4.6 --- src/services/api/review.api.ts | 38 ++++++++++++++++++++++------------ src/services/review.service.ts | 24 ++++++++++++++++++++- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/services/api/review.api.ts b/src/services/api/review.api.ts index 370b4dd..744555f 100644 --- a/src/services/api/review.api.ts +++ b/src/services/api/review.api.ts @@ -64,21 +64,33 @@ export class RealReviewApi implements IReviewApi { ): Promise { const isTeamKey = accessToken.startsWith('kodus_'); - if (isTeamKey) { - return this.requester('/cli/review', { - method: 'POST', - headers: { - 'X-Team-Key': accessToken, - }, - body: JSON.stringify({ - diff, - config, - ...metrics, - }), - }); + const headers: Record = isTeamKey + ? { 'X-Team-Key': accessToken } + : { Authorization: `Bearer ${accessToken}` }; + + let endpoint = '/cli/review'; + if (!isTeamKey) { + try { + const payload = JSON.parse( + Buffer.from(accessToken.split('.')[1], 'base64').toString(), + ); + if (payload.organizationId) { + endpoint = `/cli/review?teamId=${encodeURIComponent(payload.organizationId)}`; + } + } catch { + // Ignore if cannot decode + } } - return this.analyze(diff, accessToken, config); + return this.requester(endpoint, { + method: 'POST', + headers, + body: JSON.stringify({ + diff, + config, + ...metrics, + }), + }); } async getPullRequestSuggestions( diff --git a/src/services/review.service.ts b/src/services/review.service.ts index a5171dd..794345e 100644 --- a/src/services/review.service.ts +++ b/src/services/review.service.ts @@ -116,13 +116,35 @@ class ReviewService { return result; } + // Personal token: also send git context for repository-scoped rules + const gitInfo = await gitService.getGitInfo(); + const inferredPlatform = gitInfo.remote + ? gitService.inferPlatform(gitInfo.remote) + : undefined; + createAnalyzeApiRequestVerboseMessages({ diff, reviewConfig, mode: 'personal-token', + gitInfo: { + branch: gitInfo.branch, + remote: gitInfo.remote, + }, }).forEach((message) => this.logVerbose(message)); - const result = await api.review.analyze(diff, token, reviewConfig); + const result = await api.review.analyzeWithMetrics( + diff, + token, + reviewConfig, + { + userEmail: gitInfo.userEmail, + gitRemote: gitInfo.remote || undefined, + branch: gitInfo.branch, + commitSha: gitInfo.commitSha, + inferredPlatform, + cliVersion: CLI_VERSION, + }, + ); createAnalyzeApiResponseVerboseMessages({ summary: result.summary, From 74bcd45905504a613900756c148fb4dbe668427a Mon Sep 17 00:00:00 2001 From: Wellington Santana Date: Tue, 17 Mar 2026 19:34:20 -0300 Subject: [PATCH 2/4] fix(cli): improve git remote parsing and fix interactive loop - extractOrgRepoFromRemote now handles SSH, HTTPS, Azure DevOps, Bitbucket Server, self-hosted GitLab - Support subgroups in GitLab URLs (org/sub/repo) - Add comprehensive tests for all platform URL formats - Fix recursive loop in interactive.ts reviewFileIssues Co-Authored-By: Claude Sonnet 4.6 --- package.json | 8 +- src/ui/interactive.ts | 2 - src/utils/__tests__/git-remote.test.ts | 159 ++++++++++- src/utils/git-remote.ts | 66 ++++- yarn.lock | 370 ++++++++++++------------- 5 files changed, 389 insertions(+), 216 deletions(-) diff --git a/package.json b/package.json index bc4529b..0b8f500 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "author": "Kodus", "license": "MIT", "dependencies": { - "@inquirer/prompts": "^8.3.0", + "@inquirer/prompts": "^8.3.2", "boxen": "^8.0.1", "chalk": "^5.6.2", "clipboardy": "^5.3.1", @@ -69,8 +69,8 @@ "@types/gradient-string": "^1.1.6", "@types/node": "^25.5.0", "@types/update-notifier": "^6.0.8", - "@typescript-eslint/eslint-plugin": "^8.57.0", - "@typescript-eslint/parser": "^8.57.0", + "@typescript-eslint/eslint-plugin": "^8.57.1", + "@typescript-eslint/parser": "^8.57.1", "eslint": "^10.0.3", "prettier": "^3.8.1", "typescript": "^5.9.3", @@ -84,4 +84,4 @@ "dist", "skills" ] -} \ No newline at end of file +} diff --git a/src/ui/interactive.ts b/src/ui/interactive.ts index cb85953..5a446de 100644 --- a/src/ui/interactive.ts +++ b/src/ui/interactive.ts @@ -185,8 +185,6 @@ class InteractiveUI { console.log(''); } - // Show menu again - await this.reviewFileIssues(file, issues, fixedIssues); return; } diff --git a/src/utils/__tests__/git-remote.test.ts b/src/utils/__tests__/git-remote.test.ts index 75d5aeb..71c013d 100644 --- a/src/utils/__tests__/git-remote.test.ts +++ b/src/utils/__tests__/git-remote.test.ts @@ -11,6 +11,30 @@ describe('inferPlatformFromRemote', () => { ).toBe('GITHUB'); }); + it('detects GitHub from SSH URL', () => { + expect( + inferPlatformFromRemote('git@github.com:org/repo.git'), + ).toBe('GITHUB'); + }); + + it('detects GitLab from HTTPS URL', () => { + expect( + inferPlatformFromRemote('https://gitlab.com/org/repo.git'), + ).toBe('GITLAB'); + }); + + it('detects Bitbucket from SSH URL', () => { + expect( + inferPlatformFromRemote('git@bitbucket.org:org/repo.git'), + ).toBe('BITBUCKET'); + }); + + it('detects Azure DevOps from dev.azure.com URL', () => { + expect( + inferPlatformFromRemote('https://dev.azure.com/org/project/_git/repo'), + ).toBe('AZURE_REPOS'); + }); + it('detects Azure DevOps from visualstudio.com URL', () => { expect( inferPlatformFromRemote( @@ -19,6 +43,18 @@ describe('inferPlatformFromRemote', () => { ).toBe('AZURE_REPOS'); }); + it('detects Azure DevOps from SSH URL', () => { + expect( + inferPlatformFromRemote('git@ssh.dev.azure.com:v3/org/project/repo'), + ).toBe('AZURE_REPOS'); + }); + + it('returns undefined for self-hosted instances', () => { + expect( + inferPlatformFromRemote('https://gitlab.company.com/org/repo.git'), + ).toBeUndefined(); + }); + it('returns undefined for deceptive subdomain hosts', () => { expect( inferPlatformFromRemote( @@ -29,29 +65,126 @@ describe('inferPlatformFromRemote', () => { }); describe('extractOrgRepoFromRemote', () => { - it('extracts owner and repo from GitHub SSH URL', () => { + // ── GitHub ────────────────────────────────────────────────────────── + it('extracts from GitHub SSH URL', () => { expect(extractOrgRepoFromRemote('git@github.com:org/repo.git')).toEqual( - { - org: 'org', - repo: 'repo', - }, + { org: 'org', repo: 'repo' }, ); }); - it('extracts owner and repo from GitLab HTTPS URL', () => { + it('extracts from GitHub HTTPS URL', () => { + expect( + extractOrgRepoFromRemote('https://github.com/org/repo.git'), + ).toEqual({ org: 'org', repo: 'repo' }); + }); + + // ── GitLab ────────────────────────────────────────────────────────── + it('extracts from GitLab SSH URL', () => { + expect( + extractOrgRepoFromRemote('git@gitlab.com:group/project.git'), + ).toEqual({ org: 'group', repo: 'project' }); + }); + + it('extracts from GitLab HTTPS URL', () => { expect( extractOrgRepoFromRemote('https://gitlab.com/group/project.git'), - ).toEqual({ - org: 'group', - repo: 'project', - }); + ).toEqual({ org: 'group', repo: 'project' }); + }); + + it('extracts from GitLab SSH with subgroups', () => { + expect( + extractOrgRepoFromRemote( + 'git@gitlab.com:group/subgroup/repo.git', + ), + ).toEqual({ org: 'group', repo: 'repo' }); + }); + + it('extracts from self-hosted GitLab SSH', () => { + expect( + extractOrgRepoFromRemote('git@gitlab.company.com:org/repo.git'), + ).toEqual({ org: 'org', repo: 'repo' }); + }); + + it('extracts from self-hosted GitLab HTTPS', () => { + expect( + extractOrgRepoFromRemote( + 'https://gitlab.company.com/org/repo.git', + ), + ).toEqual({ org: 'org', repo: 'repo' }); + }); + + // ── Bitbucket ─────────────────────────────────────────────────────── + it('extracts from Bitbucket SSH URL', () => { + expect( + extractOrgRepoFromRemote('git@bitbucket.org:org/repo.git'), + ).toEqual({ org: 'org', repo: 'repo' }); + }); + + it('extracts from Bitbucket HTTPS URL', () => { + expect( + extractOrgRepoFromRemote('https://bitbucket.org/org/repo.git'), + ).toEqual({ org: 'org', repo: 'repo' }); + }); + + it('extracts from Bitbucket Server (self-hosted) HTTPS', () => { + expect( + extractOrgRepoFromRemote( + 'https://bitbucket.company.com/scm/proj/repo.git', + ), + ).toEqual({ org: 'proj', repo: 'repo' }); + }); + + it('extracts from Bitbucket Server SSH with port', () => { + expect( + extractOrgRepoFromRemote( + 'ssh://git@bitbucket.company.com:7999/proj/repo.git', + ), + ).toEqual({ org: 'proj', repo: 'repo' }); }); - it('returns null for unsupported remotes', () => { + // ── Azure DevOps ──────────────────────────────────────────────────── + it('extracts from Azure DevOps HTTPS (new)', () => { expect( extractOrgRepoFromRemote( - 'https://selfhosted.example.com/org/repo.git', + 'https://dev.azure.com/myorg/myproject/_git/myrepo', ), - ).toBeNull(); + ).toEqual({ org: 'myorg', repo: 'myrepo' }); + }); + + it('extracts from Azure DevOps HTTPS (old visualstudio.com)', () => { + expect( + extractOrgRepoFromRemote( + 'https://myorg.visualstudio.com/myproject/_git/myrepo', + ), + ).toEqual({ org: 'myorg', repo: 'myrepo' }); + }); + + it('extracts from Azure DevOps SSH (new)', () => { + expect( + extractOrgRepoFromRemote( + 'git@ssh.dev.azure.com:v3/myorg/myproject/myrepo', + ), + ).toEqual({ org: 'myorg', repo: 'myrepo' }); + }); + + it('extracts from Azure DevOps SSH (old vs-ssh)', () => { + expect( + extractOrgRepoFromRemote( + 'git@vs-ssh.visualstudio.com:v3/myorg/myproject/myrepo', + ), + ).toEqual({ org: 'myorg', repo: 'myrepo' }); + }); + + // ── Edge cases ────────────────────────────────────────────────────── + it('returns null for null input', () => { + expect(extractOrgRepoFromRemote(null)).toBeNull(); + }); + + it('returns null for empty string', () => { + expect(extractOrgRepoFromRemote('')).toBeNull(); + }); + + it('returns null for URL with no path segments', () => { + expect(extractOrgRepoFromRemote('https://github.com')).toBeNull(); }); }); diff --git a/src/utils/git-remote.ts b/src/utils/git-remote.ts index fe1fc03..51e04c6 100644 --- a/src/utils/git-remote.ts +++ b/src/utils/git-remote.ts @@ -3,21 +3,63 @@ import type { PlatformType } from '../types/cli.js'; export function extractOrgRepoFromRemote( remoteUrl: string | null | undefined, ): { org: string; repo: string } | null { - if (!remoteUrl) { - return null; + if (!remoteUrl) return null; + + const url = remoteUrl.trim(); + + // Azure DevOps SSH (new): git@ssh.dev.azure.com:v3/{org}/{project}/{repo} + // Azure DevOps SSH (old): git@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo} + const azureSsh = url.match( + /(?:ssh\.dev\.azure\.com|vs-ssh\.visualstudio\.com)[:/]v3\/([^/]+)\/[^/]+\/([^/.]+)/, + ); + if (azureSsh) return { org: azureSsh[1], repo: azureSsh[2] }; + + // Azure DevOps HTTPS (new): https://dev.azure.com/{org}/{project}/_git/{repo} + const azureHttpsNew = url.match( + /dev\.azure\.com\/([^/]+)\/[^/]+\/_git\/([^/.?]+)/, + ); + if (azureHttpsNew) return { org: azureHttpsNew[1], repo: azureHttpsNew[2] }; + + // Azure DevOps HTTPS (old): https://{org}.visualstudio.com/{project}/_git/{repo} + const azureHttpsOld = url.match( + /^https?:\/\/([^.]+)\.visualstudio\.com\/[^/]+\/_git\/([^/.?]+)/, + ); + if (azureHttpsOld) return { org: azureHttpsOld[1], repo: azureHttpsOld[2] }; + + // Bitbucket Server (self-hosted): .../scm/{proj}/{repo}.git + const bitbucketServer = url.match(/\/scm\/([^/]+)\/([^/.]+)/); + if (bitbucketServer) + return { org: bitbucketServer[1], repo: bitbucketServer[2] }; + + // SSH SCP-like: git@host:path — handles subgroups (org/sub/repo → org, repo) + const sshScp = url.match(/^[^@/]+@[^:]+:(.+)/); + if (sshScp) { + const parts = sshScp[1].replace(/\.git$/, '').split('/').filter(Boolean); + if (parts.length >= 2) + return { org: parts[0], repo: parts[parts.length - 1] }; } - const patterns = [ - /github\.com[:/]([^/]+)\/([^/.]+)/, - /gitlab\.com[:/]([^/]+)\/([^/.]+)/, - /bitbucket\.org[:/]([^/]+)\/([^/.]+)/, - ]; + // SSH with protocol: ssh://[user@]host[:port]/path + const sshProto = url.match(/^ssh:\/\/[^/]+\/(.+)/); + if (sshProto) { + const parts = sshProto[1] + .replace(/\.git$/, '') + .split('/') + .filter(Boolean); + if (parts.length >= 2) + return { org: parts[0], repo: parts[parts.length - 1] }; + } - for (const pattern of patterns) { - const match = remoteUrl.match(pattern); - if (match) { - return { org: match[1], repo: match[2] }; - } + // HTTPS: https://[user@]host/path — GitHub, GitLab, Bitbucket, self-hosted + const httpsMatch = url.match(/^https?:\/\/[^/]+\/(.+)/); + if (httpsMatch) { + const parts = httpsMatch[1] + .replace(/\.git$/, '') + .replace(/\?.*$/, '') + .split('/') + .filter(Boolean); + if (parts.length >= 2) + return { org: parts[0], repo: parts[parts.length - 1] }; } return null; diff --git a/yarn.lock b/yarn.lock index 92b115b..b8d9e2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -107,144 +107,144 @@ resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== -"@inquirer/ansi@^2.0.3": - version "2.0.3" - resolved "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.3.tgz" - integrity sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw== - -"@inquirer/checkbox@^5.1.0": - version "5.1.0" - resolved "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.0.tgz" - integrity sha512-/HjF1LN0a1h4/OFsbGKHNDtWICFU/dqXCdym719HFTyJo9IG7Otr+ziGWc9S0iQuohRZllh+WprSgd5UW5Fw0g== - dependencies: - "@inquirer/ansi" "^2.0.3" - "@inquirer/core" "^11.1.5" - "@inquirer/figures" "^2.0.3" - "@inquirer/type" "^4.0.3" +"@inquirer/ansi@^2.0.4": + version "2.0.4" + resolved "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.4.tgz#c767aba4e224297c17108820e2401d9def117172" + integrity sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg== -"@inquirer/confirm@^6.0.8": - version "6.0.8" - resolved "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.8.tgz" - integrity sha512-Di6dgmiZ9xCSUxWUReWTqDtbhXCuG2MQm2xmgSAIruzQzBqNf49b8E07/vbCYY506kDe8BiwJbegXweG8M1klw== - dependencies: - "@inquirer/core" "^11.1.5" - "@inquirer/type" "^4.0.3" - -"@inquirer/core@^11.1.5": - version "11.1.5" - resolved "https://registry.npmjs.org/@inquirer/core/-/core-11.1.5.tgz" - integrity sha512-QQPAX+lka8GyLcZ7u7Nb1h6q72iZ/oy0blilC3IB2nSt1Qqxp7akt94Jqhi/DzARuN3Eo9QwJRvtl4tmVe4T5A== - dependencies: - "@inquirer/ansi" "^2.0.3" - "@inquirer/figures" "^2.0.3" - "@inquirer/type" "^4.0.3" +"@inquirer/checkbox@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.2.tgz#8cc30b3f16625b1f29425ce68fd7b65d03759807" + integrity sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw== + dependencies: + "@inquirer/ansi" "^2.0.4" + "@inquirer/core" "^11.1.7" + "@inquirer/figures" "^2.0.4" + "@inquirer/type" "^4.0.4" + +"@inquirer/confirm@^6.0.10": + version "6.0.10" + resolved "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.10.tgz#96366b834273421233f3eae1a55b47cd19e75228" + integrity sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ== + dependencies: + "@inquirer/core" "^11.1.7" + "@inquirer/type" "^4.0.4" + +"@inquirer/core@^11.1.7": + version "11.1.7" + resolved "https://registry.npmjs.org/@inquirer/core/-/core-11.1.7.tgz#053041e54dc35d0043a9280d94f58da0e3a7b716" + integrity sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ== + dependencies: + "@inquirer/ansi" "^2.0.4" + "@inquirer/figures" "^2.0.4" + "@inquirer/type" "^4.0.4" cli-width "^4.1.0" fast-wrap-ansi "^0.2.0" mute-stream "^3.0.0" signal-exit "^4.1.0" -"@inquirer/editor@^5.0.8": - version "5.0.8" - resolved "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.8.tgz" - integrity sha512-sLcpbb9B3XqUEGrj1N66KwhDhEckzZ4nI/W6SvLXyBX8Wic3LDLENlWRvkOGpCPoserabe+MxQkpiMoI8irvyA== +"@inquirer/editor@^5.0.10": + version "5.0.10" + resolved "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.10.tgz#5e019f4b8e7f3049391b366074cf566c909f912f" + integrity sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA== dependencies: - "@inquirer/core" "^11.1.5" - "@inquirer/external-editor" "^2.0.3" - "@inquirer/type" "^4.0.3" + "@inquirer/core" "^11.1.7" + "@inquirer/external-editor" "^2.0.4" + "@inquirer/type" "^4.0.4" -"@inquirer/expand@^5.0.8": - version "5.0.8" - resolved "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.8.tgz" - integrity sha512-QieW3F1prNw3j+hxO7/NKkG1pk3oz7pOB6+5Upwu3OIwADfPX0oZVppsqlL+Vl/uBHHDSOBY0BirLctLnXwGGg== +"@inquirer/expand@^5.0.10": + version "5.0.10" + resolved "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.10.tgz#0c24970db9cf5ed3327ea0e9ce06e82103731992" + integrity sha512-fC0UHJPXsTRvY2fObiwuQYaAnHrp3aDqfwKUJSdfpgv18QUG054ezGbaRNStk/BKD5IPijeMKWej8VV8O5Q/eQ== dependencies: - "@inquirer/core" "^11.1.5" - "@inquirer/type" "^4.0.3" + "@inquirer/core" "^11.1.7" + "@inquirer/type" "^4.0.4" -"@inquirer/external-editor@^2.0.3": - version "2.0.3" - resolved "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.3.tgz" - integrity sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w== +"@inquirer/external-editor@^2.0.4": + version "2.0.4" + resolved "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.4.tgz#1178821c52014bf70bbadd664ee6fedc37a40b5c" + integrity sha512-Prenuv9C1PHj2Itx0BcAOVBTonz02Hc2Nd2DbU67PdGUaqn0nPCnV34oDyyoaZHnmfRxkpuhh/u51ThkrO+RdA== dependencies: chardet "^2.1.1" iconv-lite "^0.7.2" -"@inquirer/figures@^2.0.3": - version "2.0.3" - resolved "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.3.tgz" - integrity sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g== - -"@inquirer/input@^5.0.8": - version "5.0.8" - resolved "https://registry.npmjs.org/@inquirer/input/-/input-5.0.8.tgz" - integrity sha512-p0IJslw0AmedLEkOU+yrEX3Aj2RTpQq7ZOf8nc1DIhjzaxRWrrgeuE5Kyh39fVRgtcACaMXx/9WNo8+GjgBOfw== - dependencies: - "@inquirer/core" "^11.1.5" - "@inquirer/type" "^4.0.3" - -"@inquirer/number@^4.0.8": - version "4.0.8" - resolved "https://registry.npmjs.org/@inquirer/number/-/number-4.0.8.tgz" - integrity sha512-uGLiQah9A0F9UIvJBX52m0CnqtLaym0WpT9V4YZrjZ+YRDKZdwwoEPz06N6w8ChE2lrnsdyhY9sL+Y690Kh9gQ== - dependencies: - "@inquirer/core" "^11.1.5" - "@inquirer/type" "^4.0.3" - -"@inquirer/password@^5.0.8": - version "5.0.8" - resolved "https://registry.npmjs.org/@inquirer/password/-/password-5.0.8.tgz" - integrity sha512-zt1sF4lYLdvPqvmvHdmjOzuUUjuCQ897pdUCO8RbXMUDKXJTTyOQgtn23le+jwcb+MpHl3VAFvzIdxRAf6aPlA== - dependencies: - "@inquirer/ansi" "^2.0.3" - "@inquirer/core" "^11.1.5" - "@inquirer/type" "^4.0.3" - -"@inquirer/prompts@^8.3.0": - version "8.3.0" - resolved "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.3.0.tgz" - integrity sha512-JAj66kjdH/F1+B7LCigjARbwstt3SNUOSzMdjpsvwJmzunK88gJeXmcm95L9nw1KynvFVuY4SzXh/3Y0lvtgSg== - dependencies: - "@inquirer/checkbox" "^5.1.0" - "@inquirer/confirm" "^6.0.8" - "@inquirer/editor" "^5.0.8" - "@inquirer/expand" "^5.0.8" - "@inquirer/input" "^5.0.8" - "@inquirer/number" "^4.0.8" - "@inquirer/password" "^5.0.8" - "@inquirer/rawlist" "^5.2.4" - "@inquirer/search" "^4.1.4" - "@inquirer/select" "^5.1.0" - -"@inquirer/rawlist@^5.2.4": - version "5.2.4" - resolved "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.4.tgz" - integrity sha512-fTuJ5Cq9W286isLxwj6GGyfTjx1Zdk4qppVEPexFuA6yioCCXS4V1zfKroQqw7QdbDPN73xs2DiIAlo55+kBqg== - dependencies: - "@inquirer/core" "^11.1.5" - "@inquirer/type" "^4.0.3" - -"@inquirer/search@^4.1.4": - version "4.1.4" - resolved "https://registry.npmjs.org/@inquirer/search/-/search-4.1.4.tgz" - integrity sha512-9yPTxq7LPmYjrGn3DRuaPuPbmC6u3fiWcsE9ggfLcdgO/ICHYgxq7mEy1yJ39brVvgXhtOtvDVjDh9slJxE4LQ== - dependencies: - "@inquirer/core" "^11.1.5" - "@inquirer/figures" "^2.0.3" - "@inquirer/type" "^4.0.3" - -"@inquirer/select@^5.1.0": - version "5.1.0" - resolved "https://registry.npmjs.org/@inquirer/select/-/select-5.1.0.tgz" - integrity sha512-OyYbKnchS1u+zRe14LpYrN8S0wH1vD0p2yKISvSsJdH2TpI87fh4eZdWnpdbrGauCRWDph3NwxRmM4Pcm/hx1Q== +"@inquirer/figures@^2.0.4": + version "2.0.4" + resolved "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.4.tgz#154986941a00db8b8171d1ed0d1df566972ed173" + integrity sha512-eLBsjlS7rPS3WEhmOmh1znQ5IsQrxWzxWDxO51e4urv+iVrSnIHbq4zqJIOiyNdYLa+BVjwOtdetcQx1lWPpiQ== + +"@inquirer/input@^5.0.10": + version "5.0.10" + resolved "https://registry.npmjs.org/@inquirer/input/-/input-5.0.10.tgz#0a18347e8f16d4cb01e1d801f1ca8fcca26006dc" + integrity sha512-nvZ6qEVeX/zVtZ1dY2hTGDQpVGD3R7MYPLODPgKO8Y+RAqxkrP3i/3NwF3fZpLdaMiNuK0z2NaYIx9tPwiSegQ== + dependencies: + "@inquirer/core" "^11.1.7" + "@inquirer/type" "^4.0.4" + +"@inquirer/number@^4.0.10": + version "4.0.10" + resolved "https://registry.npmjs.org/@inquirer/number/-/number-4.0.10.tgz#e8d3a3f218c8795c0aade0cb1821df8a0f4682b4" + integrity sha512-Ht8OQstxiS3APMGjHV0aYAjRAysidWdwurWEo2i8yI5xbhOBWqizT0+MU1S2GCcuhIBg+3SgWVjEoXgfhY+XaA== + dependencies: + "@inquirer/core" "^11.1.7" + "@inquirer/type" "^4.0.4" + +"@inquirer/password@^5.0.10": + version "5.0.10" + resolved "https://registry.npmjs.org/@inquirer/password/-/password-5.0.10.tgz#53cc6613ac2cb18b018f83d0731c73783ba3c353" + integrity sha512-QbNyvIE8q2GTqKLYSsA8ATG+eETo+m31DSR0+AU7x3d2FhaTWzqQek80dj3JGTo743kQc6mhBR0erMjYw5jQ0A== + dependencies: + "@inquirer/ansi" "^2.0.4" + "@inquirer/core" "^11.1.7" + "@inquirer/type" "^4.0.4" + +"@inquirer/prompts@^8.3.2": + version "8.3.2" + resolved "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.3.2.tgz#7d2464b53011a5fbd5cc6f22b365a61c60104a2a" + integrity sha512-yFroiSj2iiBFlm59amdTvAcQFvWS6ph5oKESls/uqPBect7rTU2GbjyZO2DqxMGuIwVA8z0P4K6ViPcd/cp+0w== + dependencies: + "@inquirer/checkbox" "^5.1.2" + "@inquirer/confirm" "^6.0.10" + "@inquirer/editor" "^5.0.10" + "@inquirer/expand" "^5.0.10" + "@inquirer/input" "^5.0.10" + "@inquirer/number" "^4.0.10" + "@inquirer/password" "^5.0.10" + "@inquirer/rawlist" "^5.2.6" + "@inquirer/search" "^4.1.6" + "@inquirer/select" "^5.1.2" + +"@inquirer/rawlist@^5.2.6": + version "5.2.6" + resolved "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.6.tgz#fcc00c80e2d4597ba6010eb72e373690c6c82241" + integrity sha512-jfw0MLJ5TilNsa9zlJ6nmRM0ZFVZhhTICt4/6CU2Dv1ndY7l3sqqo1gIYZyMMDw0LvE1u1nzJNisfHEhJIxq5w== + dependencies: + "@inquirer/core" "^11.1.7" + "@inquirer/type" "^4.0.4" + +"@inquirer/search@^4.1.6": + version "4.1.6" + resolved "https://registry.npmjs.org/@inquirer/search/-/search-4.1.6.tgz#399b87074af1e7a2c8d6924fe6bd90b993f40f41" + integrity sha512-3/6kTRae98hhDevENScy7cdFEuURnSpM3JbBNg8yfXLw88HgTOl+neUuy/l9W0No5NzGsLVydhBzTIxZP7yChQ== + dependencies: + "@inquirer/core" "^11.1.7" + "@inquirer/figures" "^2.0.4" + "@inquirer/type" "^4.0.4" + +"@inquirer/select@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@inquirer/select/-/select-5.1.2.tgz#d40f6af6fe86dbdbd97587a76ba3101274fb5208" + integrity sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA== dependencies: - "@inquirer/ansi" "^2.0.3" - "@inquirer/core" "^11.1.5" - "@inquirer/figures" "^2.0.3" - "@inquirer/type" "^4.0.3" + "@inquirer/ansi" "^2.0.4" + "@inquirer/core" "^11.1.7" + "@inquirer/figures" "^2.0.4" + "@inquirer/type" "^4.0.4" -"@inquirer/type@^4.0.3": - version "4.0.3" - resolved "https://registry.npmjs.org/@inquirer/type/-/type-4.0.3.tgz" - integrity sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw== +"@inquirer/type@^4.0.4": + version "4.0.4" + resolved "https://registry.npmjs.org/@inquirer/type/-/type-4.0.4.tgz#ef66cf0ee6af7d240d5aa462dd7697be010a1837" + integrity sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA== "@jridgewell/sourcemap-codec@^1.5.5": version "1.5.5" @@ -472,100 +472,100 @@ "@types/configstore" "*" boxen "^7.1.1" -"@typescript-eslint/eslint-plugin@^8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz#6e4085604ab63f55b3dcc61ce2c16965b2c36374" - integrity sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ== +"@typescript-eslint/eslint-plugin@^8.57.1": + version "8.57.1" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz#ddfdfb30f8b5ccee7f3c21798b377c51370edd55" + integrity sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ== dependencies: "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.57.0" - "@typescript-eslint/type-utils" "8.57.0" - "@typescript-eslint/utils" "8.57.0" - "@typescript-eslint/visitor-keys" "8.57.0" + "@typescript-eslint/scope-manager" "8.57.1" + "@typescript-eslint/type-utils" "8.57.1" + "@typescript-eslint/utils" "8.57.1" + "@typescript-eslint/visitor-keys" "8.57.1" ignore "^7.0.5" natural-compare "^1.4.0" ts-api-utils "^2.4.0" -"@typescript-eslint/parser@^8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz#444c57a943e8b04f255cda18a94c8e023b46b08c" - integrity sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g== +"@typescript-eslint/parser@^8.57.1": + version "8.57.1" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz#d523e559b148264055c0a49a29d5f50c7de659c2" + integrity sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw== dependencies: - "@typescript-eslint/scope-manager" "8.57.0" - "@typescript-eslint/types" "8.57.0" - "@typescript-eslint/typescript-estree" "8.57.0" - "@typescript-eslint/visitor-keys" "8.57.0" + "@typescript-eslint/scope-manager" "8.57.1" + "@typescript-eslint/types" "8.57.1" + "@typescript-eslint/typescript-estree" "8.57.1" + "@typescript-eslint/visitor-keys" "8.57.1" debug "^4.4.3" -"@typescript-eslint/project-service@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz#2014ed527bcd0eff8aecb7e44879ae3150604ab3" - integrity sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w== +"@typescript-eslint/project-service@8.57.1": + version "8.57.1" + resolved "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz#16af9fe16eedbd7085e4fdc29baa73715c0c55c5" + integrity sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.57.0" - "@typescript-eslint/types" "^8.57.0" + "@typescript-eslint/tsconfig-utils" "^8.57.1" + "@typescript-eslint/types" "^8.57.1" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz#7d2a2aeaaef2ae70891b21939fadb4cb0b19f840" - integrity sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw== +"@typescript-eslint/scope-manager@8.57.1": + version "8.57.1" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz#4524d7e7b420cb501807499684d435ae129aaf35" + integrity sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg== dependencies: - "@typescript-eslint/types" "8.57.0" - "@typescript-eslint/visitor-keys" "8.57.0" + "@typescript-eslint/types" "8.57.1" + "@typescript-eslint/visitor-keys" "8.57.1" -"@typescript-eslint/tsconfig-utils@8.57.0", "@typescript-eslint/tsconfig-utils@^8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz#cf2f2822af3887d25dd325b6bea6c3f60a83a0b4" - integrity sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA== +"@typescript-eslint/tsconfig-utils@8.57.1", "@typescript-eslint/tsconfig-utils@^8.57.1": + version "8.57.1" + resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz#9233443ec716882a6f9e240fd900a73f0235f3d7" + integrity sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg== -"@typescript-eslint/type-utils@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz#2877af4c2e8f0998b93a07dad1c34ce1bb669448" - integrity sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ== +"@typescript-eslint/type-utils@8.57.1": + version "8.57.1" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz#c49af1347b5869ca85155547a8f34f84ab386fd9" + integrity sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA== dependencies: - "@typescript-eslint/types" "8.57.0" - "@typescript-eslint/typescript-estree" "8.57.0" - "@typescript-eslint/utils" "8.57.0" + "@typescript-eslint/types" "8.57.1" + "@typescript-eslint/typescript-estree" "8.57.1" + "@typescript-eslint/utils" "8.57.1" debug "^4.4.3" ts-api-utils "^2.4.0" -"@typescript-eslint/types@8.57.0", "@typescript-eslint/types@^8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz#4fa5385ffd1cd161fa5b9dce93e0493d491b8dc6" - integrity sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg== +"@typescript-eslint/types@8.57.1", "@typescript-eslint/types@^8.57.1": + version "8.57.1" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz#54b27a8a25a7b45b4f978c3f8e00c4c78f11142c" + integrity sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ== -"@typescript-eslint/typescript-estree@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz#e0e4a89bfebb207de314826df876e2dabc7dea04" - integrity sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q== +"@typescript-eslint/typescript-estree@8.57.1": + version "8.57.1" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz#a9fd28d4a0ec896aa9a9a7e0cead62ea24f99e76" + integrity sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g== dependencies: - "@typescript-eslint/project-service" "8.57.0" - "@typescript-eslint/tsconfig-utils" "8.57.0" - "@typescript-eslint/types" "8.57.0" - "@typescript-eslint/visitor-keys" "8.57.0" + "@typescript-eslint/project-service" "8.57.1" + "@typescript-eslint/tsconfig-utils" "8.57.1" + "@typescript-eslint/types" "8.57.1" + "@typescript-eslint/visitor-keys" "8.57.1" debug "^4.4.3" minimatch "^10.2.2" semver "^7.7.3" tinyglobby "^0.2.15" ts-api-utils "^2.4.0" -"@typescript-eslint/utils@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz#c7193385b44529b788210d20c94c11de79ad3498" - integrity sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ== +"@typescript-eslint/utils@8.57.1": + version "8.57.1" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz#e40f5a7fcff02fd24092a7b52bd6ec029fb50465" + integrity sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ== dependencies: "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.57.0" - "@typescript-eslint/types" "8.57.0" - "@typescript-eslint/typescript-estree" "8.57.0" + "@typescript-eslint/scope-manager" "8.57.1" + "@typescript-eslint/types" "8.57.1" + "@typescript-eslint/typescript-estree" "8.57.1" -"@typescript-eslint/visitor-keys@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz#23aea662279bb66209700854453807a119350f85" - integrity sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg== +"@typescript-eslint/visitor-keys@8.57.1": + version "8.57.1" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz#3af4f88118924d3be983d4b8ae84803f11fe4563" + integrity sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A== dependencies: - "@typescript-eslint/types" "8.57.0" + "@typescript-eslint/types" "8.57.1" eslint-visitor-keys "^5.0.0" "@vitest/expect@4.1.0": From 9f3dac2ff6842c94c982c4a946928caf076026cb Mon Sep 17 00:00:00 2001 From: Wellington Santana Date: Tue, 17 Mar 2026 19:36:12 -0300 Subject: [PATCH 3/4] chore: bump version to 0.4.10 Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0b8f500..0a2660e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kodus/cli", - "version": "0.4.9", + "version": "0.4.10", "description": "Kodus CLI - AI-powered code review from your terminal", "type": "module", "bin": { From 384cfb64d42d44c6dd93021c4e57e33f83d7a313 Mon Sep 17 00:00:00 2001 From: Wellington Santana Date: Tue, 17 Mar 2026 20:07:02 -0300 Subject: [PATCH 4/4] fix(api): remove duplicate logic in analyze by delegating to analyzeWithMetrics Co-Authored-By: Claude Opus 4.6 --- src/services/api/review.api.ts | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/src/services/api/review.api.ts b/src/services/api/review.api.ts index 744555f..679c876 100644 --- a/src/services/api/review.api.ts +++ b/src/services/api/review.api.ts @@ -21,39 +21,7 @@ export class RealReviewApi implements IReviewApi { accessToken: string, config?: ReviewConfig, ): Promise { - const isTeamKey = accessToken.startsWith('kodus_'); - - if (isTeamKey) { - return this.requester('/cli/review', { - method: 'POST', - headers: { - 'X-Team-Key': accessToken, - }, - body: JSON.stringify({ diff, config }), - }); - } - - let teamId: string | undefined; - try { - const payload = JSON.parse( - Buffer.from(accessToken.split('.')[1], 'base64').toString(), - ); - teamId = payload.organizationId; - } catch { - // Ignore if cannot decode - } - - const endpoint = teamId - ? `/cli/review?teamId=${encodeURIComponent(teamId)}` - : '/cli/review'; - - return this.requester(endpoint, { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ diff, config }), - }); + return this.analyzeWithMetrics(diff, accessToken, config); } async analyzeWithMetrics(