ai-lens 0.3.0 → 0.5.0

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/.commithash CHANGED
@@ -1 +1 @@
1
- b46a47e
1
+ 968235e
package/bin/ai-lens.js CHANGED
@@ -30,28 +30,12 @@ switch (command) {
30
30
  console.log(`ai-lens v${version} (${commit})`);
31
31
  break;
32
32
  }
33
- case 'admin': {
34
- const sub = process.argv[3];
35
- const { createInvite, listInvites } = await import('../cli/admin.js');
36
- if (sub === 'create-invite') await createInvite();
37
- else if (sub === 'list-invites') await listInvites();
38
- else {
39
- console.log('Usage: ai-lens admin <command>');
40
- console.log('');
41
- console.log('Commands:');
42
- console.log(' create-invite Create a team invite token');
43
- console.log(' list-invites List invite tokens');
44
- process.exit(1);
45
- }
46
- break;
47
- }
48
33
  default:
49
34
  console.log('Usage: ai-lens <command>');
50
35
  console.log('');
51
36
  console.log('Commands:');
52
37
  console.log(' init Configure AI tool hooks for event capture');
53
38
  console.log(' remove Remove AI Lens hooks and client files');
54
- console.log(' admin Admin commands (create-invite, list-invites)');
55
39
  console.log(' mcp Start the MCP server (stdio transport)');
56
40
  console.log(' version Show package version and commit hash');
57
41
  process.exit(command ? 1 : 0);
package/cli/init.js CHANGED
@@ -25,8 +25,40 @@ function ask(question) {
25
25
  });
26
26
  }
27
27
 
28
- // Default transport credentials (same as client/config.js DEFAULT_AUTH_TOKEN)
29
- const TRANSPORT_AUTH = 'collector:secret-collector-token-2026-ai-lens';
28
+ function getJson(url) {
29
+ return new Promise((resolve, reject) => {
30
+ const parsed = new URL(url);
31
+ const isHttps = parsed.protocol === 'https:';
32
+ const requestFn = isHttps ? httpsRequest : httpRequest;
33
+ const options = {
34
+ hostname: parsed.hostname,
35
+ port: parsed.port || (isHttps ? 443 : 80),
36
+ path: parsed.pathname + (parsed.search || ''),
37
+ method: 'GET',
38
+ headers: { 'Accept': 'application/json' },
39
+ timeout: 15_000,
40
+ };
41
+ const req = requestFn(options, (res) => {
42
+ let buf = '';
43
+ res.on('data', (chunk) => { buf += chunk; });
44
+ res.on('end', () => {
45
+ try {
46
+ const json = JSON.parse(buf);
47
+ if (res.statusCode >= 200 && res.statusCode < 300) {
48
+ resolve(json);
49
+ } else {
50
+ reject(new Error(json.error || `Server responded ${res.statusCode}`));
51
+ }
52
+ } catch {
53
+ reject(new Error(`Server responded ${res.statusCode}: ${buf}`));
54
+ }
55
+ });
56
+ });
57
+ req.on('error', reject);
58
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
59
+ req.end();
60
+ });
61
+ }
30
62
 
31
63
  function postJson(url, body) {
32
64
  return new Promise((resolve, reject) => {
@@ -42,7 +74,6 @@ function postJson(url, body) {
42
74
  headers: {
43
75
  'Content-Type': 'application/json',
44
76
  'Content-Length': Buffer.byteLength(data),
45
- 'Authorization': 'Basic ' + Buffer.from(TRANSPORT_AUTH).toString('base64'),
46
77
  },
47
78
  timeout: 15_000,
48
79
  };
@@ -69,6 +100,132 @@ function postJson(url, body) {
69
100
  });
70
101
  }
71
102
 
103
+ function postForm(url, params) {
104
+ return new Promise((resolve, reject) => {
105
+ const parsed = new URL(url);
106
+ const isHttps = parsed.protocol === 'https:';
107
+ const requestFn = isHttps ? httpsRequest : httpRequest;
108
+ const data = new URLSearchParams(params).toString();
109
+ const options = {
110
+ hostname: parsed.hostname,
111
+ port: parsed.port || (isHttps ? 443 : 80),
112
+ path: parsed.pathname,
113
+ method: 'POST',
114
+ headers: {
115
+ 'Content-Type': 'application/x-www-form-urlencoded',
116
+ 'Content-Length': Buffer.byteLength(data),
117
+ },
118
+ timeout: 15_000,
119
+ };
120
+ const req = requestFn(options, (res) => {
121
+ let buf = '';
122
+ res.on('data', (chunk) => { buf += chunk; });
123
+ res.on('end', () => {
124
+ try {
125
+ resolve({ status: res.statusCode, data: JSON.parse(buf) });
126
+ } catch {
127
+ reject(new Error(`Server responded ${res.statusCode}: ${buf}`));
128
+ }
129
+ });
130
+ });
131
+ req.on('error', reject);
132
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
133
+ req.write(data);
134
+ req.end();
135
+ });
136
+ }
137
+
138
+ function sleep(ms) {
139
+ return new Promise(resolve => setTimeout(resolve, ms));
140
+ }
141
+
142
+ async function deviceCodeAuth(serverUrl) {
143
+ // 1. Fetch Auth0 config from server
144
+ let config;
145
+ try {
146
+ config = await getJson(`${serverUrl}/api/auth/config`);
147
+ } catch (err) {
148
+ throw new Error(`Could not fetch auth config from server: ${err.message}`);
149
+ }
150
+
151
+ if (!config.enabled || !config.domain || !config.cliClientId) {
152
+ throw new Error('Auth0 device code flow not configured on server (missing AUTH0_CLI_CLIENT_ID)');
153
+ }
154
+
155
+ const { domain, cliClientId, audience } = config;
156
+
157
+ // 2. Request device code
158
+ const codeParams = {
159
+ client_id: cliClientId,
160
+ scope: 'openid profile email',
161
+ };
162
+ if (audience) codeParams.audience = audience;
163
+
164
+ const codeResp = await postForm(`https://${domain}/oauth/device/code`, codeParams);
165
+ if (codeResp.status !== 200) {
166
+ throw new Error(`Device code request failed: ${JSON.stringify(codeResp.data)}`);
167
+ }
168
+
169
+ const {
170
+ device_code,
171
+ user_code,
172
+ verification_uri_complete,
173
+ verification_uri,
174
+ interval: pollInterval = 5,
175
+ expires_in,
176
+ } = codeResp.data;
177
+
178
+ // 3. Show URL + code
179
+ blank();
180
+ info(' Open this URL in your browser to authenticate:');
181
+ blank();
182
+ info(` ${verification_uri_complete || verification_uri}`);
183
+ blank();
184
+ if (user_code) {
185
+ info(` Code: ${user_code}`);
186
+ blank();
187
+ }
188
+ info(` Waiting for browser login (expires in ${Math.floor(expires_in / 60)} min)...`);
189
+
190
+ // 4. Poll for token
191
+ let interval = pollInterval;
192
+ const deadline = Date.now() + expires_in * 1000;
193
+
194
+ while (Date.now() < deadline) {
195
+ await sleep(interval * 1000);
196
+
197
+ const tokenResp = await postForm(`https://${domain}/oauth/token`, {
198
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
199
+ client_id: cliClientId,
200
+ device_code,
201
+ });
202
+
203
+ if (tokenResp.status === 200 && tokenResp.data.id_token) {
204
+ // 5. Exchange JWT for personal token
205
+ const result = await postJson(`${serverUrl}/api/auth/device-token`, {
206
+ jwt: tokenResp.data.id_token,
207
+ });
208
+ return result;
209
+ }
210
+
211
+ const err = tokenResp.data.error;
212
+ if (err === 'authorization_pending') {
213
+ continue;
214
+ } else if (err === 'slow_down') {
215
+ interval += 5;
216
+ continue;
217
+ } else if (err === 'expired_token') {
218
+ throw new Error('Device code expired. Please try again.');
219
+ } else if (err === 'access_denied') {
220
+ throw new Error('Authentication was denied.');
221
+ } else {
222
+ throw new Error(`Unexpected token response: ${JSON.stringify(tokenResp.data)}`);
223
+ }
224
+ }
225
+
226
+ throw new Error('Device code expired. Please try again.');
227
+ }
228
+
72
229
  export default async function init() {
73
230
  const { version, commit } = getVersionInfo();
74
231
  initLogger(`v${version} (${commit})`);
@@ -126,41 +283,17 @@ export default async function init() {
126
283
  }
127
284
  blank();
128
285
 
129
- // Authentication (required)
286
+ // Authentication
130
287
  heading('Authentication');
131
288
  if (!currentConfig.authToken) {
132
- info('Invite token is required. Get one from your team lead.');
133
- // eslint-disable-next-line no-constant-condition
134
- while (true) {
135
- const inviteInput = await ask('Invite token: ');
136
- if (!inviteInput) {
137
- warn(' Invite token is required to continue.');
138
- continue;
139
- }
140
- if (!inviteInput.startsWith('ailens_inv_')) {
141
- warn(' Token must start with ailens_inv_. Try again.');
142
- continue;
143
- }
144
- try {
145
- let email, gitName;
146
- try {
147
- email = execSync('git config user.email', { encoding: 'utf-8' }).trim();
148
- gitName = execSync('git config user.name', { encoding: 'utf-8' }).trim();
149
- } catch {
150
- error(' Could not read git identity. Set git config user.email and user.name first.');
151
- return;
152
- }
153
- const result = await postJson(serverUrl + '/api/auth/accept-invite', {
154
- token: inviteInput, email, name: gitName,
155
- });
156
- newConfig.authToken = result.token;
157
- saveLensConfig(newConfig);
158
- success(` Authenticated! Team: ${result.team_id}`);
159
- break;
160
- } catch (err) {
161
- error(` Authentication failed: ${err.message}`);
162
- info(' Try again.');
163
- }
289
+ try {
290
+ const result = await deviceCodeAuth(serverUrl);
291
+ newConfig.authToken = result.token;
292
+ saveLensConfig(newConfig);
293
+ success(` Authenticated as ${result.name} (${result.email})`);
294
+ } catch (err) {
295
+ error(` Authentication failed: ${err.message}`);
296
+ return;
164
297
  }
165
298
  } else {
166
299
  success(' Already authenticated (token present)');
@@ -2,9 +2,21 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { z } from "zod";
4
4
  import { execSync } from "child_process";
5
+ import { readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { homedir } from "node:os";
8
+
9
+ function loadLensConfig() {
10
+ try {
11
+ return JSON.parse(readFileSync(join(homedir(), ".ai-lens", "config.json"), "utf-8"));
12
+ } catch {
13
+ return {};
14
+ }
15
+ }
5
16
 
6
- const SERVER_URL = process.env.AI_LENS_SERVER_URL || "http://168.119.103.228:13300";
7
- const AUTH_TOKEN = process.env.AI_LENS_AUTH_TOKEN;
17
+ const lensConfig = loadLensConfig();
18
+ const SERVER_URL = process.env.AI_LENS_SERVER_URL || lensConfig.serverUrl || "http://168.119.103.228:13300";
19
+ const AUTH_TOKEN = process.env.AI_LENS_AUTH_TOKEN || lensConfig.authToken;
8
20
 
9
21
  async function apiCall(path) {
10
22
  const headers = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lens",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {
package/cli/admin.js DELETED
@@ -1,120 +0,0 @@
1
- import { createInterface } from 'node:readline';
2
- import { request as httpRequest } from 'node:http';
3
- import { request as httpsRequest } from 'node:https';
4
- import { readLensConfig } from './hooks.js';
5
-
6
- function ask(question) {
7
- const rl = createInterface({ input: process.stdin, output: process.stdout });
8
- return new Promise(resolve => {
9
- rl.question(question, answer => {
10
- rl.close();
11
- resolve(answer.trim());
12
- });
13
- });
14
- }
15
-
16
- function adminRequest(method, url, body) {
17
- const secret = process.env.AI_LENS_ADMIN_SECRET;
18
- if (!secret) {
19
- throw new Error('AI_LENS_ADMIN_SECRET env var is required');
20
- }
21
- return new Promise((resolve, reject) => {
22
- const parsed = new URL(url);
23
- const isHttps = parsed.protocol === 'https:';
24
- const requestFn = isHttps ? httpsRequest : httpRequest;
25
- const data = body ? JSON.stringify(body) : null;
26
- const options = {
27
- hostname: parsed.hostname,
28
- port: parsed.port || (isHttps ? 443 : 80),
29
- path: parsed.pathname + (parsed.search || ''),
30
- method,
31
- headers: {
32
- 'X-Admin-Secret': secret,
33
- ...(data ? {
34
- 'Content-Type': 'application/json',
35
- 'Content-Length': Buffer.byteLength(data),
36
- } : {}),
37
- },
38
- timeout: 15_000,
39
- };
40
- const req = requestFn(options, (res) => {
41
- let buf = '';
42
- res.on('data', (chunk) => { buf += chunk; });
43
- res.on('end', () => {
44
- try {
45
- const json = JSON.parse(buf);
46
- if (res.statusCode >= 200 && res.statusCode < 300) {
47
- resolve(json);
48
- } else {
49
- reject(new Error(json.error || `Server responded ${res.statusCode}`));
50
- }
51
- } catch {
52
- reject(new Error(`Server responded ${res.statusCode}: ${buf}`));
53
- }
54
- });
55
- });
56
- req.on('error', reject);
57
- req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
58
- if (data) req.write(data);
59
- req.end();
60
- });
61
- }
62
-
63
- function getServerUrl() {
64
- const config = readLensConfig();
65
- return config.serverUrl || process.env.AI_LENS_SERVER_URL || 'http://localhost:3000';
66
- }
67
-
68
- export async function createInvite() {
69
- const serverUrl = getServerUrl();
70
- const teamId = await ask('Team ID: ');
71
- if (!teamId) {
72
- console.error('Team ID is required');
73
- process.exit(1);
74
- }
75
- const label = await ask('Label (optional): ');
76
- const maxUsesStr = await ask('Max uses (optional, Enter = unlimited): ');
77
- const maxUses = maxUsesStr ? parseInt(maxUsesStr, 10) : undefined;
78
-
79
- try {
80
- const result = await adminRequest('POST', `${serverUrl}/api/auth/admin/invite-tokens`, {
81
- team_id: teamId,
82
- label: label || undefined,
83
- max_uses: maxUses,
84
- });
85
- console.log('\nInvite token created:');
86
- console.log(` Token: ${result.token}`);
87
- console.log(` Team: ${result.team_id}`);
88
- if (result.label) console.log(` Label: ${result.label}`);
89
- if (result.max_uses) console.log(` Max uses: ${result.max_uses}`);
90
- console.log('\nShare this token with developers. They can use it during: npx ai-lens init');
91
- } catch (err) {
92
- console.error(`Error: ${err.message}`);
93
- process.exit(1);
94
- }
95
- }
96
-
97
- export async function listInvites() {
98
- const serverUrl = getServerUrl();
99
- const teamId = await ask('Team ID (optional, Enter = all): ');
100
- const query = teamId ? `?team_id=${encodeURIComponent(teamId)}` : '';
101
-
102
- try {
103
- const tokens = await adminRequest('GET', `${serverUrl}/api/auth/admin/invite-tokens${query}`);
104
- if (tokens.length === 0) {
105
- console.log('No invite tokens found.');
106
- return;
107
- }
108
- console.log(`\n${'ID'.padEnd(38)}${'Team'.padEnd(20)}${'Label'.padEnd(25)}${'Uses'.padEnd(10)}${'Created'}`);
109
- console.log('-'.repeat(110));
110
- for (const t of tokens) {
111
- const uses = t.max_uses ? `${t.uses}/${t.max_uses}` : `${t.uses}`;
112
- console.log(
113
- `${t.id.padEnd(38)}${(t.team_id || '').padEnd(20)}${(t.label || '').padEnd(25)}${uses.padEnd(10)}${t.created_at}`,
114
- );
115
- }
116
- } catch (err) {
117
- console.error(`Error: ${err.message}`);
118
- process.exit(1);
119
- }
120
- }