@hackersbaby/plugin 0.1.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.
- package/dist/cli.js +248 -0
- package/dist/cli.js.map +1 -0
- package/dist/hooks/session-end.js +235 -0
- package/dist/hooks/session-end.js.map +1 -0
- package/dist/hooks/session-start.js +226 -0
- package/dist/hooks/session-start.js.map +1 -0
- package/dist/hooks/tool-call.js +361 -0
- package/dist/hooks/tool-call.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +236 -0
- package/dist/index.js.map +1 -0
- package/package.json +37 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
"use strict";
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
// src/config.ts
|
|
28
|
+
var import_fs = __toESM(require("fs"));
|
|
29
|
+
var import_path = __toESM(require("path"));
|
|
30
|
+
var import_os = __toESM(require("os"));
|
|
31
|
+
var CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".hackersbaby");
|
|
32
|
+
var CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
|
|
33
|
+
var QUEUE_FILE = import_path.default.join(CONFIG_DIR, "queue.jsonl");
|
|
34
|
+
function ensureConfigDir() {
|
|
35
|
+
if (!import_fs.default.existsSync(CONFIG_DIR)) {
|
|
36
|
+
import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function loadConfig() {
|
|
40
|
+
try {
|
|
41
|
+
if (!import_fs.default.existsSync(CONFIG_FILE)) return null;
|
|
42
|
+
const raw = import_fs.default.readFileSync(CONFIG_FILE, "utf-8");
|
|
43
|
+
return JSON.parse(raw);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function saveConfig(config) {
|
|
49
|
+
ensureConfigDir();
|
|
50
|
+
import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/api-client.ts
|
|
54
|
+
var APIClient = class {
|
|
55
|
+
config = loadConfig();
|
|
56
|
+
get headers() {
|
|
57
|
+
return {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
Authorization: `Bearer ${this.config?.token || ""}`
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async sendBatch(batch) {
|
|
63
|
+
try {
|
|
64
|
+
const server = this.config?.server || "https://hackers.baby";
|
|
65
|
+
const res = await fetch(`${server}/api/scores/batch`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: this.headers,
|
|
68
|
+
body: JSON.stringify(batch)
|
|
69
|
+
});
|
|
70
|
+
return res.ok;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async refreshTokenIfNeeded() {
|
|
76
|
+
if (!this.config) return;
|
|
77
|
+
try {
|
|
78
|
+
const parts = this.config.token.split(".");
|
|
79
|
+
if (parts.length !== 3) return;
|
|
80
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64").toString("utf-8"));
|
|
81
|
+
const expMs = payload.exp * 1e3;
|
|
82
|
+
const oneDayMs = 24 * 60 * 60 * 1e3;
|
|
83
|
+
if (Date.now() + oneDayMs < expMs) return;
|
|
84
|
+
const res = await fetch(`${this.config.server}/api/auth/cli-token/refresh`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: this.headers,
|
|
87
|
+
body: JSON.stringify({ refresh_token: this.config.refresh_token })
|
|
88
|
+
});
|
|
89
|
+
if (res.ok) {
|
|
90
|
+
const data = await res.json();
|
|
91
|
+
if (data.token) {
|
|
92
|
+
this.config.token = data.token;
|
|
93
|
+
if (data.refresh_token) this.config.refresh_token = data.refresh_token;
|
|
94
|
+
saveConfig(this.config);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async getStatus() {
|
|
101
|
+
const server = this.config?.server || "https://hackers.baby";
|
|
102
|
+
const res = await fetch(`${server}/api/users/me`, { headers: this.headers });
|
|
103
|
+
if (!res.ok) throw new Error(`getStatus failed: ${res.status}`);
|
|
104
|
+
return res.json();
|
|
105
|
+
}
|
|
106
|
+
async getRank() {
|
|
107
|
+
const server = this.config?.server || "https://hackers.baby";
|
|
108
|
+
const res = await fetch(`${server}/api/users/me/rank`, { headers: this.headers });
|
|
109
|
+
if (!res.ok) throw new Error(`getRank failed: ${res.status}`);
|
|
110
|
+
return res.json();
|
|
111
|
+
}
|
|
112
|
+
async getLeaderboard() {
|
|
113
|
+
const server = this.config?.server || "https://hackers.baby";
|
|
114
|
+
const res = await fetch(`${server}/api/leaderboard/global?limit=10`, { headers: this.headers });
|
|
115
|
+
if (!res.ok) throw new Error(`getLeaderboard failed: ${res.status}`);
|
|
116
|
+
return res.json();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/commands/status.ts
|
|
121
|
+
async function statusCommand() {
|
|
122
|
+
const client = new APIClient();
|
|
123
|
+
try {
|
|
124
|
+
const [me, rank] = await Promise.all([client.getStatus(), client.getRank()]);
|
|
125
|
+
return [
|
|
126
|
+
"hackers.baby Status",
|
|
127
|
+
` Score: ${me.total_score?.toLocaleString() || 0}`,
|
|
128
|
+
` Global Rank: #${rank.global || "-"}`,
|
|
129
|
+
` Streak: ${me.streak_days || 0} days`,
|
|
130
|
+
` Languages: ${(me.languages || []).join(", ") || "none set"}`
|
|
131
|
+
].join("\n");
|
|
132
|
+
} catch {
|
|
133
|
+
return "Error fetching status. Run `hackersbaby init` to set up.";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/commands/leaderboard.ts
|
|
138
|
+
async function leaderboardCommand() {
|
|
139
|
+
const client = new APIClient();
|
|
140
|
+
try {
|
|
141
|
+
const data = await client.getLeaderboard();
|
|
142
|
+
const lines = ["Global Leaderboard", ""];
|
|
143
|
+
for (const entry of data.entries || []) {
|
|
144
|
+
const marker = entry.rank <= 3 ? ["", "#1", "#2", "#3"][entry.rank] : `#${entry.rank}`;
|
|
145
|
+
lines.push(` ${marker.padEnd(4)} ${entry.username.padEnd(20)} ${entry.score.toLocaleString()}`);
|
|
146
|
+
}
|
|
147
|
+
return lines.join("\n");
|
|
148
|
+
} catch {
|
|
149
|
+
return "Error fetching leaderboard.";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/commands/dashboard.ts
|
|
154
|
+
var import_child_process = require("child_process");
|
|
155
|
+
async function dashboardCommand() {
|
|
156
|
+
const config = loadConfig();
|
|
157
|
+
const url = `${config?.server || "https://hackers.baby"}/dashboard`;
|
|
158
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
159
|
+
const args2 = process.platform === "win32" ? ["/c", "start", url] : [url];
|
|
160
|
+
(0, import_child_process.execFile)(cmd, args2, () => {
|
|
161
|
+
});
|
|
162
|
+
return `Opening dashboard at ${url}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/cli.ts
|
|
166
|
+
var import_fs2 = __toESM(require("fs"));
|
|
167
|
+
var import_path2 = __toESM(require("path"));
|
|
168
|
+
var import_child_process2 = require("child_process");
|
|
169
|
+
var import_readline = __toESM(require("readline"));
|
|
170
|
+
var args = process.argv.slice(2);
|
|
171
|
+
var command = args[0];
|
|
172
|
+
async function main() {
|
|
173
|
+
switch (command) {
|
|
174
|
+
case "init":
|
|
175
|
+
await init();
|
|
176
|
+
break;
|
|
177
|
+
case "status":
|
|
178
|
+
console.log(await statusCommand());
|
|
179
|
+
break;
|
|
180
|
+
case "leaderboard":
|
|
181
|
+
console.log(await leaderboardCommand());
|
|
182
|
+
break;
|
|
183
|
+
case "dashboard":
|
|
184
|
+
console.log(await dashboardCommand());
|
|
185
|
+
break;
|
|
186
|
+
default:
|
|
187
|
+
console.log("Usage: hackersbaby <init|status|leaderboard|dashboard>");
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function init() {
|
|
192
|
+
ensureConfigDir();
|
|
193
|
+
const server = process.env.CODEHEROES_SERVER || "http://localhost:3000";
|
|
194
|
+
console.log("Opening browser for GitHub authentication...");
|
|
195
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
196
|
+
const browserArgs = process.platform === "win32" ? ["/c", "start", `${server}/cli-setup`] : [`${server}/cli-setup`];
|
|
197
|
+
(0, import_child_process2.execFile)(cmd, browserArgs, () => {
|
|
198
|
+
});
|
|
199
|
+
const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
200
|
+
const question = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
201
|
+
console.log("After authenticating, paste your CLI token:");
|
|
202
|
+
const token = await question("Token: ");
|
|
203
|
+
const refresh_token = await question("Refresh Token: ");
|
|
204
|
+
try {
|
|
205
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
|
206
|
+
saveConfig({
|
|
207
|
+
token,
|
|
208
|
+
refresh_token,
|
|
209
|
+
user_id: payload.userId,
|
|
210
|
+
server,
|
|
211
|
+
preferences: { dashboard_port: 3847, batch_interval_ms: 1e4 }
|
|
212
|
+
});
|
|
213
|
+
console.log("Configuration saved! hackers.baby is active.");
|
|
214
|
+
registerHooks();
|
|
215
|
+
} catch {
|
|
216
|
+
console.error("Invalid token format");
|
|
217
|
+
}
|
|
218
|
+
rl.close();
|
|
219
|
+
}
|
|
220
|
+
function registerHooks() {
|
|
221
|
+
const claudeSettingsPath = import_path2.default.join(process.env.HOME || "", ".claude", "settings.json");
|
|
222
|
+
let settings = {};
|
|
223
|
+
if (import_fs2.default.existsSync(claudeSettingsPath)) {
|
|
224
|
+
settings = JSON.parse(import_fs2.default.readFileSync(claudeSettingsPath, "utf-8"));
|
|
225
|
+
}
|
|
226
|
+
if (!settings.hooks) settings.hooks = {};
|
|
227
|
+
const distDir = import_path2.default.resolve(__dirname);
|
|
228
|
+
const hookEntries = {
|
|
229
|
+
SessionStart: { command: `node ${import_path2.default.join(distDir, "hooks", "session-start.js")}`, timeout: 10 },
|
|
230
|
+
PostToolUse: { command: `node ${import_path2.default.join(distDir, "hooks", "tool-call.js")}`, timeout: 5 },
|
|
231
|
+
SessionEnd: { command: `node ${import_path2.default.join(distDir, "hooks", "session-end.js")}`, timeout: 30 }
|
|
232
|
+
};
|
|
233
|
+
for (const [event, cfg] of Object.entries(hookEntries)) {
|
|
234
|
+
if (!settings.hooks[event]) settings.hooks[event] = [];
|
|
235
|
+
const hookDef = {
|
|
236
|
+
matcher: cfg.matcher || "",
|
|
237
|
+
hooks: [{ type: "command", command: cfg.command, timeout: cfg.timeout }]
|
|
238
|
+
};
|
|
239
|
+
const exists = settings.hooks[event].some(
|
|
240
|
+
(entry) => entry.hooks?.some((h) => h.command === cfg.command)
|
|
241
|
+
);
|
|
242
|
+
if (!exists) settings.hooks[event].push(hookDef);
|
|
243
|
+
}
|
|
244
|
+
import_fs2.default.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2));
|
|
245
|
+
console.log("Claude Code hooks registered.");
|
|
246
|
+
}
|
|
247
|
+
main().catch(console.error);
|
|
248
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/api-client.ts","../src/commands/status.ts","../src/commands/leaderboard.ts","../src/commands/dashboard.ts","../src/cli.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 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 { APIClient } from '../api-client';\n\nexport async function statusCommand(): Promise<string> {\n const client = new APIClient();\n try {\n const [me, rank] = await Promise.all([client.getStatus(), client.getRank()]);\n return [\n 'hackers.baby Status',\n ` Score: ${(me as any).total_score?.toLocaleString() || 0}`,\n ` Global Rank: #${(rank as any).global || '-'}`,\n ` Streak: ${(me as any).streak_days || 0} days`,\n ` Languages: ${((me as any).languages || []).join(', ') || 'none set'}`,\n ].join('\\n');\n } catch {\n return 'Error fetching status. Run `hackersbaby init` to set up.';\n }\n}\n","import { APIClient } from '../api-client';\n\nexport async function leaderboardCommand(): Promise<string> {\n const client = new APIClient();\n try {\n const data = await client.getLeaderboard();\n const lines = ['Global Leaderboard', ''];\n for (const entry of (data as any).entries || []) {\n const marker = entry.rank <= 3 ? ['', '#1', '#2', '#3'][entry.rank] : `#${entry.rank}`;\n lines.push(` ${marker.padEnd(4)} ${entry.username.padEnd(20)} ${entry.score.toLocaleString()}`);\n }\n return lines.join('\\n');\n } catch {\n return 'Error fetching leaderboard.';\n }\n}\n","import { loadConfig } from '../config';\nimport { execFile } from 'child_process';\n\nexport async function dashboardCommand(): Promise<string> {\n const config = loadConfig();\n const url = `${config?.server || 'https://hackers.baby'}/dashboard`;\n\n const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';\n const args = process.platform === 'win32' ? ['/c', 'start', url] : [url];\n execFile(cmd, args, () => {});\n\n return `Opening dashboard at ${url}`;\n}\n","#!/usr/bin/env node\nimport { loadConfig, saveConfig, ensureConfigDir } from './config';\nimport { statusCommand } from './commands/status';\nimport { leaderboardCommand } from './commands/leaderboard';\nimport { dashboardCommand } from './commands/dashboard';\nimport fs from 'fs';\nimport path from 'path';\nimport { execFile } from 'child_process';\nimport readline from 'readline';\n\nconst args = process.argv.slice(2);\nconst command = args[0];\n\nasync function main() {\n switch (command) {\n case 'init': await init(); break;\n case 'status': console.log(await statusCommand()); break;\n case 'leaderboard': console.log(await leaderboardCommand()); break;\n case 'dashboard': console.log(await dashboardCommand()); break;\n default: console.log('Usage: hackersbaby <init|status|leaderboard|dashboard>'); break;\n }\n}\n\nasync function init() {\n ensureConfigDir();\n const server = process.env.CODEHEROES_SERVER || 'http://localhost:3000';\n console.log('Opening browser for GitHub authentication...');\n\n const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';\n const browserArgs = process.platform === 'win32' ? ['/c', 'start', `${server}/cli-setup`] : [`${server}/cli-setup`];\n execFile(cmd, browserArgs, () => {});\n\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n\n const question = (q: string): Promise<string> => new Promise((resolve) => rl.question(q, resolve));\n\n console.log('After authenticating, paste your CLI token:');\n const token = await question('Token: ');\n const refresh_token = await question('Refresh Token: ');\n\n try {\n const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());\n saveConfig({\n token, refresh_token,\n user_id: payload.userId,\n server,\n preferences: { dashboard_port: 3847, batch_interval_ms: 10000 },\n });\n console.log('Configuration saved! hackers.baby is active.');\n registerHooks();\n } catch {\n console.error('Invalid token format');\n }\n rl.close();\n}\n\nfunction registerHooks() {\n const claudeSettingsPath = path.join(process.env.HOME || '', '.claude', 'settings.json');\n let settings: Record<string, any> = {};\n if (fs.existsSync(claudeSettingsPath)) {\n settings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf-8'));\n }\n if (!settings.hooks) settings.hooks = {};\n\n const distDir = path.resolve(__dirname);\n const hookEntries: Record<string, { command: string; timeout: number; matcher?: string }> = {\n SessionStart: { command: `node ${path.join(distDir, 'hooks', 'session-start.js')}`, timeout: 10 },\n PostToolUse: { command: `node ${path.join(distDir, 'hooks', 'tool-call.js')}`, timeout: 5 },\n SessionEnd: { command: `node ${path.join(distDir, 'hooks', 'session-end.js')}`, timeout: 30 },\n };\n\n for (const [event, cfg] of Object.entries(hookEntries)) {\n if (!settings.hooks[event]) settings.hooks[event] = [];\n const hookDef = {\n matcher: cfg.matcher || '',\n hooks: [{ type: 'command', command: cfg.command, timeout: cfg.timeout }],\n };\n // Avoid duplicates by checking if command already registered\n const exists = settings.hooks[event].some((entry: any) =>\n entry.hooks?.some((h: any) => h.command === cfg.command)\n );\n if (!exists) settings.hooks[event].push(hookDef);\n }\n\n fs.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2));\n console.log('Claude Code hooks registered.');\n}\n\nmain().catch(console.error);\n"],"mappings":"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","names":["path","os","fs","args","import_fs","import_path","import_child_process","readline","path","fs"]}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/collector.ts
|
|
27
|
+
var import_uuid = require("uuid");
|
|
28
|
+
|
|
29
|
+
// src/config.ts
|
|
30
|
+
var import_fs = __toESM(require("fs"));
|
|
31
|
+
var import_path = __toESM(require("path"));
|
|
32
|
+
var import_os = __toESM(require("os"));
|
|
33
|
+
var CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".hackersbaby");
|
|
34
|
+
var CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
|
|
35
|
+
var QUEUE_FILE = import_path.default.join(CONFIG_DIR, "queue.jsonl");
|
|
36
|
+
function ensureConfigDir() {
|
|
37
|
+
if (!import_fs.default.existsSync(CONFIG_DIR)) {
|
|
38
|
+
import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function loadConfig() {
|
|
42
|
+
try {
|
|
43
|
+
if (!import_fs.default.existsSync(CONFIG_FILE)) return null;
|
|
44
|
+
const raw = import_fs.default.readFileSync(CONFIG_FILE, "utf-8");
|
|
45
|
+
return JSON.parse(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function saveConfig(config) {
|
|
51
|
+
ensureConfigDir();
|
|
52
|
+
import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/api-client.ts
|
|
56
|
+
var APIClient = class {
|
|
57
|
+
config = loadConfig();
|
|
58
|
+
get headers() {
|
|
59
|
+
return {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
Authorization: `Bearer ${this.config?.token || ""}`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async sendBatch(batch) {
|
|
65
|
+
try {
|
|
66
|
+
const server = this.config?.server || "https://hackers.baby";
|
|
67
|
+
const res = await fetch(`${server}/api/scores/batch`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: this.headers,
|
|
70
|
+
body: JSON.stringify(batch)
|
|
71
|
+
});
|
|
72
|
+
return res.ok;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async refreshTokenIfNeeded() {
|
|
78
|
+
if (!this.config) return;
|
|
79
|
+
try {
|
|
80
|
+
const parts = this.config.token.split(".");
|
|
81
|
+
if (parts.length !== 3) return;
|
|
82
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64").toString("utf-8"));
|
|
83
|
+
const expMs = payload.exp * 1e3;
|
|
84
|
+
const oneDayMs = 24 * 60 * 60 * 1e3;
|
|
85
|
+
if (Date.now() + oneDayMs < expMs) return;
|
|
86
|
+
const res = await fetch(`${this.config.server}/api/auth/cli-token/refresh`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: this.headers,
|
|
89
|
+
body: JSON.stringify({ refresh_token: this.config.refresh_token })
|
|
90
|
+
});
|
|
91
|
+
if (res.ok) {
|
|
92
|
+
const data = await res.json();
|
|
93
|
+
if (data.token) {
|
|
94
|
+
this.config.token = data.token;
|
|
95
|
+
if (data.refresh_token) this.config.refresh_token = data.refresh_token;
|
|
96
|
+
saveConfig(this.config);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async getStatus() {
|
|
103
|
+
const server = this.config?.server || "https://hackers.baby";
|
|
104
|
+
const res = await fetch(`${server}/api/users/me`, { headers: this.headers });
|
|
105
|
+
if (!res.ok) throw new Error(`getStatus failed: ${res.status}`);
|
|
106
|
+
return res.json();
|
|
107
|
+
}
|
|
108
|
+
async getRank() {
|
|
109
|
+
const server = this.config?.server || "https://hackers.baby";
|
|
110
|
+
const res = await fetch(`${server}/api/users/me/rank`, { headers: this.headers });
|
|
111
|
+
if (!res.ok) throw new Error(`getRank failed: ${res.status}`);
|
|
112
|
+
return res.json();
|
|
113
|
+
}
|
|
114
|
+
async getLeaderboard() {
|
|
115
|
+
const server = this.config?.server || "https://hackers.baby";
|
|
116
|
+
const res = await fetch(`${server}/api/leaderboard/global?limit=10`, { headers: this.headers });
|
|
117
|
+
if (!res.ok) throw new Error(`getLeaderboard failed: ${res.status}`);
|
|
118
|
+
return res.json();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/queue.ts
|
|
123
|
+
var import_fs2 = __toESM(require("fs"));
|
|
124
|
+
var MAX_QUEUE_SIZE = 1e4;
|
|
125
|
+
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
126
|
+
function enqueueEvents(events) {
|
|
127
|
+
ensureConfigDir();
|
|
128
|
+
const lines = events.map((e) => JSON.stringify(e)).join("\n");
|
|
129
|
+
import_fs2.default.appendFileSync(QUEUE_FILE, lines + "\n", "utf-8");
|
|
130
|
+
try {
|
|
131
|
+
const content = import_fs2.default.readFileSync(QUEUE_FILE, "utf-8");
|
|
132
|
+
const allLines = content.split("\n").filter((l) => l.trim() !== "");
|
|
133
|
+
if (allLines.length > MAX_QUEUE_SIZE) {
|
|
134
|
+
const trimmed = allLines.slice(allLines.length - MAX_QUEUE_SIZE);
|
|
135
|
+
import_fs2.default.writeFileSync(QUEUE_FILE, trimmed.join("\n") + "\n", "utf-8");
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function drainQueue() {
|
|
141
|
+
try {
|
|
142
|
+
if (!import_fs2.default.existsSync(QUEUE_FILE)) return [];
|
|
143
|
+
const content = import_fs2.default.readFileSync(QUEUE_FILE, "utf-8");
|
|
144
|
+
import_fs2.default.writeFileSync(QUEUE_FILE, "", "utf-8");
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const events = [];
|
|
147
|
+
for (const line of content.split("\n")) {
|
|
148
|
+
if (!line.trim()) continue;
|
|
149
|
+
try {
|
|
150
|
+
const event = JSON.parse(line);
|
|
151
|
+
const age = now - new Date(event.timestamp).getTime();
|
|
152
|
+
if (age <= MAX_AGE_MS) {
|
|
153
|
+
events.push(event);
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return events;
|
|
159
|
+
} catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/collector.ts
|
|
165
|
+
var EventCollector = class {
|
|
166
|
+
sessionId;
|
|
167
|
+
buffer = [];
|
|
168
|
+
flushInterval = null;
|
|
169
|
+
client;
|
|
170
|
+
constructor(sessionId) {
|
|
171
|
+
this.sessionId = sessionId ?? (0, import_uuid.v4)();
|
|
172
|
+
this.client = new APIClient();
|
|
173
|
+
}
|
|
174
|
+
addEvent(event) {
|
|
175
|
+
this.buffer.push({
|
|
176
|
+
...event,
|
|
177
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async start() {
|
|
181
|
+
await this.client.refreshTokenIfNeeded();
|
|
182
|
+
const queued = drainQueue();
|
|
183
|
+
if (queued.length > 0) {
|
|
184
|
+
const batch = { session_id: this.sessionId, events: queued };
|
|
185
|
+
const ok = await this.client.sendBatch(batch);
|
|
186
|
+
if (!ok) {
|
|
187
|
+
enqueueEvents(queued);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const config = loadConfig();
|
|
191
|
+
const intervalMs = config?.preferences?.batch_interval_ms ?? 1e4;
|
|
192
|
+
this.flushInterval = setInterval(() => {
|
|
193
|
+
this.flush().catch(() => {
|
|
194
|
+
});
|
|
195
|
+
}, intervalMs);
|
|
196
|
+
}
|
|
197
|
+
async flush() {
|
|
198
|
+
if (this.buffer.length === 0) return;
|
|
199
|
+
const events = this.buffer.splice(0);
|
|
200
|
+
const batch = { session_id: this.sessionId, events };
|
|
201
|
+
const ok = await this.client.sendBatch(batch);
|
|
202
|
+
if (!ok) {
|
|
203
|
+
enqueueEvents(events);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async stop() {
|
|
207
|
+
if (this.flushInterval !== null) {
|
|
208
|
+
clearInterval(this.flushInterval);
|
|
209
|
+
this.flushInterval = null;
|
|
210
|
+
}
|
|
211
|
+
this.addEvent({ action_type: "session_completed", value: 1, metadata: {} });
|
|
212
|
+
await this.flush();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/hooks/session-end.ts
|
|
217
|
+
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() {
|
|
222
|
+
try {
|
|
223
|
+
if (!import_fs3.default.existsSync(stateFile)) return void 0;
|
|
224
|
+
const data = JSON.parse(import_fs3.default.readFileSync(stateFile, "utf-8"));
|
|
225
|
+
return data.sessionId;
|
|
226
|
+
} catch {
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
(async () => {
|
|
231
|
+
const collector = new EventCollector(loadSessionId());
|
|
232
|
+
await collector.stop();
|
|
233
|
+
if (import_fs3.default.existsSync(stateFile)) import_fs3.default.unlinkSync(stateFile);
|
|
234
|
+
})();
|
|
235
|
+
//# sourceMappingURL=session-end.js.map
|
|
@@ -0,0 +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"]}
|