codeant-cli 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeant-cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,6 +41,7 @@
41
41
  "ink": "^5.0.1",
42
42
  "minimatch": "^10.1.1",
43
43
  "open": "^10.1.0",
44
+ "posthog-node": "^5.28.5",
44
45
  "react": "^18.3.1"
45
46
  },
46
47
  "devDependencies": {
@@ -6,6 +6,7 @@ import { fetchApi } from '../utils/fetchApi.js';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
8
  import { executeTool } from '../tools/executeTool.js';
9
+ import { track, shutdown as analyticsShutdown } from '../utils/analytics.js';
9
10
  import {
10
11
  renderInitializing,
11
12
  renderFetchingDiff,
@@ -51,6 +52,9 @@ export default function Review({ scanType = 'all', lastNCommits = 1, failOn = 'C
51
52
  let cancelled = false;
52
53
 
53
54
  async function runReview() {
55
+ const reviewStartTime = Date.now();
56
+ track('review_triggered', { scan_type: scanType, source: 'cli_interactive' });
57
+
54
58
  try {
55
59
  // ── Fetch diff ─────────────────────────────────────────────────────
56
60
  setStatus('fetching_diff');
@@ -140,9 +144,24 @@ export default function Review({ scanType = 'all', lastNCommits = 1, failOn = 'C
140
144
  setSummary(null);
141
145
  setStatus('done');
142
146
 
147
+ const labelCounts = {};
148
+ for (const i of reviewIssues) { labelCounts[i.label] = (labelCounts[i.label] || 0) + 1; }
149
+ track('review_completed', {
150
+ scan_type: scanType,
151
+ source: 'cli_interactive',
152
+ issue_count: reviewIssues.length,
153
+ file_count: perFileRequests.length,
154
+ duration_ms: Date.now() - reviewStartTime,
155
+ label_counts: labelCounts,
156
+ });
157
+ await analyticsShutdown();
158
+
143
159
  } catch (err) {
144
160
  if (!cancelled) {
145
- setError(err.message);
161
+ const errMsg = err instanceof Error ? err.message : String(err);
162
+ track('review_error', { scan_type: scanType, source: 'cli_interactive', error: errMsg });
163
+ await analyticsShutdown();
164
+ setError(errMsg);
146
165
  setStatus('error');
147
166
  }
148
167
  }
package/src/index.js CHANGED
@@ -13,6 +13,7 @@ import Review from './commands/review.js';
13
13
  import Welcome from './components/Welcome.js';
14
14
  import * as scm from './scm/index.js';
15
15
  import { setConfigValue } from './utils/config.js';
16
+ import { track, shutdown as analyticsShutdown } from './utils/analytics.js';
16
17
 
17
18
  // Read version from package.json — createRequire works with ncc bundling
18
19
  const require = createRequire(import.meta.url);
@@ -324,5 +325,25 @@ program
324
325
  }));
325
326
  });
326
327
 
328
+ // ─── Analytics tracking (for external callers like Claude Code skills) ───
329
+ program
330
+ .command('track')
331
+ .description('Send an analytics event')
332
+ .requiredOption('--event <name>', 'Event name (e.g. skill_invoked, suggestions_applied)')
333
+ .option('--props <json>', 'JSON string of event properties', '{}')
334
+ .action(async (options) => {
335
+ try {
336
+ const props = JSON.parse(options.props);
337
+ if (typeof props !== 'object' || props === null || Array.isArray(props)) {
338
+ process.stderr.write('--props must be a JSON object\n');
339
+ return;
340
+ }
341
+ track(options.event, props);
342
+ await analyticsShutdown();
343
+ } catch (err) {
344
+ process.stderr.write(`Invalid --props JSON: ${err instanceof Error ? err.message : String(err)}\n`);
345
+ }
346
+ });
347
+
327
348
  program.parse();
328
349
  }
@@ -1,6 +1,8 @@
1
1
  import ReviewApiHelper from './utils/reviewApiHelper.js';
2
2
  import { fetchApi } from './utils/fetchApi.js';
3
3
  import { executeTool } from './tools/executeTool.js';
4
+ import { getConfigValue, setConfigValue } from './utils/config.js';
5
+ import { track } from './utils/analytics.js';
4
6
 
5
7
  const MAX_TURNS = 5;
6
8
 
@@ -84,6 +86,11 @@ export async function runReviewHeadless(options = {}) {
84
86
  onFilesReady = () => {},
85
87
  } = options;
86
88
 
89
+ // If the CLI config doesn't have an apiKey but the extension passed one, persist it
90
+ if (apiKey && !getConfigValue('apiKey')) {
91
+ setConfigValue('apiKey', apiKey);
92
+ }
93
+
87
94
  // Temporarily set env vars so fetchApi picks them up
88
95
  const prevToken = process.env.CODEANT_API_TOKEN;
89
96
  const prevUrl = process.env.CODEANT_API_URL;
@@ -91,6 +98,9 @@ export async function runReviewHeadless(options = {}) {
91
98
  if (apiKey) process.env.CODEANT_API_TOKEN = apiKey;
92
99
  if (baseUrl) process.env.CODEANT_API_URL = baseUrl;
93
100
 
101
+ const reviewStartTime = Date.now();
102
+ track('review_triggered', { scan_type: scanType, source: 'headless' });
103
+
94
104
  try {
95
105
  // ── Fetch diff ──────────────────────────────────────────────────────
96
106
  onProgress('Fetching diff...');
@@ -107,6 +117,7 @@ export async function runReviewHeadless(options = {}) {
107
117
  onFilesReady(meta?.reviewed_files || [], meta);
108
118
 
109
119
  if (!requestBody?.diff_content?.length) {
120
+ track('review_completed', { scan_type: scanType, source: 'headless', issue_count: 0, file_count: 0, duration_ms: Date.now() - reviewStartTime, no_files: true });
110
121
  return { issues: [], meta, error: null, noFiles: true };
111
122
  }
112
123
 
@@ -157,9 +168,19 @@ export async function runReviewHeadless(options = {}) {
157
168
  label: issue.label || 'Code Quality',
158
169
  }));
159
170
 
171
+ const labelCounts = {};
172
+ for (const i of issues) { labelCounts[i.label] = (labelCounts[i.label] || 0) + 1; }
173
+ track('review_completed', {
174
+ scan_type: scanType, source: 'headless', issue_count: issues.length,
175
+ file_count: perFileRequests.length, duration_ms: Date.now() - reviewStartTime,
176
+ label_counts: labelCounts,
177
+ });
178
+
160
179
  return { issues, meta, error: null, noFiles: false };
161
180
  } catch (err) {
162
- return { issues: [], meta: null, error: err.message, noFiles: false };
181
+ const errMsg = err instanceof Error ? err.message : String(err);
182
+ track('review_error', { scan_type: scanType, source: 'headless', error: errMsg });
183
+ return { issues: [], meta: null, error: errMsg, noFiles: false };
163
184
  } finally {
164
185
  // Restore env vars
165
186
  if (prevToken !== undefined) process.env.CODEANT_API_TOKEN = prevToken;
@@ -0,0 +1,45 @@
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
+ function getClient() {
10
+ if (!client) {
11
+ client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST, flushAt: 1, flushInterval: 0 });
12
+ }
13
+ return client;
14
+ }
15
+
16
+ /** Use the API key directly as distinct ID */
17
+ function getDistinctId() {
18
+ return process.env.CODEANT_API_TOKEN || getConfigValue('apiKey') || 'anonymous';
19
+ }
20
+
21
+ /**
22
+ * Track an analytics event. Fire-and-forget — never throws or blocks the CLI.
23
+ * @param {string} event - Event name (e.g. 'review_triggered')
24
+ * @param {Object} [properties={}] - Event properties
25
+ */
26
+ export function track(event, properties = {}) {
27
+ try {
28
+ getClient().capture({
29
+ distinctId: getDistinctId(),
30
+ event,
31
+ properties: { source: 'cli', cli_version: process.env.npm_package_version || 'unknown', ...properties },
32
+ });
33
+ } catch (_) {
34
+ // Analytics should never break the CLI
35
+ }
36
+ }
37
+
38
+ /** Flush pending events. Call before process exits. */
39
+ export async function shutdown() {
40
+ try {
41
+ if (client) await client.shutdown();
42
+ } catch (_) {
43
+ // noop
44
+ }
45
+ }