ai-lens 0.8.43 → 0.8.45
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/cli/hooks.js +26 -18
- package/cli/status.js +2 -1
- package/client/capture.js +63 -0
- package/client/redact.js +77 -4
- package/client/sender.js +1 -1
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
0dbf7ca
|
package/cli/hooks.js
CHANGED
|
@@ -115,9 +115,16 @@ export function captureCommand(useTilde = false, rawPath = false, customPath = n
|
|
|
115
115
|
? '~/.ai-lens/client/capture.js'
|
|
116
116
|
: CAPTURE_PATH.replace(/\\/g, '/');
|
|
117
117
|
const pathPart = (customPath != null || rawPath) ? capturePath : shellEscape(capturePath);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
if (nodePath === '/usr/bin/env node') return `/usr/bin/env node ${pathPart}`;
|
|
119
|
+
// Claude Code on Windows executes hooks via cmd.exe. A quoted executable path
|
|
120
|
+
// at the start of the line (e.g. "C:/Program Files/node.exe" args) is not
|
|
121
|
+
// recognised as a command by cmd.exe. Use bare `node` for Claude Code hooks
|
|
122
|
+
// (rawPath=true) when the path contains spaces; Cursor hooks use PowerShell
|
|
123
|
+
// where quoting works fine.
|
|
124
|
+
if (process.platform === 'win32' && rawPath && nodePath.includes(' ')) {
|
|
125
|
+
return `node ${pathPart}`;
|
|
126
|
+
}
|
|
127
|
+
return `${shellEscape(nodePath.replace(/\\/g, '/'))} ${pathPart}`;
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
// Cursor on Windows executes hooks via PowerShell, which treats a quoted path like
|
|
@@ -193,22 +200,23 @@ const CLAUDE_CODE_HOOKS = {
|
|
|
193
200
|
SubagentStop: () => ({ matcher: '', hooks: [{ type: 'command', command: captureCommand(true, true) }] }),
|
|
194
201
|
};
|
|
195
202
|
|
|
196
|
-
// Use
|
|
203
|
+
// Use absolute path — Cursor does not expand ~ in hook commands (it passes the
|
|
204
|
+
// literal string to Node, which resolves it relative to CWD).
|
|
197
205
|
const CURSOR_HOOKS = {
|
|
198
|
-
sessionStart: () => ({ command: cursorCaptureCommand(
|
|
199
|
-
beforeSubmitPrompt: () => ({ command: cursorCaptureCommand(
|
|
200
|
-
postToolUse: () => ({ command: cursorCaptureCommand(
|
|
201
|
-
postToolUseFailure: () => ({ command: cursorCaptureCommand(
|
|
202
|
-
afterFileEdit: () => ({ command: cursorCaptureCommand(
|
|
203
|
-
afterShellExecution: () => ({ command: cursorCaptureCommand(
|
|
204
|
-
afterMCPExecution: () => ({ command: cursorCaptureCommand(
|
|
205
|
-
subagentStart: () => ({ command: cursorCaptureCommand(
|
|
206
|
-
subagentStop: () => ({ command: cursorCaptureCommand(
|
|
207
|
-
preCompact: () => ({ command: cursorCaptureCommand(
|
|
208
|
-
afterAgentResponse: () => ({ command: cursorCaptureCommand(
|
|
209
|
-
afterAgentThought: () => ({ command: cursorCaptureCommand(
|
|
210
|
-
stop: () => ({ command: cursorCaptureCommand(
|
|
211
|
-
sessionEnd: () => ({ command: cursorCaptureCommand(
|
|
206
|
+
sessionStart: () => ({ command: cursorCaptureCommand(false) }),
|
|
207
|
+
beforeSubmitPrompt: () => ({ command: cursorCaptureCommand(false) }),
|
|
208
|
+
postToolUse: () => ({ command: cursorCaptureCommand(false) }),
|
|
209
|
+
postToolUseFailure: () => ({ command: cursorCaptureCommand(false) }),
|
|
210
|
+
afterFileEdit: () => ({ command: cursorCaptureCommand(false) }),
|
|
211
|
+
afterShellExecution: () => ({ command: cursorCaptureCommand(false) }),
|
|
212
|
+
afterMCPExecution: () => ({ command: cursorCaptureCommand(false) }),
|
|
213
|
+
subagentStart: () => ({ command: cursorCaptureCommand(false) }),
|
|
214
|
+
subagentStop: () => ({ command: cursorCaptureCommand(false) }),
|
|
215
|
+
preCompact: () => ({ command: cursorCaptureCommand(false) }),
|
|
216
|
+
afterAgentResponse: () => ({ command: cursorCaptureCommand(false) }),
|
|
217
|
+
afterAgentThought: () => ({ command: cursorCaptureCommand(false) }),
|
|
218
|
+
stop: () => ({ command: cursorCaptureCommand(false) }),
|
|
219
|
+
sessionEnd: () => ({ command: cursorCaptureCommand(false) }),
|
|
212
220
|
};
|
|
213
221
|
|
|
214
222
|
// Same as CURSOR_HOOKS but command uses ~/.ai-lens/... (for --project-hooks)
|
package/cli/status.js
CHANGED
|
@@ -323,11 +323,12 @@ function checkSystem() {
|
|
|
323
323
|
const arch = osArch() || 'unknown';
|
|
324
324
|
const nodeVersion = process.version || 'unknown';
|
|
325
325
|
const platform = process.platform === 'darwin' ? 'macOS' : process.platform;
|
|
326
|
+
const cwd = process.cwd();
|
|
326
327
|
const summary = `${platform} ${osVersion} ${arch}, Node ${nodeVersion}`;
|
|
327
328
|
return {
|
|
328
329
|
ok: true,
|
|
329
330
|
summary,
|
|
330
|
-
detail: `Platform: ${platform}\nOS version: ${osVersion}\nArchitecture: ${arch}\nNode: ${nodeVersion}`,
|
|
331
|
+
detail: `Platform: ${platform}\nOS version: ${osVersion}\nArchitecture: ${arch}\nNode: ${nodeVersion}\nCWD: ${cwd}`,
|
|
331
332
|
};
|
|
332
333
|
}
|
|
333
334
|
|
package/client/capture.js
CHANGED
|
@@ -191,6 +191,55 @@ function getCachedSessionPath(sessionId) {
|
|
|
191
191
|
try { return readFileSync(dst, 'utf-8').trim() || null; } catch { return null; }
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// =============================================================================
|
|
195
|
+
// Project Path Refinement from Tool Events
|
|
196
|
+
// =============================================================================
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Extract an absolute file path from tool input.
|
|
200
|
+
* Covers Read/Edit/Write (file_path), Glob/Grep (path).
|
|
201
|
+
*/
|
|
202
|
+
function extractFilePath(toolInput) {
|
|
203
|
+
if (!toolInput || typeof toolInput !== 'object') return null;
|
|
204
|
+
const p = toolInput.file_path || toolInput.path;
|
|
205
|
+
return (typeof p === 'string' && p.startsWith('/')) ? p : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Walk up from a path to find the nearest .git directory.
|
|
210
|
+
* Returns the git root (parent of .git) or null.
|
|
211
|
+
*/
|
|
212
|
+
function findGitRoot(filePath) {
|
|
213
|
+
let dir = dirname(filePath);
|
|
214
|
+
while (dir && dir !== '/' && dir.length > 1) {
|
|
215
|
+
try {
|
|
216
|
+
if (existsSync(join(dir, '.git'))) return dir;
|
|
217
|
+
} catch {}
|
|
218
|
+
const parent = dirname(dir);
|
|
219
|
+
if (parent === dir) break;
|
|
220
|
+
dir = parent;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Refine project_path using file paths from tool events.
|
|
227
|
+
* Picks the deepest (most specific) git root — correct for nested repos
|
|
228
|
+
* like etl-pipelines/bk-reports-automation/.
|
|
229
|
+
* Only refines "downward" (more specific), never upward.
|
|
230
|
+
*/
|
|
231
|
+
function refineProjectPath(event, projectPath, sessionId) {
|
|
232
|
+
const toolInput = event.tool_input || event.input;
|
|
233
|
+
const filePath = extractFilePath(toolInput);
|
|
234
|
+
if (!filePath) return projectPath;
|
|
235
|
+
const gitRoot = findGitRoot(filePath);
|
|
236
|
+
if (gitRoot && (!projectPath || gitRoot.length > projectPath.length)) {
|
|
237
|
+
if (sessionId) cacheSessionPath(sessionId, gitRoot);
|
|
238
|
+
return gitRoot;
|
|
239
|
+
}
|
|
240
|
+
return projectPath;
|
|
241
|
+
}
|
|
242
|
+
|
|
194
243
|
// =============================================================================
|
|
195
244
|
// Deduplication: drop consecutive identical event types per session
|
|
196
245
|
// =============================================================================
|
|
@@ -354,6 +403,11 @@ function normalizeClaudeCode(event) {
|
|
|
354
403
|
data = { hook: hookType };
|
|
355
404
|
}
|
|
356
405
|
|
|
406
|
+
// Refine project_path from file paths in tool events (ANL-518)
|
|
407
|
+
if (hookType === 'PostToolUse' || hookType === 'PostToolUseFailure') {
|
|
408
|
+
projectPath = refineProjectPath(event, projectPath, sessionId);
|
|
409
|
+
}
|
|
410
|
+
|
|
357
411
|
if (event.permission_mode) {
|
|
358
412
|
data.permission_mode = event.permission_mode;
|
|
359
413
|
}
|
|
@@ -523,6 +577,15 @@ function normalizeCursor(event) {
|
|
|
523
577
|
data = { hook_event_name: hookName };
|
|
524
578
|
}
|
|
525
579
|
|
|
580
|
+
// Refine project_path from file paths in tool events (ANL-518)
|
|
581
|
+
if (hookName === 'postToolUse' || hookName === 'postToolUseFailure' || hookName === 'afterFileEdit') {
|
|
582
|
+
// Cursor passes file_path at top level for afterFileEdit
|
|
583
|
+
const cursorEvent = hookName === 'afterFileEdit' && event.file_path
|
|
584
|
+
? { ...event, tool_input: { file_path: event.file_path } }
|
|
585
|
+
: event;
|
|
586
|
+
projectPath = refineProjectPath(cursorEvent, projectPath, sessionId);
|
|
587
|
+
}
|
|
588
|
+
|
|
526
589
|
if (event.permission_mode) {
|
|
527
590
|
data.permission_mode = event.permission_mode;
|
|
528
591
|
}
|
package/client/redact.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Secret redaction utility (client-side copy).
|
|
3
|
+
* ⚠️ KEEP IN SYNC with server/utils/redact.js — see test/server/redact.test.js sync test.
|
|
3
4
|
* Replaces tokens, passwords, API keys and other secrets with [REDACTED:TYPE] tags.
|
|
4
5
|
* Standalone — no imports, safe to run on developer machines.
|
|
5
6
|
*/
|
|
@@ -15,15 +16,63 @@ const PATTERNS = [
|
|
|
15
16
|
// GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_)
|
|
16
17
|
{ type: 'GITHUB_TOKEN', re: /gh[pousr]_[A-Za-z0-9_]{36,}/g },
|
|
17
18
|
|
|
19
|
+
// GitLab tokens (glpat-, gldt-, glrt-, glcbt-, glsoat-)
|
|
20
|
+
{
|
|
21
|
+
type: 'GITLAB_TOKEN',
|
|
22
|
+
re: /gl(?:pat|dt|rt|cbt|soat)-[A-Za-z0-9_-]{20,}/g,
|
|
23
|
+
replacer: (m) => {
|
|
24
|
+
const prefix = m.slice(0, m.indexOf('-'));
|
|
25
|
+
const typeMap = { glpat: 'GITLAB_PAT', gldt: 'GITLAB_DEPLOY_TOKEN', glrt: 'GITLAB_RUNNER_TOKEN', glcbt: 'GITLAB_CLUSTER_TOKEN', glsoat: 'GITLAB_OAUTH_TOKEN' };
|
|
26
|
+
return `[REDACTED:${typeMap[prefix] || 'GITLAB_TOKEN'}]`;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
|
|
18
30
|
// Anthropic API key
|
|
19
31
|
{ type: 'ANTHROPIC_KEY', re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
|
|
20
32
|
|
|
21
33
|
// OpenAI project key
|
|
22
34
|
{ type: 'OPENAI_KEY', re: /sk-proj-[A-Za-z0-9_-]{20,}/g },
|
|
23
35
|
|
|
36
|
+
// Google API key (AIzaSy...)
|
|
37
|
+
{ type: 'GOOGLE_API_KEY', re: /AIza[A-Za-z0-9_-]{35}/g },
|
|
38
|
+
|
|
39
|
+
// Google OAuth access token (ya29....)
|
|
40
|
+
{ type: 'GOOGLE_OAUTH', re: /ya29\.[A-Za-z0-9_-]{50,}/g },
|
|
41
|
+
|
|
42
|
+
// GCP service account email (project-id@project-id.iam.gserviceaccount.com)
|
|
43
|
+
{ type: 'GCP_SERVICE_ACCOUNT', re: /[a-z0-9-]+@[a-z0-9-]+\.iam\.gserviceaccount\.com/g },
|
|
44
|
+
|
|
45
|
+
// x-api-key header value
|
|
46
|
+
{
|
|
47
|
+
type: 'API_KEY_HEADER',
|
|
48
|
+
re: /(["']?x-api-key["']?\s*[:=]\s*["']?)[^\s"',)}\]]+/gi,
|
|
49
|
+
replacer: (m, prefix) => `${prefix}[REDACTED:API_KEY_HEADER]`,
|
|
50
|
+
},
|
|
51
|
+
|
|
24
52
|
// Generic sk- key (must come after sk-ant- and sk-proj-)
|
|
25
53
|
{ type: 'API_KEY', re: /sk-[A-Za-z0-9_-]{32,}/g },
|
|
26
54
|
|
|
55
|
+
// Stripe secret key (live only — test keys intentionally not redacted)
|
|
56
|
+
{ type: 'STRIPE_SECRET', re: /sk_live_[A-Za-z0-9]{24,}/g },
|
|
57
|
+
|
|
58
|
+
// Stripe restricted key
|
|
59
|
+
{ type: 'STRIPE_RESTRICTED', re: /rk_live_[A-Za-z0-9]{24,}/g },
|
|
60
|
+
|
|
61
|
+
// npm token
|
|
62
|
+
{ type: 'NPM_TOKEN', re: /npm_[A-Za-z0-9]{36,}/g },
|
|
63
|
+
|
|
64
|
+
// PyPI token
|
|
65
|
+
{ type: 'PYPI_TOKEN', re: /pypi-[A-Za-z0-9_-]{16,}/g },
|
|
66
|
+
|
|
67
|
+
// SendGrid API key (SG.{22}.{43})
|
|
68
|
+
{ type: 'SENDGRID_KEY', re: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g },
|
|
69
|
+
|
|
70
|
+
// Twilio API key (SK + 32 lowercase hex chars)
|
|
71
|
+
{ type: 'TWILIO_KEY', re: /SK[a-f0-9]{32}/g },
|
|
72
|
+
|
|
73
|
+
// Telegram bot token (8-10 digit bot ID : 35-char secret)
|
|
74
|
+
{ type: 'TELEGRAM_BOT_TOKEN', re: /\b\d{8,10}:[A-Za-z0-9_-]{35}\b/g },
|
|
75
|
+
|
|
27
76
|
// Slack tokens (xox{b,p,a,o,s}-)
|
|
28
77
|
{ type: 'SLACK_TOKEN', re: /xo[xabposr]{1,2}-[A-Za-z0-9-]{10,}/g },
|
|
29
78
|
|
|
@@ -36,22 +85,46 @@ const PATTERNS = [
|
|
|
36
85
|
// PEM private keys — full block or truncated (just the header + base64 content)
|
|
37
86
|
{ type: 'PRIVATE_KEY', re: /-----BEGIN[A-Z ]*PRIVATE KEY-----[A-Za-z0-9+/=\s\\.]*(?:-----END[A-Z ]*PRIVATE KEY-----)?/g },
|
|
38
87
|
|
|
88
|
+
// Meilisearch master/API key (mc_ + 32+ hex chars)
|
|
89
|
+
{ type: 'MEILISEARCH_KEY', re: /\bmc_[0-9a-f]{32,}\b/g },
|
|
90
|
+
|
|
39
91
|
// Connection string password (://user:password@host) — redacts password only
|
|
40
92
|
{ type: 'CONNECTION_STRING', re: /:\/\/([^:@\s]+):([^@\s]{3,})@/g, replacer: (m, user, _pw) => `://${user}:[REDACTED:CONNECTION_STRING]@` },
|
|
41
93
|
|
|
42
|
-
//
|
|
43
|
-
|
|
94
|
+
// MySQL/mysqldump CLI -p<password> syntax (mysql -u user -pSECRET dbname)
|
|
95
|
+
{ type: 'MYSQL_PASSWORD', re: /(\bmysql\w*\b[^\n]*\s-p)(\S{8,})/g, replacer: (m, prefix, _pw) => `${prefix}[REDACTED:MYSQL_PASSWORD]` },
|
|
96
|
+
|
|
97
|
+
// Environment variables with PASSWORD/PASSWD keyword — lower 6-char minimum
|
|
98
|
+
// Catches BASIC_AUTH_PASSWORD=236716, db_password=secret, etc.
|
|
99
|
+
{
|
|
100
|
+
type: 'ENV_VAR',
|
|
101
|
+
re: /([A-Za-z_]{0,50}(?:PASSWORD|PASSWD)[A-Za-z_]{0,50}\s*=\s*["']?)([^\s"';\\\`|>{}\[\]]{6,})/gi,
|
|
102
|
+
replacer: (m, prefix, _val) => `${prefix}[REDACTED:ENV_VAR]`,
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// Environment variables with other secret keywords — standard 8-char minimum
|
|
106
|
+
// Catches aws_secret_access_key=..., AI_LENS_AUTH_TOKEN=..., RUSTAT_KEY=..., etc.
|
|
107
|
+
// Case-insensitive to catch lowercase config files. _KEY added for API key vars.
|
|
44
108
|
// Skips template refs like ${VAR} since value excludes { } chars.
|
|
45
109
|
{
|
|
46
110
|
type: 'ENV_VAR',
|
|
47
|
-
re: /([A-
|
|
111
|
+
re: /([A-Za-z_]{0,50}(?:SECRET|TOKEN|API_KEY|APIKEY|_KEY)[A-Za-z_]{0,50}\s*=\s*["']?)([^\s"';\\\`|>{}\[\]]{8,})/gi,
|
|
48
112
|
replacer: (m, prefix, _val) => `${prefix}[REDACTED:ENV_VAR]`,
|
|
49
113
|
},
|
|
50
114
|
|
|
51
115
|
// Key-value pairs: password=..., token: ..., etc.
|
|
116
|
+
// Uses \w* prefix/suffix to match compound names like aws_secret_access_key
|
|
117
|
+
{
|
|
118
|
+
type: 'KEY_VALUE',
|
|
119
|
+
re: /(\b\w{0,30}(?:password|passwd|token|secret|api_key|apikey|api_secret|access_token|auth_token|private_key|client_secret|credential|authorization|session_key|encryption_key|signing_key|refresh_token|id_token)\w{0,30}["']?\s*[:=]\s*["']?)([^\s"',;\[\]{}]{8,})/gi,
|
|
120
|
+
replacer: (m, prefix, _val) => `${prefix}[REDACTED:KEY_VALUE]`,
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// Russian key-value pairs: пароль=..., секрет: ..., токен: ..., ключ: ...
|
|
124
|
+
// Separate pattern because \b does not work with Cyrillic (\w is ASCII-only)
|
|
52
125
|
{
|
|
53
126
|
type: 'KEY_VALUE',
|
|
54
|
-
re: /(\
|
|
127
|
+
re: /((?:^|[\s"',:={}([\-])(?:пароль|парол[а-я]*|секрет[а-я]*|токен[а-я]*|ключ[а-я]*)["']?\s*[:=\-—–]?\s*["']?)([^\s"',;\[\]{}]{6,})/gi,
|
|
55
128
|
replacer: (m, prefix, _val) => `${prefix}[REDACTED:KEY_VALUE]`,
|
|
56
129
|
},
|
|
57
130
|
];
|
package/client/sender.js
CHANGED
|
@@ -60,7 +60,7 @@ export function rotateLog(logPath = LOG_PATH, maxAgeDays = LOG_MAX_AGE_DAYS) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export const MAX_QUEUE_SIZE = 10_000;
|
|
63
|
-
export const MAX_CHUNK_BYTES =
|
|
63
|
+
export const MAX_CHUNK_BYTES = 512 * 1024; // 512 KB per POST — small chunks avoid ECONNRESET on corporate proxies/TLS inspection
|
|
64
64
|
export const LOCK_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
|
|
65
65
|
|
|
66
66
|
// =============================================================================
|