@hackersbaby/plugin 0.5.4 → 0.5.5

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.
@@ -35,7 +35,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
35
35
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
36
36
 
37
37
  // src/config.ts
38
- function sessionStateFile(source) {
38
+ function sessionStateFile(sessionId) {
39
+ return import_path.default.join(SESSIONS_DIR, `${sessionId}.json`);
40
+ }
41
+ function legacySessionStateFile(source) {
39
42
  return import_path.default.join(CONFIG_DIR, `active-session-${source}.json`);
40
43
  }
41
44
  function ensureConfigDir() {
@@ -56,7 +59,7 @@ function saveConfig(config) {
56
59
  ensureConfigDir();
57
60
  import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
58
61
  }
59
- var import_fs, import_path, import_os, CONFIG_DIR, CONFIG_FILE, QUEUE_FILE;
62
+ var import_fs, import_path, import_os, CONFIG_DIR, CONFIG_FILE, QUEUE_FILE, SESSIONS_DIR;
60
63
  var init_config = __esm({
61
64
  "src/config.ts"() {
62
65
  "use strict";
@@ -66,6 +69,7 @@ var init_config = __esm({
66
69
  CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".hackersbaby");
67
70
  CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
68
71
  QUEUE_FILE = import_path.default.join(CONFIG_DIR, "queue.jsonl");
72
+ SESSIONS_DIR = import_path.default.join(CONFIG_DIR, "sessions");
69
73
  }
70
74
  });
71
75
 
@@ -508,40 +512,69 @@ var init_collector = __esm({
508
512
  var import_fs3 = __toESM(require("fs"));
509
513
  var import_path2 = __toESM(require("path"));
510
514
  init_config();
511
- function loadSessionState(source) {
515
+ function ensureSessionsDir() {
516
+ if (!import_fs3.default.existsSync(SESSIONS_DIR)) {
517
+ import_fs3.default.mkdirSync(SESSIONS_DIR, { recursive: true });
518
+ }
519
+ }
520
+ function loadSessionState(sessionId) {
512
521
  try {
513
- const file = sessionStateFile(source);
522
+ const file = sessionStateFile(sessionId);
514
523
  if (!import_fs3.default.existsSync(file)) return void 0;
515
524
  return JSON.parse(import_fs3.default.readFileSync(file, "utf-8"));
516
525
  } catch {
517
526
  return void 0;
518
527
  }
519
528
  }
529
+ function findSessionBySource(source) {
530
+ try {
531
+ if (!import_fs3.default.existsSync(SESSIONS_DIR)) return void 0;
532
+ const files = import_fs3.default.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
533
+ for (const f of files) {
534
+ try {
535
+ const state = JSON.parse(import_fs3.default.readFileSync(import_path2.default.join(SESSIONS_DIR, f), "utf-8"));
536
+ if (state.source === source) return state;
537
+ } catch {
538
+ }
539
+ }
540
+ } catch {
541
+ }
542
+ try {
543
+ const legacyFile = legacySessionStateFile(source);
544
+ if (import_fs3.default.existsSync(legacyFile)) {
545
+ return JSON.parse(import_fs3.default.readFileSync(legacyFile, "utf-8"));
546
+ }
547
+ } catch {
548
+ }
549
+ return void 0;
550
+ }
520
551
  function loadSessionId(source) {
521
- return loadSessionState(source)?.sessionId;
552
+ return findSessionBySource(source)?.sessionId;
522
553
  }
523
- function updateSessionState(source, updates) {
524
- const state = loadSessionState(source);
554
+ function updateSessionState(sessionId, updates) {
555
+ const state = loadSessionState(sessionId);
525
556
  if (!state) return;
526
- const file = sessionStateFile(source);
557
+ const file = sessionStateFile(sessionId);
527
558
  import_fs3.default.writeFileSync(file, JSON.stringify({ ...state, ...updates }));
528
559
  }
529
560
  function createCollector(sessionId, source) {
530
561
  const { EventCollector: EventCollector2 } = (init_collector(), __toCommonJS(collector_exports));
531
562
  const collector = new EventCollector2(sessionId, source);
532
- const state = loadSessionState(source);
533
- if (state?.sessionSecret && state?.chainHash && state.sessionId === sessionId) {
563
+ const resolvedId = collector.sessionId;
564
+ const state = loadSessionState(resolvedId);
565
+ if (state?.sessionSecret && state?.chainHash && state.sessionId === resolvedId) {
534
566
  collector.restoreCrypto(state.sessionSecret, state.chainHash);
535
567
  collector.setCryptoUpdateCallback((chainHash, secret) => {
536
- updateSessionState(source, { chainHash, sessionSecret: secret });
568
+ updateSessionState(resolvedId, { chainHash, sessionSecret: secret });
537
569
  });
538
570
  }
539
571
  return collector;
540
572
  }
541
573
  var LOCK_STALE_MS = 5e3;
542
574
  var LOCK_POLL_MS = 20;
543
- function lockFilePath(source) {
544
- return import_path2.default.join(CONFIG_DIR, `lock-${source}`);
575
+ function lockFilePath(sessionId) {
576
+ ensureSessionsDir();
577
+ return import_path2.default.join(SESSIONS_DIR, `lock-${sessionId}`);
545
578
  }
546
579
  function tryAcquireLock(lockPath) {
547
580
  try {
@@ -574,8 +607,8 @@ function clearStaleLock(lockPath) {
574
607
  function sleep(ms) {
575
608
  return new Promise((r) => setTimeout(r, ms));
576
609
  }
577
- async function acquireLock(source) {
578
- const lockPath = lockFilePath(source);
610
+ async function acquireLock(sessionId) {
611
+ const lockPath = lockFilePath(sessionId);
579
612
  const deadline = Date.now() + LOCK_STALE_MS;
580
613
  while (Date.now() < deadline) {
581
614
  if (tryAcquireLock(lockPath)) return;
@@ -589,18 +622,18 @@ async function acquireLock(source) {
589
622
  if (!tryAcquireLock(lockPath)) {
590
623
  }
591
624
  }
592
- function releaseLock(source) {
625
+ function releaseLock(sessionId) {
593
626
  try {
594
- import_fs3.default.unlinkSync(lockFilePath(source));
627
+ import_fs3.default.unlinkSync(lockFilePath(sessionId));
595
628
  } catch {
596
629
  }
597
630
  }
598
- async function withSessionLock(source, fn) {
599
- await acquireLock(source);
631
+ async function withSessionLock(sessionId, fn) {
632
+ await acquireLock(sessionId);
600
633
  try {
601
634
  return await fn();
602
635
  } finally {
603
- releaseLock(source);
636
+ releaseLock(sessionId);
604
637
  }
605
638
  }
606
639
  function readStdin() {
@@ -632,7 +665,8 @@ async function handleSubagent(source) {
632
665
  const detected = detectSource(raw);
633
666
  const hookData = normalizePayload(detected, raw);
634
667
  const sessionId = hookData.session_id || loadSessionId(detected);
635
- await withSessionLock(detected, async () => {
668
+ if (!sessionId) return;
669
+ await withSessionLock(sessionId, async () => {
636
670
  const collector = createCollector(sessionId, detected);
637
671
  collector.addEvent({
638
672
  action_type: "subagent_spawned",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/config.ts","../../../../shared/dist/types.js","../../../../shared/src/scoring.ts","../../../../shared/src/anticheat.ts","../../../../shared/src/index.ts","../../../src/api-client.ts","../../../src/queue.ts","../../../src/collector.ts","../../../src/hooks/shared/utils.ts","../../../src/hooks/shared/subagent.ts","../../../src/hooks/cursor/subagent.ts"],"sourcesContent":["import fs from 'fs';\nimport path from 'path';\nimport os from 'os';\n\nexport const CONFIG_DIR = path.join(os.homedir(), '.hackersbaby');\nexport const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');\nexport const QUEUE_FILE = path.join(CONFIG_DIR, 'queue.jsonl');\n\nexport type Source = 'claude' | 'cursor';\n\nexport interface PluginConfig {\n token: string;\n refresh_token: string;\n user_id: string;\n server: string;\n targets?: Source[];\n preferences: {\n dashboard_port: number;\n batch_interval_ms: number;\n };\n}\n\nexport function sessionStateFile(source: Source): string {\n return path.join(CONFIG_DIR, `active-session-${source}.json`);\n}\n\nexport function ensureConfigDir(): void {\n if (!fs.existsSync(CONFIG_DIR)) {\n fs.mkdirSync(CONFIG_DIR, { recursive: true });\n }\n}\n\nexport function loadConfig(): PluginConfig | null {\n try {\n if (!fs.existsSync(CONFIG_FILE)) return null;\n const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');\n return JSON.parse(raw) as PluginConfig;\n } catch {\n return null;\n }\n}\n\nexport function saveConfig(config: PluginConfig): void {\n ensureConfigDir();\n fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');\n}\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\n//# sourceMappingURL=types.js.map","import type { ActionType } from './types';\n\nexport const SESSION_BASE_POINT_CAP = 20000;\nexport const TOOL_CALLS_PER_HOUR_CAP = 4000;\nexport const MIN_SESSION_TOKENS = 2000;\n\nexport const BASE_POINTS: Record<ActionType, { perUnit: number; unitSize: number }> = {\n token_input: { perUnit: 1, unitSize: 1000 },\n token_output: { perUnit: 2, unitSize: 1000 },\n tool_call: { perUnit: 5, unitSize: 1 },\n file_created: { perUnit: 15, unitSize: 1 },\n file_edited: { perUnit: 10, unitSize: 1 },\n commit: { perUnit: 50, unitSize: 1 },\n session_completed: { perUnit: 100, unitSize: 1 },\n deployment: { perUnit: 200, unitSize: 1 },\n prompt_submitted: { perUnit: 15, unitSize: 1 },\n subagent_spawned: { perUnit: 40, unitSize: 1 },\n context_compacted: { perUnit: 30, unitSize: 1 },\n stop_response: { perUnit: 10, unitSize: 1 },\n};\n\nexport const DEPLOY_PATTERNS = [\n /^vercel\\s+(--prod|deploy)/, /^netlify\\s+deploy/, /^fly\\s+deploy/, /^railway\\s+up/,\n /^git\\s+push\\s+\\S+\\s+(main|master|production)/,\n /^(npm|yarn|pnpm)\\s+run\\s+deploy/, /^(yarn|pnpm)\\s+deploy/,\n];\n\nexport function getBasePoints(actionType: ActionType, value: number): number {\n const config = BASE_POINTS[actionType];\n return Math.floor(value / config.unitSize) * config.perUnit;\n}\n\ninterface MultiplierInput { streak_days: number; commit_quality: boolean; language_count: number; }\n\nexport function calculateMultiplier(input: MultiplierInput): number {\n let streak = 1.0;\n if (input.streak_days >= 30) streak = 3.0;\n else if (input.streak_days >= 7) streak = 2.0;\n else if (input.streak_days >= 3) streak = 1.5;\n const quality = input.commit_quality ? 1.5 : 1.0;\n const diversity = input.language_count >= 3 ? 1.2 : 1.0;\n return streak * quality * diversity;\n}\n\nexport function calculateSessionScore(events: Array<{ action_type: ActionType; value: number }>, multiplier: number): number {\n let totalBase = 0;\n for (const e of events) totalBase += getBasePoints(e.action_type, e.value);\n return Math.floor(Math.min(totalBase, SESSION_BASE_POINT_CAP) * multiplier);\n}\n\nexport function isDeployCommand(command: string): boolean {\n return DEPLOY_PATTERNS.some((p) => p.test(command.trim()));\n}\n","/**\n * Anti-cheat cryptographic primitives.\n * Shared between plugin (client) and server.\n *\n * - Event chain: each event hash includes the previous, creating a tamper-evident chain\n * - Batch HMAC: the entire batch is signed with a server-issued session secret\n */\n\nimport { createHmac, createHash } from 'crypto';\n\n/** Hash a single event, chaining it to the previous hash */\nexport function hashEvent(\n event: { action_type: string; value: number; timestamp: string },\n prevHash: string,\n): string {\n const payload = `${prevHash}|${event.action_type}|${event.value}|${event.timestamp}`;\n return createHash('sha256').update(payload).digest('hex').slice(0, 16);\n}\n\n/** Compute the chain hash for an array of events */\nexport function computeChainHash(\n events: Array<{ action_type: string; value: number; timestamp: string }>,\n initialHash: string = '0000000000000000',\n): string {\n let hash = initialHash;\n for (const event of events) {\n hash = hashEvent(event, hash);\n }\n return hash;\n}\n\n/** Sign a batch with HMAC-SHA256 using the session secret */\nexport function signBatch(\n sessionId: string,\n events: Array<{ action_type: string; value: number; timestamp: string }>,\n chainHash: string,\n secret: string,\n): string {\n // Canonical form: sessionId + chainHash + event count + sum of values\n const valueSum = events.reduce((s, e) => s + e.value, 0);\n const message = `${sessionId}|${chainHash}|${events.length}|${valueSum}`;\n return createHmac('sha256', secret).update(message).digest('hex');\n}\n\n/** Verify a batch HMAC signature */\nexport function verifyBatchSignature(\n sessionId: string,\n events: Array<{ action_type: string; value: number; timestamp: string }>,\n chainHash: string,\n signature: string,\n secret: string,\n): boolean {\n const expected = signBatch(sessionId, events, chainHash, secret);\n // Constant-time comparison\n if (expected.length !== signature.length) return false;\n let mismatch = 0;\n for (let i = 0; i < expected.length; i++) {\n mismatch |= expected.charCodeAt(i) ^ signature.charCodeAt(i);\n }\n return mismatch === 0;\n}\n","export * from './types';\nexport * from './scoring';\nexport * from './anticheat';\n","import { loadConfig, saveConfig } from './config';\nimport { computeChainHash, signBatch } from '@hackersbaby/shared';\nimport type { EventBatch } from '@hackersbaby/shared';\n\nexport class APIClient {\n private config = loadConfig();\n private sessionSecret: string | null = null;\n private chainHash: string = '0000000000000000';\n private onChainUpdate: ((chainHash: string, secret: string) => void) | null = null;\n\n /** Set callback to persist chain state between hook processes */\n setChainUpdateCallback(cb: (chainHash: string, secret: string) => void) {\n this.onChainUpdate = cb;\n }\n\n /** Restore crypto state from a previous session */\n restoreCryptoState(secret: string, chainHash: string) {\n this.sessionSecret = secret;\n this.chainHash = chainHash;\n }\n\n private get headers(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config?.token || ''}`,\n };\n }\n\n /** Get current crypto state for persistence */\n getCryptoState(): { secret: string; chainHash: string } | null {\n if (!this.sessionSecret) return null;\n return { secret: this.sessionSecret, chainHash: this.chainHash };\n }\n\n /** Register session with server and get cryptographic secret */\n async startSession(sessionId: string): Promise<boolean> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/sessions/start`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify({ session_id: sessionId }),\n });\n if (res.ok) {\n const data = await res.json() as { session_secret: string; initial_chain_hash: string };\n this.sessionSecret = data.session_secret;\n this.chainHash = data.initial_chain_hash;\n return true;\n }\n return false;\n } catch {\n return false;\n }\n }\n\n async sendBatch(batch: EventBatch): Promise<boolean> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\n\n // Sign the batch if we have a session secret\n if (this.sessionSecret) {\n const prevChainHash = this.chainHash;\n const newChainHash = computeChainHash(batch.events, prevChainHash);\n const signature = signBatch(batch.session_id, batch.events, newChainHash, this.sessionSecret);\n\n batch.signature = signature;\n batch.chain_hash = newChainHash;\n batch.prev_chain_hash = prevChainHash;\n\n const res = await fetch(`${server}/api/scores/batch`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify(batch),\n });\n\n if (res.ok) {\n // Only advance chain hash after server confirms acceptance\n this.chainHash = newChainHash;\n if (this.onChainUpdate) {\n this.onChainUpdate(this.chainHash, this.sessionSecret);\n }\n }\n\n return res.ok;\n }\n\n const res = await fetch(`${server}/api/scores/batch`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify(batch),\n });\n return res.ok;\n } catch {\n return false;\n }\n }\n\n async refreshTokenIfNeeded(): Promise<void> {\n if (!this.config) return;\n try {\n const parts = this.config.token.split('.');\n if (parts.length !== 3) return;\n const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));\n const expMs = payload.exp * 1000;\n const oneDayMs = 24 * 60 * 60 * 1000;\n if (Date.now() + oneDayMs < expMs) return; // more than 1 day remaining, no refresh needed\n\n const res = await fetch(`${this.config.server}/api/auth/cli-token/refresh`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify({ refresh_token: this.config.refresh_token }),\n });\n if (res.ok) {\n const data = (await res.json()) as { token?: string; refresh_token?: string };\n if (data.token) {\n this.config.token = data.token;\n if (data.refresh_token) this.config.refresh_token = data.refresh_token;\n saveConfig(this.config);\n }\n }\n } catch {\n // silently ignore refresh errors\n }\n }\n\n async getStatus(): Promise<Record<string, unknown>> {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/users/me`, { headers: this.headers });\n if (!res.ok) throw new Error(`getStatus failed: ${res.status}`);\n return res.json() as Promise<Record<string, unknown>>;\n }\n\n async getRank(): Promise<Record<string, unknown>> {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/users/me/rank`, { headers: this.headers });\n if (!res.ok) throw new Error(`getRank failed: ${res.status}`);\n return res.json() as Promise<Record<string, unknown>>;\n }\n\n async getLeaderboard(): Promise<Record<string, unknown>> {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/leaderboard/global?limit=10`, { headers: this.headers });\n if (!res.ok) throw new Error(`getLeaderboard failed: ${res.status}`);\n return res.json() as Promise<Record<string, unknown>>;\n }\n\n async getPublicStats(): Promise<Record<string, unknown> | null> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/stats/public`);\n if (!res.ok) return null;\n return res.json() as Promise<Record<string, unknown>>;\n } catch {\n return null;\n }\n }\n\n async getNotifications(): Promise<{ notifications: Array<{ type: string; message: string }> } | null> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/users/me/notifications`, { headers: this.headers });\n if (!res.ok) return null;\n return res.json() as Promise<{ notifications: Array<{ type: string; message: string }> }>;\n } catch {\n return null;\n }\n }\n\n async claimReferral(code: string): Promise<Record<string, unknown> | null> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/referral/claim`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify({ referral_code: code }),\n });\n if (!res.ok) return null;\n return res.json() as Promise<Record<string, unknown>>;\n } catch {\n return null;\n }\n }\n\n async getReferralStats(): Promise<Record<string, unknown> | null> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/referral/stats`, { headers: this.headers });\n if (!res.ok) return null;\n return res.json() as Promise<Record<string, unknown>>;\n } catch {\n return null;\n }\n }\n}\n","import fs from 'fs';\nimport { QUEUE_FILE, ensureConfigDir } from './config';\nimport type { RawEvent } from '@hackersbaby/shared';\n\nconst MAX_QUEUE_SIZE = 10_000;\nconst MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nexport function enqueueEvents(events: RawEvent[]): void {\n ensureConfigDir();\n const lines = events.map((e) => JSON.stringify(e)).join('\\n');\n fs.appendFileSync(QUEUE_FILE, lines + '\\n', 'utf-8');\n\n // Trim to MAX_QUEUE_SIZE\n try {\n const content = fs.readFileSync(QUEUE_FILE, 'utf-8');\n const allLines = content.split('\\n').filter((l) => l.trim() !== '');\n if (allLines.length > MAX_QUEUE_SIZE) {\n const trimmed = allLines.slice(allLines.length - MAX_QUEUE_SIZE);\n fs.writeFileSync(QUEUE_FILE, trimmed.join('\\n') + '\\n', 'utf-8');\n }\n } catch {\n // ignore read/write errors on trim\n }\n}\n\nexport function drainQueue(): RawEvent[] {\n try {\n if (!fs.existsSync(QUEUE_FILE)) return [];\n const content = fs.readFileSync(QUEUE_FILE, 'utf-8');\n fs.writeFileSync(QUEUE_FILE, '', 'utf-8');\n\n const now = Date.now();\n const events: RawEvent[] = [];\n for (const line of content.split('\\n')) {\n if (!line.trim()) continue;\n try {\n const event = JSON.parse(line) as RawEvent;\n const age = now - new Date(event.timestamp).getTime();\n if (age <= MAX_AGE_MS) {\n events.push(event);\n }\n } catch {\n // skip malformed lines\n }\n }\n return events;\n } catch {\n return [];\n }\n}\n","import { v4 as uuidv4 } from 'uuid';\nimport { APIClient } from './api-client';\nimport { enqueueEvents, drainQueue } from './queue';\nimport { loadConfig } from './config';\nimport type { Source } from './config';\nimport type { RawEvent, ActionType } from '@hackersbaby/shared';\n\ntype RawEventInput = Omit<RawEvent, 'timestamp'>;\n\nexport class EventCollector {\n readonly sessionId: string;\n private buffer: RawEvent[] = [];\n private flushInterval: ReturnType<typeof setInterval> | null = null;\n readonly client: APIClient;\n private source?: Source;\n\n constructor(sessionId?: string, source?: Source) {\n this.sessionId = sessionId ?? uuidv4();\n this.client = new APIClient();\n this.source = source;\n }\n\n /** Restore crypto state from persisted session data */\n restoreCrypto(secret: string, chainHash: string) {\n this.client.restoreCryptoState(secret, chainHash);\n }\n\n /** Set callback to persist crypto state after each batch */\n setCryptoUpdateCallback(cb: (chainHash: string, secret: string) => void) {\n this.client.setChainUpdateCallback(cb);\n }\n\n addEvent(event: RawEventInput): void {\n const metadata = this.source\n ? { ...event.metadata, source: this.source }\n : event.metadata;\n this.buffer.push({\n ...event,\n metadata,\n timestamp: new Date().toISOString(),\n });\n }\n\n async start(): Promise<void> {\n await this.client.refreshTokenIfNeeded();\n\n // Register session with server for cryptographic signing\n await this.client.startSession(this.sessionId);\n\n // Clear stale queue — events from previous sessions have old timestamps\n // that the server will reject, and retrying them risks chain desync.\n drainQueue();\n\n const config = loadConfig();\n const intervalMs = config?.preferences?.batch_interval_ms ?? 10_000;\n this.flushInterval = setInterval(() => {\n this.flush().catch(() => {});\n }, intervalMs);\n }\n\n async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n const events = this.buffer.splice(0);\n const batch = { session_id: this.sessionId, events };\n const ok = await this.client.sendBatch(batch);\n if (!ok) {\n enqueueEvents(events);\n }\n }\n\n async stop(): Promise<void> {\n if (this.flushInterval !== null) {\n clearInterval(this.flushInterval);\n this.flushInterval = null;\n }\n this.addEvent({ action_type: 'session_completed' as ActionType, value: 1, metadata: {} });\n await this.flush();\n }\n}\n","import fs from 'fs';\nimport path from 'path';\nimport type { Source } from '../../config';\nimport { sessionStateFile, CONFIG_DIR } from '../../config';\n\nexport interface SessionState {\n sessionId: string;\n pid: number;\n source: Source;\n sessionSecret?: string;\n chainHash?: string;\n}\n\nexport function loadSessionState(source: Source): SessionState | undefined {\n try {\n const file = sessionStateFile(source);\n if (!fs.existsSync(file)) return undefined;\n return JSON.parse(fs.readFileSync(file, 'utf-8')) as SessionState;\n } catch {\n return undefined;\n }\n}\n\nexport function loadSessionId(source: Source): string | undefined {\n return loadSessionState(source)?.sessionId;\n}\n\nexport function updateSessionState(source: Source, updates: Partial<SessionState>): void {\n const state = loadSessionState(source);\n if (!state) return;\n const file = sessionStateFile(source);\n fs.writeFileSync(file, JSON.stringify({ ...state, ...updates }));\n}\n\nexport function saveSessionState(state: SessionState): void {\n const file = sessionStateFile(state.source);\n fs.writeFileSync(file, JSON.stringify(state));\n}\n\n/** Create an EventCollector with crypto state restored from the session file */\nexport function createCollector(sessionId: string | undefined, source: Source): import('../../collector').EventCollector {\n const { EventCollector } = require('../../collector');\n const collector = new EventCollector(sessionId, source);\n\n // Restore crypto state from session file\n const state = loadSessionState(source);\n if (state?.sessionSecret && state?.chainHash && state.sessionId === sessionId) {\n collector.restoreCrypto(state.sessionSecret, state.chainHash);\n\n // Update session file after each signed batch\n collector.setCryptoUpdateCallback((chainHash: string, secret: string) => {\n updateSessionState(source, { chainHash, sessionSecret: secret });\n });\n }\n\n return collector;\n}\n\nexport function removeSessionState(source: Source): void {\n const file = sessionStateFile(source);\n if (fs.existsSync(file)) fs.unlinkSync(file);\n}\n\n// --- File-based lock to serialize batch sending across concurrent hook processes ---\n\nconst LOCK_STALE_MS = 5_000; // 5 seconds — force-remove stale locks\nconst LOCK_POLL_MS = 20; // polling interval\n\nfunction lockFilePath(source: Source): string {\n return path.join(CONFIG_DIR, `lock-${source}`);\n}\n\nfunction tryAcquireLock(lockPath: string): boolean {\n try {\n const fd = fs.openSync(lockPath, 'wx');\n fs.writeSync(fd, `${process.pid}\\n${Date.now()}`);\n fs.closeSync(fd);\n return true;\n } catch (e: any) {\n if (e.code === 'EEXIST') return false;\n throw e;\n }\n}\n\nfunction clearStaleLock(lockPath: string): boolean {\n try {\n const content = fs.readFileSync(lockPath, 'utf-8');\n const lockTime = parseInt(content.split('\\n')[1] || '0', 10);\n if (Date.now() - lockTime > LOCK_STALE_MS) {\n try { fs.unlinkSync(lockPath); } catch {}\n return true; // was stale, removed\n }\n } catch { return true; /* vanished — retry */ }\n return false;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\nasync function acquireLock(source: Source): Promise<void> {\n const lockPath = lockFilePath(source);\n const deadline = Date.now() + LOCK_STALE_MS;\n\n while (Date.now() < deadline) {\n if (tryAcquireLock(lockPath)) return;\n clearStaleLock(lockPath);\n await sleep(LOCK_POLL_MS + Math.random() * 30);\n }\n\n // Timeout — force-remove and take lock\n try { fs.unlinkSync(lockPath); } catch {}\n if (!tryAcquireLock(lockPath)) {\n // Another process grabbed it — proceed without lock (best-effort)\n }\n}\n\nfunction releaseLock(source: Source): void {\n try { fs.unlinkSync(lockFilePath(source)); } catch {}\n}\n\n/**\n * Execute a callback while holding a file lock for the given source.\n * Serializes all hook processes for the same source (claude/cursor)\n * to prevent concurrent batch sends from causing chain hash desync.\n */\nexport async function withSessionLock<T>(source: Source, fn: () => Promise<T>): Promise<T> {\n await acquireLock(source);\n try {\n return await fn();\n } finally {\n releaseLock(source);\n }\n}\n\nexport function readStdin(): Promise<string> {\n return new Promise((resolve) => {\n let input = '';\n process.stdin.on('data', (chunk) => { input += chunk; });\n process.stdin.on('end', () => resolve(input));\n });\n}\n\n/**\n * Detect source at runtime. Cursor sets CURSOR_VERSION env var\n * and includes cursor_version in the hook payload.\n * Since Cursor reads ~/.claude/settings.json directly, both tools\n * fire the same hooks — so we detect rather than relying on entry points.\n */\nexport function detectSource(payload?: Record<string, unknown>): Source {\n if (process.env.CURSOR_VERSION) return 'cursor';\n if (payload?.cursor_version) return 'cursor';\n return 'claude';\n}\n\n/**\n * Normalize hook payload across tools.\n * Currently an identity function — Cursor's payload format matches Claude Code's.\n * If formats diverge, add field mappings here (e.g., raw.toolName → tool_name).\n */\nexport function normalizePayload(source: Source, raw: Record<string, unknown>): Record<string, unknown> {\n if (source === 'cursor') {\n return { ...raw };\n }\n return raw;\n}\n","import type { Source } from '../../config';\nimport { loadSessionId, readStdin, normalizePayload, detectSource, createCollector, withSessionLock } from './utils';\n\nexport async function handleSubagent(source: Source): Promise<void> {\n const input = await readStdin();\n try {\n const raw = JSON.parse(input);\n const detected = detectSource(raw);\n const hookData = normalizePayload(detected, raw);\n const sessionId = hookData.session_id || loadSessionId(detected);\n\n await withSessionLock(detected, async () => {\n const collector = createCollector(sessionId, detected);\n\n // PRIVACY: Only track agent type. No task content sent.\n collector.addEvent({\n action_type: 'subagent_spawned',\n value: 1,\n metadata: {\n agent_type: hookData.agent_type || 'unknown',\n },\n });\n\n await collector.flush();\n });\n } catch {}\n}\n","import { handleSubagent } from '../shared/subagent';\nhandleSubagent('cursor');\n"],"mappings":"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","names":["path","fs","os","exports","exports","exports","computeChainHash","signBatch","exports","res","fs","import_fs","uuidv4","import_fs","import_path","fs","EventCollector","path","fs"]}
1
+ {"version":3,"sources":["../../../src/config.ts","../../../../shared/dist/types.js","../../../../shared/src/scoring.ts","../../../../shared/src/anticheat.ts","../../../../shared/src/index.ts","../../../src/api-client.ts","../../../src/queue.ts","../../../src/collector.ts","../../../src/hooks/shared/utils.ts","../../../src/hooks/shared/subagent.ts","../../../src/hooks/cursor/subagent.ts"],"sourcesContent":["import fs from 'fs';\nimport path from 'path';\nimport os from 'os';\n\nexport const CONFIG_DIR = path.join(os.homedir(), '.hackersbaby');\nexport const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');\nexport const QUEUE_FILE = path.join(CONFIG_DIR, 'queue.jsonl');\n\nexport type Source = 'claude' | 'cursor';\n\nexport interface PluginConfig {\n token: string;\n refresh_token: string;\n user_id: string;\n server: string;\n targets?: Source[];\n preferences: {\n dashboard_port: number;\n batch_interval_ms: number;\n };\n}\n\nexport const SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');\n\nexport function sessionStateFile(sessionId: string): string {\n return path.join(SESSIONS_DIR, `${sessionId}.json`);\n}\n\n/** @deprecated — only used for migrating old per-source state files */\nexport function legacySessionStateFile(source: Source): string {\n return path.join(CONFIG_DIR, `active-session-${source}.json`);\n}\n\nexport function ensureConfigDir(): void {\n if (!fs.existsSync(CONFIG_DIR)) {\n fs.mkdirSync(CONFIG_DIR, { recursive: true });\n }\n}\n\nexport function loadConfig(): PluginConfig | null {\n try {\n if (!fs.existsSync(CONFIG_FILE)) return null;\n const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');\n return JSON.parse(raw) as PluginConfig;\n } catch {\n return null;\n }\n}\n\nexport function saveConfig(config: PluginConfig): void {\n ensureConfigDir();\n fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');\n}\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\n//# sourceMappingURL=types.js.map","import type { ActionType } from './types';\n\nexport const SESSION_BASE_POINT_CAP = 20000;\nexport const TOOL_CALLS_PER_HOUR_CAP = 4000;\nexport const MIN_SESSION_TOKENS = 2000;\n\nexport const BASE_POINTS: Record<ActionType, { perUnit: number; unitSize: number }> = {\n token_input: { perUnit: 1, unitSize: 1000 },\n token_output: { perUnit: 2, unitSize: 1000 },\n tool_call: { perUnit: 5, unitSize: 1 },\n file_created: { perUnit: 15, unitSize: 1 },\n file_edited: { perUnit: 10, unitSize: 1 },\n commit: { perUnit: 50, unitSize: 1 },\n session_completed: { perUnit: 100, unitSize: 1 },\n deployment: { perUnit: 200, unitSize: 1 },\n prompt_submitted: { perUnit: 15, unitSize: 1 },\n subagent_spawned: { perUnit: 40, unitSize: 1 },\n context_compacted: { perUnit: 30, unitSize: 1 },\n stop_response: { perUnit: 10, unitSize: 1 },\n};\n\nexport const DEPLOY_PATTERNS = [\n /^vercel\\s+(--prod|deploy)/, /^netlify\\s+deploy/, /^fly\\s+deploy/, /^railway\\s+up/,\n /^git\\s+push\\s+\\S+\\s+(main|master|production)/,\n /^(npm|yarn|pnpm)\\s+run\\s+deploy/, /^(yarn|pnpm)\\s+deploy/,\n];\n\nexport function getBasePoints(actionType: ActionType, value: number): number {\n const config = BASE_POINTS[actionType];\n return Math.floor(value / config.unitSize) * config.perUnit;\n}\n\ninterface MultiplierInput { streak_days: number; commit_quality: boolean; language_count: number; }\n\nexport function calculateMultiplier(input: MultiplierInput): number {\n let streak = 1.0;\n if (input.streak_days >= 30) streak = 3.0;\n else if (input.streak_days >= 7) streak = 2.0;\n else if (input.streak_days >= 3) streak = 1.5;\n const quality = input.commit_quality ? 1.5 : 1.0;\n const diversity = input.language_count >= 3 ? 1.2 : 1.0;\n return streak * quality * diversity;\n}\n\nexport function calculateSessionScore(events: Array<{ action_type: ActionType; value: number }>, multiplier: number): number {\n let totalBase = 0;\n for (const e of events) totalBase += getBasePoints(e.action_type, e.value);\n return Math.floor(Math.min(totalBase, SESSION_BASE_POINT_CAP) * multiplier);\n}\n\nexport function isDeployCommand(command: string): boolean {\n return DEPLOY_PATTERNS.some((p) => p.test(command.trim()));\n}\n","/**\n * Anti-cheat cryptographic primitives.\n * Shared between plugin (client) and server.\n *\n * - Event chain: each event hash includes the previous, creating a tamper-evident chain\n * - Batch HMAC: the entire batch is signed with a server-issued session secret\n */\n\nimport { createHmac, createHash } from 'crypto';\n\n/** Hash a single event, chaining it to the previous hash */\nexport function hashEvent(\n event: { action_type: string; value: number; timestamp: string },\n prevHash: string,\n): string {\n const payload = `${prevHash}|${event.action_type}|${event.value}|${event.timestamp}`;\n return createHash('sha256').update(payload).digest('hex').slice(0, 16);\n}\n\n/** Compute the chain hash for an array of events */\nexport function computeChainHash(\n events: Array<{ action_type: string; value: number; timestamp: string }>,\n initialHash: string = '0000000000000000',\n): string {\n let hash = initialHash;\n for (const event of events) {\n hash = hashEvent(event, hash);\n }\n return hash;\n}\n\n/** Sign a batch with HMAC-SHA256 using the session secret */\nexport function signBatch(\n sessionId: string,\n events: Array<{ action_type: string; value: number; timestamp: string }>,\n chainHash: string,\n secret: string,\n): string {\n // Canonical form: sessionId + chainHash + event count + sum of values\n const valueSum = events.reduce((s, e) => s + e.value, 0);\n const message = `${sessionId}|${chainHash}|${events.length}|${valueSum}`;\n return createHmac('sha256', secret).update(message).digest('hex');\n}\n\n/** Verify a batch HMAC signature */\nexport function verifyBatchSignature(\n sessionId: string,\n events: Array<{ action_type: string; value: number; timestamp: string }>,\n chainHash: string,\n signature: string,\n secret: string,\n): boolean {\n const expected = signBatch(sessionId, events, chainHash, secret);\n // Constant-time comparison\n if (expected.length !== signature.length) return false;\n let mismatch = 0;\n for (let i = 0; i < expected.length; i++) {\n mismatch |= expected.charCodeAt(i) ^ signature.charCodeAt(i);\n }\n return mismatch === 0;\n}\n","export * from './types';\nexport * from './scoring';\nexport * from './anticheat';\n","import { loadConfig, saveConfig } from './config';\nimport { computeChainHash, signBatch } from '@hackersbaby/shared';\nimport type { EventBatch } from '@hackersbaby/shared';\n\nexport class APIClient {\n private config = loadConfig();\n private sessionSecret: string | null = null;\n private chainHash: string = '0000000000000000';\n private onChainUpdate: ((chainHash: string, secret: string) => void) | null = null;\n\n /** Set callback to persist chain state between hook processes */\n setChainUpdateCallback(cb: (chainHash: string, secret: string) => void) {\n this.onChainUpdate = cb;\n }\n\n /** Restore crypto state from a previous session */\n restoreCryptoState(secret: string, chainHash: string) {\n this.sessionSecret = secret;\n this.chainHash = chainHash;\n }\n\n private get headers(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config?.token || ''}`,\n };\n }\n\n /** Get current crypto state for persistence */\n getCryptoState(): { secret: string; chainHash: string } | null {\n if (!this.sessionSecret) return null;\n return { secret: this.sessionSecret, chainHash: this.chainHash };\n }\n\n /** Register session with server and get cryptographic secret */\n async startSession(sessionId: string): Promise<boolean> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/sessions/start`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify({ session_id: sessionId }),\n });\n if (res.ok) {\n const data = await res.json() as { session_secret: string; initial_chain_hash: string };\n this.sessionSecret = data.session_secret;\n this.chainHash = data.initial_chain_hash;\n return true;\n }\n return false;\n } catch {\n return false;\n }\n }\n\n async sendBatch(batch: EventBatch): Promise<boolean> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\n\n // Sign the batch if we have a session secret\n if (this.sessionSecret) {\n const prevChainHash = this.chainHash;\n const newChainHash = computeChainHash(batch.events, prevChainHash);\n const signature = signBatch(batch.session_id, batch.events, newChainHash, this.sessionSecret);\n\n batch.signature = signature;\n batch.chain_hash = newChainHash;\n batch.prev_chain_hash = prevChainHash;\n\n const res = await fetch(`${server}/api/scores/batch`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify(batch),\n });\n\n if (res.ok) {\n // Only advance chain hash after server confirms acceptance\n this.chainHash = newChainHash;\n if (this.onChainUpdate) {\n this.onChainUpdate(this.chainHash, this.sessionSecret);\n }\n }\n\n return res.ok;\n }\n\n const res = await fetch(`${server}/api/scores/batch`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify(batch),\n });\n return res.ok;\n } catch {\n return false;\n }\n }\n\n async refreshTokenIfNeeded(): Promise<void> {\n if (!this.config) return;\n try {\n const parts = this.config.token.split('.');\n if (parts.length !== 3) return;\n const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));\n const expMs = payload.exp * 1000;\n const oneDayMs = 24 * 60 * 60 * 1000;\n if (Date.now() + oneDayMs < expMs) return; // more than 1 day remaining, no refresh needed\n\n const res = await fetch(`${this.config.server}/api/auth/cli-token/refresh`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify({ refresh_token: this.config.refresh_token }),\n });\n if (res.ok) {\n const data = (await res.json()) as { token?: string; refresh_token?: string };\n if (data.token) {\n this.config.token = data.token;\n if (data.refresh_token) this.config.refresh_token = data.refresh_token;\n saveConfig(this.config);\n }\n }\n } catch {\n // silently ignore refresh errors\n }\n }\n\n async getStatus(): Promise<Record<string, unknown>> {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/users/me`, { headers: this.headers });\n if (!res.ok) throw new Error(`getStatus failed: ${res.status}`);\n return res.json() as Promise<Record<string, unknown>>;\n }\n\n async getRank(): Promise<Record<string, unknown>> {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/users/me/rank`, { headers: this.headers });\n if (!res.ok) throw new Error(`getRank failed: ${res.status}`);\n return res.json() as Promise<Record<string, unknown>>;\n }\n\n async getLeaderboard(): Promise<Record<string, unknown>> {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/leaderboard/global?limit=10`, { headers: this.headers });\n if (!res.ok) throw new Error(`getLeaderboard failed: ${res.status}`);\n return res.json() as Promise<Record<string, unknown>>;\n }\n\n async getPublicStats(): Promise<Record<string, unknown> | null> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/stats/public`);\n if (!res.ok) return null;\n return res.json() as Promise<Record<string, unknown>>;\n } catch {\n return null;\n }\n }\n\n async getNotifications(): Promise<{ notifications: Array<{ type: string; message: string }> } | null> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/users/me/notifications`, { headers: this.headers });\n if (!res.ok) return null;\n return res.json() as Promise<{ notifications: Array<{ type: string; message: string }> }>;\n } catch {\n return null;\n }\n }\n\n async claimReferral(code: string): Promise<Record<string, unknown> | null> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/referral/claim`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify({ referral_code: code }),\n });\n if (!res.ok) return null;\n return res.json() as Promise<Record<string, unknown>>;\n } catch {\n return null;\n }\n }\n\n async getReferralStats(): Promise<Record<string, unknown> | null> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\n const res = await fetch(`${server}/api/referral/stats`, { headers: this.headers });\n if (!res.ok) return null;\n return res.json() as Promise<Record<string, unknown>>;\n } catch {\n return null;\n }\n }\n}\n","import fs from 'fs';\nimport { QUEUE_FILE, ensureConfigDir } from './config';\nimport type { RawEvent } from '@hackersbaby/shared';\n\nconst MAX_QUEUE_SIZE = 10_000;\nconst MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nexport function enqueueEvents(events: RawEvent[]): void {\n ensureConfigDir();\n const lines = events.map((e) => JSON.stringify(e)).join('\\n');\n fs.appendFileSync(QUEUE_FILE, lines + '\\n', 'utf-8');\n\n // Trim to MAX_QUEUE_SIZE\n try {\n const content = fs.readFileSync(QUEUE_FILE, 'utf-8');\n const allLines = content.split('\\n').filter((l) => l.trim() !== '');\n if (allLines.length > MAX_QUEUE_SIZE) {\n const trimmed = allLines.slice(allLines.length - MAX_QUEUE_SIZE);\n fs.writeFileSync(QUEUE_FILE, trimmed.join('\\n') + '\\n', 'utf-8');\n }\n } catch {\n // ignore read/write errors on trim\n }\n}\n\nexport function drainQueue(): RawEvent[] {\n try {\n if (!fs.existsSync(QUEUE_FILE)) return [];\n const content = fs.readFileSync(QUEUE_FILE, 'utf-8');\n fs.writeFileSync(QUEUE_FILE, '', 'utf-8');\n\n const now = Date.now();\n const events: RawEvent[] = [];\n for (const line of content.split('\\n')) {\n if (!line.trim()) continue;\n try {\n const event = JSON.parse(line) as RawEvent;\n const age = now - new Date(event.timestamp).getTime();\n if (age <= MAX_AGE_MS) {\n events.push(event);\n }\n } catch {\n // skip malformed lines\n }\n }\n return events;\n } catch {\n return [];\n }\n}\n","import { v4 as uuidv4 } from 'uuid';\nimport { APIClient } from './api-client';\nimport { enqueueEvents, drainQueue } from './queue';\nimport { loadConfig } from './config';\nimport type { Source } from './config';\nimport type { RawEvent, ActionType } from '@hackersbaby/shared';\n\ntype RawEventInput = Omit<RawEvent, 'timestamp'>;\n\nexport class EventCollector {\n readonly sessionId: string;\n private buffer: RawEvent[] = [];\n private flushInterval: ReturnType<typeof setInterval> | null = null;\n readonly client: APIClient;\n private source?: Source;\n\n constructor(sessionId?: string, source?: Source) {\n this.sessionId = sessionId ?? uuidv4();\n this.client = new APIClient();\n this.source = source;\n }\n\n /** Restore crypto state from persisted session data */\n restoreCrypto(secret: string, chainHash: string) {\n this.client.restoreCryptoState(secret, chainHash);\n }\n\n /** Set callback to persist crypto state after each batch */\n setCryptoUpdateCallback(cb: (chainHash: string, secret: string) => void) {\n this.client.setChainUpdateCallback(cb);\n }\n\n addEvent(event: RawEventInput): void {\n const metadata = this.source\n ? { ...event.metadata, source: this.source }\n : event.metadata;\n this.buffer.push({\n ...event,\n metadata,\n timestamp: new Date().toISOString(),\n });\n }\n\n async start(): Promise<void> {\n await this.client.refreshTokenIfNeeded();\n\n // Register session with server for cryptographic signing\n await this.client.startSession(this.sessionId);\n\n // Clear stale queue — events from previous sessions have old timestamps\n // that the server will reject, and retrying them risks chain desync.\n drainQueue();\n\n const config = loadConfig();\n const intervalMs = config?.preferences?.batch_interval_ms ?? 10_000;\n this.flushInterval = setInterval(() => {\n this.flush().catch(() => {});\n }, intervalMs);\n }\n\n async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n const events = this.buffer.splice(0);\n const batch = { session_id: this.sessionId, events };\n const ok = await this.client.sendBatch(batch);\n if (!ok) {\n enqueueEvents(events);\n }\n }\n\n async stop(): Promise<void> {\n if (this.flushInterval !== null) {\n clearInterval(this.flushInterval);\n this.flushInterval = null;\n }\n this.addEvent({ action_type: 'session_completed' as ActionType, value: 1, metadata: {} });\n await this.flush();\n }\n}\n","import fs from 'fs';\nimport path from 'path';\nimport type { Source } from '../../config';\nimport { sessionStateFile, SESSIONS_DIR, CONFIG_DIR, legacySessionStateFile } from '../../config';\n\nexport interface SessionState {\n sessionId: string;\n pid: number;\n source: Source;\n sessionSecret?: string;\n chainHash?: string;\n}\n\nfunction ensureSessionsDir(): void {\n if (!fs.existsSync(SESSIONS_DIR)) {\n fs.mkdirSync(SESSIONS_DIR, { recursive: true });\n }\n}\n\nexport function loadSessionState(sessionId: string): SessionState | undefined {\n try {\n const file = sessionStateFile(sessionId);\n if (!fs.existsSync(file)) return undefined;\n return JSON.parse(fs.readFileSync(file, 'utf-8')) as SessionState;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Fallback: find any session file for a given source.\n * Used when session_id is missing from hook payload (legacy/edge case).\n */\nexport function findSessionBySource(source: Source): SessionState | undefined {\n try {\n if (!fs.existsSync(SESSIONS_DIR)) return undefined;\n const files = fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'));\n for (const f of files) {\n try {\n const state = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf-8')) as SessionState;\n if (state.source === source) return state;\n } catch {}\n }\n } catch {}\n // Also check legacy file as last resort\n try {\n const legacyFile = legacySessionStateFile(source);\n if (fs.existsSync(legacyFile)) {\n return JSON.parse(fs.readFileSync(legacyFile, 'utf-8')) as SessionState;\n }\n } catch {}\n return undefined;\n}\n\nexport function loadSessionId(source: Source): string | undefined {\n return findSessionBySource(source)?.sessionId;\n}\n\nexport function updateSessionState(sessionId: string, updates: Partial<SessionState>): void {\n const state = loadSessionState(sessionId);\n if (!state) return;\n const file = sessionStateFile(sessionId);\n fs.writeFileSync(file, JSON.stringify({ ...state, ...updates }));\n}\n\nexport function saveSessionState(state: SessionState): void {\n ensureSessionsDir();\n const file = sessionStateFile(state.sessionId);\n fs.writeFileSync(file, JSON.stringify(state));\n}\n\n/** Create an EventCollector with crypto state restored from the session file */\nexport function createCollector(sessionId: string | undefined, source: Source): import('../../collector').EventCollector {\n const { EventCollector } = require('../../collector');\n const collector = new EventCollector(sessionId, source);\n\n // Restore crypto state from session file (keyed by sessionId)\n const resolvedId = collector.sessionId;\n const state = loadSessionState(resolvedId);\n if (state?.sessionSecret && state?.chainHash && state.sessionId === resolvedId) {\n collector.restoreCrypto(state.sessionSecret, state.chainHash);\n\n // Update session file after each signed batch\n collector.setCryptoUpdateCallback((chainHash: string, secret: string) => {\n updateSessionState(resolvedId, { chainHash, sessionSecret: secret });\n });\n }\n\n return collector;\n}\n\nexport function removeSessionState(sessionId: string): void {\n try {\n const file = sessionStateFile(sessionId);\n if (fs.existsSync(file)) fs.unlinkSync(file);\n } catch {}\n}\n\n/**\n * Clean up stale session files older than maxAge (default 24h).\n * Called during session-start to prevent buildup.\n */\nexport function cleanStaleSessionFiles(maxAgeMs: number = 7 * 24 * 60 * 60 * 1000): void {\n try {\n if (!fs.existsSync(SESSIONS_DIR)) return;\n const now = Date.now();\n for (const f of fs.readdirSync(SESSIONS_DIR)) {\n if (!f.endsWith('.json')) continue;\n const filePath = path.join(SESSIONS_DIR, f);\n try {\n const stat = fs.statSync(filePath);\n if (now - stat.mtimeMs > maxAgeMs) {\n fs.unlinkSync(filePath);\n }\n } catch {}\n }\n } catch {}\n}\n\n// --- File-based lock to serialize batch sending across concurrent hook processes ---\n\nconst LOCK_STALE_MS = 5_000; // 5 seconds — force-remove stale locks\nconst LOCK_POLL_MS = 20; // polling interval\n\nfunction lockFilePath(sessionId: string): string {\n ensureSessionsDir();\n return path.join(SESSIONS_DIR, `lock-${sessionId}`);\n}\n\nfunction tryAcquireLock(lockPath: string): boolean {\n try {\n const fd = fs.openSync(lockPath, 'wx');\n fs.writeSync(fd, `${process.pid}\\n${Date.now()}`);\n fs.closeSync(fd);\n return true;\n } catch (e: any) {\n if (e.code === 'EEXIST') return false;\n throw e;\n }\n}\n\nfunction clearStaleLock(lockPath: string): boolean {\n try {\n const content = fs.readFileSync(lockPath, 'utf-8');\n const lockTime = parseInt(content.split('\\n')[1] || '0', 10);\n if (Date.now() - lockTime > LOCK_STALE_MS) {\n try { fs.unlinkSync(lockPath); } catch {}\n return true; // was stale, removed\n }\n } catch { return true; /* vanished — retry */ }\n return false;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\nasync function acquireLock(sessionId: string): Promise<void> {\n const lockPath = lockFilePath(sessionId);\n const deadline = Date.now() + LOCK_STALE_MS;\n\n while (Date.now() < deadline) {\n if (tryAcquireLock(lockPath)) return;\n clearStaleLock(lockPath);\n await sleep(LOCK_POLL_MS + Math.random() * 30);\n }\n\n // Timeout — force-remove and take lock\n try { fs.unlinkSync(lockPath); } catch {}\n if (!tryAcquireLock(lockPath)) {\n // Another process grabbed it — proceed without lock (best-effort)\n }\n}\n\nfunction releaseLock(sessionId: string): void {\n try { fs.unlinkSync(lockFilePath(sessionId)); } catch {}\n}\n\n/**\n * Execute a callback while holding a file lock for the given session.\n * Serializes all hook processes for the same session\n * to prevent concurrent batch sends from causing chain hash desync.\n */\nexport async function withSessionLock<T>(sessionId: string, fn: () => Promise<T>): Promise<T> {\n await acquireLock(sessionId);\n try {\n return await fn();\n } finally {\n releaseLock(sessionId);\n }\n}\n\nexport function readStdin(): Promise<string> {\n return new Promise((resolve) => {\n let input = '';\n process.stdin.on('data', (chunk) => { input += chunk; });\n process.stdin.on('end', () => resolve(input));\n });\n}\n\n/**\n * Detect source at runtime. Cursor sets CURSOR_VERSION env var\n * and includes cursor_version in the hook payload.\n * Since Cursor reads ~/.claude/settings.json directly, both tools\n * fire the same hooks — so we detect rather than relying on entry points.\n */\nexport function detectSource(payload?: Record<string, unknown>): Source {\n if (process.env.CURSOR_VERSION) return 'cursor';\n if (payload?.cursor_version) return 'cursor';\n return 'claude';\n}\n\n/**\n * Normalize hook payload across tools.\n * Currently an identity function — Cursor's payload format matches Claude Code's.\n * If formats diverge, add field mappings here (e.g., raw.toolName → tool_name).\n */\nexport function normalizePayload(source: Source, raw: Record<string, unknown>): Record<string, unknown> {\n if (source === 'cursor') {\n return { ...raw };\n }\n return raw;\n}\n","import type { Source } from '../../config';\nimport { loadSessionId, readStdin, normalizePayload, detectSource, createCollector, withSessionLock } from './utils';\n\nexport async function handleSubagent(source: Source): Promise<void> {\n const input = await readStdin();\n try {\n const raw = JSON.parse(input);\n const detected = detectSource(raw);\n const hookData = normalizePayload(detected, raw);\n const sessionId = hookData.session_id || loadSessionId(detected);\n if (!sessionId) return; // No active session\n\n await withSessionLock(sessionId, async () => {\n const collector = createCollector(sessionId, detected);\n\n // PRIVACY: Only track agent type. No task content sent.\n collector.addEvent({\n action_type: 'subagent_spawned',\n value: 1,\n metadata: {\n agent_type: hookData.agent_type || 'unknown',\n },\n });\n\n await collector.flush();\n });\n } catch {}\n}\n","import { handleSubagent } from '../shared/subagent';\nhandleSubagent('cursor');\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwBO,SAAS,iBAAiB,WAA2B;AAC1D,SAAO,YAAAA,QAAK,KAAK,cAAc,GAAG,SAAS,OAAO;AACpD;AAGO,SAAS,uBAAuB,QAAwB;AAC7D,SAAO,YAAAA,QAAK,KAAK,YAAY,kBAAkB,MAAM,OAAO;AAC9D;AAEO,SAAS,kBAAwB;AACtC,MAAI,CAAC,UAAAC,QAAG,WAAW,UAAU,GAAG;AAC9B,cAAAA,QAAG,UAAU,YAAY,EAAE,WAAW,KAAK,CAAC;AAAA,EAC9C;AACF;AAEO,SAAS,aAAkC;AAChD,MAAI;AACF,QAAI,CAAC,UAAAA,QAAG,WAAW,WAAW,EAAG,QAAO;AACxC,UAAM,MAAM,UAAAA,QAAG,aAAa,aAAa,OAAO;AAChD,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,WAAW,QAA4B;AACrD,kBAAgB;AAChB,YAAAA,QAAG,cAAc,aAAa,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,OAAO;AACxE;AApDA,eACA,aACA,WAEa,YACA,aACA,YAgBA;AAtBb;AAAA;AAAA;AAAA,gBAAe;AACf,kBAAiB;AACjB,gBAAe;AAER,IAAM,aAAa,YAAAD,QAAK,KAAK,UAAAE,QAAG,QAAQ,GAAG,cAAc;AACzD,IAAM,cAAc,YAAAF,QAAK,KAAK,YAAY,aAAa;AACvD,IAAM,aAAa,YAAAA,QAAK,KAAK,YAAY,aAAa;AAgBtD,IAAM,eAAe,YAAAA,QAAK,KAAK,YAAY,UAAU;AAAA;AAAA;;;ACtB5D;AAAA,4BAAAG,UAAA;AAAA;AACA,WAAO,eAAeA,UAAS,cAAc,EAAE,OAAO,KAAK,CAAC;AAAA;AAAA;;;;;;;;AC0B5D,IAAAC,SAAA,gBAAA;AAOA,IAAAA,SAAA,sBAAA;AAUA,IAAAA,SAAA,wBAAA;AAMA,IAAAA,SAAA,kBAAA;AAhDa,IAAAA,SAAA,yBAAyB;AACzB,IAAAA,SAAA,0BAA0B;AAC1B,IAAAA,SAAA,qBAAqB;AAErB,IAAAA,SAAA,cAAyE;MACpF,aAAoB,EAAE,SAAS,GAAK,UAAU,IAAI;MAClD,cAAoB,EAAE,SAAS,GAAK,UAAU,IAAI;MAClD,WAAoB,EAAE,SAAS,GAAK,UAAU,EAAC;MAC/C,cAAoB,EAAE,SAAS,IAAK,UAAU,EAAC;MAC/C,aAAoB,EAAE,SAAS,IAAK,UAAU,EAAC;MAC/C,QAAoB,EAAE,SAAS,IAAK,UAAU,EAAC;MAC/C,mBAAoB,EAAE,SAAS,KAAK,UAAU,EAAC;MAC/C,YAAoB,EAAE,SAAS,KAAK,UAAU,EAAC;MAC/C,kBAAoB,EAAE,SAAS,IAAK,UAAU,EAAC;MAC/C,kBAAoB,EAAE,SAAS,IAAK,UAAU,EAAC;MAC/C,mBAAoB,EAAE,SAAS,IAAK,UAAU,EAAC;MAC/C,eAAoB,EAAE,SAAS,IAAK,UAAU,EAAC;;AAGpC,IAAAA,SAAA,kBAAkB;MAC7B;MAA6B;MAAqB;MAAiB;MACnE;MACA;MAAmC;;AAGrC,aAAgB,cAAc,YAAwB,OAAa;AACjE,YAAM,SAASA,SAAA,YAAY,UAAU;AACrC,aAAO,KAAK,MAAM,QAAQ,OAAO,QAAQ,IAAI,OAAO;IACtD;AAIA,aAAgB,oBAAoB,OAAsB;AACxD,UAAI,SAAS;AACb,UAAI,MAAM,eAAe;AAAI,iBAAS;eAC7B,MAAM,eAAe;AAAG,iBAAS;eACjC,MAAM,eAAe;AAAG,iBAAS;AAC1C,YAAM,UAAU,MAAM,iBAAiB,MAAM;AAC7C,YAAM,YAAY,MAAM,kBAAkB,IAAI,MAAM;AACpD,aAAO,SAAS,UAAU;IAC5B;AAEA,aAAgB,sBAAsB,QAA2D,YAAkB;AACjH,UAAI,YAAY;AAChB,iBAAW,KAAK;AAAQ,qBAAa,cAAc,EAAE,aAAa,EAAE,KAAK;AACzE,aAAO,KAAK,MAAM,KAAK,IAAI,WAAWA,SAAA,sBAAsB,IAAI,UAAU;IAC5E;AAEA,aAAgB,gBAAgB,SAAe;AAC7C,aAAOA,SAAA,gBAAgB,KAAK,CAAC,MAAM,EAAE,KAAK,QAAQ,KAAI,CAAE,CAAC;IAC3D;;;;;;;;;ACzCA,IAAAC,SAAA,YAAA;AASA,IAAAA,SAAA,mBAAAC;AAYA,IAAAD,SAAA,YAAAE;AAaA,IAAAF,SAAA,uBAAA;AArCA,QAAA,WAAA,QAAA,QAAA;AAGA,aAAgB,UACd,OACA,UAAgB;AAEhB,YAAM,UAAU,GAAG,QAAQ,IAAI,MAAM,WAAW,IAAI,MAAM,KAAK,IAAI,MAAM,SAAS;AAClF,cAAO,GAAA,SAAA,YAAW,QAAQ,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;IACvE;AAGA,aAAgBC,kBACd,QACA,cAAsB,oBAAkB;AAExC,UAAI,OAAO;AACX,iBAAW,SAAS,QAAQ;AAC1B,eAAO,UAAU,OAAO,IAAI;MAC9B;AACA,aAAO;IACT;AAGA,aAAgBC,WACd,WACA,QACA,WACA,QAAc;AAGd,YAAM,WAAW,OAAO,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,OAAO,CAAC;AACvD,YAAM,UAAU,GAAG,SAAS,IAAI,SAAS,IAAI,OAAO,MAAM,IAAI,QAAQ;AACtE,cAAO,GAAA,SAAA,YAAW,UAAU,MAAM,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;IAClE;AAGA,aAAgB,qBACd,WACA,QACA,WACA,WACA,QAAc;AAEd,YAAM,WAAWA,WAAU,WAAW,QAAQ,WAAW,MAAM;AAE/D,UAAI,SAAS,WAAW,UAAU;AAAQ,eAAO;AACjD,UAAI,WAAW;AACf,eAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,oBAAY,SAAS,WAAW,CAAC,IAAI,UAAU,WAAW,CAAC;MAC7D;AACA,aAAO,aAAa;IACtB;;;;;;;;;;;;;;;;;;;;;;;;;AC5DA,iBAAA,iBAAAC,QAAA;AACA,iBAAA,mBAAAA,QAAA;AACA,iBAAA,qBAAAA,QAAA;;;;;ACFA,IACA,eAGa;AAJb;AAAA;AAAA;AAAA;AACA,oBAA4C;AAGrC,IAAM,YAAN,MAAgB;AAAA,MACb,SAAS,WAAW;AAAA,MACpB,gBAA+B;AAAA,MAC/B,YAAoB;AAAA,MACpB,gBAAsE;AAAA;AAAA,MAG9E,uBAAuB,IAAiD;AACtE,aAAK,gBAAgB;AAAA,MACvB;AAAA;AAAA,MAGA,mBAAmB,QAAgB,WAAmB;AACpD,aAAK,gBAAgB;AACrB,aAAK,YAAY;AAAA,MACnB;AAAA,MAEA,IAAY,UAAkC;AAC5C,eAAO;AAAA,UACL,gBAAgB;AAAA,UAChB,eAAe,UAAU,KAAK,QAAQ,SAAS,EAAE;AAAA,QACnD;AAAA,MACF;AAAA;AAAA,MAGA,iBAA+D;AAC7D,YAAI,CAAC,KAAK,cAAe,QAAO;AAChC,eAAO,EAAE,QAAQ,KAAK,eAAe,WAAW,KAAK,UAAU;AAAA,MACjE;AAAA;AAAA,MAGA,MAAM,aAAa,WAAqC;AACtD,YAAI;AACF,gBAAM,SAAS,KAAK,QAAQ,UAAU;AACtC,gBAAM,MAAM,MAAM,MAAM,GAAG,MAAM,uBAAuB;AAAA,YACtD,QAAQ;AAAA,YACR,SAAS,KAAK;AAAA,YACd,MAAM,KAAK,UAAU,EAAE,YAAY,UAAU,CAAC;AAAA,UAChD,CAAC;AACD,cAAI,IAAI,IAAI;AACV,kBAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,iBAAK,gBAAgB,KAAK;AAC1B,iBAAK,YAAY,KAAK;AACtB,mBAAO;AAAA,UACT;AACA,iBAAO;AAAA,QACT,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,MAEA,MAAM,UAAU,OAAqC;AACnD,YAAI;AACF,gBAAM,SAAS,KAAK,QAAQ,UAAU;AAGtC,cAAI,KAAK,eAAe;AACtB,kBAAM,gBAAgB,KAAK;AAC3B,kBAAM,mBAAe,gCAAiB,MAAM,QAAQ,aAAa;AACjE,kBAAM,gBAAY,yBAAU,MAAM,YAAY,MAAM,QAAQ,cAAc,KAAK,aAAa;AAE5F,kBAAM,YAAY;AAClB,kBAAM,aAAa;AACnB,kBAAM,kBAAkB;AAExB,kBAAMC,OAAM,MAAM,MAAM,GAAG,MAAM,qBAAqB;AAAA,cACpD,QAAQ;AAAA,cACR,SAAS,KAAK;AAAA,cACd,MAAM,KAAK,UAAU,KAAK;AAAA,YAC5B,CAAC;AAED,gBAAIA,KAAI,IAAI;AAEV,mBAAK,YAAY;AACjB,kBAAI,KAAK,eAAe;AACtB,qBAAK,cAAc,KAAK,WAAW,KAAK,aAAa;AAAA,cACvD;AAAA,YACF;AAEA,mBAAOA,KAAI;AAAA,UACb;AAEA,gBAAM,MAAM,MAAM,MAAM,GAAG,MAAM,qBAAqB;AAAA,YACpD,QAAQ;AAAA,YACR,SAAS,KAAK;AAAA,YACd,MAAM,KAAK,UAAU,KAAK;AAAA,UAC5B,CAAC;AACD,iBAAO,IAAI;AAAA,QACb,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,MAEA,MAAM,uBAAsC;AAC1C,YAAI,CAAC,KAAK,OAAQ;AAClB,YAAI;AACF,gBAAM,QAAQ,KAAK,OAAO,MAAM,MAAM,GAAG;AACzC,cAAI,MAAM,WAAW,EAAG;AACxB,gBAAM,UAAU,KAAK,MAAM,OAAO,KAAK,MAAM,CAAC,GAAG,QAAQ,EAAE,SAAS,OAAO,CAAC;AAC5E,gBAAM,QAAQ,QAAQ,MAAM;AAC5B,gBAAM,WAAW,KAAK,KAAK,KAAK;AAChC,cAAI,KAAK,IAAI,IAAI,WAAW,MAAO;AAEnC,gBAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,MAAM,+BAA+B;AAAA,YAC1E,QAAQ;AAAA,YACR,SAAS,KAAK;AAAA,YACd,MAAM,KAAK,UAAU,EAAE,eAAe,KAAK,OAAO,cAAc,CAAC;AAAA,UACnE,CAAC;AACD,cAAI,IAAI,IAAI;AACV,kBAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,gBAAI,KAAK,OAAO;AACd,mBAAK,OAAO,QAAQ,KAAK;AACzB,kBAAI,KAAK,cAAe,MAAK,OAAO,gBAAgB,KAAK;AACzD,yBAAW,KAAK,MAAM;AAAA,YACxB;AAAA,UACF;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,MAEA,MAAM,YAA8C;AAClD,cAAM,SAAS,KAAK,QAAQ,UAAU;AACtC,cAAM,MAAM,MAAM,MAAM,GAAG,MAAM,iBAAiB,EAAE,SAAS,KAAK,QAAQ,CAAC;AAC3E,YAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,qBAAqB,IAAI,MAAM,EAAE;AAC9D,eAAO,IAAI,KAAK;AAAA,MAClB;AAAA,MAEA,MAAM,UAA4C;AAChD,cAAM,SAAS,KAAK,QAAQ,UAAU;AACtC,cAAM,MAAM,MAAM,MAAM,GAAG,MAAM,sBAAsB,EAAE,SAAS,KAAK,QAAQ,CAAC;AAChF,YAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,mBAAmB,IAAI,MAAM,EAAE;AAC5D,eAAO,IAAI,KAAK;AAAA,MAClB;AAAA,MAEA,MAAM,iBAAmD;AACvD,cAAM,SAAS,KAAK,QAAQ,UAAU;AACtC,cAAM,MAAM,MAAM,MAAM,GAAG,MAAM,oCAAoC,EAAE,SAAS,KAAK,QAAQ,CAAC;AAC9F,YAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,EAAE;AACnE,eAAO,IAAI,KAAK;AAAA,MAClB;AAAA,MAEA,MAAM,iBAA0D;AAC9D,YAAI;AACF,gBAAM,SAAS,KAAK,QAAQ,UAAU;AACtC,gBAAM,MAAM,MAAM,MAAM,GAAG,MAAM,mBAAmB;AACpD,cAAI,CAAC,IAAI,GAAI,QAAO;AACpB,iBAAO,IAAI,KAAK;AAAA,QAClB,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,MAEA,MAAM,mBAAgG;AACpG,YAAI;AACF,gBAAM,SAAS,KAAK,QAAQ,UAAU;AACtC,gBAAM,MAAM,MAAM,MAAM,GAAG,MAAM,+BAA+B,EAAE,SAAS,KAAK,QAAQ,CAAC;AACzF,cAAI,CAAC,IAAI,GAAI,QAAO;AACpB,iBAAO,IAAI,KAAK;AAAA,QAClB,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,MAEA,MAAM,cAAc,MAAuD;AACzE,YAAI;AACF,gBAAM,SAAS,KAAK,QAAQ,UAAU;AACtC,gBAAM,MAAM,MAAM,MAAM,GAAG,MAAM,uBAAuB;AAAA,YACtD,QAAQ;AAAA,YACR,SAAS,KAAK;AAAA,YACd,MAAM,KAAK,UAAU,EAAE,eAAe,KAAK,CAAC;AAAA,UAC9C,CAAC;AACD,cAAI,CAAC,IAAI,GAAI,QAAO;AACpB,iBAAO,IAAI,KAAK;AAAA,QAClB,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,MAEA,MAAM,mBAA4D;AAChE,YAAI;AACF,gBAAM,SAAS,KAAK,QAAQ,UAAU;AACtC,gBAAM,MAAM,MAAM,MAAM,GAAG,MAAM,uBAAuB,EAAE,SAAS,KAAK,QAAQ,CAAC;AACjF,cAAI,CAAC,IAAI,GAAI,QAAO;AACpB,iBAAO,IAAI,KAAK;AAAA,QAClB,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA;AAAA;;;AC1LO,SAAS,cAAc,QAA0B;AACtD,kBAAgB;AAChB,QAAM,QAAQ,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,IAAI;AAC5D,aAAAC,QAAG,eAAe,YAAY,QAAQ,MAAM,OAAO;AAGnD,MAAI;AACF,UAAM,UAAU,WAAAA,QAAG,aAAa,YAAY,OAAO;AACnD,UAAM,WAAW,QAAQ,MAAM,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,MAAM,EAAE;AAClE,QAAI,SAAS,SAAS,gBAAgB;AACpC,YAAM,UAAU,SAAS,MAAM,SAAS,SAAS,cAAc;AAC/D,iBAAAA,QAAG,cAAc,YAAY,QAAQ,KAAK,IAAI,IAAI,MAAM,OAAO;AAAA,IACjE;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,aAAyB;AACvC,MAAI;AACF,QAAI,CAAC,WAAAA,QAAG,WAAW,UAAU,EAAG,QAAO,CAAC;AACxC,UAAM,UAAU,WAAAA,QAAG,aAAa,YAAY,OAAO;AACnD,eAAAA,QAAG,cAAc,YAAY,IAAI,OAAO;AAExC,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,SAAqB,CAAC;AAC5B,eAAW,QAAQ,QAAQ,MAAM,IAAI,GAAG;AACtC,UAAI,CAAC,KAAK,KAAK,EAAG;AAClB,UAAI;AACF,cAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,cAAM,MAAM,MAAM,IAAI,KAAK,MAAM,SAAS,EAAE,QAAQ;AACpD,YAAI,OAAO,YAAY;AACrB,iBAAO,KAAK,KAAK;AAAA,QACnB;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAjDA,IAAAC,YAIM,gBACA;AALN;AAAA;AAAA;AAAA,IAAAA,aAAe;AACf;AAGA,IAAM,iBAAiB;AACvB,IAAM,aAAa,KAAK,KAAK,KAAK;AAAA;AAAA;;;ACLlC;AAAA;AAAA;AAAA;AAAA,iBASa;AATb;AAAA;AAAA;AAAA,kBAA6B;AAC7B;AACA;AACA;AAMO,IAAM,iBAAN,MAAqB;AAAA,MACjB;AAAA,MACD,SAAqB,CAAC;AAAA,MACtB,gBAAuD;AAAA,MACtD;AAAA,MACD;AAAA,MAER,YAAY,WAAoB,QAAiB;AAC/C,aAAK,YAAY,iBAAa,YAAAC,IAAO;AACrC,aAAK,SAAS,IAAI,UAAU;AAC5B,aAAK,SAAS;AAAA,MAChB;AAAA;AAAA,MAGA,cAAc,QAAgB,WAAmB;AAC/C,aAAK,OAAO,mBAAmB,QAAQ,SAAS;AAAA,MAClD;AAAA;AAAA,MAGA,wBAAwB,IAAiD;AACvE,aAAK,OAAO,uBAAuB,EAAE;AAAA,MACvC;AAAA,MAEA,SAAS,OAA4B;AACnC,cAAM,WAAW,KAAK,SAClB,EAAE,GAAG,MAAM,UAAU,QAAQ,KAAK,OAAO,IACzC,MAAM;AACV,aAAK,OAAO,KAAK;AAAA,UACf,GAAG;AAAA,UACH;AAAA,UACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,CAAC;AAAA,MACH;AAAA,MAEA,MAAM,QAAuB;AAC3B,cAAM,KAAK,OAAO,qBAAqB;AAGvC,cAAM,KAAK,OAAO,aAAa,KAAK,SAAS;AAI7C,mBAAW;AAEX,cAAM,SAAS,WAAW;AAC1B,cAAM,aAAa,QAAQ,aAAa,qBAAqB;AAC7D,aAAK,gBAAgB,YAAY,MAAM;AACrC,eAAK,MAAM,EAAE,MAAM,MAAM;AAAA,UAAC,CAAC;AAAA,QAC7B,GAAG,UAAU;AAAA,MACf;AAAA,MAEA,MAAM,QAAuB;AAC3B,YAAI,KAAK,OAAO,WAAW,EAAG;AAC9B,cAAM,SAAS,KAAK,OAAO,OAAO,CAAC;AACnC,cAAM,QAAQ,EAAE,YAAY,KAAK,WAAW,OAAO;AACnD,cAAM,KAAK,MAAM,KAAK,OAAO,UAAU,KAAK;AAC5C,YAAI,CAAC,IAAI;AACP,wBAAc,MAAM;AAAA,QACtB;AAAA,MACF;AAAA,MAEA,MAAM,OAAsB;AAC1B,YAAI,KAAK,kBAAkB,MAAM;AAC/B,wBAAc,KAAK,aAAa;AAChC,eAAK,gBAAgB;AAAA,QACvB;AACA,aAAK,SAAS,EAAE,aAAa,qBAAmC,OAAO,GAAG,UAAU,CAAC,EAAE,CAAC;AACxF,cAAM,KAAK,MAAM;AAAA,MACnB;AAAA,IACF;AAAA;AAAA;;;AC9EA,IAAAC,aAAe;AACf,IAAAC,eAAiB;AAEjB;AAUA,SAAS,oBAA0B;AACjC,MAAI,CAAC,WAAAC,QAAG,WAAW,YAAY,GAAG;AAChC,eAAAA,QAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;AAAA,EAChD;AACF;AAEO,SAAS,iBAAiB,WAA6C;AAC5E,MAAI;AACF,UAAM,OAAO,iBAAiB,SAAS;AACvC,QAAI,CAAC,WAAAA,QAAG,WAAW,IAAI,EAAG,QAAO;AACjC,WAAO,KAAK,MAAM,WAAAA,QAAG,aAAa,MAAM,OAAO,CAAC;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,oBAAoB,QAA0C;AAC5E,MAAI;AACF,QAAI,CAAC,WAAAA,QAAG,WAAW,YAAY,EAAG,QAAO;AACzC,UAAM,QAAQ,WAAAA,QAAG,YAAY,YAAY,EAAE,OAAO,OAAK,EAAE,SAAS,OAAO,CAAC;AAC1E,eAAW,KAAK,OAAO;AACrB,UAAI;AACF,cAAM,QAAQ,KAAK,MAAM,WAAAA,QAAG,aAAa,aAAAC,QAAK,KAAK,cAAc,CAAC,GAAG,OAAO,CAAC;AAC7E,YAAI,MAAM,WAAW,OAAQ,QAAO;AAAA,MACtC,QAAQ;AAAA,MAAC;AAAA,IACX;AAAA,EACF,QAAQ;AAAA,EAAC;AAET,MAAI;AACF,UAAM,aAAa,uBAAuB,MAAM;AAChD,QAAI,WAAAD,QAAG,WAAW,UAAU,GAAG;AAC7B,aAAO,KAAK,MAAM,WAAAA,QAAG,aAAa,YAAY,OAAO,CAAC;AAAA,IACxD;AAAA,EACF,QAAQ;AAAA,EAAC;AACT,SAAO;AACT;AAEO,SAAS,cAAc,QAAoC;AAChE,SAAO,oBAAoB,MAAM,GAAG;AACtC;AAEO,SAAS,mBAAmB,WAAmB,SAAsC;AAC1F,QAAM,QAAQ,iBAAiB,SAAS;AACxC,MAAI,CAAC,MAAO;AACZ,QAAM,OAAO,iBAAiB,SAAS;AACvC,aAAAA,QAAG,cAAc,MAAM,KAAK,UAAU,EAAE,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;AACjE;AASO,SAAS,gBAAgB,WAA+B,QAA0D;AACvH,QAAM,EAAE,gBAAAE,gBAAe,IAAI;AAC3B,QAAM,YAAY,IAAIA,gBAAe,WAAW,MAAM;AAGtD,QAAM,aAAa,UAAU;AAC7B,QAAM,QAAQ,iBAAiB,UAAU;AACzC,MAAI,OAAO,iBAAiB,OAAO,aAAa,MAAM,cAAc,YAAY;AAC9E,cAAU,cAAc,MAAM,eAAe,MAAM,SAAS;AAG5D,cAAU,wBAAwB,CAAC,WAAmB,WAAmB;AACvE,yBAAmB,YAAY,EAAE,WAAW,eAAe,OAAO,CAAC;AAAA,IACrE,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAgCA,IAAM,gBAAgB;AACtB,IAAM,eAAe;AAErB,SAAS,aAAa,WAA2B;AAC/C,oBAAkB;AAClB,SAAO,aAAAC,QAAK,KAAK,cAAc,QAAQ,SAAS,EAAE;AACpD;AAEA,SAAS,eAAe,UAA2B;AACjD,MAAI;AACF,UAAM,KAAK,WAAAC,QAAG,SAAS,UAAU,IAAI;AACrC,eAAAA,QAAG,UAAU,IAAI,GAAG,QAAQ,GAAG;AAAA,EAAK,KAAK,IAAI,CAAC,EAAE;AAChD,eAAAA,QAAG,UAAU,EAAE;AACf,WAAO;AAAA,EACT,SAAS,GAAQ;AACf,QAAI,EAAE,SAAS,SAAU,QAAO;AAChC,UAAM;AAAA,EACR;AACF;AAEA,SAAS,eAAe,UAA2B;AACjD,MAAI;AACF,UAAM,UAAU,WAAAA,QAAG,aAAa,UAAU,OAAO;AACjD,UAAM,WAAW,SAAS,QAAQ,MAAM,IAAI,EAAE,CAAC,KAAK,KAAK,EAAE;AAC3D,QAAI,KAAK,IAAI,IAAI,WAAW,eAAe;AACzC,UAAI;AAAE,mBAAAA,QAAG,WAAW,QAAQ;AAAA,MAAG,QAAQ;AAAA,MAAC;AACxC,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAE,WAAO;AAAA,EAA6B;AAC9C,SAAO;AACT;AAEA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAC7C;AAEA,eAAe,YAAY,WAAkC;AAC3D,QAAM,WAAW,aAAa,SAAS;AACvC,QAAM,WAAW,KAAK,IAAI,IAAI;AAE9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,QAAI,eAAe,QAAQ,EAAG;AAC9B,mBAAe,QAAQ;AACvB,UAAM,MAAM,eAAe,KAAK,OAAO,IAAI,EAAE;AAAA,EAC/C;AAGA,MAAI;AAAE,eAAAA,QAAG,WAAW,QAAQ;AAAA,EAAG,QAAQ;AAAA,EAAC;AACxC,MAAI,CAAC,eAAe,QAAQ,GAAG;AAAA,EAE/B;AACF;AAEA,SAAS,YAAY,WAAyB;AAC5C,MAAI;AAAE,eAAAA,QAAG,WAAW,aAAa,SAAS,CAAC;AAAA,EAAG,QAAQ;AAAA,EAAC;AACzD;AAOA,eAAsB,gBAAmB,WAAmB,IAAkC;AAC5F,QAAM,YAAY,SAAS;AAC3B,MAAI;AACF,WAAO,MAAM,GAAG;AAAA,EAClB,UAAE;AACA,gBAAY,SAAS;AAAA,EACvB;AACF;AAEO,SAAS,YAA6B;AAC3C,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,QAAI,QAAQ;AACZ,YAAQ,MAAM,GAAG,QAAQ,CAAC,UAAU;AAAE,eAAS;AAAA,IAAO,CAAC;AACvD,YAAQ,MAAM,GAAG,OAAO,MAAM,QAAQ,KAAK,CAAC;AAAA,EAC9C,CAAC;AACH;AAQO,SAAS,aAAa,SAA2C;AACtE,MAAI,QAAQ,IAAI,eAAgB,QAAO;AACvC,MAAI,SAAS,eAAgB,QAAO;AACpC,SAAO;AACT;AAOO,SAAS,iBAAiB,QAAgB,KAAuD;AACtG,MAAI,WAAW,UAAU;AACvB,WAAO,EAAE,GAAG,IAAI;AAAA,EAClB;AACA,SAAO;AACT;;;AC3NA,eAAsB,eAAe,QAA+B;AAClE,QAAM,QAAQ,MAAM,UAAU;AAC9B,MAAI;AACF,UAAM,MAAM,KAAK,MAAM,KAAK;AAC5B,UAAM,WAAW,aAAa,GAAG;AACjC,UAAM,WAAW,iBAAiB,UAAU,GAAG;AAC/C,UAAM,YAAY,SAAS,cAAc,cAAc,QAAQ;AAC/D,QAAI,CAAC,UAAW;AAEhB,UAAM,gBAAgB,WAAW,YAAY;AAC3C,YAAM,YAAY,gBAAgB,WAAW,QAAQ;AAGrD,gBAAU,SAAS;AAAA,QACjB,aAAa;AAAA,QACb,OAAO;AAAA,QACP,UAAU;AAAA,UACR,YAAY,SAAS,cAAc;AAAA,QACrC;AAAA,MACF,CAAC;AAED,YAAM,UAAU,MAAM;AAAA,IACxB,CAAC;AAAA,EACH,QAAQ;AAAA,EAAC;AACX;;;AC1BA,eAAe,QAAQ;","names":["path","fs","os","exports","exports","exports","computeChainHash","signBatch","exports","res","fs","import_fs","uuidv4","import_fs","import_path","fs","path","EventCollector","path","fs"]}
@@ -173,7 +173,10 @@ var require_dist = __commonJS({
173
173
  });
174
174
 
175
175
  // src/config.ts
176
- function sessionStateFile(source) {
176
+ function sessionStateFile(sessionId) {
177
+ return import_path.default.join(SESSIONS_DIR, `${sessionId}.json`);
178
+ }
179
+ function legacySessionStateFile(source) {
177
180
  return import_path.default.join(CONFIG_DIR, `active-session-${source}.json`);
178
181
  }
179
182
  function ensureConfigDir() {
@@ -194,7 +197,7 @@ function saveConfig(config) {
194
197
  ensureConfigDir();
195
198
  import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
196
199
  }
197
- var import_fs, import_path, import_os, CONFIG_DIR, CONFIG_FILE, QUEUE_FILE;
200
+ var import_fs, import_path, import_os, CONFIG_DIR, CONFIG_FILE, QUEUE_FILE, SESSIONS_DIR;
198
201
  var init_config = __esm({
199
202
  "src/config.ts"() {
200
203
  "use strict";
@@ -204,6 +207,7 @@ var init_config = __esm({
204
207
  CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".hackersbaby");
205
208
  CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
206
209
  QUEUE_FILE = import_path.default.join(CONFIG_DIR, "queue.jsonl");
210
+ SESSIONS_DIR = import_path.default.join(CONFIG_DIR, "sessions");
207
211
  }
208
212
  });
209
213
 
@@ -511,40 +515,69 @@ var import_shared2 = __toESM(require_dist());
511
515
  var import_fs3 = __toESM(require("fs"));
512
516
  var import_path2 = __toESM(require("path"));
513
517
  init_config();
514
- function loadSessionState(source) {
518
+ function ensureSessionsDir() {
519
+ if (!import_fs3.default.existsSync(SESSIONS_DIR)) {
520
+ import_fs3.default.mkdirSync(SESSIONS_DIR, { recursive: true });
521
+ }
522
+ }
523
+ function loadSessionState(sessionId) {
515
524
  try {
516
- const file = sessionStateFile(source);
525
+ const file = sessionStateFile(sessionId);
517
526
  if (!import_fs3.default.existsSync(file)) return void 0;
518
527
  return JSON.parse(import_fs3.default.readFileSync(file, "utf-8"));
519
528
  } catch {
520
529
  return void 0;
521
530
  }
522
531
  }
532
+ function findSessionBySource(source) {
533
+ try {
534
+ if (!import_fs3.default.existsSync(SESSIONS_DIR)) return void 0;
535
+ const files = import_fs3.default.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
536
+ for (const f of files) {
537
+ try {
538
+ const state = JSON.parse(import_fs3.default.readFileSync(import_path2.default.join(SESSIONS_DIR, f), "utf-8"));
539
+ if (state.source === source) return state;
540
+ } catch {
541
+ }
542
+ }
543
+ } catch {
544
+ }
545
+ try {
546
+ const legacyFile = legacySessionStateFile(source);
547
+ if (import_fs3.default.existsSync(legacyFile)) {
548
+ return JSON.parse(import_fs3.default.readFileSync(legacyFile, "utf-8"));
549
+ }
550
+ } catch {
551
+ }
552
+ return void 0;
553
+ }
523
554
  function loadSessionId(source) {
524
- return loadSessionState(source)?.sessionId;
555
+ return findSessionBySource(source)?.sessionId;
525
556
  }
526
- function updateSessionState(source, updates) {
527
- const state = loadSessionState(source);
557
+ function updateSessionState(sessionId, updates) {
558
+ const state = loadSessionState(sessionId);
528
559
  if (!state) return;
529
- const file = sessionStateFile(source);
560
+ const file = sessionStateFile(sessionId);
530
561
  import_fs3.default.writeFileSync(file, JSON.stringify({ ...state, ...updates }));
531
562
  }
532
563
  function createCollector(sessionId, source) {
533
564
  const { EventCollector: EventCollector2 } = (init_collector(), __toCommonJS(collector_exports));
534
565
  const collector = new EventCollector2(sessionId, source);
535
- const state = loadSessionState(source);
536
- if (state?.sessionSecret && state?.chainHash && state.sessionId === sessionId) {
566
+ const resolvedId = collector.sessionId;
567
+ const state = loadSessionState(resolvedId);
568
+ if (state?.sessionSecret && state?.chainHash && state.sessionId === resolvedId) {
537
569
  collector.restoreCrypto(state.sessionSecret, state.chainHash);
538
570
  collector.setCryptoUpdateCallback((chainHash, secret) => {
539
- updateSessionState(source, { chainHash, sessionSecret: secret });
571
+ updateSessionState(resolvedId, { chainHash, sessionSecret: secret });
540
572
  });
541
573
  }
542
574
  return collector;
543
575
  }
544
576
  var LOCK_STALE_MS = 5e3;
545
577
  var LOCK_POLL_MS = 20;
546
- function lockFilePath(source) {
547
- return import_path2.default.join(CONFIG_DIR, `lock-${source}`);
578
+ function lockFilePath(sessionId) {
579
+ ensureSessionsDir();
580
+ return import_path2.default.join(SESSIONS_DIR, `lock-${sessionId}`);
548
581
  }
549
582
  function tryAcquireLock(lockPath) {
550
583
  try {
@@ -577,8 +610,8 @@ function clearStaleLock(lockPath) {
577
610
  function sleep(ms) {
578
611
  return new Promise((r) => setTimeout(r, ms));
579
612
  }
580
- async function acquireLock(source) {
581
- const lockPath = lockFilePath(source);
613
+ async function acquireLock(sessionId) {
614
+ const lockPath = lockFilePath(sessionId);
582
615
  const deadline = Date.now() + LOCK_STALE_MS;
583
616
  while (Date.now() < deadline) {
584
617
  if (tryAcquireLock(lockPath)) return;
@@ -592,18 +625,18 @@ async function acquireLock(source) {
592
625
  if (!tryAcquireLock(lockPath)) {
593
626
  }
594
627
  }
595
- function releaseLock(source) {
628
+ function releaseLock(sessionId) {
596
629
  try {
597
- import_fs3.default.unlinkSync(lockFilePath(source));
630
+ import_fs3.default.unlinkSync(lockFilePath(sessionId));
598
631
  } catch {
599
632
  }
600
633
  }
601
- async function withSessionLock(source, fn) {
602
- await acquireLock(source);
634
+ async function withSessionLock(sessionId, fn) {
635
+ await acquireLock(sessionId);
603
636
  try {
604
637
  return await fn();
605
638
  } finally {
606
- releaseLock(source);
639
+ releaseLock(sessionId);
607
640
  }
608
641
  }
609
642
  function readStdin() {
@@ -637,7 +670,8 @@ async function handleToolCall(source) {
637
670
  const toolName = hookData.tool_name || "unknown";
638
671
  const toolInput = hookData.tool_input || {};
639
672
  const sessionId = hookData.session_id || loadSessionId(detected);
640
- await withSessionLock(detected, async () => {
673
+ if (!sessionId) return;
674
+ await withSessionLock(sessionId, async () => {
641
675
  const collector = createCollector(sessionId, detected);
642
676
  collector.addEvent({ action_type: "tool_call", value: 1, metadata: { tool_name: toolName } });
643
677
  if (hookData.input_tokens) {