ai-lens 0.8.52 → 0.8.55
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 +1 -1
- package/cli/hooks.js +3 -2
- package/cli/init.js +5 -2
- package/cli/status.js +111 -27
- package/client/config.js +1 -0
- package/client/sender.js +47 -1
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
93a1682
|
package/bin/ai-lens.js
CHANGED
package/cli/hooks.js
CHANGED
|
@@ -174,11 +174,12 @@ export function installClientFiles() {
|
|
|
174
174
|
'{"type":"module"}\n',
|
|
175
175
|
);
|
|
176
176
|
|
|
177
|
-
// Write version.json so installed capture.js can identify itself
|
|
177
|
+
// Write version.json so installed capture.js can identify itself.
|
|
178
|
+
// packageRoot lets sender.js find bin/ai-lens.js for background status reports.
|
|
178
179
|
const { version, commit } = getVersionInfo();
|
|
179
180
|
writeFileSync(
|
|
180
181
|
join(CLIENT_INSTALL_DIR, 'version.json'),
|
|
181
|
-
JSON.stringify({ version, commit }) + '\n',
|
|
182
|
+
JSON.stringify({ version, commit, packageRoot: PKG_ROOT }) + '\n',
|
|
182
183
|
);
|
|
183
184
|
}
|
|
184
185
|
|
package/cli/init.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from './logger.js';
|
|
13
13
|
import { getGitIdentity } from '../client/config.js';
|
|
14
14
|
import { migrateIfNeeded } from '../client/sender.js';
|
|
15
|
+
import { stopCodexWatcher } from '../client/codex-watcher.js';
|
|
15
16
|
import {
|
|
16
17
|
CAPTURE_PATH, REPO_CAPTURE_PATH, detectInstalledTools, getCursorToolConfig, getClaudeCodeToolConfig,
|
|
17
18
|
analyzeToolHooks, buildMergedConfig, writeHooksConfig, describePlan,
|
|
@@ -470,7 +471,7 @@ export default async function init() {
|
|
|
470
471
|
);
|
|
471
472
|
projects = (projectsInput && projectsInput.trim() && projectsInput.trim().toLowerCase() !== 'all')
|
|
472
473
|
? projectsInput
|
|
473
|
-
: (projectHooksDefault || null);
|
|
474
|
+
: (currentProjects || projectHooksDefault || null);
|
|
474
475
|
}
|
|
475
476
|
// Guard: non-string (e.g. array from corrupt config) → treat as unset
|
|
476
477
|
if (projects && typeof projects !== 'string') {
|
|
@@ -786,11 +787,13 @@ export default async function init() {
|
|
|
786
787
|
saveLensConfig(newConfig);
|
|
787
788
|
|
|
788
789
|
if (enableCodex) {
|
|
790
|
+
// Stop existing watcher so the new one picks up fresh config (projects, etc.)
|
|
791
|
+
await stopCodexWatcher();
|
|
789
792
|
const watcherPath = flags.useRepoPath
|
|
790
793
|
? join(dirname(REPO_CAPTURE_PATH), 'codex-watcher.js')
|
|
791
794
|
: join(dirname(CAPTURE_PATH), 'codex-watcher.js');
|
|
792
795
|
if (startCodexWatcher(watcherPath)) {
|
|
793
|
-
success(' Codex watcher
|
|
796
|
+
success(' Codex watcher restarted');
|
|
794
797
|
} else {
|
|
795
798
|
warn(` Codex watcher not started: missing ${watcherPath}`);
|
|
796
799
|
}
|
package/cli/status.js
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir, release as osRelease, arch as osArch } from 'node:os';
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
|
|
7
7
|
import { getVersionInfo, readLensConfig, detectInstalledTools, getCursorToolConfig, getClaudeCodeToolConfig, analyzeToolHooks, checkHooksDisabled, CAPTURE_PATH, TOOL_CONFIGS } from './hooks.js';
|
|
8
|
-
import { DATA_DIR, PENDING_DIR, SENDING_DIR, SESSION_PATHS_DIR, LOG_PATH, CAPTURE_LOG_PATH, getGitIdentity, getMonitoredProjects } from '../client/config.js';
|
|
8
|
+
import { DATA_DIR, PENDING_DIR, SENDING_DIR, SESSION_PATHS_DIR, LOG_PATH, CAPTURE_LOG_PATH, LAST_STATUS_REPORT_PATH, getGitIdentity, getMonitoredProjects } from '../client/config.js';
|
|
9
9
|
import { isLockStale } from '../client/sender.js';
|
|
10
10
|
import { readCodexWatcherLock, resolveWatchedCodexDirs } from '../client/codex-watcher.js';
|
|
11
11
|
import { initLogger, info, success, warn, error, heading, blank } from './logger.js';
|
|
@@ -92,6 +92,42 @@ function expandTilde(pathStr) {
|
|
|
92
92
|
return pathStr;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Detect install mode from the capture.js path in hook commands.
|
|
97
|
+
* Returns { ok, summary, detail } for the status output.
|
|
98
|
+
*/
|
|
99
|
+
function detectInstallMode(tools) {
|
|
100
|
+
const copyDir = join(homedir(), '.ai-lens', 'client') + '/';
|
|
101
|
+
const paths = [];
|
|
102
|
+
for (const tool of tools) {
|
|
103
|
+
const cmd = extractHookCommand(tool);
|
|
104
|
+
if (!cmd) continue;
|
|
105
|
+
const m = cmd.match(/["']([^"']*capture\.js)["']|(\S*capture\.js)/);
|
|
106
|
+
if (m) paths.push({ tool: tool.name, raw: m[1] || m[2], resolved: expandTilde(m[1] || m[2]) });
|
|
107
|
+
}
|
|
108
|
+
if (paths.length === 0) {
|
|
109
|
+
return { ok: null, summary: 'unknown', detail: 'No hook commands found — cannot determine install mode' };
|
|
110
|
+
}
|
|
111
|
+
const modes = paths.map(p => {
|
|
112
|
+
if (p.resolved.startsWith(copyDir)) return 'copy';
|
|
113
|
+
return 'repo-path';
|
|
114
|
+
});
|
|
115
|
+
const unique = [...new Set(modes)];
|
|
116
|
+
const mode = unique.length === 1 ? unique[0] : 'mixed';
|
|
117
|
+
const detail = paths.map(p => {
|
|
118
|
+
const m = p.resolved.startsWith(copyDir) ? 'copy' : 'repo-path';
|
|
119
|
+
return ` ${p.tool}: ${m} (${p.raw})`;
|
|
120
|
+
}).join('\n');
|
|
121
|
+
|
|
122
|
+
if (mode === 'copy') {
|
|
123
|
+
return { ok: true, summary: 'copy (~/.ai-lens/client/)', detail: `Client files copied to ~/.ai-lens/client/\nUpdate: npx -y ai-lens init --yes\n${detail}` };
|
|
124
|
+
}
|
|
125
|
+
if (mode === 'repo-path') {
|
|
126
|
+
return { ok: true, summary: 'repo-path (auto-update on git pull)', detail: `Hooks point directly to repo/package source\nUpdate: git pull (or npx cache refresh)\n${detail}` };
|
|
127
|
+
}
|
|
128
|
+
return { ok: null, summary: `mixed (${unique.join(' + ')})`, detail: `Different tools use different install modes:\n${detail}` };
|
|
129
|
+
}
|
|
130
|
+
|
|
95
131
|
function validateHookCommandPaths(tool) {
|
|
96
132
|
const command = extractHookCommand(tool);
|
|
97
133
|
if (!command) return null;
|
|
@@ -1059,43 +1095,83 @@ function buildReport(results, timestamp, warnings = [], allTools = TOOL_CONFIGS)
|
|
|
1059
1095
|
return lines.join('\n');
|
|
1060
1096
|
}
|
|
1061
1097
|
|
|
1098
|
+
// ---------------------------------------------------------------------------
|
|
1099
|
+
// Report mode: POST structured status to server
|
|
1100
|
+
// ---------------------------------------------------------------------------
|
|
1101
|
+
|
|
1102
|
+
async function sendStatusReport(results, warnings, clientVersion, clientCommit, serverUrl, authToken) {
|
|
1103
|
+
if (!serverUrl || !authToken) return;
|
|
1104
|
+
|
|
1105
|
+
const payload = {
|
|
1106
|
+
timestamp: new Date().toISOString(),
|
|
1107
|
+
client_version: clientVersion,
|
|
1108
|
+
client_commit: clientCommit,
|
|
1109
|
+
node_version: process.version,
|
|
1110
|
+
os: `${process.platform} ${osRelease()} ${osArch()}`,
|
|
1111
|
+
checks: results.map(({ label, ok, summary, detail }) => ({ label, ok, summary, detail })),
|
|
1112
|
+
warnings: warnings.map(({ msg, action }) => ({ msg, action })),
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
try {
|
|
1116
|
+
const res = await fetch(`${serverUrl}/api/client-reports`, {
|
|
1117
|
+
method: 'POST',
|
|
1118
|
+
headers: {
|
|
1119
|
+
'Content-Type': 'application/json',
|
|
1120
|
+
'X-Auth-Token': authToken,
|
|
1121
|
+
'X-Client-Version': `${clientVersion}+${clientCommit}`,
|
|
1122
|
+
},
|
|
1123
|
+
body: JSON.stringify(payload),
|
|
1124
|
+
signal: AbortSignal.timeout(15_000),
|
|
1125
|
+
});
|
|
1126
|
+
if (res.ok) {
|
|
1127
|
+
try { writeFileSync(LAST_STATUS_REPORT_PATH, new Date().toISOString()); } catch {}
|
|
1128
|
+
}
|
|
1129
|
+
} catch {
|
|
1130
|
+
// Silent — report is best-effort
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1062
1134
|
// ---------------------------------------------------------------------------
|
|
1063
1135
|
// Main
|
|
1064
1136
|
// ---------------------------------------------------------------------------
|
|
1065
1137
|
|
|
1066
|
-
export default async function status() {
|
|
1138
|
+
export default async function status({ report = false } = {}) {
|
|
1067
1139
|
const versionResult = checkVersion();
|
|
1068
|
-
initLogger(versionResult.summary);
|
|
1140
|
+
if (!report) initLogger(versionResult.summary);
|
|
1069
1141
|
|
|
1070
1142
|
const { version, commit } = getVersionInfo();
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1143
|
+
if (!report) {
|
|
1144
|
+
blank();
|
|
1145
|
+
info(`${BOLD}AI Lens Status v${version} (${commit})${RESET}`);
|
|
1146
|
+
info('='.repeat(40));
|
|
1147
|
+
blank();
|
|
1148
|
+
}
|
|
1075
1149
|
|
|
1076
1150
|
const results = [];
|
|
1077
1151
|
|
|
1078
1152
|
function printLine(label, result) {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1153
|
+
if (!report) {
|
|
1154
|
+
const icon = result.ok === true ? CHECK : result.ok === false ? CROSS : `${DIM}-${RESET}`;
|
|
1155
|
+
const pad = ' '.repeat(Math.max(1, 17 - label.length));
|
|
1156
|
+
info(`${label}:${pad}${icon} ${result.summary}`);
|
|
1157
|
+
}
|
|
1082
1158
|
results.push({ label, ...result });
|
|
1083
1159
|
}
|
|
1084
1160
|
|
|
1085
1161
|
// 1. System info
|
|
1086
1162
|
const sys = checkSystem();
|
|
1087
|
-
info(`${'System:'} ${sys.summary}`);
|
|
1163
|
+
if (!report) info(`${'System:'} ${sys.summary}`);
|
|
1088
1164
|
results.push({ label: 'System', ...sys });
|
|
1089
1165
|
|
|
1090
1166
|
// 2. Tool versions
|
|
1091
1167
|
const claude = checkToolVersion('Claude');
|
|
1092
1168
|
const cursor = checkToolVersion('Cursor');
|
|
1093
1169
|
if (claude.ok !== null) {
|
|
1094
|
-
info(`${'Claude Code:'} ${claude.summary}`);
|
|
1170
|
+
if (!report) info(`${'Claude Code:'} ${claude.summary}`);
|
|
1095
1171
|
results.push({ label: 'Claude Code version', ...claude });
|
|
1096
1172
|
}
|
|
1097
1173
|
if (cursor.ok !== null) {
|
|
1098
|
-
info(`${'Cursor:'} ${cursor.summary}`);
|
|
1174
|
+
if (!report) info(`${'Cursor:'} ${cursor.summary}`);
|
|
1099
1175
|
results.push({ label: 'Cursor version', ...cursor });
|
|
1100
1176
|
}
|
|
1101
1177
|
|
|
@@ -1155,6 +1231,10 @@ export default async function status() {
|
|
|
1155
1231
|
detail: `Global hooks active: ${hasGlobalHooks}\nProject hooks active: ${hasProjectHooks}${hasGlobalHooks && hasProjectHooks ? '\nWarning: both global and project hooks are active — events may be captured twice. Run init with --project-hooks to consolidate.' : ''}`,
|
|
1156
1232
|
});
|
|
1157
1233
|
|
|
1234
|
+
// 6c. Install mode: detect how capture.js is referenced in hooks
|
|
1235
|
+
const installMode = detectInstallMode(toolsWithProject);
|
|
1236
|
+
printLine('Install mode', installMode);
|
|
1237
|
+
|
|
1158
1238
|
// 7. Queue (before capture test so test event doesn't show as pending)
|
|
1159
1239
|
const queueResult = checkQueue();
|
|
1160
1240
|
printLine('Queue', queueResult);
|
|
@@ -1200,7 +1280,7 @@ export default async function status() {
|
|
|
1200
1280
|
serverReachable: serverResult.ok === true,
|
|
1201
1281
|
});
|
|
1202
1282
|
|
|
1203
|
-
if (warnings.length > 0) {
|
|
1283
|
+
if (!report && warnings.length > 0) {
|
|
1204
1284
|
blank();
|
|
1205
1285
|
info('='.repeat(40));
|
|
1206
1286
|
info(`${BOLD}Warnings${RESET}`);
|
|
@@ -1211,19 +1291,23 @@ export default async function status() {
|
|
|
1211
1291
|
}
|
|
1212
1292
|
}
|
|
1213
1293
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1294
|
+
if (report) {
|
|
1295
|
+
// --report mode: POST structured JSON to server, update marker file
|
|
1296
|
+
await sendStatusReport(results, warnings, version, commit, serverUrl, authToken);
|
|
1297
|
+
} else {
|
|
1298
|
+
// Normal mode: write text report file
|
|
1299
|
+
const timestamp = new Date().toISOString();
|
|
1300
|
+
// Merge global TOOL_CONFIGS (always listed, even if not installed) with project tools
|
|
1301
|
+
const allToolsForReport = [...TOOL_CONFIGS, ...toolsWithProject.filter(t => !TOOL_CONFIGS.includes(t))];
|
|
1302
|
+
const reportText = buildReport(results, timestamp, warnings, allToolsForReport);
|
|
1303
|
+
try {
|
|
1304
|
+
writeFileSync(REPORT_PATH, reportText);
|
|
1305
|
+
blank();
|
|
1306
|
+
info(`${DIM}Full report \u2192 ${REPORT_PATH}${RESET}`);
|
|
1307
|
+
} catch (err) {
|
|
1308
|
+
blank();
|
|
1309
|
+
error(`Could not write report: ${err.message}`);
|
|
1310
|
+
}
|
|
1224
1311
|
blank();
|
|
1225
|
-
error(`Could not write report: ${err.message}`);
|
|
1226
1312
|
}
|
|
1227
|
-
|
|
1228
|
-
blank();
|
|
1229
1313
|
}
|
package/client/config.js
CHANGED
|
@@ -22,6 +22,7 @@ export const GIT_REMOTES_DIR = join(DATA_DIR, 'git-remotes');
|
|
|
22
22
|
export const LOG_PATH = join(DATA_DIR, 'sender.log');
|
|
23
23
|
export const CAPTURE_LOG_PATH = join(DATA_DIR, 'capture.log');
|
|
24
24
|
export const SENDER_BACKOFF_PATH = join(DATA_DIR, 'sender-backoff.json');
|
|
25
|
+
export const LAST_STATUS_REPORT_PATH = join(DATA_DIR, 'last-status-report');
|
|
25
26
|
export const LOG_MAX_AGE_DAYS = 30;
|
|
26
27
|
const GIT_ROOT_CACHE = new Map();
|
|
27
28
|
// Pipe stderr (instead of inheriting it) so that "fatal: not a git repository"
|
package/client/sender.js
CHANGED
|
@@ -12,16 +12,18 @@
|
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
14
|
readFileSync, writeFileSync, writeSync, unlinkSync, rmSync, renameSync, realpathSync,
|
|
15
|
-
readdirSync, mkdirSync, existsSync, openSync, closeSync,
|
|
15
|
+
readdirSync, mkdirSync, existsSync, openSync, closeSync, statSync,
|
|
16
16
|
} from 'node:fs';
|
|
17
17
|
import { join, dirname } from 'node:path';
|
|
18
18
|
import { randomUUID } from 'node:crypto';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { spawn } from 'node:child_process';
|
|
20
21
|
import {
|
|
21
22
|
ensureDataDir,
|
|
22
23
|
PENDING_DIR,
|
|
23
24
|
SENDING_DIR,
|
|
24
25
|
SENDER_BACKOFF_PATH,
|
|
26
|
+
LAST_STATUS_REPORT_PATH,
|
|
25
27
|
STORAGE_VERSION_PATH,
|
|
26
28
|
CURRENT_STORAGE_VERSION,
|
|
27
29
|
QUEUE_PATH,
|
|
@@ -633,6 +635,49 @@ async function postEvents(serverUrl, events, identity, authToken) {
|
|
|
633
635
|
throw new Error(`Server responded ${res.status}: ${data}`);
|
|
634
636
|
}
|
|
635
637
|
|
|
638
|
+
// =============================================================================
|
|
639
|
+
// Status report (once per 24h)
|
|
640
|
+
// =============================================================================
|
|
641
|
+
|
|
642
|
+
const STATUS_REPORT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
643
|
+
|
|
644
|
+
function resolveStatusBin() {
|
|
645
|
+
// 1. Relative to sender.js — works for --use-repo-path installs
|
|
646
|
+
const relBin = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'ai-lens.js');
|
|
647
|
+
if (existsSync(relBin)) return relBin;
|
|
648
|
+
// 2. packageRoot from version.json — works for copy installs (npx, global)
|
|
649
|
+
try {
|
|
650
|
+
const { packageRoot } = getClientVersion();
|
|
651
|
+
if (packageRoot) {
|
|
652
|
+
const pkgBin = join(packageRoot, 'bin', 'ai-lens.js');
|
|
653
|
+
if (existsSync(pkgBin)) return pkgBin;
|
|
654
|
+
}
|
|
655
|
+
} catch {}
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function trySpawnStatusReport() {
|
|
660
|
+
try {
|
|
661
|
+
if (existsSync(LAST_STATUS_REPORT_PATH)) {
|
|
662
|
+
const mtime = statSync(LAST_STATUS_REPORT_PATH).mtimeMs;
|
|
663
|
+
if (Date.now() - mtime < STATUS_REPORT_INTERVAL_MS) return;
|
|
664
|
+
}
|
|
665
|
+
const binPath = resolveStatusBin();
|
|
666
|
+
if (!binPath) return;
|
|
667
|
+
// Touch marker immediately to prevent multiple spawns
|
|
668
|
+
writeFileSync(LAST_STATUS_REPORT_PATH, new Date().toISOString());
|
|
669
|
+
const child = spawn(process.execPath, [binPath, 'status', '--report'], {
|
|
670
|
+
detached: true,
|
|
671
|
+
stdio: 'ignore',
|
|
672
|
+
windowsHide: true,
|
|
673
|
+
});
|
|
674
|
+
child.on('error', () => {});
|
|
675
|
+
child.unref();
|
|
676
|
+
} catch {
|
|
677
|
+
// Best-effort — don't break sender
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
636
681
|
// =============================================================================
|
|
637
682
|
// Main
|
|
638
683
|
// =============================================================================
|
|
@@ -698,6 +743,7 @@ async function main() {
|
|
|
698
743
|
}
|
|
699
744
|
clearSenderBackoff();
|
|
700
745
|
commitQueue(sendingDir, acquiredFiles);
|
|
746
|
+
trySpawnStatusReport();
|
|
701
747
|
} catch (err) {
|
|
702
748
|
const unsentIds = new Set(
|
|
703
749
|
events.filter(e => !sentEventIds.has(e.event_id)).map(e => e.event_id).filter(Boolean)
|