claude-code-rehab 1.0.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.
Files changed (2) hide show
  1. package/index.mjs +117 -0
  2. package/package.json +11 -0
package/index.mjs ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code Rehab — Lightweight data server
3
+ // Run: node server.mjs
4
+ // Then open the app on your phone and enter this computer's IP
5
+
6
+ import { createServer } from "http";
7
+ import { createReadStream, existsSync, readdirSync, statSync, readFileSync } from "fs";
8
+ import { join, basename } from "path";
9
+ import { homedir, networkInterfaces } from "os";
10
+ import { createInterface } from "readline";
11
+
12
+ const PORT = 3456;
13
+ const CLAUDE_DIR = join(homedir(), ".claude", "projects");
14
+
15
+ function mapModel(m) { return m?.toLowerCase().includes("opus") ? "opus" : "sonnet"; }
16
+ function projName(cwd) { return cwd ? basename(cwd) : "unknown"; }
17
+
18
+ function dayKey(ts) { const d = new Date(ts); d.setHours(0,0,0,0); return d.getTime(); }
19
+
20
+ async function parseFile(fp, dirName) {
21
+ return new Promise((resolve) => {
22
+ const fileId = basename(fp, ".jsonl");
23
+ let project = dirName.split("-").filter(Boolean).pop() || dirName;
24
+ const days = {};
25
+ let hasData = false;
26
+ const rl = createInterface({ input: createReadStream(fp, { encoding: "utf-8" }), crlfDelay: Infinity });
27
+ rl.on("line", (line) => {
28
+ if (!line.trim()) return;
29
+ try {
30
+ const obj = JSON.parse(line);
31
+ const ts = obj.timestamp ? new Date(obj.timestamp).getTime() : null;
32
+ if (!ts || isNaN(ts)) return;
33
+ const dk = dayKey(ts);
34
+ if (!days[dk]) days[dk] = { inp: 0, out: 0, msgs: 0, models: { opus: 0, sonnet: 0 }, minTs: ts, maxTs: ts };
35
+ const b = days[dk];
36
+ if (ts < b.minTs) b.minTs = ts; if (ts > b.maxTs) b.maxTs = ts;
37
+ if (obj.type === "user") { b.msgs++; if (obj.cwd) project = projName(obj.cwd); }
38
+ if (obj.type === "assistant" && obj.message) {
39
+ if (obj.message.model) b.models[mapModel(obj.message.model)]++;
40
+ if (obj.message.usage) { hasData = true; b.inp += obj.message.usage.input_tokens || 0; b.out += obj.message.usage.output_tokens || 0; }
41
+ }
42
+ } catch {}
43
+ });
44
+ rl.on("close", () => {
45
+ if (!hasData) return resolve(null);
46
+ const sessions = [];
47
+ for (const [dk, b] of Object.entries(days)) {
48
+ if (!b.inp && !b.out) continue;
49
+ sessions.push({
50
+ id: `${fileId}-${dk}`, project,
51
+ model: b.models.opus >= b.models.sonnet ? "opus" : "sonnet",
52
+ startTime: b.minTs, endTime: b.maxTs,
53
+ durationMin: Math.max(1, Math.round((b.maxTs - b.minTs) / 60000)),
54
+ inputTokens: b.inp, outputTokens: b.out, messages: b.msgs,
55
+ });
56
+ }
57
+ resolve(sessions.length ? sessions : null);
58
+ });
59
+ rl.on("error", () => resolve(null));
60
+ });
61
+ }
62
+
63
+ async function scan() {
64
+ if (!existsSync(CLAUDE_DIR)) return [];
65
+ const sessions = [];
66
+ const dirs = readdirSync(CLAUDE_DIR).filter(d => statSync(join(CLAUDE_DIR, d)).isDirectory());
67
+ for (const dir of dirs) {
68
+ const dp = join(CLAUDE_DIR, dir);
69
+ let files; try { files = readdirSync(dp).filter(f => f.endsWith(".jsonl")); } catch { continue; }
70
+ for (const f of files) {
71
+ const r = await parseFile(join(dp, f), dir);
72
+ if (r) sessions.push(...r);
73
+ }
74
+ }
75
+ return sessions.sort((a, b) => a.startTime - b.startTime);
76
+ }
77
+
78
+ function getLocalIPs() {
79
+ const ifs = networkInterfaces();
80
+ const ips = [];
81
+ for (const name of Object.keys(ifs)) {
82
+ for (const iface of ifs[name]) {
83
+ if (iface.family === "IPv4" && !iface.internal) ips.push(iface.address);
84
+ }
85
+ }
86
+ return ips;
87
+ }
88
+
89
+ let cached = null, lastScan = 0;
90
+
91
+ const server = createServer(async (req, res) => {
92
+ res.setHeader("Access-Control-Allow-Origin", "*");
93
+ res.setHeader("Access-Control-Allow-Methods", "GET");
94
+
95
+ if (req.url === "/api/sessions") {
96
+ // Cache for 15 seconds
97
+ if (!cached || Date.now() - lastScan > 15000) {
98
+ cached = await scan();
99
+ lastScan = Date.now();
100
+ }
101
+ res.setHeader("Content-Type", "application/json");
102
+ res.end(JSON.stringify({ sessions: cached, meta: { sessionCount: cached.length, lastRefresh: lastScan, ready: true } }));
103
+ } else {
104
+ res.writeHead(200, { "Content-Type": "text/plain" });
105
+ res.end("Claude Code Rehab data server running. Connect from the app.");
106
+ }
107
+ });
108
+
109
+ server.listen(PORT, "0.0.0.0", () => {
110
+ const ips = getLocalIPs();
111
+ console.log("\n Claude Code Rehab — Data Server\n");
112
+ console.log(` Local: http://localhost:${PORT}`);
113
+ ips.forEach(ip => console.log(` Network: http://${ip}:${PORT}`));
114
+ console.log(`\n Open the app on your phone and enter:`);
115
+ ips.forEach(ip => console.log(` ${ip}:${PORT}`));
116
+ console.log("\n Ctrl+C to stop\n");
117
+ });
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "claude-code-rehab",
3
+ "version": "1.0.0",
4
+ "description": "Claude Code usage data server — connects to the Rehab dashboard",
5
+ "type": "module",
6
+ "bin": {
7
+ "claude-code-rehab": "./index.mjs"
8
+ },
9
+ "keywords": ["claude", "claude-code", "usage", "monitor", "rehab"],
10
+ "license": "MIT"
11
+ }