@hackersbaby/plugin 0.5.3 → 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.
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/hooks/compact.js +55 -21
- package/dist/hooks/compact.js.map +1 -1
- package/dist/hooks/cursor/compact.js +55 -21
- package/dist/hooks/cursor/compact.js.map +1 -1
- package/dist/hooks/cursor/prompt-submit.js +55 -21
- package/dist/hooks/cursor/prompt-submit.js.map +1 -1
- package/dist/hooks/cursor/session-end.js +67 -29
- package/dist/hooks/cursor/session-end.js.map +1 -1
- package/dist/hooks/cursor/session-start.js +34 -7
- package/dist/hooks/cursor/session-start.js.map +1 -1
- package/dist/hooks/cursor/stop.js +55 -21
- package/dist/hooks/cursor/stop.js.map +1 -1
- package/dist/hooks/cursor/subagent.js +55 -21
- package/dist/hooks/cursor/subagent.js.map +1 -1
- package/dist/hooks/cursor/tool-call.js +55 -21
- package/dist/hooks/cursor/tool-call.js.map +1 -1
- package/dist/hooks/prompt-submit.js +55 -21
- package/dist/hooks/prompt-submit.js.map +1 -1
- package/dist/hooks/session-end.js +67 -29
- package/dist/hooks/session-end.js.map +1 -1
- package/dist/hooks/session-start.js +34 -7
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop.js +55 -21
- package/dist/hooks/stop.js.map +1 -1
- package/dist/hooks/subagent.js +55 -21
- package/dist/hooks/subagent.js.map +1 -1
- package/dist/hooks/tool-call.js +55 -21
- package/dist/hooks/tool-call.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -30,8 +30,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
|
|
32
32
|
// src/config.ts
|
|
33
|
-
function sessionStateFile(
|
|
34
|
-
return import_path.default.join(
|
|
33
|
+
function sessionStateFile(sessionId) {
|
|
34
|
+
return import_path.default.join(SESSIONS_DIR, `${sessionId}.json`);
|
|
35
35
|
}
|
|
36
36
|
function ensureConfigDir() {
|
|
37
37
|
if (!import_fs.default.existsSync(CONFIG_DIR)) {
|
|
@@ -51,7 +51,7 @@ function saveConfig(config) {
|
|
|
51
51
|
ensureConfigDir();
|
|
52
52
|
import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
53
53
|
}
|
|
54
|
-
var import_fs, import_path, import_os, CONFIG_DIR, CONFIG_FILE, QUEUE_FILE;
|
|
54
|
+
var import_fs, import_path, import_os, CONFIG_DIR, CONFIG_FILE, QUEUE_FILE, SESSIONS_DIR;
|
|
55
55
|
var init_config = __esm({
|
|
56
56
|
"src/config.ts"() {
|
|
57
57
|
"use strict";
|
|
@@ -61,6 +61,7 @@ var init_config = __esm({
|
|
|
61
61
|
CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".hackersbaby");
|
|
62
62
|
CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
|
|
63
63
|
QUEUE_FILE = import_path.default.join(CONFIG_DIR, "queue.jsonl");
|
|
64
|
+
SESSIONS_DIR = import_path.default.join(CONFIG_DIR, "sessions");
|
|
64
65
|
}
|
|
65
66
|
});
|
|
66
67
|
|
|
@@ -502,11 +503,36 @@ init_config();
|
|
|
502
503
|
|
|
503
504
|
// src/hooks/shared/utils.ts
|
|
504
505
|
var import_fs3 = __toESM(require("fs"));
|
|
506
|
+
var import_path2 = __toESM(require("path"));
|
|
505
507
|
init_config();
|
|
508
|
+
function ensureSessionsDir() {
|
|
509
|
+
if (!import_fs3.default.existsSync(SESSIONS_DIR)) {
|
|
510
|
+
import_fs3.default.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
506
513
|
function saveSessionState(state) {
|
|
507
|
-
|
|
514
|
+
ensureSessionsDir();
|
|
515
|
+
const file = sessionStateFile(state.sessionId);
|
|
508
516
|
import_fs3.default.writeFileSync(file, JSON.stringify(state));
|
|
509
517
|
}
|
|
518
|
+
function cleanStaleSessionFiles(maxAgeMs = 7 * 24 * 60 * 60 * 1e3) {
|
|
519
|
+
try {
|
|
520
|
+
if (!import_fs3.default.existsSync(SESSIONS_DIR)) return;
|
|
521
|
+
const now = Date.now();
|
|
522
|
+
for (const f of import_fs3.default.readdirSync(SESSIONS_DIR)) {
|
|
523
|
+
if (!f.endsWith(".json")) continue;
|
|
524
|
+
const filePath = import_path2.default.join(SESSIONS_DIR, f);
|
|
525
|
+
try {
|
|
526
|
+
const stat = import_fs3.default.statSync(filePath);
|
|
527
|
+
if (now - stat.mtimeMs > maxAgeMs) {
|
|
528
|
+
import_fs3.default.unlinkSync(filePath);
|
|
529
|
+
}
|
|
530
|
+
} catch {
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} catch {
|
|
534
|
+
}
|
|
535
|
+
}
|
|
510
536
|
function readStdin() {
|
|
511
537
|
return new Promise((resolve) => {
|
|
512
538
|
let input = "";
|
|
@@ -531,17 +557,17 @@ function normalizePayload(source, raw) {
|
|
|
531
557
|
// src/hooks/shared/session-start.ts
|
|
532
558
|
async function handleSessionStart(source) {
|
|
533
559
|
let payloadSessionId;
|
|
560
|
+
let detected = source;
|
|
534
561
|
try {
|
|
535
562
|
const input = await readStdin();
|
|
536
563
|
const raw = JSON.parse(input);
|
|
537
|
-
|
|
538
|
-
const hookData = normalizePayload(
|
|
564
|
+
detected = detectSource(raw);
|
|
565
|
+
const hookData = normalizePayload(detected, raw);
|
|
539
566
|
if (typeof hookData.session_id === "string" && hookData.session_id.length > 0) {
|
|
540
567
|
payloadSessionId = hookData.session_id;
|
|
541
568
|
}
|
|
542
569
|
} catch {
|
|
543
570
|
}
|
|
544
|
-
const detected = detectSource();
|
|
545
571
|
const collector = new EventCollector(payloadSessionId, detected);
|
|
546
572
|
collector.setCryptoUpdateCallback((chainHash, secret) => {
|
|
547
573
|
saveSessionState({
|
|
@@ -553,6 +579,7 @@ async function handleSessionStart(source) {
|
|
|
553
579
|
});
|
|
554
580
|
});
|
|
555
581
|
await collector.start();
|
|
582
|
+
cleanStaleSessionFiles();
|
|
556
583
|
const crypto = collector.client.getCryptoState();
|
|
557
584
|
saveSessionState({
|
|
558
585
|
sessionId: collector.sessionId,
|
|
@@ -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/session-start.ts","../../../src/hooks/shared/utils.ts","../../../src/hooks/cursor/session-start.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 { EventCollector } from '../../collector';\nimport { APIClient } from '../../api-client';\nimport { loadConfig } from '../../config';\nimport type { Source } from '../../config';\nimport { saveSessionState, readStdin, detectSource, normalizePayload } from './utils';\n\nexport async function handleSessionStart(source: Source): Promise<void> {\n // Read session_id from Claude Code's hook payload so we register\n // the SAME session ID that subsequent hooks will use.\n let payloadSessionId: string | undefined;\n try {\n const input = await readStdin();\n const raw = JSON.parse(input);\n const detected = detectSource(raw);\n const hookData = normalizePayload(detected, raw);\n if (typeof hookData.session_id === 'string' && hookData.session_id.length > 0) {\n payloadSessionId = hookData.session_id;\n }\n } catch {}\n\n const detected = detectSource();\n const collector = new EventCollector(payloadSessionId, detected);\n\n // Persist crypto state after each batch\n collector.setCryptoUpdateCallback((chainHash, secret) => {\n saveSessionState({\n sessionId: collector.sessionId,\n pid: process.pid,\n source: detected,\n sessionSecret: secret,\n chainHash,\n });\n });\n\n await collector.start();\n\n // Save initial session state including crypto secrets from handshake\n const crypto = collector.client.getCryptoState();\n saveSessionState({\n sessionId: collector.sessionId,\n pid: process.pid,\n source: detected,\n sessionSecret: crypto?.secret,\n chainHash: crypto?.chainHash,\n });\n\n // Show notifications\n try {\n const config = loadConfig();\n if (config?.token) {\n const client = new APIClient();\n const data = await client.getNotifications();\n if (data?.notifications?.length) {\n for (const n of data.notifications) {\n if (n.type === 'rank_passed') {\n process.stderr.write(`\\n\\x1b[33m⚔️ ${n.message}\\x1b[0m\\n\\n`);\n } else {\n process.stderr.write(`\\n\\x1b[36m💎 ${n.message}\\x1b[0m\\n\\n`);\n }\n }\n }\n }\n } catch {}\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 { handleSessionStart } from '../shared/session-start';\nhandleSessionStart('cursor');\n"],"mappings":"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","names":["path","fs","os","exports","exports","exports","computeChainHash","signBatch","exports","res","fs","import_fs","uuidv4","import_fs","fs","detected"]}
|
|
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/session-start.ts","../../../src/hooks/shared/utils.ts","../../../src/hooks/cursor/session-start.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 { EventCollector } from '../../collector';\nimport { APIClient } from '../../api-client';\nimport { loadConfig } from '../../config';\nimport type { Source } from '../../config';\nimport { saveSessionState, readStdin, detectSource, normalizePayload, cleanStaleSessionFiles } from './utils';\n\nexport async function handleSessionStart(source: Source): Promise<void> {\n // Read session_id from Claude Code's hook payload so we register\n // the SAME session ID that subsequent hooks will use.\n let payloadSessionId: string | undefined;\n let detected: Source = source;\n try {\n const input = await readStdin();\n const raw = JSON.parse(input);\n detected = detectSource(raw);\n const hookData = normalizePayload(detected, raw);\n if (typeof hookData.session_id === 'string' && hookData.session_id.length > 0) {\n payloadSessionId = hookData.session_id;\n }\n } catch {}\n\n const collector = new EventCollector(payloadSessionId, detected);\n\n // Persist crypto state after each batch\n collector.setCryptoUpdateCallback((chainHash, secret) => {\n saveSessionState({\n sessionId: collector.sessionId,\n pid: process.pid,\n source: detected,\n sessionSecret: secret,\n chainHash,\n });\n });\n\n await collector.start();\n\n // Clean up stale session files from previous sessions (>24h old)\n cleanStaleSessionFiles();\n\n // Save initial session state including crypto secrets from handshake\n const crypto = collector.client.getCryptoState();\n saveSessionState({\n sessionId: collector.sessionId,\n pid: process.pid,\n source: detected,\n sessionSecret: crypto?.secret,\n chainHash: crypto?.chainHash,\n });\n\n // Show notifications\n try {\n const config = loadConfig();\n if (config?.token) {\n const client = new APIClient();\n const data = await client.getNotifications();\n if (data?.notifications?.length) {\n for (const n of data.notifications) {\n if (n.type === 'rank_passed') {\n process.stderr.write(`\\n\\x1b[33m⚔️ ${n.message}\\x1b[0m\\n\\n`);\n } else {\n process.stderr.write(`\\n\\x1b[36m💎 ${n.message}\\x1b[0m\\n\\n`);\n }\n }\n }\n }\n } catch {}\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 { handleSessionStart } from '../shared/session-start';\nhandleSessionStart('cursor');\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwBO,SAAS,iBAAiB,WAA2B;AAC1D,SAAO,YAAAA,QAAK,KAAK,cAAc,GAAG,SAAS,OAAO;AACpD;AAOO,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,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;AACA;AACA;;;ACFA,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;AAgDO,SAAS,iBAAiB,OAA2B;AAC1D,oBAAkB;AAClB,QAAM,OAAO,iBAAiB,MAAM,SAAS;AAC7C,aAAAC,QAAG,cAAc,MAAM,KAAK,UAAU,KAAK,CAAC;AAC9C;AAiCO,SAAS,uBAAuB,WAAmB,IAAI,KAAK,KAAK,KAAK,KAAY;AACvF,MAAI;AACF,QAAI,CAAC,WAAAC,QAAG,WAAW,YAAY,EAAG;AAClC,UAAM,MAAM,KAAK,IAAI;AACrB,eAAW,KAAK,WAAAA,QAAG,YAAY,YAAY,GAAG;AAC5C,UAAI,CAAC,EAAE,SAAS,OAAO,EAAG;AAC1B,YAAM,WAAW,aAAAC,QAAK,KAAK,cAAc,CAAC;AAC1C,UAAI;AACF,cAAM,OAAO,WAAAD,QAAG,SAAS,QAAQ;AACjC,YAAI,MAAM,KAAK,UAAU,UAAU;AACjC,qBAAAA,QAAG,WAAW,QAAQ;AAAA,QACxB;AAAA,MACF,QAAQ;AAAA,MAAC;AAAA,IACX;AAAA,EACF,QAAQ;AAAA,EAAC;AACX;AA2EO,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;;;ADxNA,eAAsB,mBAAmB,QAA+B;AAGtE,MAAI;AACJ,MAAI,WAAmB;AACvB,MAAI;AACF,UAAM,QAAQ,MAAM,UAAU;AAC9B,UAAM,MAAM,KAAK,MAAM,KAAK;AAC5B,eAAW,aAAa,GAAG;AAC3B,UAAM,WAAW,iBAAiB,UAAU,GAAG;AAC/C,QAAI,OAAO,SAAS,eAAe,YAAY,SAAS,WAAW,SAAS,GAAG;AAC7E,yBAAmB,SAAS;AAAA,IAC9B;AAAA,EACF,QAAQ;AAAA,EAAC;AAET,QAAM,YAAY,IAAI,eAAe,kBAAkB,QAAQ;AAG/D,YAAU,wBAAwB,CAAC,WAAW,WAAW;AACvD,qBAAiB;AAAA,MACf,WAAW,UAAU;AAAA,MACrB,KAAK,QAAQ;AAAA,MACb,QAAQ;AAAA,MACR,eAAe;AAAA,MACf;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAED,QAAM,UAAU,MAAM;AAGtB,yBAAuB;AAGvB,QAAM,SAAS,UAAU,OAAO,eAAe;AAC/C,mBAAiB;AAAA,IACf,WAAW,UAAU;AAAA,IACrB,KAAK,QAAQ;AAAA,IACb,QAAQ;AAAA,IACR,eAAe,QAAQ;AAAA,IACvB,WAAW,QAAQ;AAAA,EACrB,CAAC;AAGD,MAAI;AACF,UAAM,SAAS,WAAW;AAC1B,QAAI,QAAQ,OAAO;AACjB,YAAM,SAAS,IAAI,UAAU;AAC7B,YAAM,OAAO,MAAM,OAAO,iBAAiB;AAC3C,UAAI,MAAM,eAAe,QAAQ;AAC/B,mBAAW,KAAK,KAAK,eAAe;AAClC,cAAI,EAAE,SAAS,eAAe;AAC5B,oBAAQ,OAAO,MAAM;AAAA,wBAAiB,EAAE,OAAO;AAAA;AAAA,CAAa;AAAA,UAC9D,OAAO;AACL,oBAAQ,OAAO,MAAM;AAAA,oBAAgB,EAAE,OAAO;AAAA;AAAA,CAAa;AAAA,UAC7D;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAAC;AACX;;;AEjEA,mBAAmB,QAAQ;","names":["path","fs","os","exports","exports","exports","computeChainHash","signBatch","exports","res","fs","import_fs","uuidv4","import_fs","import_path","fs","fs","fs","path"]}
|
|
@@ -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(
|
|
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
|
|
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(
|
|
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
|
|
552
|
+
return findSessionBySource(source)?.sessionId;
|
|
522
553
|
}
|
|
523
|
-
function updateSessionState(
|
|
524
|
-
const state = loadSessionState(
|
|
554
|
+
function updateSessionState(sessionId, updates) {
|
|
555
|
+
const state = loadSessionState(sessionId);
|
|
525
556
|
if (!state) return;
|
|
526
|
-
const file = sessionStateFile(
|
|
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
|
|
533
|
-
|
|
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(
|
|
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(
|
|
544
|
-
|
|
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(
|
|
578
|
-
const lockPath = lockFilePath(
|
|
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(
|
|
625
|
+
function releaseLock(sessionId) {
|
|
593
626
|
try {
|
|
594
|
-
import_fs3.default.unlinkSync(lockFilePath(
|
|
627
|
+
import_fs3.default.unlinkSync(lockFilePath(sessionId));
|
|
595
628
|
} catch {
|
|
596
629
|
}
|
|
597
630
|
}
|
|
598
|
-
async function withSessionLock(
|
|
599
|
-
await acquireLock(
|
|
631
|
+
async function withSessionLock(sessionId, fn) {
|
|
632
|
+
await acquireLock(sessionId);
|
|
600
633
|
try {
|
|
601
634
|
return await fn();
|
|
602
635
|
} finally {
|
|
603
|
-
releaseLock(
|
|
636
|
+
releaseLock(sessionId);
|
|
604
637
|
}
|
|
605
638
|
}
|
|
606
639
|
function readStdin() {
|
|
@@ -632,7 +665,8 @@ async function handleStop(source) {
|
|
|
632
665
|
const detected = detectSource(raw);
|
|
633
666
|
const hookData = normalizePayload(detected, raw);
|
|
634
667
|
const sessionId = hookData.session_id || loadSessionId(detected);
|
|
635
|
-
|
|
668
|
+
if (!sessionId) return;
|
|
669
|
+
await withSessionLock(sessionId, async () => {
|
|
636
670
|
const collector = createCollector(sessionId, detected);
|
|
637
671
|
collector.addEvent({
|
|
638
672
|
action_type: "stop_response",
|