ai-lens 0.7.2 → 0.7.3
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/README.md +1 -1
- package/cli/init.js +102 -3
- package/cli/status.js +102 -1
- package/client/capture.js +41 -7
- package/client/config.js +9 -3
- package/client/sender.js +27 -5
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
75f2d5f
|
package/README.md
CHANGED
package/cli/init.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
initLogger, info, success, warn, error,
|
|
10
10
|
heading, detail, blank, getLogPath,
|
|
11
11
|
} from './logger.js';
|
|
12
|
+
import { getGitIdentity } from '../client/config.js';
|
|
12
13
|
import {
|
|
13
14
|
CAPTURE_PATH, detectInstalledTools,
|
|
14
15
|
analyzeToolHooks, buildMergedConfig, writeHooksConfig, describePlan,
|
|
@@ -226,6 +227,38 @@ async function deviceCodeAuth(serverUrl) {
|
|
|
226
227
|
throw new Error('Device code expired. Please try again.');
|
|
227
228
|
}
|
|
228
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Validate an existing auth token against the server.
|
|
232
|
+
* Returns true if token is valid, false if invalid/revoked/unreachable.
|
|
233
|
+
*/
|
|
234
|
+
async function validateExistingToken(serverUrl, token) {
|
|
235
|
+
if (!token || !token.startsWith('ailens_dev_')) return false;
|
|
236
|
+
try {
|
|
237
|
+
const parsed = new URL(`${serverUrl}/api/auth/verify`);
|
|
238
|
+
const isHttps = parsed.protocol === 'https:';
|
|
239
|
+
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
240
|
+
const status = await new Promise((resolve, reject) => {
|
|
241
|
+
const req = requestFn({
|
|
242
|
+
hostname: parsed.hostname,
|
|
243
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
244
|
+
path: parsed.pathname,
|
|
245
|
+
method: 'GET',
|
|
246
|
+
headers: { 'X-Auth-Token': token },
|
|
247
|
+
timeout: 10_000,
|
|
248
|
+
}, (res) => {
|
|
249
|
+
res.resume();
|
|
250
|
+
resolve(res.statusCode);
|
|
251
|
+
});
|
|
252
|
+
req.on('error', reject);
|
|
253
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
254
|
+
req.end();
|
|
255
|
+
});
|
|
256
|
+
return status === 200;
|
|
257
|
+
} catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
229
262
|
// =============================================================================
|
|
230
263
|
// CLI flags for non-interactive mode
|
|
231
264
|
// =============================================================================
|
|
@@ -277,7 +310,7 @@ export default async function init() {
|
|
|
277
310
|
if (tools.length === 0) {
|
|
278
311
|
warn('No supported AI tools detected.');
|
|
279
312
|
info('Looked for ~/.claude/ and ~/.cursor/ directories.');
|
|
280
|
-
info('Install Claude Code or Cursor, then re-run: npx ai-lens init');
|
|
313
|
+
info('Install Claude Code or Cursor, then re-run: npx -y ai-lens init');
|
|
281
314
|
return;
|
|
282
315
|
}
|
|
283
316
|
|
|
@@ -334,6 +367,16 @@ export default async function init() {
|
|
|
334
367
|
|
|
335
368
|
// Authentication
|
|
336
369
|
heading('Authentication');
|
|
370
|
+
if (currentConfig.authToken) {
|
|
371
|
+
const valid = await validateExistingToken(serverUrl, currentConfig.authToken);
|
|
372
|
+
if (valid) {
|
|
373
|
+
success(' Already authenticated (token verified)');
|
|
374
|
+
} else {
|
|
375
|
+
warn(' Existing token is invalid or revoked — re-authenticating...');
|
|
376
|
+
currentConfig.authToken = null;
|
|
377
|
+
newConfig.authToken = null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
337
380
|
if (!currentConfig.authToken) {
|
|
338
381
|
try {
|
|
339
382
|
const result = await deviceCodeAuth(serverUrl);
|
|
@@ -348,8 +391,6 @@ export default async function init() {
|
|
|
348
391
|
return;
|
|
349
392
|
}
|
|
350
393
|
}
|
|
351
|
-
} else {
|
|
352
|
-
success(' Already authenticated (token present)');
|
|
353
394
|
}
|
|
354
395
|
blank();
|
|
355
396
|
|
|
@@ -594,6 +635,64 @@ export default async function init() {
|
|
|
594
635
|
} catch (err) {
|
|
595
636
|
warn(` Token: could not verify — ${err.message}`);
|
|
596
637
|
}
|
|
638
|
+
|
|
639
|
+
// 3. E2E: POST a test event
|
|
640
|
+
try {
|
|
641
|
+
const { name: gitName, email: gitEmail } = getGitIdentity();
|
|
642
|
+
const testEvent = [{
|
|
643
|
+
source: 'cli',
|
|
644
|
+
session_id: `e2e-test-${Date.now()}`,
|
|
645
|
+
type: 'E2eTest',
|
|
646
|
+
timestamp: new Date().toISOString(),
|
|
647
|
+
project_path: process.cwd(),
|
|
648
|
+
data: { trigger: 'init' },
|
|
649
|
+
}];
|
|
650
|
+
const headers = {
|
|
651
|
+
'Content-Type': 'application/json',
|
|
652
|
+
'X-Auth-Token': finalConfig.authToken,
|
|
653
|
+
};
|
|
654
|
+
if (gitEmail) headers['X-Developer-Git-Email'] = gitEmail;
|
|
655
|
+
if (gitName) headers['X-Developer-Name'] = gitName;
|
|
656
|
+
|
|
657
|
+
const parsed = new URL(`${verifyUrl}/api/events`);
|
|
658
|
+
const isHttps = parsed.protocol === 'https:';
|
|
659
|
+
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
660
|
+
const data = JSON.stringify(testEvent);
|
|
661
|
+
headers['Content-Length'] = String(Buffer.byteLength(data));
|
|
662
|
+
|
|
663
|
+
const e2eBody = await new Promise((resolve, reject) => {
|
|
664
|
+
const req = requestFn({
|
|
665
|
+
hostname: parsed.hostname,
|
|
666
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
667
|
+
path: parsed.pathname,
|
|
668
|
+
method: 'POST',
|
|
669
|
+
headers,
|
|
670
|
+
timeout: 10_000,
|
|
671
|
+
}, (res) => {
|
|
672
|
+
let buf = '';
|
|
673
|
+
res.on('data', (chunk) => { buf += chunk; });
|
|
674
|
+
res.on('end', () => {
|
|
675
|
+
try {
|
|
676
|
+
resolve({ status: res.statusCode, data: JSON.parse(buf) });
|
|
677
|
+
} catch {
|
|
678
|
+
reject(new Error(`Server responded ${res.statusCode}: ${buf}`));
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
req.on('error', reject);
|
|
683
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
684
|
+
req.write(data);
|
|
685
|
+
req.end();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
if (e2eBody.status === 200 && e2eBody.data.received >= 1) {
|
|
689
|
+
success(' E2E: event accepted by server');
|
|
690
|
+
} else {
|
|
691
|
+
error(` E2E: unexpected response (HTTP ${e2eBody.status})`);
|
|
692
|
+
}
|
|
693
|
+
} catch (err) {
|
|
694
|
+
error(` E2E: ${err.message}`);
|
|
695
|
+
}
|
|
597
696
|
}
|
|
598
697
|
} else {
|
|
599
698
|
warn(' No server URL configured — skipping verification');
|
package/cli/status.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
|
|
6
6
|
import { getVersionInfo, readLensConfig, detectInstalledTools, analyzeToolHooks, CAPTURE_PATH, TOOL_CONFIGS } from './hooks.js';
|
|
7
|
-
import { DATA_DIR, QUEUE_PATH, LOG_PATH, getGitIdentity } from '../client/config.js';
|
|
7
|
+
import { DATA_DIR, QUEUE_PATH, LOG_PATH, CAPTURE_LOG_PATH, getGitIdentity } from '../client/config.js';
|
|
8
8
|
import { initLogger, info, success, warn, error, heading, blank } from './logger.js';
|
|
9
9
|
|
|
10
10
|
// ANSI helpers
|
|
@@ -286,6 +286,48 @@ function checkSenderLog() {
|
|
|
286
286
|
};
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
function checkCaptureLog() {
|
|
290
|
+
if (!existsSync(CAPTURE_LOG_PATH)) {
|
|
291
|
+
return { ok: true, summary: 'no drops logged', detail: 'Capture log does not exist (no events dropped)' };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let lines;
|
|
295
|
+
try {
|
|
296
|
+
lines = readFileSync(CAPTURE_LOG_PATH, 'utf-8').split('\n').filter(Boolean);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
return { ok: false, summary: `error reading log: ${err.message}`, detail: `Error: ${err.message}` };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Count entries by category (reason for drops, msg for errors)
|
|
302
|
+
const counts = {};
|
|
303
|
+
let lastTs = null;
|
|
304
|
+
let hasErrors = false;
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
try {
|
|
307
|
+
const entry = JSON.parse(line);
|
|
308
|
+
const category = entry.reason || entry.msg || 'unknown';
|
|
309
|
+
counts[category] = (counts[category] || 0) + 1;
|
|
310
|
+
lastTs = entry.ts;
|
|
311
|
+
if (entry.msg) hasErrors = true;
|
|
312
|
+
} catch { /* non-JSON line */ }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const total = lines.length;
|
|
316
|
+
const breakdown = Object.entries(counts).map(([r, n]) => `${r}: ${n}`).join(', ');
|
|
317
|
+
|
|
318
|
+
let summary = `${total} entries`;
|
|
319
|
+
if (breakdown) summary += ` (${breakdown})`;
|
|
320
|
+
if (lastTs) summary += `, last ${relativeTime(lastTs)}`;
|
|
321
|
+
|
|
322
|
+
const last10 = lines.slice(-10);
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
ok: !hasErrors,
|
|
326
|
+
summary,
|
|
327
|
+
detail: `Log: ${CAPTURE_LOG_PATH}\nTotal: ${total}\n\nLast 10 entries:\n${last10.join('\n')}`,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
289
331
|
async function checkServer(serverUrl) {
|
|
290
332
|
if (!serverUrl) {
|
|
291
333
|
return { ok: false, summary: 'no server URL configured', detail: 'Cannot check server: no serverUrl in config' };
|
|
@@ -340,6 +382,44 @@ async function checkToken(serverUrl, authToken) {
|
|
|
340
382
|
}
|
|
341
383
|
}
|
|
342
384
|
|
|
385
|
+
async function checkE2e(serverUrl, authToken) {
|
|
386
|
+
if (!serverUrl || !authToken) {
|
|
387
|
+
const missing = !serverUrl ? 'server URL' : 'auth token';
|
|
388
|
+
return { ok: false, summary: `no ${missing} configured`, detail: `Cannot run E2E test: missing ${missing}` };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const { name, email } = getGitIdentity();
|
|
392
|
+
const testEvent = [{
|
|
393
|
+
source: 'cli',
|
|
394
|
+
session_id: `e2e-test-${Date.now()}`,
|
|
395
|
+
type: 'E2eTest',
|
|
396
|
+
timestamp: new Date().toISOString(),
|
|
397
|
+
project_path: process.cwd(),
|
|
398
|
+
data: { trigger: 'status' },
|
|
399
|
+
}];
|
|
400
|
+
|
|
401
|
+
const url = `${serverUrl}/api/events`;
|
|
402
|
+
try {
|
|
403
|
+
const res = await fetch(url, {
|
|
404
|
+
method: 'POST',
|
|
405
|
+
headers: {
|
|
406
|
+
'Content-Type': 'application/json',
|
|
407
|
+
'X-Auth-Token': authToken,
|
|
408
|
+
...(email && { 'X-Developer-Git-Email': email }),
|
|
409
|
+
...(name && { 'X-Developer-Name': name }),
|
|
410
|
+
},
|
|
411
|
+
body: JSON.stringify(testEvent),
|
|
412
|
+
});
|
|
413
|
+
const body = await res.json();
|
|
414
|
+
if (res.ok && body.received >= 1) {
|
|
415
|
+
return { ok: true, summary: 'event accepted', detail: `POST ${url} → ${res.status}, received: ${body.received}` };
|
|
416
|
+
}
|
|
417
|
+
return { ok: false, summary: `unexpected response (${res.status})`, detail: `POST ${url} → ${res.status}\nBody: ${JSON.stringify(body)}` };
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return { ok: false, summary: `failed (${err.message})`, detail: `POST ${url}\nError: ${err.message}` };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
343
423
|
// ---------------------------------------------------------------------------
|
|
344
424
|
// Report file generation
|
|
345
425
|
// ---------------------------------------------------------------------------
|
|
@@ -423,6 +503,20 @@ function buildReport(results, timestamp) {
|
|
|
423
503
|
}
|
|
424
504
|
lines.push('');
|
|
425
505
|
|
|
506
|
+
// Capture drops log (last 100 lines)
|
|
507
|
+
lines.push(`${'='.repeat(60)}`);
|
|
508
|
+
lines.push(`Capture drops (${CAPTURE_LOG_PATH}):`);
|
|
509
|
+
try {
|
|
510
|
+
const capLines = readFileSync(CAPTURE_LOG_PATH, 'utf-8').split('\n').filter(Boolean);
|
|
511
|
+
lines.push(`Total: ${capLines.length} drops`);
|
|
512
|
+
for (const cl of capLines.slice(-100)) {
|
|
513
|
+
lines.push(cl);
|
|
514
|
+
}
|
|
515
|
+
} catch {
|
|
516
|
+
lines.push('(not found)');
|
|
517
|
+
}
|
|
518
|
+
lines.push('');
|
|
519
|
+
|
|
426
520
|
return lines.join('\n');
|
|
427
521
|
}
|
|
428
522
|
|
|
@@ -498,6 +592,9 @@ export default async function status() {
|
|
|
498
592
|
// 8. Sender log
|
|
499
593
|
printLine('Sender log', checkSenderLog());
|
|
500
594
|
|
|
595
|
+
// 8b. Capture drops
|
|
596
|
+
printLine('Capture drops', checkCaptureLog());
|
|
597
|
+
|
|
501
598
|
// 9. Server connectivity
|
|
502
599
|
const serverUrl = configResult.serverUrl || readLensConfig().serverUrl;
|
|
503
600
|
const serverResult = await checkServer(serverUrl);
|
|
@@ -508,6 +605,10 @@ export default async function status() {
|
|
|
508
605
|
const tokenResult = await checkToken(serverUrl, authToken);
|
|
509
606
|
printLine('Token', tokenResult);
|
|
510
607
|
|
|
608
|
+
// 11. E2E connectivity test
|
|
609
|
+
const e2eResult = await checkE2e(serverUrl, authToken);
|
|
610
|
+
printLine('E2E test', e2eResult);
|
|
611
|
+
|
|
511
612
|
// Write report file
|
|
512
613
|
const timestamp = new Date().toISOString();
|
|
513
614
|
const report = buildReport(results, timestamp);
|
package/client/capture.js
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
QUEUE_PATH,
|
|
18
18
|
SESSION_PATHS_PATH,
|
|
19
19
|
LAST_EVENTS_PATH,
|
|
20
|
+
CAPTURE_LOG_PATH,
|
|
21
|
+
captureLog,
|
|
20
22
|
getServerUrl,
|
|
21
23
|
getGitIdentity,
|
|
22
24
|
getGitMetadata,
|
|
@@ -31,6 +33,13 @@ try {
|
|
|
31
33
|
|
|
32
34
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
35
|
|
|
36
|
+
function logDrop(reason, meta = {}) {
|
|
37
|
+
try {
|
|
38
|
+
const entry = { ts: new Date().toISOString(), reason, ...meta };
|
|
39
|
+
appendFileSync(CAPTURE_LOG_PATH, JSON.stringify(entry) + '\n');
|
|
40
|
+
} catch { /* best-effort */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
// =============================================================================
|
|
35
44
|
// Truncation (reused from ai-session-lens prompts.js approach)
|
|
36
45
|
// =============================================================================
|
|
@@ -117,7 +126,9 @@ function cacheSessionPath(sessionId, projectPath) {
|
|
|
117
126
|
const paths = loadSessionPaths();
|
|
118
127
|
if (paths[sessionId] !== projectPath) {
|
|
119
128
|
paths[sessionId] = projectPath;
|
|
120
|
-
|
|
129
|
+
try {
|
|
130
|
+
saveSessionPaths(paths);
|
|
131
|
+
} catch { /* cache write failed — event proceeds without cached path */ }
|
|
121
132
|
}
|
|
122
133
|
}
|
|
123
134
|
|
|
@@ -159,7 +170,9 @@ export function isDuplicateEvent(sessionId, type) {
|
|
|
159
170
|
const dominated = DEDUP_TYPES.has(type) && prev === type;
|
|
160
171
|
if (prev !== type) {
|
|
161
172
|
cache[sessionId] = type;
|
|
162
|
-
|
|
173
|
+
try {
|
|
174
|
+
saveLastEvents(cache);
|
|
175
|
+
} catch { /* dedup cache write failed — proceed with stale state */ }
|
|
163
176
|
}
|
|
164
177
|
return dominated;
|
|
165
178
|
}
|
|
@@ -322,7 +335,12 @@ function normalizeCursor(event) {
|
|
|
322
335
|
const hookName = event.hook_event_name;
|
|
323
336
|
const type = CURSOR_TYPE_MAP[hookName] || hookName;
|
|
324
337
|
const timestamp = new Date().toISOString();
|
|
325
|
-
|
|
338
|
+
let projectPath = Array.isArray(event.workspace_roots) ? event.workspace_roots[0] : null;
|
|
339
|
+
if (projectPath) {
|
|
340
|
+
cacheSessionPath(sessionId, projectPath);
|
|
341
|
+
} else {
|
|
342
|
+
projectPath = getCachedSessionPath(sessionId);
|
|
343
|
+
}
|
|
326
344
|
|
|
327
345
|
let data = {};
|
|
328
346
|
switch (hookName) {
|
|
@@ -494,18 +512,20 @@ async function main() {
|
|
|
494
512
|
|
|
495
513
|
const unified = normalizeEvent(event);
|
|
496
514
|
if (!unified || !unified.session_id) {
|
|
497
|
-
|
|
515
|
+
logDrop('normalize_failed', { hook: event.hook_event_name });
|
|
498
516
|
process.exit(0);
|
|
499
517
|
}
|
|
500
518
|
|
|
501
519
|
// Deduplicate consecutive identical event types (e.g. repeated Stop from idle sessions)
|
|
502
520
|
if (isDuplicateEvent(unified.session_id, unified.type)) {
|
|
521
|
+
logDrop('duplicate', { type: unified.type, session_id: unified.session_id });
|
|
503
522
|
process.exit(0);
|
|
504
523
|
}
|
|
505
524
|
|
|
506
525
|
// Filter by monitored projects (if configured)
|
|
507
526
|
const monitored = getMonitoredProjects();
|
|
508
527
|
if (monitored && !monitored.some(p => unified.project_path === p || unified.project_path?.startsWith(p + '/'))) {
|
|
528
|
+
logDrop('project_filter', { type: unified.type, source: unified.source, session_id: unified.session_id, project_path: unified.project_path, monitored });
|
|
509
529
|
process.exit(0);
|
|
510
530
|
}
|
|
511
531
|
|
|
@@ -513,6 +533,7 @@ async function main() {
|
|
|
513
533
|
const identity = getGitIdentity();
|
|
514
534
|
const email = identity.email || event.user_email || null;
|
|
515
535
|
if (!email) {
|
|
536
|
+
logDrop('no_email', { type: unified.type, session_id: unified.session_id });
|
|
516
537
|
process.exit(0);
|
|
517
538
|
}
|
|
518
539
|
unified.developer_email = email;
|
|
@@ -525,14 +546,27 @@ async function main() {
|
|
|
525
546
|
unified.git_commit = gitMeta.git_commit;
|
|
526
547
|
|
|
527
548
|
// Append to queue
|
|
528
|
-
|
|
549
|
+
try {
|
|
550
|
+
appendToQueue(unified);
|
|
551
|
+
} catch (err) {
|
|
552
|
+
captureLog({ msg: 'queue-write-failed', error: err.message, type: unified.type, session_id: unified.session_id });
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
529
555
|
|
|
530
556
|
// Always try to spawn sender — atomic rename in sender handles dedup
|
|
531
|
-
|
|
557
|
+
try {
|
|
558
|
+
trySpawnSender();
|
|
559
|
+
} catch (err) {
|
|
560
|
+
captureLog({ msg: 'sender-spawn-failed', error: err.message });
|
|
561
|
+
// event is queued — sender will be spawned on next capture
|
|
562
|
+
}
|
|
532
563
|
}
|
|
533
564
|
|
|
534
565
|
// Only run main when executed directly (not when imported for testing)
|
|
535
566
|
const isDirectRun = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
536
567
|
if (isDirectRun) {
|
|
537
|
-
main().catch(() =>
|
|
568
|
+
main().catch((err) => {
|
|
569
|
+
try { captureLog({ msg: 'capture-error', error: err.message }); } catch {}
|
|
570
|
+
process.exit(1);
|
|
571
|
+
});
|
|
538
572
|
}
|
package/client/config.js
CHANGED
|
@@ -11,12 +11,20 @@ export const SESSION_PATHS_PATH = join(DATA_DIR, 'session-paths.json');
|
|
|
11
11
|
export const GIT_REMOTES_PATH = join(DATA_DIR, 'git-remotes.json');
|
|
12
12
|
export const LAST_EVENTS_PATH = join(DATA_DIR, 'last-events.json');
|
|
13
13
|
export const LOG_PATH = join(DATA_DIR, 'sender.log');
|
|
14
|
+
export const CAPTURE_LOG_PATH = join(DATA_DIR, 'capture.log');
|
|
14
15
|
|
|
15
16
|
export function log(fields) {
|
|
16
17
|
const entry = { ts: new Date().toISOString(), ...fields };
|
|
17
18
|
appendFileSync(LOG_PATH, JSON.stringify(entry) + '\n');
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
export function captureLog(fields) {
|
|
22
|
+
const entry = { ts: new Date().toISOString(), ...fields };
|
|
23
|
+
try {
|
|
24
|
+
appendFileSync(CAPTURE_LOG_PATH, JSON.stringify(entry) + '\n');
|
|
25
|
+
} catch { /* last resort — disk may be full */ }
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
let _configCache;
|
|
21
29
|
function loadConfig() {
|
|
22
30
|
if (_configCache !== undefined) return _configCache;
|
|
@@ -52,10 +60,8 @@ export function getMonitoredProjects() {
|
|
|
52
60
|
.map(p => p.endsWith('/') ? p.slice(0, -1) : p);
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
const DEFAULT_AUTH_TOKEN = 'collector:secret-collector-token-2026-ai-lens';
|
|
56
|
-
|
|
57
63
|
export function getAuthToken() {
|
|
58
|
-
return process.env.AI_LENS_AUTH_TOKEN || loadConfig().authToken ||
|
|
64
|
+
return process.env.AI_LENS_AUTH_TOKEN || loadConfig().authToken || null;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
export function getGitIdentity() {
|
package/client/sender.js
CHANGED
|
@@ -84,24 +84,43 @@ export function buildRollbackContent(unsentContent, existingContent) {
|
|
|
84
84
|
* renameSync is atomic on POSIX — acts as a mutex.
|
|
85
85
|
* Returns { events, sendingPath } or null if nothing to send.
|
|
86
86
|
*/
|
|
87
|
-
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} [queuePath] - Override queue path (for testing); defaults to QUEUE_PATH
|
|
89
|
+
* @param {string} [sendingPath] - Override sending path (for testing); defaults to SENDING_PATH
|
|
90
|
+
*/
|
|
91
|
+
export function acquireQueue(queuePath = QUEUE_PATH, sendingPath = SENDING_PATH) {
|
|
92
|
+
// Recover orphaned sending file from a previously crashed sender
|
|
88
93
|
try {
|
|
89
|
-
|
|
94
|
+
const orphaned = readFileSync(sendingPath, 'utf-8');
|
|
95
|
+
if (orphaned.trim()) {
|
|
96
|
+
let existing = '';
|
|
97
|
+
try { existing = readFileSync(queuePath, 'utf-8'); } catch { /* no queue yet */ }
|
|
98
|
+
writeFileSync(queuePath, orphaned + existing);
|
|
99
|
+
log({ msg: 'orphan-recovered', bytes: Buffer.byteLength(orphaned) });
|
|
100
|
+
}
|
|
101
|
+
unlinkSync(sendingPath);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (err.code !== 'ENOENT') throw err;
|
|
104
|
+
// No orphan — normal path
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
renameSync(queuePath, sendingPath);
|
|
90
109
|
} catch (err) {
|
|
91
110
|
if (err.code === 'ENOENT') return null; // no queue or another sender got it
|
|
92
111
|
throw err;
|
|
93
112
|
}
|
|
94
113
|
|
|
95
|
-
const content = readFileSync(
|
|
114
|
+
const content = readFileSync(sendingPath, 'utf-8');
|
|
96
115
|
const { events, dropped, overflow } = parseQueueContent(content);
|
|
97
116
|
if (dropped > 0) log({ msg: 'queue-corruption', dropped });
|
|
98
117
|
if (overflow > 0) log({ msg: 'queue-overflow', dropped: overflow, kept: MAX_QUEUE_SIZE });
|
|
99
118
|
if (events.length === 0) {
|
|
100
|
-
unlinkSync(
|
|
119
|
+
unlinkSync(sendingPath);
|
|
101
120
|
return null;
|
|
102
121
|
}
|
|
103
122
|
|
|
104
|
-
return { events, sendingPath
|
|
123
|
+
return { events, sendingPath };
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
/**
|
|
@@ -289,6 +308,9 @@ async function main() {
|
|
|
289
308
|
const unsentEvents = events.filter(e => !sentEventIds.has(e.event_id));
|
|
290
309
|
partialRollback(sendingPath, unsentEvents, events.length);
|
|
291
310
|
log({ msg: 'failed', error: err.message, sent: events.length - unsentEvents.length, unsent: unsentEvents.length, server: serverUrl });
|
|
311
|
+
if (err.message.includes('401')) {
|
|
312
|
+
log({ msg: 'auth-failed', error: 'Token invalid or revoked. Run: npx -y ai-lens init' });
|
|
313
|
+
}
|
|
292
314
|
}
|
|
293
315
|
}
|
|
294
316
|
|