litmus-cli 1.0.19 → 1.0.20
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/dist/lib/hook-logger.cjs +7 -2
- package/dist/lib/watcher.cjs +178 -0
- package/package.json +1 -1
package/dist/lib/hook-logger.cjs
CHANGED
|
@@ -146,7 +146,12 @@ async function main() {
|
|
|
146
146
|
const raw = await readStdin()
|
|
147
147
|
if (!raw.trim()) return
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
// Detect Cursor firing Claude Code hooks — Cursor sets CURSOR_TRACE_ID
|
|
150
|
+
const effectiveTool = (tool === "claude" && process.env.CURSOR_TRACE_ID)
|
|
151
|
+
? "cursor"
|
|
152
|
+
: tool
|
|
153
|
+
|
|
154
|
+
const { prompt, sessionId } = extractPrompt(raw, effectiveTool)
|
|
150
155
|
if (!prompt) return
|
|
151
156
|
|
|
152
157
|
const truncated = prompt.length > MAX_PROMPT_LENGTH
|
|
@@ -156,7 +161,7 @@ async function main() {
|
|
|
156
161
|
const event = {
|
|
157
162
|
ts: new Date().toISOString(),
|
|
158
163
|
type: "ai_prompt",
|
|
159
|
-
tool,
|
|
164
|
+
tool: effectiveTool,
|
|
160
165
|
prompt: truncated,
|
|
161
166
|
}
|
|
162
167
|
if (sessionId) event.sessionId = sessionId
|
package/dist/lib/watcher.cjs
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
const fs = require("fs")
|
|
18
18
|
const path = require("path")
|
|
19
|
+
const crypto = require("crypto")
|
|
19
20
|
const { execSync, execFile } = require("child_process")
|
|
21
|
+
const dns = require("dns")
|
|
20
22
|
const https = require("https")
|
|
21
23
|
const http = require("http")
|
|
22
24
|
|
|
@@ -122,7 +124,16 @@ function shouldIgnore(relPath) {
|
|
|
122
124
|
return IGNORED.some((re) => re.test(relPath))
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
// ── Hash chain for tamper evidence ──────────────────────────────
|
|
128
|
+
let prevHash = "0".repeat(64) // genesis
|
|
129
|
+
let nextSeq = 0
|
|
130
|
+
|
|
125
131
|
function emit(event) {
|
|
132
|
+
event._seq = nextSeq++
|
|
133
|
+
event._prevHash = prevHash
|
|
134
|
+
const hash = crypto.createHash("sha256").update(JSON.stringify(event)).digest("hex")
|
|
135
|
+
event._hash = hash
|
|
136
|
+
prevHash = hash
|
|
126
137
|
log.write(JSON.stringify(event) + "\n")
|
|
127
138
|
if (uploadConfig && uploadBuffer.length < MAX_BUFFER_SIZE) {
|
|
128
139
|
uploadBuffer.push(event)
|
|
@@ -195,6 +206,10 @@ process.on("uncaughtException", (err) => {
|
|
|
195
206
|
try {
|
|
196
207
|
const watcher = fs.watch(projectDir, { recursive: true }, (eventType, filename) => {
|
|
197
208
|
if (!filename) return
|
|
209
|
+
|
|
210
|
+
// Check for AI artifact files before applying ignore filter (artifacts are dotfiles)
|
|
211
|
+
checkArtifact(filename)
|
|
212
|
+
|
|
198
213
|
if (shouldIgnore(filename)) return
|
|
199
214
|
|
|
200
215
|
// Check for burst edits on raw events (before debounce — needs real timing)
|
|
@@ -322,6 +337,16 @@ const AI_TOOL_PATTERNS = {
|
|
|
322
337
|
codeium: /\bcodeium\b/,
|
|
323
338
|
codex: /\bcodex\b/,
|
|
324
339
|
copilot: /\bcopilot\b/,
|
|
340
|
+
windsurf: /\bwindsurf\b/,
|
|
341
|
+
ollama: /\bollama\b/,
|
|
342
|
+
tabnine: /\btabnine\b/,
|
|
343
|
+
supermaven: /\bsupermaven\b/,
|
|
344
|
+
cline: /\bcline\b/,
|
|
345
|
+
cody: /\bsourcegraph-cody\b|\bcody-agent\b/,
|
|
346
|
+
"amazon-q": /\bamazon-q\b/,
|
|
347
|
+
"roo-code": /\broo-code\b|\broo_code\b/,
|
|
348
|
+
pearai: /\bpearai\b/,
|
|
349
|
+
ghostwriter: /\bghostwriter\b/,
|
|
325
350
|
}
|
|
326
351
|
|
|
327
352
|
function detectEnvironment() {
|
|
@@ -353,6 +378,159 @@ function detectEnvironment() {
|
|
|
353
378
|
detectEnvironment() // Run immediately on startup
|
|
354
379
|
setInterval(detectEnvironment, 10 * 60 * 1000) // Every 10 minutes
|
|
355
380
|
|
|
381
|
+
// ── AI artifact file detection ──────────────────────────────────
|
|
382
|
+
const AI_ARTIFACT_PATTERNS = [
|
|
383
|
+
{ tool: "cursor", paths: [".cursorrules", ".cursorignore"] },
|
|
384
|
+
{ tool: "windsurf", paths: [".windsurfrules"] },
|
|
385
|
+
{ tool: "aider", paths: [".aider.chat.history.md", ".aider.input.history", ".aider.tags.cache"] },
|
|
386
|
+
{ tool: "continue", paths: [".continue"] },
|
|
387
|
+
{ tool: "codeium", paths: [".codeium"] },
|
|
388
|
+
{ tool: "tabnine", paths: [".tabnine"] },
|
|
389
|
+
{ tool: "codex", paths: ["codex.md", ".codex"] },
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
const seenArtifacts = new Set()
|
|
393
|
+
|
|
394
|
+
function checkArtifact(relPath) {
|
|
395
|
+
if (seenArtifacts.has(relPath)) return
|
|
396
|
+
for (const { tool, paths } of AI_ARTIFACT_PATTERNS) {
|
|
397
|
+
for (const p of paths) {
|
|
398
|
+
if (relPath === p || relPath.startsWith(p + "/") || relPath.startsWith(p + "\\")) {
|
|
399
|
+
seenArtifacts.add(relPath)
|
|
400
|
+
emit({
|
|
401
|
+
ts: new Date().toISOString(),
|
|
402
|
+
type: "ai_artifact_detected",
|
|
403
|
+
tool,
|
|
404
|
+
file: relPath,
|
|
405
|
+
})
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Startup scan for pre-existing artifacts
|
|
413
|
+
for (const { tool, paths } of AI_ARTIFACT_PATTERNS) {
|
|
414
|
+
for (const p of paths) {
|
|
415
|
+
try {
|
|
416
|
+
if (fs.existsSync(path.join(projectDir, p))) {
|
|
417
|
+
seenArtifacts.add(p)
|
|
418
|
+
emit({
|
|
419
|
+
ts: new Date().toISOString(),
|
|
420
|
+
type: "ai_artifact_detected",
|
|
421
|
+
tool,
|
|
422
|
+
file: p,
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
} catch { /* skip */ }
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── Network monitoring (AI API endpoint detection) ──────────────
|
|
430
|
+
const AI_NETWORK_HOSTS = {
|
|
431
|
+
"api.openai.com": "openai",
|
|
432
|
+
"chat.openai.com": "chatgpt",
|
|
433
|
+
"chatgpt.com": "chatgpt",
|
|
434
|
+
"api.anthropic.com": "claude_api",
|
|
435
|
+
"claude.ai": "claude_web",
|
|
436
|
+
"generativelanguage.googleapis.com": "gemini_api",
|
|
437
|
+
"gemini.google.com": "gemini_web",
|
|
438
|
+
"copilot-proxy.githubusercontent.com": "copilot",
|
|
439
|
+
"githubcopilot.com": "copilot",
|
|
440
|
+
"api2.cursor.sh": "cursor",
|
|
441
|
+
"cursor.sh": "cursor",
|
|
442
|
+
"server.codeium.com": "codeium",
|
|
443
|
+
"api.perplexity.ai": "perplexity",
|
|
444
|
+
"perplexity.ai": "perplexity",
|
|
445
|
+
"api.deepseek.com": "deepseek",
|
|
446
|
+
"api.groq.com": "groq",
|
|
447
|
+
"api.mistral.ai": "mistral",
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// IP -> tool name cache (refreshed every 30 min)
|
|
451
|
+
const ipToTool = new Map()
|
|
452
|
+
let lastDnsRefresh = 0
|
|
453
|
+
const DNS_REFRESH_MS = 30 * 60 * 1000
|
|
454
|
+
|
|
455
|
+
function refreshDnsCache() {
|
|
456
|
+
lastDnsRefresh = Date.now()
|
|
457
|
+
for (const [host, tool] of Object.entries(AI_NETWORK_HOSTS)) {
|
|
458
|
+
dns.resolve4(host, { ttl: false }, (err, addresses) => {
|
|
459
|
+
if (!err && addresses) {
|
|
460
|
+
for (const ip of addresses) {
|
|
461
|
+
ipToTool.set(ip, { tool, host })
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
refreshDnsCache() // Resolve on startup
|
|
469
|
+
|
|
470
|
+
let lastNetworkTools = "" // Dedup: JSON stringified tool set from previous check
|
|
471
|
+
|
|
472
|
+
function detectNetworkAI() {
|
|
473
|
+
if (Date.now() - lastDnsRefresh > DNS_REFRESH_MS) refreshDnsCache()
|
|
474
|
+
if (ipToTool.size === 0) return // DNS hasn't resolved yet
|
|
475
|
+
|
|
476
|
+
execFile("lsof", ["-i", "-n", "-P"], { timeout: 10_000, encoding: "utf8", maxBuffer: 2 * 1024 * 1024 }, (err, stdout) => {
|
|
477
|
+
if (err) {
|
|
478
|
+
// lsof unavailable — try ss on Linux
|
|
479
|
+
if (err.code === "ENOENT" && process.platform === "linux") {
|
|
480
|
+
execFile("ss", ["-tnp"], { timeout: 5000, encoding: "utf8", maxBuffer: 1024 * 1024 }, (ssErr, ssOut) => {
|
|
481
|
+
if (!ssErr && ssOut) parseNetworkOutput(ssOut, true)
|
|
482
|
+
})
|
|
483
|
+
}
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
if (stdout) parseNetworkOutput(stdout, false)
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function parseNetworkOutput(output, isSS) {
|
|
491
|
+
const detected = new Map() // tool -> Set<host>
|
|
492
|
+
|
|
493
|
+
for (const line of output.split("\n")) {
|
|
494
|
+
// lsof: match destination IP in "->IP:port" pattern
|
|
495
|
+
// ss: match destination IP in "IP:port" pattern after local address
|
|
496
|
+
const ipMatch = isSS
|
|
497
|
+
? line.match(/\s(\d+\.\d+\.\d+\.\d+):(\d+)\s*$/)
|
|
498
|
+
: line.match(/->(\d+\.\d+\.\d+\.\d+):(\d+)/)
|
|
499
|
+
if (!ipMatch) continue
|
|
500
|
+
|
|
501
|
+
const ip = ipMatch[1]
|
|
502
|
+
const entry = ipToTool.get(ip)
|
|
503
|
+
if (entry) {
|
|
504
|
+
if (!detected.has(entry.tool)) detected.set(entry.tool, new Set())
|
|
505
|
+
detected.get(entry.tool).add(entry.host)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (detected.size === 0) return
|
|
510
|
+
|
|
511
|
+
// Dedup: don't re-emit if same tools detected as last check
|
|
512
|
+
const toolKey = JSON.stringify([...detected.keys()].sort())
|
|
513
|
+
if (toolKey === lastNetworkTools) return
|
|
514
|
+
lastNetworkTools = toolKey
|
|
515
|
+
|
|
516
|
+
const tools = []
|
|
517
|
+
const hosts = []
|
|
518
|
+
for (const [tool, hostSet] of detected) {
|
|
519
|
+
tools.push(tool)
|
|
520
|
+
for (const h of hostSet) hosts.push(h)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
emit({
|
|
524
|
+
ts: new Date().toISOString(),
|
|
525
|
+
type: "network_ai_detected",
|
|
526
|
+
tools,
|
|
527
|
+
hosts,
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
detectNetworkAI() // Run immediately
|
|
532
|
+
setInterval(detectNetworkAI, 5 * 60 * 1000) // Every 5 minutes
|
|
533
|
+
|
|
356
534
|
// ── Auto-submit at deadline ──────────────────────────────────────
|
|
357
535
|
// NOTE: Deadline calculation duplicated from config.ts getEffectiveDeadline()
|
|
358
536
|
// (this file is CommonJS and cannot import the TS module). Keep both in sync.
|