codeant-cli 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp/server.js CHANGED
@@ -13,7 +13,13 @@ import { runStartScan } from '../commands/scans/start-scan.js';
13
13
  import { runReviewHeadless } from '../reviewHeadless.js';
14
14
  import * as scm from '../scm/index.js';
15
15
  import { isAlreadyLoggedIn, runLoginFlow } from '../utils/loginFlow.js';
16
- import { getConfigValue, setConfigValue } from '../utils/config.js';
16
+ import { getConfigValue } from '../utils/config.js';
17
+ import { runHotlistGet, runHotlistList } from '../hotlist/client.js';
18
+ import { logoutCodeAnt } from '../utils/logout.js';
19
+ import { runApiRequest } from '../commands/api/request.js';
20
+ import { runOrganizationAntipatterns } from '../findings/antipatterns.js';
21
+ import { runCloudFindingGet, runCloudFindings, runCloudHistory } from '../findings/cloud.js';
22
+ import { runPentestHistory, runPentestIssues, runPentestReport } from '../findings/pentest.js';
17
23
 
18
24
  const require = createRequire(import.meta.url);
19
25
  const pkg = require('../../package.json');
@@ -215,6 +221,224 @@ export async function startMcpServer() {
215
221
  }
216
222
  );
217
223
 
224
+ // ─── Organization Hotlist findings (read-only) ──────────────────────────
225
+ server.registerTool(
226
+ 'codeant_hotlist_list',
227
+ {
228
+ title: 'List prioritized Hotlist findings',
229
+ description: 'Query the organization-wide Hotlist using the same stable IDs, ranking, filters, and pagination as the CodeAnt app. Use this for cross-repository security prioritization and agent triage.',
230
+ inputSchema: {
231
+ org: z.string().optional().describe('Organization name. Auto-picked when exactly one connection matches.'),
232
+ service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
233
+ providerBaseUrl: z.string().url().optional().describe('Override only for a self-hosted provider.'),
234
+ search: z.string().optional(),
235
+ types: z.array(z.string()).optional(),
236
+ locations: z.array(z.string()).optional(),
237
+ severities: z.array(z.enum(['critical', 'high', 'medium', 'low', 'unknown'])).optional(),
238
+ ticketStatuses: z.array(z.enum(['created', 'not_created'])).optional(),
239
+ compliance: z.array(z.string()).optional(),
240
+ validation: z.array(z.enum(['exploit_confirmed'])).optional(),
241
+ limit: z.number().int().positive().max(100).optional(),
242
+ cursor: z.string().optional(),
243
+ all: z.boolean().optional().describe('Fetch every matching page. Default false.'),
244
+ maxWaitSeconds: z.number().int().nonnegative().max(600).optional(),
245
+ },
246
+ annotations: READ,
247
+ },
248
+ async (input) => {
249
+ try { return ok(await runHotlistList(input)); } catch (err) { return fail(err); }
250
+ }
251
+ );
252
+
253
+ server.registerTool(
254
+ 'codeant_hotlist_get',
255
+ {
256
+ title: 'Get a Hotlist finding',
257
+ description: 'Fetch one complete Hotlist finding by its 32-character stable ID. Use the ID displayed in the app or returned by codeant_hotlist_list.',
258
+ inputSchema: {
259
+ findingId: z.string().regex(/^[0-9a-f]{32}$/i),
260
+ org: z.string().optional(),
261
+ service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
262
+ providerBaseUrl: z.string().url().optional(),
263
+ maxWaitSeconds: z.number().int().nonnegative().max(600).optional(),
264
+ },
265
+ annotations: READ,
266
+ },
267
+ async (input) => {
268
+ try { return ok(await runHotlistGet(input)); } catch (err) { return fail(err); }
269
+ }
270
+ );
271
+
272
+ server.registerTool(
273
+ 'codeant_findings_antipatterns',
274
+ {
275
+ title: 'List organization anti-pattern findings',
276
+ description: 'Fetch anti-pattern findings across selected repositories, or every repository in the organization when repos is omitted.',
277
+ inputSchema: {
278
+ org: z.string().optional(),
279
+ service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
280
+ providerBaseUrl: z.string().url().optional(),
281
+ repos: z.array(z.string()).optional().describe('Repositories in owner/repo form. Omit to query every repository.'),
282
+ },
283
+ annotations: READ,
284
+ },
285
+ async (input) => {
286
+ try { return ok(await runOrganizationAntipatterns(input)); } catch (err) { return fail(err); }
287
+ }
288
+ );
289
+
290
+ server.registerTool(
291
+ 'codeant_cloud_scan_history',
292
+ {
293
+ title: 'List cloud security scan history',
294
+ description: 'List AWS, Azure, or GCP CSPM, VM, or container scans visible in the CodeAnt Cloud Security UI. Cloud findings are organization/account scoped, not repository scoped.',
295
+ inputSchema: {
296
+ org: z.string().optional(),
297
+ service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
298
+ providerBaseUrl: z.string().url().optional(),
299
+ provider: z.enum(['aws', 'azure', 'gcp', 'all']).optional().describe('Default all.'),
300
+ kind: z.enum(['cspm', 'vm', 'container']).optional().describe('Default cspm.'),
301
+ latest: z.boolean().optional().describe('Return latest scans instead of complete history. CSPM only.'),
302
+ },
303
+ annotations: READ,
304
+ },
305
+ async (input) => {
306
+ try { return ok(await runCloudHistory(input)); } catch (err) { return fail(err); }
307
+ }
308
+ );
309
+
310
+ server.registerTool(
311
+ 'codeant_cloud_findings_list',
312
+ {
313
+ title: 'List cloud security findings',
314
+ description: 'Fetch findings for one AWS, Azure, or GCP CSPM, VM, or container scan using the same endpoint as the app.',
315
+ inputSchema: {
316
+ org: z.string().optional(),
317
+ service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
318
+ providerBaseUrl: z.string().url().optional(),
319
+ provider: z.enum(['aws', 'azure', 'gcp']),
320
+ kind: z.enum(['cspm', 'vm', 'container']).optional().describe('Default cspm.'),
321
+ scanId: z.string(),
322
+ accountId: z.string().optional().describe('Optional AWS account ID.'),
323
+ tenantId: z.string().optional().describe('Required for Azure.'),
324
+ projectId: z.string().optional().describe('Required for GCP.'),
325
+ cloudService: z.string().optional(),
326
+ severity: z.string().optional(),
327
+ status: z.string().optional(),
328
+ framework: z.string().optional(),
329
+ subscriptionId: z.string().optional(),
330
+ exploitAttemptedOnly: z.boolean().optional(),
331
+ minDaysUnused: z.number().int().nonnegative().optional(),
332
+ },
333
+ annotations: READ,
334
+ },
335
+ async (input) => {
336
+ try { return ok(await runCloudFindings(input)); } catch (err) { return fail(err); }
337
+ }
338
+ );
339
+
340
+ server.registerTool(
341
+ 'codeant_cloud_finding_get',
342
+ {
343
+ title: 'Get cloud security finding detail',
344
+ description: 'Fetch complete detail for one CSPM, VM, or container finding UID.',
345
+ inputSchema: {
346
+ org: z.string().optional(),
347
+ service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
348
+ providerBaseUrl: z.string().url().optional(),
349
+ provider: z.enum(['aws', 'azure', 'gcp']),
350
+ kind: z.enum(['cspm', 'vm', 'container']).optional().describe('Default cspm.'),
351
+ scanId: z.string(),
352
+ uid: z.string(),
353
+ accountId: z.string().optional(),
354
+ tenantId: z.string().optional().describe('Required for Azure.'),
355
+ projectId: z.string().optional().describe('Required for GCP.'),
356
+ cloudService: z.string().optional(),
357
+ },
358
+ annotations: READ,
359
+ },
360
+ async (input) => {
361
+ try { return ok(await runCloudFindingGet(input)); } catch (err) { return fail(err); }
362
+ }
363
+ );
364
+
365
+ server.registerTool(
366
+ 'codeant_pentest_history',
367
+ {
368
+ title: 'List pentest engagements',
369
+ description: 'List every pentest engagement visible in the CodeAnt Pentesting UI, including status and finding counts.',
370
+ inputSchema: {
371
+ org: z.string().optional(),
372
+ service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
373
+ providerBaseUrl: z.string().url().optional(),
374
+ },
375
+ annotations: READ,
376
+ },
377
+ async (input) => {
378
+ try { return ok(await runPentestHistory(input)); } catch (err) { return fail(err); }
379
+ }
380
+ );
381
+
382
+ server.registerTool(
383
+ 'codeant_pentest_issues',
384
+ {
385
+ title: 'List pentest issues',
386
+ description: 'Fetch all available open issues for one pentest engagement. The backend applies the same entitlement redaction as the UI.',
387
+ inputSchema: {
388
+ org: z.string().optional(),
389
+ service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
390
+ providerBaseUrl: z.string().url().optional(),
391
+ reportId: z.string(),
392
+ variant: z.enum(['prod', 'test']).optional(),
393
+ },
394
+ annotations: READ,
395
+ },
396
+ async (input) => {
397
+ try { return ok(await runPentestIssues(input)); } catch (err) { return fail(err); }
398
+ }
399
+ );
400
+
401
+ server.registerTool(
402
+ 'codeant_pentest_report',
403
+ {
404
+ title: 'Get pentest report',
405
+ description: 'Fetch the full customer report for one pentest engagement. The backend applies the same entitlement redaction as the UI.',
406
+ inputSchema: {
407
+ org: z.string().optional(),
408
+ service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
409
+ providerBaseUrl: z.string().url().optional(),
410
+ reportId: z.string(),
411
+ variant: z.enum(['prod', 'test']).optional(),
412
+ },
413
+ annotations: READ,
414
+ },
415
+ async (input) => {
416
+ try { return ok(await runPentestReport(input)); } catch (err) { return fail(err); }
417
+ }
418
+ );
419
+
420
+ // Generic GET keeps newly-added read APIs available without a CLI release.
421
+ // Non-GET requests are registered below only when write mode is enabled.
422
+ server.registerTool(
423
+ 'codeant_api_get',
424
+ {
425
+ title: 'Call a CodeAnt GET API',
426
+ description: 'Call any authenticated GET endpoint on the configured CodeAnt API host. The path must be relative (for example /extension/scans2/validate); absolute URLs are rejected.',
427
+ inputSchema: {
428
+ path: z.string().startsWith('/'),
429
+ org: z.string().optional().describe('Organization name. Required when the login has multiple matching connections.'),
430
+ service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
431
+ providerBaseUrl: z.string().url().optional().describe('Override only for a self-hosted provider.'),
432
+ query: z.record(z.union([z.string(), z.number(), z.boolean(), z.array(z.string())])).optional(),
433
+ headers: z.array(z.string()).optional().describe('Optional repeatable "Name: value" headers. Authorization cannot be overridden.'),
434
+ },
435
+ annotations: READ,
436
+ },
437
+ async (input) => {
438
+ try { return ok(await runApiRequest({ method: 'GET', ...input })); } catch (err) { return fail(err); }
439
+ }
440
+ );
441
+
218
442
  // ─── Pull requests (SCM, read-only) ──────────────────────────────────────
219
443
  server.registerTool(
220
444
  'codeant_pr_list',
@@ -419,16 +643,39 @@ export async function startMcpServer() {
419
643
  },
420
644
  async () => {
421
645
  try {
422
- const wasLoggedIn = !!(process.env.CODEANT_API_TOKEN?.trim() || getConfigValue('apiKeyV2'));
423
- setConfigValue('apiKeyV2', null);
424
- delete process.env.CODEANT_API_TOKEN;
425
- return ok({ wasLoggedIn, status: wasLoggedIn ? 'logged_out' : 'not_logged_in' });
646
+ const result = await logoutCodeAnt();
647
+ return ok({
648
+ ...result,
649
+ status: result.wasLoggedIn ? 'logged_out' : 'not_logged_in',
650
+ });
426
651
  } catch (err) { return fail(err); }
427
652
  }
428
653
  );
429
654
 
430
655
  // ─── Write-side tools (gated behind CODEANT_READ_ONLY=0) ─────────────────
431
656
  if (!readOnly) {
657
+ server.registerTool(
658
+ 'codeant_api_request',
659
+ {
660
+ title: 'Call a CodeAnt write API',
661
+ description: 'Call an authenticated POST, PUT, PATCH, or DELETE endpoint on the configured CodeAnt API host. WRITE OPERATION — only enabled when CODEANT_READ_ONLY=0. Absolute URLs are rejected.',
662
+ inputSchema: {
663
+ method: z.enum(['POST', 'PUT', 'PATCH', 'DELETE']),
664
+ path: z.string().startsWith('/'),
665
+ org: z.string().optional().describe('Organization name. Required when the login has multiple matching connections.'),
666
+ service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
667
+ providerBaseUrl: z.string().url().optional().describe('Override only for a self-hosted provider.'),
668
+ query: z.record(z.union([z.string(), z.number(), z.boolean(), z.array(z.string())])).optional(),
669
+ body: z.unknown().optional(),
670
+ headers: z.array(z.string()).optional(),
671
+ },
672
+ annotations: WRITE_NON_DESTRUCTIVE,
673
+ },
674
+ async (input) => {
675
+ try { return ok(await runApiRequest(input)); } catch (err) { return fail(err); }
676
+ }
677
+ );
678
+
432
679
  server.registerTool(
433
680
  'codeant_scans_start',
434
681
  {
@@ -23,13 +23,61 @@ const RETRYABLE_CAUSES = new Set([
23
23
 
24
24
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
25
25
 
26
- const fetchApi = async (endpoint, method = 'GET', body = null) => {
27
- const url = endpoint.startsWith('http') ? endpoint : `${getBaseUrl()}${endpoint}`;
26
+ function responseMessage(data, status) {
27
+ if (typeof data?.error?.message === 'string') return data.error.message;
28
+ if (typeof data?.message === 'string') return data.message;
29
+ if (typeof data?.error === 'string') return data.error;
30
+ return `HTTP error ${status}`;
31
+ }
32
+
33
+ function appendQuery(url, query) {
34
+ if (!query || typeof query !== 'object') return url;
35
+ const parsed = new URL(url);
36
+ for (const [key, value] of Object.entries(query)) {
37
+ if (value === undefined || value === null) continue;
38
+ if (Array.isArray(value)) {
39
+ for (const item of value) parsed.searchParams.append(key, String(item));
40
+ } else {
41
+ parsed.searchParams.set(key, String(value));
42
+ }
43
+ }
44
+ return parsed.toString();
45
+ }
46
+
47
+ async function parseResponse(response) {
48
+ if (response.status === 204 || response.status === 205) return null;
49
+ const text = await response.text();
50
+ if (!text) return null;
51
+ const contentType = response.headers.get('content-type') || '';
52
+ if (contentType.includes('json') || /^[\s]*[\[{]/.test(text)) {
53
+ try {
54
+ return JSON.parse(text);
55
+ } catch {
56
+ // A mislabeled response should still be inspectable by the caller.
57
+ }
58
+ }
59
+ return text;
60
+ }
61
+
62
+ const fetchApiResponse = async (endpoint, {
63
+ method = 'GET',
64
+ body = null,
65
+ headers = {},
66
+ query = null,
67
+ allowHttpError = false,
68
+ tenant = null,
69
+ signal = undefined,
70
+ } = {}) => {
71
+ const baseUrl = getBaseUrl();
72
+ const resolvedUrl = endpoint.startsWith('http') ? endpoint : `${baseUrl}${endpoint}`;
73
+ const url = appendQuery(resolvedUrl, query);
74
+ const normalizedMethod = String(method || 'GET').toUpperCase();
28
75
 
29
76
  const options = {
30
- method,
77
+ method: normalizedMethod,
31
78
  headers: {
32
79
  'Content-Type': 'application/json',
80
+ ...headers,
33
81
  },
34
82
  };
35
83
 
@@ -38,10 +86,18 @@ const fetchApi = async (endpoint, method = 'GET', body = null) => {
38
86
  if (token) {
39
87
  options.headers['Authorization'] = `Bearer ${token}`;
40
88
  }
89
+ if (tenant) {
90
+ options.headers['X-CodeAnt-CLI-Org'] = tenant.organization;
91
+ options.headers['X-CodeAnt-CLI-Service'] = tenant.service;
92
+ options.headers['X-CodeAnt-CLI-Base-URL'] = tenant.providerBaseUrl;
93
+ }
41
94
 
42
- if (body && method !== 'GET') {
95
+ if (body !== null && body !== undefined && !['GET', 'HEAD'].includes(normalizedMethod)) {
43
96
  options.body = JSON.stringify(body);
44
97
  }
98
+ if (signal) {
99
+ options.signal = signal;
100
+ }
45
101
 
46
102
  // Retry transient network/cold-start failures up to 2 times.
47
103
  const MAX_ATTEMPTS = 3;
@@ -55,13 +111,21 @@ const fetchApi = async (endpoint, method = 'GET', body = null) => {
55
111
  throw new Error('Access denied (403). Please run `codeant logout` and then `codeant login` to re-authenticate.');
56
112
  }
57
113
 
58
- const data = await response.json();
114
+ const data = await parseResponse(response);
59
115
 
60
- if (!response.ok) {
61
- throw new Error(data.message || `HTTP error ${response.status}`);
116
+ if (!response.ok && !allowHttpError) {
117
+ const error = new Error(responseMessage(data, response.status));
118
+ error.status = response.status;
119
+ error.data = data;
120
+ throw error;
62
121
  }
63
122
 
64
- return data;
123
+ return {
124
+ ok: response.ok,
125
+ status: response.status,
126
+ headers: Object.fromEntries(response.headers.entries()),
127
+ data,
128
+ };
65
129
  } catch (err) {
66
130
  lastErr = err;
67
131
  const cause = err?.cause?.code || err?.cause?.message || err?.cause || '';
@@ -78,4 +142,14 @@ const fetchApi = async (endpoint, method = 'GET', body = null) => {
78
142
  throw lastErr;
79
143
  };
80
144
 
81
- export { fetchApi };
145
+ const fetchApi = async (endpoint, method = 'GET', body = null) => {
146
+ const response = await fetchApiResponse(endpoint, { method, body });
147
+ return response.data;
148
+ };
149
+
150
+ const fetchAppApi = async (endpoint, method = 'GET', body = null, tenant) => {
151
+ const response = await fetchApiResponse(endpoint, { method, body, tenant });
152
+ return response.data;
153
+ };
154
+
155
+ export { fetchApi, fetchApiResponse, fetchAppApi };
@@ -6,7 +6,7 @@ const HOOK_MARKER = '# codeant-push-protection';
6
6
  const HOOK_MARKER_END = '# end-codeant-push-protection';
7
7
 
8
8
  /**
9
- * Build the full pre-push hook script (with shebang).
9
+ * Build the full pre-commit hook script (with shebang).
10
10
  */
11
11
  function buildHookScript(cliPath) {
12
12
  return `#!/bin/sh
@@ -21,27 +21,28 @@ ${buildHookBlock(cliPath)}
21
21
  */
22
22
  function buildHookBlock(cliPath) {
23
23
  if (cliPath) {
24
+ const quotedCliPath = `'${cliPath.replace(/'/g, "'\\''")}'`;
24
25
  return `${HOOK_MARKER}
25
- # Auto-installed by CodeAnt AI — blocks pushes containing secrets.
26
+ # Auto-installed by CodeAnt AI — blocks commits containing secrets.
26
27
  # To disable: delete this hook or run "codeant push-protection disable"
27
28
  # Uses the CLI bundled with the VS Code extension.
28
29
  # Reopen stdin from terminal so the CLI can show an interactive bypass prompt.
29
- # In non-interactive environments (CI), this silently fails and the push is blocked.
30
+ # In non-interactive environments (CI), this silently fails and the commit is blocked.
30
31
  exec < /dev/tty 2>/dev/null || true
31
- if [ -f "${cliPath}" ] && command -v node >/dev/null 2>&1; then
32
- node "${cliPath}" secrets --committed --hook
32
+ if [ -f ${quotedCliPath} ] && command -v node >/dev/null 2>&1; then
33
+ node ${quotedCliPath} secrets --staged --hook
33
34
  else
34
35
  command -v codeant >/dev/null 2>&1 || exit 0
35
- codeant secrets --committed --hook
36
+ codeant secrets --staged --hook
36
37
  fi
37
38
  ${HOOK_MARKER_END}`;
38
39
  }
39
40
  return `${HOOK_MARKER}
40
- # Auto-installed by CodeAnt AI — blocks pushes containing secrets.
41
+ # Auto-installed by CodeAnt AI — blocks commits containing secrets.
41
42
  # To disable: delete this hook or run "codeant push-protection disable"
42
43
  exec < /dev/tty 2>/dev/null || true
43
44
  command -v codeant >/dev/null 2>&1 || exit 0
44
- codeant secrets --committed --hook
45
+ codeant secrets --staged --hook
45
46
  ${HOOK_MARKER_END}`;
46
47
  }
47
48
 
@@ -97,7 +98,7 @@ function getHooksDir(gitRoot) {
97
98
  }
98
99
 
99
100
  /**
100
- * Install a pre-push hook that runs secret scanning before push.
101
+ * Install a pre-commit hook that runs secret scanning before commit.
101
102
  *
102
103
  * @param {string} workspacePath - Path to the git repository
103
104
  * @param {string} [cliPath] - Absolute path to the codeant CLI entry point (from extension node_modules)
@@ -113,7 +114,7 @@ export function installPushProtectionHook(workspacePath, cliPath) {
113
114
  if (!existsSync(hooksDir)) {
114
115
  mkdirSync(hooksDir, { recursive: true });
115
116
  }
116
- const hookPath = path.join(hooksDir, 'pre-push');
117
+ const hookPath = path.join(hooksDir, 'pre-commit');
117
118
 
118
119
  // If hook already exists, check if it's ours
119
120
  if (existsSync(hookPath)) {
@@ -127,14 +128,14 @@ export function installPushProtectionHook(workspacePath, cliPath) {
127
128
  }
128
129
  // There's a user-managed hook — append only for shell hooks to avoid breaking non-shell scripts
129
130
  const firstLine = existing.split('\n', 1)[0] || '';
130
- const isShellHook = firstLine.startsWith('#!') ? /\/(ba|z|k)?sh(\s|$)/.test(firstLine) : true;
131
+ const isShellHook = firstLine.startsWith('#!') ? /^#!\s*(?:\/\S*\/|\/usr\/bin\/env\s+(?:-S\s+)?)(ba|z|k)?sh(\s|$)/.test(firstLine) : true;
131
132
  if (!isShellHook) {
132
- return { installed: false, hookPath, message: 'Existing pre-push hook is non-shell; cannot append CodeAnt block safely' };
133
+ return { installed: false, hookPath, message: 'Existing pre-commit hook is non-shell; cannot append CodeAnt block safely' };
133
134
  }
134
135
  const appended = existing.trimEnd() + '\n\n' + buildHookBlock(cliPath) + '\n';
135
136
  writeFileSync(hookPath, appended, 'utf-8');
136
137
  chmodSync(hookPath, 0o755);
137
- return { installed: true, hookPath, message: 'Hook appended to existing pre-push' };
138
+ return { installed: true, hookPath, message: 'Hook appended to existing pre-commit' };
138
139
  }
139
140
 
140
141
  writeFileSync(hookPath, buildHookScript(cliPath), 'utf-8');
@@ -143,7 +144,7 @@ export function installPushProtectionHook(workspacePath, cliPath) {
143
144
  }
144
145
 
145
146
  /**
146
- * Remove the CodeAnt pre-push hook (or just our section if appended).
147
+ * Remove the CodeAnt pre-commit hook (or just our section if appended).
147
148
  *
148
149
  * @param {string} workspacePath
149
150
  * @returns {{ removed: boolean, message: string }}
@@ -155,10 +156,10 @@ export function removePushProtectionHook(workspacePath) {
155
156
  }
156
157
 
157
158
  const hooksDir = getHooksDir(gitRoot);
158
- const hookPath = path.join(hooksDir, 'pre-push');
159
+ const hookPath = path.join(hooksDir, 'pre-commit');
159
160
 
160
161
  if (!existsSync(hookPath)) {
161
- return { removed: false, message: 'No pre-push hook found' };
162
+ return { removed: false, message: 'No pre-commit hook found' };
162
163
  }
163
164
 
164
165
  const content = readFileSync(hookPath, 'utf-8');
@@ -0,0 +1,31 @@
1
+ import { getConfigValue, setConfigValue } from './config.js';
2
+ import { fetchApiResponse } from './fetchApi.js';
3
+
4
+ const DEFAULT_LOGOUT_REVOCATION_TIMEOUT_MS = 3_000;
5
+
6
+ export async function logoutCodeAnt({
7
+ revocationTimeoutMs = DEFAULT_LOGOUT_REVOCATION_TIMEOUT_MS,
8
+ } = {}) {
9
+ const token = process.env.CODEANT_API_TOKEN?.trim() || getConfigValue('apiKeyV2');
10
+ if (!token) return { wasLoggedIn: false, serverRevoked: false };
11
+
12
+ const controller = new AbortController();
13
+ const timeout = setTimeout(() => controller.abort(), revocationTimeoutMs);
14
+ let serverRevoked = false;
15
+ let warning;
16
+ try {
17
+ await fetchApiResponse('/extension/logout', {
18
+ method: 'POST',
19
+ body: {},
20
+ signal: controller.signal,
21
+ });
22
+ serverRevoked = true;
23
+ } catch (error) {
24
+ warning = `The local token was cleared, but server revocation could not be confirmed: ${error.message}`;
25
+ } finally {
26
+ clearTimeout(timeout);
27
+ setConfigValue('apiKeyV2', null);
28
+ delete process.env.CODEANT_API_TOKEN;
29
+ }
30
+ return { wasLoggedIn: true, serverRevoked, warning };
31
+ }