@hackersbaby/plugin 0.2.1 → 0.4.0

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.
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  "use strict";
2
3
  var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
@@ -32,6 +33,9 @@ var import_os = __toESM(require("os"));
32
33
  var CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".hackersbaby");
33
34
  var CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
34
35
  var QUEUE_FILE = import_path.default.join(CONFIG_DIR, "queue.jsonl");
36
+ function sessionStateFile(source) {
37
+ return import_path.default.join(CONFIG_DIR, `active-session-${source}.json`);
38
+ }
35
39
  function ensureConfigDir() {
36
40
  if (!import_fs.default.existsSync(CONFIG_DIR)) {
37
41
  import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
@@ -116,6 +120,50 @@ var APIClient = class {
116
120
  if (!res.ok) throw new Error(`getLeaderboard failed: ${res.status}`);
117
121
  return res.json();
118
122
  }
123
+ async getPublicStats() {
124
+ try {
125
+ const server = this.config?.server || "https://hackers.baby";
126
+ const res = await fetch(`${server}/api/stats/public`);
127
+ if (!res.ok) return null;
128
+ return res.json();
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+ async getNotifications() {
134
+ try {
135
+ const server = this.config?.server || "https://hackers.baby";
136
+ const res = await fetch(`${server}/api/users/me/notifications`, { headers: this.headers });
137
+ if (!res.ok) return null;
138
+ return res.json();
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+ async claimReferral(code) {
144
+ try {
145
+ const server = this.config?.server || "https://hackers.baby";
146
+ const res = await fetch(`${server}/api/referral/claim`, {
147
+ method: "POST",
148
+ headers: this.headers,
149
+ body: JSON.stringify({ referral_code: code })
150
+ });
151
+ if (!res.ok) return null;
152
+ return res.json();
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+ async getReferralStats() {
158
+ try {
159
+ const server = this.config?.server || "https://hackers.baby";
160
+ const res = await fetch(`${server}/api/referral/stats`, { headers: this.headers });
161
+ if (!res.ok) return null;
162
+ return res.json();
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
119
167
  };
120
168
 
121
169
  // src/queue.ts
@@ -166,13 +214,17 @@ var EventCollector = class {
166
214
  buffer = [];
167
215
  flushInterval = null;
168
216
  client;
169
- constructor(sessionId) {
217
+ source;
218
+ constructor(sessionId, source) {
170
219
  this.sessionId = sessionId ?? (0, import_uuid.v4)();
171
220
  this.client = new APIClient();
221
+ this.source = source;
172
222
  }
173
223
  addEvent(event) {
224
+ const metadata = this.source ? { ...event.metadata, source: this.source } : event.metadata;
174
225
  this.buffer.push({
175
226
  ...event,
227
+ metadata,
176
228
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
177
229
  });
178
230
  }
@@ -212,29 +264,41 @@ var EventCollector = class {
212
264
  }
213
265
  };
214
266
 
215
- // src/hooks/prompt-submit.ts
267
+ // src/hooks/shared/utils.ts
216
268
  var import_fs3 = __toESM(require("fs"));
217
- var import_path2 = __toESM(require("path"));
218
- var import_os2 = __toESM(require("os"));
219
- var stateFile = import_path2.default.join(import_os2.default.homedir(), ".hackersbaby", "active-session.json");
220
- function loadSessionId() {
269
+ function loadSessionId(source) {
221
270
  try {
222
- if (!import_fs3.default.existsSync(stateFile)) return void 0;
223
- const data = JSON.parse(import_fs3.default.readFileSync(stateFile, "utf-8"));
271
+ const file = sessionStateFile(source);
272
+ if (!import_fs3.default.existsSync(file)) return void 0;
273
+ const data = JSON.parse(import_fs3.default.readFileSync(file, "utf-8"));
224
274
  return data.sessionId;
225
275
  } catch {
226
276
  return void 0;
227
277
  }
228
278
  }
229
- var input = "";
230
- process.stdin.on("data", (chunk) => {
231
- input += chunk;
232
- });
233
- process.stdin.on("end", async () => {
279
+ function readStdin() {
280
+ return new Promise((resolve) => {
281
+ let input = "";
282
+ process.stdin.on("data", (chunk) => {
283
+ input += chunk;
284
+ });
285
+ process.stdin.on("end", () => resolve(input));
286
+ });
287
+ }
288
+ function normalizePayload(source, raw) {
289
+ if (source === "cursor") {
290
+ return { ...raw };
291
+ }
292
+ return raw;
293
+ }
294
+
295
+ // src/hooks/shared/prompt-submit.ts
296
+ async function handlePromptSubmit(source) {
297
+ const input = await readStdin();
234
298
  try {
235
- const hookData = JSON.parse(input);
236
- const sessionId = hookData.session_id || loadSessionId();
237
- const collector = new EventCollector(sessionId);
299
+ const hookData = normalizePayload(source, JSON.parse(input));
300
+ const sessionId = hookData.session_id || loadSessionId(source);
301
+ const collector = new EventCollector(sessionId, source);
238
302
  const promptLength = typeof hookData.prompt === "string" ? hookData.prompt.length : 0;
239
303
  collector.addEvent({
240
304
  action_type: "prompt_submitted",
@@ -247,5 +311,8 @@ process.stdin.on("end", async () => {
247
311
  await collector.flush();
248
312
  } catch {
249
313
  }
250
- });
314
+ }
315
+
316
+ // src/hooks/prompt-submit.ts
317
+ handlePromptSubmit("claude");
251
318
  //# sourceMappingURL=prompt-submit.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collector.ts","../../src/config.ts","../../src/api-client.ts","../../src/queue.ts","../../src/hooks/prompt-submit.ts"],"sourcesContent":["import { v4 as uuidv4 } from 'uuid';\nimport { APIClient } from './api-client';\nimport { enqueueEvents, drainQueue } from './queue';\nimport { loadConfig } 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 private client: APIClient;\n\n constructor(sessionId?: string) {\n this.sessionId = sessionId ?? uuidv4();\n this.client = new APIClient();\n }\n\n addEvent(event: RawEventInput): void {\n this.buffer.push({\n ...event,\n timestamp: new Date().toISOString(),\n });\n }\n\n async start(): Promise<void> {\n await this.client.refreshTokenIfNeeded();\n\n const queued = drainQueue();\n if (queued.length > 0) {\n const batch = { session_id: this.sessionId, events: queued };\n const ok = await this.client.sendBatch(batch);\n if (!ok) {\n enqueueEvents(queued);\n }\n }\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 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 interface PluginConfig {\n token: string;\n refresh_token: string;\n user_id: string;\n server: string;\n preferences: {\n dashboard_port: number;\n batch_interval_ms: number;\n };\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","import { loadConfig, saveConfig } from './config';\nimport type { EventBatch } from '@hackersbaby/shared';\n\nexport class APIClient {\n private config = loadConfig();\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 async sendBatch(batch: EventBatch): Promise<boolean> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\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","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 { EventCollector } from '../collector';\nimport fs from 'fs';\nimport path from 'path';\nimport os from 'os';\n\nconst stateFile = path.join(os.homedir(), '.hackersbaby', 'active-session.json');\n\nfunction loadSessionId(): string | undefined {\n try {\n if (!fs.existsSync(stateFile)) return undefined;\n const data = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));\n return data.sessionId;\n } catch {\n return undefined;\n }\n}\n\nlet input = '';\nprocess.stdin.on('data', (chunk) => { input += chunk; });\nprocess.stdin.on('end', async () => {\n try {\n const hookData = JSON.parse(input);\n const sessionId = hookData.session_id || loadSessionId();\n const collector = new EventCollector(sessionId);\n\n // PRIVACY: We only track that a prompt was submitted and its character length.\n // The actual prompt text is NEVER sent to the server.\n const promptLength = typeof hookData.prompt === 'string' ? hookData.prompt.length : 0;\n\n collector.addEvent({\n action_type: 'prompt_submitted',\n value: 1,\n metadata: {\n char_length: promptLength,\n has_images: Array.isArray(hookData.images) && hookData.images.length > 0,\n },\n });\n\n await collector.flush();\n } catch {}\n});\n"],"mappings":"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","names":["path","os","fs","import_fs","fs","uuidv4","import_fs","import_path","import_os","path","os","fs"]}
1
+ {"version":3,"sources":["../../src/collector.ts","../../src/config.ts","../../src/api-client.ts","../../src/queue.ts","../../src/hooks/shared/utils.ts","../../src/hooks/shared/prompt-submit.ts","../../src/hooks/prompt-submit.ts"],"sourcesContent":["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 private 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 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 const queued = drainQueue();\n if (queued.length > 0) {\n const batch = { session_id: this.sessionId, events: queued };\n const ok = await this.client.sendBatch(batch);\n if (!ok) {\n enqueueEvents(queued);\n }\n }\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 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","import { loadConfig, saveConfig } from './config';\nimport type { EventBatch } from '@hackersbaby/shared';\n\nexport class APIClient {\n private config = loadConfig();\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 async sendBatch(batch: EventBatch): Promise<boolean> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\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 fs from 'fs';\nimport type { Source } from '../../config';\nimport { sessionStateFile } from '../../config';\n\nexport interface SessionState {\n sessionId: string;\n pid: number;\n source: Source;\n}\n\nexport function loadSessionId(source: Source): string | undefined {\n try {\n const file = sessionStateFile(source);\n if (!fs.existsSync(file)) return undefined;\n const data = JSON.parse(fs.readFileSync(file, 'utf-8'));\n return data.sessionId;\n } catch {\n return undefined;\n }\n}\n\nexport function saveSessionState(state: SessionState): void {\n const file = sessionStateFile(state.source);\n fs.writeFileSync(file, JSON.stringify(state));\n}\n\nexport function removeSessionState(source: Source): void {\n const file = sessionStateFile(source);\n if (fs.existsSync(file)) fs.unlinkSync(file);\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 * 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 { EventCollector } from '../../collector';\nimport type { Source } from '../../config';\nimport { loadSessionId, readStdin, normalizePayload } from './utils';\n\nexport async function handlePromptSubmit(source: Source): Promise<void> {\n const input = await readStdin();\n try {\n const hookData = normalizePayload(source, JSON.parse(input));\n const sessionId = hookData.session_id || loadSessionId(source);\n const collector = new EventCollector(sessionId, source);\n\n // PRIVACY: We only track that a prompt was submitted and its character length.\n // The actual prompt text is NEVER sent to the server.\n const promptLength = typeof hookData.prompt === 'string' ? hookData.prompt.length : 0;\n\n collector.addEvent({\n action_type: 'prompt_submitted',\n value: 1,\n metadata: {\n char_length: promptLength,\n has_images: Array.isArray(hookData.images) && hookData.images.length > 0,\n },\n });\n\n await collector.flush();\n } catch {}\n}\n","import { handlePromptSubmit } from './shared/prompt-submit';\nhandlePromptSubmit('claude');\n"],"mappings":"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","names":["path","os","fs","import_fs","fs","uuidv4","import_fs","fs"]}
@@ -33,6 +33,9 @@ var import_os = __toESM(require("os"));
33
33
  var CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".hackersbaby");
34
34
  var CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
35
35
  var QUEUE_FILE = import_path.default.join(CONFIG_DIR, "queue.jsonl");
36
+ function sessionStateFile(source) {
37
+ return import_path.default.join(CONFIG_DIR, `active-session-${source}.json`);
38
+ }
36
39
  function ensureConfigDir() {
37
40
  if (!import_fs.default.existsSync(CONFIG_DIR)) {
38
41
  import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
@@ -117,6 +120,50 @@ var APIClient = class {
117
120
  if (!res.ok) throw new Error(`getLeaderboard failed: ${res.status}`);
118
121
  return res.json();
119
122
  }
123
+ async getPublicStats() {
124
+ try {
125
+ const server = this.config?.server || "https://hackers.baby";
126
+ const res = await fetch(`${server}/api/stats/public`);
127
+ if (!res.ok) return null;
128
+ return res.json();
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+ async getNotifications() {
134
+ try {
135
+ const server = this.config?.server || "https://hackers.baby";
136
+ const res = await fetch(`${server}/api/users/me/notifications`, { headers: this.headers });
137
+ if (!res.ok) return null;
138
+ return res.json();
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+ async claimReferral(code) {
144
+ try {
145
+ const server = this.config?.server || "https://hackers.baby";
146
+ const res = await fetch(`${server}/api/referral/claim`, {
147
+ method: "POST",
148
+ headers: this.headers,
149
+ body: JSON.stringify({ referral_code: code })
150
+ });
151
+ if (!res.ok) return null;
152
+ return res.json();
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+ async getReferralStats() {
158
+ try {
159
+ const server = this.config?.server || "https://hackers.baby";
160
+ const res = await fetch(`${server}/api/referral/stats`, { headers: this.headers });
161
+ if (!res.ok) return null;
162
+ return res.json();
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
120
167
  };
121
168
 
122
169
  // src/queue.ts
@@ -167,13 +214,17 @@ var EventCollector = class {
167
214
  buffer = [];
168
215
  flushInterval = null;
169
216
  client;
170
- constructor(sessionId) {
217
+ source;
218
+ constructor(sessionId, source) {
171
219
  this.sessionId = sessionId ?? (0, import_uuid.v4)();
172
220
  this.client = new APIClient();
221
+ this.source = source;
173
222
  }
174
223
  addEvent(event) {
224
+ const metadata = this.source ? { ...event.metadata, source: this.source } : event.metadata;
175
225
  this.buffer.push({
176
226
  ...event,
227
+ metadata,
177
228
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
178
229
  });
179
230
  }
@@ -213,23 +264,30 @@ var EventCollector = class {
213
264
  }
214
265
  };
215
266
 
216
- // src/hooks/session-end.ts
267
+ // src/hooks/shared/utils.ts
217
268
  var import_fs3 = __toESM(require("fs"));
218
- var import_path2 = __toESM(require("path"));
219
- var import_os2 = __toESM(require("os"));
220
- var stateFile = import_path2.default.join(import_os2.default.homedir(), ".hackersbaby", "active-session.json");
221
- function loadSessionId() {
269
+ function loadSessionId(source) {
222
270
  try {
223
- if (!import_fs3.default.existsSync(stateFile)) return void 0;
224
- const data = JSON.parse(import_fs3.default.readFileSync(stateFile, "utf-8"));
271
+ const file = sessionStateFile(source);
272
+ if (!import_fs3.default.existsSync(file)) return void 0;
273
+ const data = JSON.parse(import_fs3.default.readFileSync(file, "utf-8"));
225
274
  return data.sessionId;
226
275
  } catch {
227
276
  return void 0;
228
277
  }
229
278
  }
230
- (async () => {
231
- const collector = new EventCollector(loadSessionId());
279
+ function removeSessionState(source) {
280
+ const file = sessionStateFile(source);
281
+ if (import_fs3.default.existsSync(file)) import_fs3.default.unlinkSync(file);
282
+ }
283
+
284
+ // src/hooks/shared/session-end.ts
285
+ async function handleSessionEnd(source) {
286
+ const collector = new EventCollector(loadSessionId(source), source);
232
287
  await collector.stop();
233
- if (import_fs3.default.existsSync(stateFile)) import_fs3.default.unlinkSync(stateFile);
234
- })();
288
+ removeSessionState(source);
289
+ }
290
+
291
+ // src/hooks/session-end.ts
292
+ handleSessionEnd("claude");
235
293
  //# sourceMappingURL=session-end.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collector.ts","../../src/config.ts","../../src/api-client.ts","../../src/queue.ts","../../src/hooks/session-end.ts"],"sourcesContent":["import { v4 as uuidv4 } from 'uuid';\nimport { APIClient } from './api-client';\nimport { enqueueEvents, drainQueue } from './queue';\nimport { loadConfig } 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 private client: APIClient;\n\n constructor(sessionId?: string) {\n this.sessionId = sessionId ?? uuidv4();\n this.client = new APIClient();\n }\n\n addEvent(event: RawEventInput): void {\n this.buffer.push({\n ...event,\n timestamp: new Date().toISOString(),\n });\n }\n\n async start(): Promise<void> {\n await this.client.refreshTokenIfNeeded();\n\n const queued = drainQueue();\n if (queued.length > 0) {\n const batch = { session_id: this.sessionId, events: queued };\n const ok = await this.client.sendBatch(batch);\n if (!ok) {\n enqueueEvents(queued);\n }\n }\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 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 interface PluginConfig {\n token: string;\n refresh_token: string;\n user_id: string;\n server: string;\n preferences: {\n dashboard_port: number;\n batch_interval_ms: number;\n };\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","import { loadConfig, saveConfig } from './config';\nimport type { EventBatch } from '@hackersbaby/shared';\n\nexport class APIClient {\n private config = loadConfig();\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 async sendBatch(batch: EventBatch): Promise<boolean> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\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","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","#!/usr/bin/env node\nimport { EventCollector } from '../collector';\nimport fs from 'fs';\nimport path from 'path';\nimport os from 'os';\n\nconst stateFile = path.join(os.homedir(), '.hackersbaby', 'active-session.json');\n\nfunction loadSessionId(): string | undefined {\n try {\n if (!fs.existsSync(stateFile)) return undefined;\n const data = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));\n return data.sessionId;\n } catch {\n return undefined;\n }\n}\n\n(async () => {\n const collector = new EventCollector(loadSessionId());\n await collector.stop();\n if (fs.existsSync(stateFile)) fs.unlinkSync(stateFile);\n})();\n"],"mappings":"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","names":["path","os","fs","import_fs","fs","uuidv4","import_fs","import_path","import_os","path","os","fs"]}
1
+ {"version":3,"sources":["../../src/collector.ts","../../src/config.ts","../../src/api-client.ts","../../src/queue.ts","../../src/hooks/shared/utils.ts","../../src/hooks/shared/session-end.ts","../../src/hooks/session-end.ts"],"sourcesContent":["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 private 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 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 const queued = drainQueue();\n if (queued.length > 0) {\n const batch = { session_id: this.sessionId, events: queued };\n const ok = await this.client.sendBatch(batch);\n if (!ok) {\n enqueueEvents(queued);\n }\n }\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 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","import { loadConfig, saveConfig } from './config';\nimport type { EventBatch } from '@hackersbaby/shared';\n\nexport class APIClient {\n private config = loadConfig();\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 async sendBatch(batch: EventBatch): Promise<boolean> {\n try {\n const server = this.config?.server || 'https://hackers.baby';\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 fs from 'fs';\nimport type { Source } from '../../config';\nimport { sessionStateFile } from '../../config';\n\nexport interface SessionState {\n sessionId: string;\n pid: number;\n source: Source;\n}\n\nexport function loadSessionId(source: Source): string | undefined {\n try {\n const file = sessionStateFile(source);\n if (!fs.existsSync(file)) return undefined;\n const data = JSON.parse(fs.readFileSync(file, 'utf-8'));\n return data.sessionId;\n } catch {\n return undefined;\n }\n}\n\nexport function saveSessionState(state: SessionState): void {\n const file = sessionStateFile(state.source);\n fs.writeFileSync(file, JSON.stringify(state));\n}\n\nexport function removeSessionState(source: Source): void {\n const file = sessionStateFile(source);\n if (fs.existsSync(file)) fs.unlinkSync(file);\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 * 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 { EventCollector } from '../../collector';\nimport type { Source } from '../../config';\nimport { loadSessionId, removeSessionState } from './utils';\n\nexport async function handleSessionEnd(source: Source): Promise<void> {\n const collector = new EventCollector(loadSessionId(source), source);\n await collector.stop();\n removeSessionState(source);\n}\n","import { handleSessionEnd } from './shared/session-end';\nhandleSessionEnd('claude');\n"],"mappings":"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","names":["path","os","fs","import_fs","fs","uuidv4","import_fs","fs","fs"]}
@@ -33,6 +33,9 @@ var import_os = __toESM(require("os"));
33
33
  var CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".hackersbaby");
34
34
  var CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
35
35
  var QUEUE_FILE = import_path.default.join(CONFIG_DIR, "queue.jsonl");
36
+ function sessionStateFile(source) {
37
+ return import_path.default.join(CONFIG_DIR, `active-session-${source}.json`);
38
+ }
36
39
  function ensureConfigDir() {
37
40
  if (!import_fs.default.existsSync(CONFIG_DIR)) {
38
41
  import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
@@ -117,6 +120,50 @@ var APIClient = class {
117
120
  if (!res.ok) throw new Error(`getLeaderboard failed: ${res.status}`);
118
121
  return res.json();
119
122
  }
123
+ async getPublicStats() {
124
+ try {
125
+ const server = this.config?.server || "https://hackers.baby";
126
+ const res = await fetch(`${server}/api/stats/public`);
127
+ if (!res.ok) return null;
128
+ return res.json();
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+ async getNotifications() {
134
+ try {
135
+ const server = this.config?.server || "https://hackers.baby";
136
+ const res = await fetch(`${server}/api/users/me/notifications`, { headers: this.headers });
137
+ if (!res.ok) return null;
138
+ return res.json();
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+ async claimReferral(code) {
144
+ try {
145
+ const server = this.config?.server || "https://hackers.baby";
146
+ const res = await fetch(`${server}/api/referral/claim`, {
147
+ method: "POST",
148
+ headers: this.headers,
149
+ body: JSON.stringify({ referral_code: code })
150
+ });
151
+ if (!res.ok) return null;
152
+ return res.json();
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+ async getReferralStats() {
158
+ try {
159
+ const server = this.config?.server || "https://hackers.baby";
160
+ const res = await fetch(`${server}/api/referral/stats`, { headers: this.headers });
161
+ if (!res.ok) return null;
162
+ return res.json();
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
120
167
  };
121
168
 
122
169
  // src/queue.ts
@@ -167,13 +214,17 @@ var EventCollector = class {
167
214
  buffer = [];
168
215
  flushInterval = null;
169
216
  client;
170
- constructor(sessionId) {
217
+ source;
218
+ constructor(sessionId, source) {
171
219
  this.sessionId = sessionId ?? (0, import_uuid.v4)();
172
220
  this.client = new APIClient();
221
+ this.source = source;
173
222
  }
174
223
  addEvent(event) {
224
+ const metadata = this.source ? { ...event.metadata, source: this.source } : event.metadata;
175
225
  this.buffer.push({
176
226
  ...event,
227
+ metadata,
177
228
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
178
229
  });
179
230
  }
@@ -213,14 +264,43 @@ var EventCollector = class {
213
264
  }
214
265
  };
215
266
 
216
- // src/hooks/session-start.ts
267
+ // src/hooks/shared/utils.ts
217
268
  var import_fs3 = __toESM(require("fs"));
218
- var import_path2 = __toESM(require("path"));
219
- var import_os2 = __toESM(require("os"));
220
- var collector = new EventCollector();
221
- var stateFile = import_path2.default.join(import_os2.default.homedir(), ".hackersbaby", "active-session.json");
222
- (async () => {
269
+ function saveSessionState(state) {
270
+ const file = sessionStateFile(state.source);
271
+ import_fs3.default.writeFileSync(file, JSON.stringify(state));
272
+ }
273
+
274
+ // src/hooks/shared/session-start.ts
275
+ async function handleSessionStart(source) {
276
+ const collector = new EventCollector(void 0, source);
223
277
  await collector.start();
224
- import_fs3.default.writeFileSync(stateFile, JSON.stringify({ sessionId: collector.sessionId, pid: process.pid }));
225
- })();
278
+ saveSessionState({ sessionId: collector.sessionId, pid: process.pid, source });
279
+ try {
280
+ const config = loadConfig();
281
+ if (config?.token) {
282
+ const client = new APIClient();
283
+ const data = await client.getNotifications();
284
+ if (data?.notifications?.length) {
285
+ for (const n of data.notifications) {
286
+ if (n.type === "rank_passed") {
287
+ process.stderr.write(`
288
+ \x1B[33m\u2694\uFE0F ${n.message}\x1B[0m
289
+
290
+ `);
291
+ } else {
292
+ process.stderr.write(`
293
+ \x1B[36m\u{1F48E} ${n.message}\x1B[0m
294
+
295
+ `);
296
+ }
297
+ }
298
+ }
299
+ }
300
+ } catch {
301
+ }
302
+ }
303
+
304
+ // src/hooks/session-start.ts
305
+ handleSessionStart("claude");
226
306
  //# sourceMappingURL=session-start.js.map