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 +1 -1
- package/bin/ai-lens.js +0 -16
- package/cli/init.js +169 -36
- package/mcp-server/index.js +14 -2
- package/package.json +1 -1
- package/cli/admin.js +0 -120
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
286
|
+
// Authentication
|
|
130
287
|
heading('Authentication');
|
|
131
288
|
if (!currentConfig.authToken) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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)');
|
package/mcp-server/index.js
CHANGED
|
@@ -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
|
|
7
|
-
const
|
|
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
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
|
-
}
|