codeslop 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/bin/wasted.js ADDED
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { loadConfig, saveConfig } from '../lib/config.js';
4
+ import { CATEGORIES } from '../lib/categories.js';
5
+ import { addEvent, todayStats, allTimeStats, getSessions, startSession, getDailySummary } from '../lib/store.js';
6
+ import { syncDaily, syncEvent } from '../lib/sync.js';
7
+ import { wasteCost } from '../lib/cost.js';
8
+ import { randomUUID } from 'crypto';
9
+ import { existsSync, statSync, readdirSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { homedir } from 'os';
12
+
13
+ const args = process.argv.slice(2);
14
+ const cmd = args[0] || 'help';
15
+ const R='\x1b[31m',G='\x1b[32m',Y='\x1b[33m',D='\x1b[2m',B='\x1b[1m',X='\x1b[0m',BG_R='\x1b[41m',BG_G='\x1b[42m';
16
+ const fmt = n => n.toLocaleString('en-US');
17
+ const bar = (v, max, w=20) => { const f=Math.round((v/Math.max(max,1))*w); return `${R}${'█'.repeat(f)}${D}${'░'.repeat(w-f)}${X}`; };
18
+
19
+ const CMDS = {
20
+ init, log: quickLog, report, today, me, stats, sessions,
21
+ sync, stream, watch, scan, lb: leaderboard, leaderboard,
22
+ cats: showCats, categories: showCats,
23
+ };
24
+
25
+ async function main() { return (CMDS[cmd] || help)(); }
26
+
27
+ function help() {
28
+ console.log(`
29
+ ${R}${B} codeslop ${X}${D}— tracking every dollar your AI agent lights on fire${X}
30
+
31
+ ${B}setup${X}
32
+ ${G}codeslop init${X} [name] create your profile
33
+ ${G}codeslop stream${X} off|daily|live set sync mode (default: daily)
34
+
35
+ ${B}record${X}
36
+ ${G}codeslop log${X} log waste interactively
37
+ ${G}codeslop report${X} CAT TOK [M] quick report
38
+
39
+ ${B}view${X}
40
+ ${G}codeslop today${X} today's local stats
41
+ ${G}codeslop sessions${X} recent sessions
42
+ ${G}codeslop me${X} all-time local stats
43
+
44
+ ${B}global${X}
45
+ ${G}codeslop sync${X} push summary to codeslop.org
46
+ ${G}codeslop stats${X} global waste stats
47
+ ${G}codeslop watch${X} live global feed
48
+ ${G}codeslop lb${X} wall of shame
49
+
50
+ ${B}other${X}
51
+ ${G}codeslop scan${X} detect installed AI clients
52
+ ${G}codeslop cats${X} list categories
53
+
54
+ ${D}data stored locally at ~/.wasted/
55
+ only aggregated summaries leave your machine (stream: daily)
56
+ no prompts, no file paths, no descriptions are ever sent.
57
+ codeslop.org${X}
58
+ `);
59
+ }
60
+
61
+ async function init() {
62
+ const config = loadConfig();
63
+ if (config.user_id) {
64
+ console.log(`\n already set up as ${B}${config.nickname}${X} ${D}(${config.user_id})${X}`);
65
+ console.log(` stream: ${config.stream} country: ${config.country || 'not set'}\n`);
66
+ return;
67
+ }
68
+ const user_id = randomUUID().slice(0, 12);
69
+ const nickname = args[1] || process.env.USER || 'anon';
70
+ config.user_id = user_id;
71
+ config.nickname = nickname;
72
+ saveConfig(config);
73
+ try {
74
+ await fetch(config.api_url+'/api/users',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({user_id,nickname})});
75
+ } catch {}
76
+ console.log(`
77
+ ${G}${B}✓${X} ${B}${nickname}${X} ${D}${user_id}${X}
78
+
79
+ ${D}stream mode:${X} ${Y}daily${X} ${D}(aggregated summaries only)${X}
80
+ ${D}local db:${X} ~/.wasted/wasted.db
81
+ ${D}config:${X} ~/.wasted/config.json
82
+
83
+ ${G}codeslop log${X} → record waste
84
+ ${G}codeslop today${X} → see today's stats
85
+ ${G}codeslop scan${X} → detect AI clients
86
+ `);
87
+ }
88
+
89
+ function requireUser() {
90
+ const c = loadConfig();
91
+ if (!c.user_id) { console.log(`\n ${R}run ${G}codeslop init${R} first${X}\n`); return null; }
92
+ return c;
93
+ }
94
+
95
+ async function quickLog() {
96
+ if (!requireUser()) return;
97
+ const config = loadConfig();
98
+ const catKeys = Object.keys(CATEGORIES);
99
+ console.log(`\n ${B}category:${X}\n`);
100
+ catKeys.forEach((k,i) => console.log(` ${D}${String(i+1).padStart(2)}${X} ${CATEGORIES[k].icon} ${B}${CATEGORIES[k].name}${X}`));
101
+
102
+ const rl = (await import('readline')).createInterface({input:process.stdin,output:process.stdout});
103
+ const ask = q => new Promise(r => rl.question(q, r));
104
+
105
+ const ci = parseInt(await ask(`\n ${B}#${X} `)) - 1;
106
+ if (ci<0||ci>=catKeys.length) { rl.close(); return; }
107
+ const tokens = parseInt(await ask(` ${B}tokens:${X} `)) || 500;
108
+ const model = await ask(` ${B}model:${X} `) || 'unknown';
109
+ const client = await ask(` ${B}client:${X} `) || 'unknown';
110
+ const note = await ask(` ${B}note (local only):${X} `) || null;
111
+ rl.close();
112
+
113
+ const category = catKeys[ci];
114
+ const cost = wasteCost(model, category, tokens);
115
+
116
+ addEvent(category, tokens, { model, client, note, cost });
117
+
118
+ // Live sync if enabled
119
+ if (config.stream === 'live') {
120
+ await syncEvent({ category, model, client, tokens, cost });
121
+ }
122
+
123
+ console.log(`\n ${CATEGORIES[category].icon} ${R}${B}${fmt(tokens)}${X} tokens → ${B}${CATEGORIES[category].name}${X} ${D}(stored locally)${X}\n`);
124
+ }
125
+
126
+ async function report() {
127
+ const config = requireUser();
128
+ if (!config) return;
129
+ const [, category, tokStr, model, client] = args;
130
+ if (!category || !CATEGORIES[category]) {
131
+ console.log(`\n ${D}usage: codeslop report <category> <tokens> [model] [client]${X}`);
132
+ console.log(` ${D}cats: ${Object.keys(CATEGORIES).join(', ')}${X}\n`);
133
+ return;
134
+ }
135
+ const tokens = parseInt(tokStr) || 500;
136
+ const cost = wasteCost(model, category, tokens);
137
+ addEvent(category, tokens, { model: model||'unknown', client: client||'unknown', cost });
138
+ if (config.stream === 'live') await syncEvent({ category, model: model||'unknown', client: client||'unknown', tokens, cost });
139
+ console.log(`\n ${CATEGORIES[category].icon} ${R}${fmt(tokens)}${X} → ${B}${CATEGORIES[category].name}${X}\n`);
140
+ }
141
+
142
+ function today() {
143
+ const s = todayStats();
144
+ const t = s.totals;
145
+ console.log(`\n ${R}${B}today${X} — ${R}$${t.cost.toFixed(2)}${X} — ${fmt(t.tokens)} tokens — ${t.events} events\n`);
146
+ if (s.by_category.length) {
147
+ const max = s.by_category[0]?.v || 1;
148
+ console.log(` ${D}categories${X}`);
149
+ s.by_category.forEach(c => {
150
+ const m = CATEGORIES[c.k] || {icon:'?',name:c.k};
151
+ console.log(` ${m.icon} ${m.name.padEnd(18)} ${bar(c.v,max)} ${D}${fmt(c.v)}${X}`);
152
+ });
153
+ }
154
+ if (s.by_client.length) {
155
+ const max = s.by_client[0]?.v || 1;
156
+ console.log(`\n ${D}clients${X}`);
157
+ s.by_client.forEach(c => console.log(` ${c.k.padEnd(20)} ${bar(c.v,max,15)} ${D}${fmt(c.v)}${X}`));
158
+ }
159
+ if (s.by_model.length) {
160
+ const max = s.by_model[0]?.v || 1;
161
+ console.log(`\n ${D}models${X}`);
162
+ s.by_model.forEach(c => console.log(` ${c.k.padEnd(20)} ${bar(c.v,max,15)} ${D}${fmt(c.v)}${X}`));
163
+ }
164
+ if (s.sessions.length) {
165
+ console.log(`\n ${D}sessions${X}`);
166
+ s.sessions.forEach(ss => console.log(` ${D}${ss.id}${X} ${ss.client.padEnd(14)} ${R}${fmt(ss.total_tokens)}${X} tok`));
167
+ }
168
+ console.log();
169
+ }
170
+
171
+ function me() {
172
+ const s = allTimeStats();
173
+ const t = s.totals;
174
+ const config = loadConfig();
175
+ console.log(`\n ${R}${B}${config.nickname || 'you'}${X} — ${R}$${t.cost.toFixed(2)}${X} — ${fmt(t.tokens)} tokens — ${Y}${Math.floor(t.cost/12)} burritos${X}\n`);
176
+ if (s.by_category.length) {
177
+ const max = s.by_category[0]?.v || 1;
178
+ s.by_category.forEach(c => {
179
+ const m = CATEGORIES[c.k] || {icon:'?',name:c.k};
180
+ console.log(` ${m.icon} ${m.name.padEnd(18)} ${bar(c.v,max)} ${D}${fmt(c.v)}${X}`);
181
+ });
182
+ }
183
+ console.log();
184
+ }
185
+
186
+ function sessions() {
187
+ const ss = getSessions(20);
188
+ if (!ss.length) { console.log(`\n ${D}no sessions yet${X}\n`); return; }
189
+ console.log(`\n ${B}recent sessions${X}\n`);
190
+ ss.forEach(s => {
191
+ const dur = s.ended_at ? `${D}ended${X}` : `${G}active${X}`;
192
+ console.log(` ${D}${s.id}${X} ${s.client.padEnd(14)} ${s.model||'—'.padEnd(12)} ${R}${fmt(s.total_tokens).padStart(8)}${X} tok ${dur}`);
193
+ });
194
+ console.log();
195
+ }
196
+
197
+ async function sync() {
198
+ const config = requireUser();
199
+ if (!config) return;
200
+ if (config.stream === 'off') { console.log(`\n ${D}stream is off. run ${G}codeslop stream daily${D} to enable.${X}\n`); return; }
201
+
202
+ console.log(`\n ${D}syncing...${X}`);
203
+ const summary = getDailySummary();
204
+ if (!summary.length) { console.log(` ${D}nothing to sync${X}\n`); return; }
205
+
206
+ console.log(` ${D}${summary.length} aggregated rows (no descriptions sent)${X}`);
207
+ const result = await syncDaily();
208
+ if (result.synced) {
209
+ console.log(` ${G}✓${X} synced ${result.synced} summaries\n`);
210
+ } else {
211
+ console.log(` ${R}couldn't reach API${X}\n`);
212
+ }
213
+ }
214
+
215
+ function stream() {
216
+ const mode = args[1];
217
+ const config = loadConfig();
218
+ if (!mode) {
219
+ console.log(`\n stream mode: ${B}${config.stream}${X}`);
220
+ console.log(`\n ${D}off${X} — nothing leaves your machine`);
221
+ console.log(` ${D}daily${X} — aggregated summary, no descriptions`);
222
+ console.log(` ${D}live${X} — events streamed (still no raw content)\n`);
223
+ console.log(` ${D}usage: codeslop stream off|daily|live${X}\n`);
224
+ return;
225
+ }
226
+ if (!['off','daily','live'].includes(mode)) { console.log(`\n ${R}must be off, daily, or live${X}\n`); return; }
227
+ config.stream = mode;
228
+ saveConfig(config);
229
+ const labels = { off:`${R}off${X} — local only`, daily:`${Y}daily${X} — aggregated summaries`, live:`${G}live${X} — real-time` };
230
+ console.log(`\n stream: ${labels[mode]}\n`);
231
+ }
232
+
233
+ async function watch() {
234
+ const config = loadConfig();
235
+ console.log(`\n ${R}${B}live feed${X} ${D}ctrl+c to stop${X}\n`);
236
+ try {
237
+ const res = await fetch(config.api_url+'/api/live',{headers:{Accept:'text/event-stream'}});
238
+ const reader = res.body.getReader();
239
+ const dec = new TextDecoder();
240
+ let buf = '';
241
+ while (true) {
242
+ const {done,value} = await reader.read();
243
+ if (done) break;
244
+ buf += dec.decode(value,{stream:true});
245
+ const lines = buf.split('\n'); buf = lines.pop();
246
+ for (const l of lines) {
247
+ if (!l.startsWith('data: ')) continue;
248
+ try {
249
+ const d = JSON.parse(l.slice(6));
250
+ if (d.type==='init') console.log(` ${G}●${X} connected — ${d.watching} watching — $${d.today.cost.toFixed(2)} today\n`);
251
+ else if (d.type==='event'&&d.ev) {
252
+ const e=d.ev, m=CATEGORIES[e.category]||{icon:'🗑️',name:e.category};
253
+ const t=new Date().toLocaleTimeString('en-US',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
254
+ console.log(` ${D}${t}${X} ${m.icon} ${m.name.padEnd(16)} ${R}${fmt(e.tokens).padStart(7)}${X} ${D}${e.client}/${e.model}${X}`);
255
+ }
256
+ } catch{}
257
+ }
258
+ }
259
+ } catch { console.log(` ${R}can't connect${X}\n`); }
260
+ }
261
+
262
+ async function stats() {
263
+ const config = loadConfig();
264
+ try {
265
+ const d = await (await fetch(config.api_url+'/api/stats')).json();
266
+ console.log(`\n ${R}${B}global${X} — ${R}$${d.today.cost.toFixed(2)}${X} today — ${fmt(d.today.tokens)} tokens — ${d.users} users\n`);
267
+ if (d.by_category.length) {
268
+ const max = d.by_category[0]?.v||1;
269
+ d.by_category.forEach(c => {
270
+ const m = CATEGORIES[c.k]||{icon:'?',name:c.k};
271
+ console.log(` ${m.icon} ${m.name.padEnd(18)} ${bar(c.v,max)} ${D}${fmt(c.v)}${X}`);
272
+ });
273
+ }
274
+ if (d.by_client.length) {
275
+ console.log();
276
+ const max = d.by_client[0]?.v||1;
277
+ d.by_client.forEach(c => console.log(` ${(c.k||'?').padEnd(20)} ${bar(c.v,max,15)} ${D}${fmt(c.v)}${X}`));
278
+ }
279
+ console.log();
280
+ } catch { console.log(`\n ${R}can't reach API${X}\n`); }
281
+ }
282
+
283
+ async function leaderboard() {
284
+ const config = loadConfig();
285
+ try {
286
+ const d = await (await fetch(config.api_url+'/api/leaderboard')).json();
287
+ console.log(`\n ${R}${B}wall of shame${X}\n`);
288
+ if (!d.length) { console.log(` ${D}empty${X}\n`); return; }
289
+ d.forEach((u,i) => {
290
+ const medal = i===0?'👑':i===1?'🥈':i===2?'🥉':' ';
291
+ console.log(` ${medal} ${D}#${i+1}${X} ${B}${(u.nickname||u.id).padEnd(16)}${X} ${R}$${u.total_cost.toFixed(2).padStart(10)}${X} ${D}${fmt(u.total_tokens)} tok${X}`);
292
+ });
293
+ console.log();
294
+ } catch { console.log(`\n ${R}can't reach API${X}\n`); }
295
+ }
296
+
297
+ function showCats() {
298
+ console.log(`\n ${B}waste categories${X}\n`);
299
+ Object.entries(CATEGORIES).forEach(([k,c]) => console.log(` ${c.icon} ${B}${c.name.padEnd(18)}${X} ${D}${k}${X}`));
300
+ console.log();
301
+ }
302
+
303
+ function scan() {
304
+ console.log(`\n ${B}scanning...${X}\n`);
305
+ const home = homedir();
306
+ const checks = [
307
+ {name:'Claude Code', paths:[join(home,'.claude')]},
308
+ {name:'Cursor', paths:[join(home,'.cursor'), join(home,'Library','Application Support','Cursor')]},
309
+ {name:'Windsurf', paths:[join(home,'.windsurf'), join(home,'Library','Application Support','Windsurf')]},
310
+ {name:'Copilot', paths:[join(home,'.copilot')]},
311
+ {name:'Aider', paths:[join(home,'.aider.chat.history.md')]},
312
+ {name:'OpenCode', paths:[join(home,'.opencode')]},
313
+ ];
314
+ let found = 0;
315
+ for (const c of checks) {
316
+ const exists = c.paths.some(p => existsSync(p));
317
+ if (exists) { console.log(` ${G}●${X} ${B}${c.name}${X}`); found++; }
318
+ else { console.log(` ${D}○ ${c.name}${X}`); }
319
+ }
320
+ console.log(`\n ${found} client(s) detected\n`);
321
+ }
322
+
323
+ main().catch(e => { console.error(e); process.exit(1); });
package/lib/api.js ADDED
@@ -0,0 +1,23 @@
1
+ import { loadConfig } from './config.js';
2
+
3
+ async function request(method, path, body) {
4
+ const config = loadConfig();
5
+ const url = config.api_url + path;
6
+ const opts = {
7
+ method,
8
+ headers: { 'Content-Type': 'application/json' },
9
+ };
10
+ if (body) opts.body = JSON.stringify(body);
11
+ const res = await fetch(url, opts);
12
+ return res.json();
13
+ }
14
+
15
+ export const api = {
16
+ reportWaste: (data) => request('POST', '/api/report', data),
17
+ reportBatch: (data) => request('POST', '/api/report/batch', data),
18
+ getStats: () => request('GET', '/api/stats'),
19
+ getUserStats: (id) => request('GET', `/api/users/${id}`),
20
+ getLeaderboard: () => request('GET', '/api/leaderboard'),
21
+ getRecent: (n = 30) => request('GET', `/api/recent?n=${n}`),
22
+ register: (user_id, nickname) => request('POST', '/api/users', { user_id, nickname }),
23
+ };
@@ -0,0 +1,12 @@
1
+ export const CATEGORIES = {
2
+ yapping: { icon: '🦜', name: 'Yapping', desc: 'Model monologued. Nobody asked.' },
3
+ groundhog_day: { icon: '🔄', name: 'Groundhog Day', desc: 'Same action retried 3+ times.' },
4
+ ghost_agent: { icon: '👻', name: 'Ghost Agent', desc: 'Sub-agent spawned. Did nothing.' },
5
+ prompt_salad: { icon: '🥗', name: 'Prompt Salad', desc: 'Vague prompt. Model guessed.' },
6
+ tool_spam: { icon: '🔨', name: 'Tool Spam', desc: 'Tool calls that went nowhere.' },
7
+ context_stuffing: { icon: '🧸', name: 'Context Stuffing', desc: 'Huge context. Never referenced.' },
8
+ hallucination_trip: { icon: '🍄', name: 'Hallucination Trip', desc: 'Confidently wrong. Redo.' },
9
+ ping_pong: { icon: '🏓', name: 'Ping Pong', desc: '3+ turns. Zero progress.' },
10
+ skill_issue: { icon: '💀', name: 'Skill Issue', desc: 'Skill misfired completely.' },
11
+ overthink: { icon: '🧠', name: 'Overthink', desc: '200 lines. Needed 10.' },
12
+ };
package/lib/config.js ADDED
@@ -0,0 +1,24 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ const DIR = join(homedir(), '.wasted');
6
+ const FILE = join(DIR, 'config.json');
7
+
8
+ const DEFAULTS = {
9
+ user_id: null,
10
+ nickname: null,
11
+ api_url: process.env.WASTED_API_URL || 'https://codeslop.org',
12
+ stream: 'daily', // off | daily | live
13
+ country: null, // opt-in, e.g. "US" or "IN"
14
+ };
15
+
16
+ export function loadConfig() {
17
+ try { return { ...DEFAULTS, ...JSON.parse(readFileSync(FILE, 'utf8')) }; }
18
+ catch { return { ...DEFAULTS }; }
19
+ }
20
+
21
+ export function saveConfig(config) {
22
+ mkdirSync(DIR, { recursive: true });
23
+ writeFileSync(FILE, JSON.stringify(config, null, 2) + '\n');
24
+ }
package/lib/cost.js ADDED
@@ -0,0 +1,2 @@
1
+ // Re-export from shared pricing module.
2
+ export { wasteCost } from '../../shared/pricing.js';
package/lib/store.js ADDED
@@ -0,0 +1,113 @@
1
+ import Database from 'better-sqlite3';
2
+ import { mkdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ import { randomUUID } from 'crypto';
6
+ import { wasteCost } from '../../shared/pricing.js';
7
+
8
+ const DIR = join(homedir(), '.wasted');
9
+ const DB_PATH = join(DIR, 'wasted.db');
10
+
11
+ let _db;
12
+ function db() {
13
+ if (_db) return _db;
14
+ mkdirSync(DIR, { recursive: true });
15
+ _db = new Database(DB_PATH);
16
+ _db.pragma('journal_mode = WAL');
17
+ _db.exec(`
18
+ CREATE TABLE IF NOT EXISTS sessions (
19
+ id TEXT PRIMARY KEY,
20
+ client TEXT NOT NULL,
21
+ model TEXT,
22
+ started_at TEXT DEFAULT (datetime('now')),
23
+ ended_at TEXT,
24
+ total_tokens INTEGER DEFAULT 0,
25
+ total_cost REAL DEFAULT 0
26
+ );
27
+ CREATE TABLE IF NOT EXISTS events (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ session_id TEXT,
30
+ category TEXT NOT NULL,
31
+ model TEXT NOT NULL DEFAULT 'unknown',
32
+ client TEXT NOT NULL DEFAULT 'unknown',
33
+ tokens INTEGER NOT NULL,
34
+ cost REAL NOT NULL,
35
+ note TEXT,
36
+ synced INTEGER DEFAULT 0,
37
+ created_at TEXT DEFAULT (datetime('now')),
38
+ FOREIGN KEY(session_id) REFERENCES sessions(id)
39
+ );
40
+ CREATE INDEX IF NOT EXISTS idx_ev_sync ON events(synced);
41
+ CREATE INDEX IF NOT EXISTS idx_ev_date ON events(created_at);
42
+ CREATE INDEX IF NOT EXISTS idx_sess_date ON sessions(started_at);
43
+ `);
44
+ return _db;
45
+ }
46
+
47
+ // Sessions
48
+ export function startSession(client, model) {
49
+ const id = randomUUID().slice(0, 8);
50
+ db().prepare('INSERT INTO sessions(id,client,model) VALUES(?,?,?)').run(id, client, model || null);
51
+ return id;
52
+ }
53
+
54
+ export function endSession(id) {
55
+ db().prepare('UPDATE sessions SET ended_at=datetime("now") WHERE id=?').run(id);
56
+ }
57
+
58
+ export function getSessions(limit = 20) {
59
+ return db().prepare('SELECT * FROM sessions ORDER BY started_at DESC LIMIT ?').all(limit);
60
+ }
61
+
62
+ // Events
63
+ export function addEvent(category, tokens, opts = {}) {
64
+ // Cost should be pre-calculated by the caller using wasteCost()
65
+ const cost = opts.cost || wasteCost(opts.model || 'unknown', category, tokens);
66
+ db().prepare('INSERT INTO events(session_id,category,model,client,tokens,cost,note) VALUES(?,?,?,?,?,?,?)').run(
67
+ opts.session_id || null, category, opts.model || 'unknown', opts.client || 'unknown', tokens, cost, opts.note || null
68
+ );
69
+ if (opts.session_id) {
70
+ db().prepare('UPDATE sessions SET total_tokens=total_tokens+?, total_cost=total_cost+? WHERE id=?').run(tokens, cost, opts.session_id);
71
+ }
72
+ }
73
+
74
+ // Stats
75
+ export function todayStats() {
76
+ return {
77
+ totals: db().prepare(`SELECT COALESCE(SUM(cost),0) as cost, COALESCE(SUM(tokens),0) as tokens, COUNT(*) as events FROM events WHERE date(created_at)=date('now')`).get(),
78
+ by_category: db().prepare(`SELECT category as k, SUM(tokens) as v, COUNT(*) as n FROM events WHERE date(created_at)=date('now') GROUP BY category ORDER BY v DESC`).all(),
79
+ by_client: db().prepare(`SELECT client as k, SUM(tokens) as v, COUNT(*) as n FROM events WHERE date(created_at)=date('now') GROUP BY client ORDER BY v DESC`).all(),
80
+ by_model: db().prepare(`SELECT model as k, SUM(tokens) as v, COUNT(*) as n FROM events WHERE date(created_at)=date('now') GROUP BY model ORDER BY v DESC`).all(),
81
+ sessions: db().prepare(`SELECT * FROM sessions WHERE date(started_at)=date('now') ORDER BY started_at DESC`).all(),
82
+ };
83
+ }
84
+
85
+ export function allTimeStats() {
86
+ return {
87
+ totals: db().prepare(`SELECT COALESCE(SUM(cost),0) as cost, COALESCE(SUM(tokens),0) as tokens, COUNT(*) as events FROM events`).get(),
88
+ by_category: db().prepare(`SELECT category as k, SUM(tokens) as v, COUNT(*) as n FROM events GROUP BY category ORDER BY v DESC`).all(),
89
+ };
90
+ }
91
+
92
+ // Sync: get unsynced events, mark them synced
93
+ export function getUnsyncedEvents() {
94
+ return db().prepare('SELECT * FROM events WHERE synced=0 ORDER BY created_at ASC LIMIT 500').all();
95
+ }
96
+
97
+ export function markSynced(ids) {
98
+ const placeholders = ids.map(() => '?').join(',');
99
+ db().prepare(`UPDATE events SET synced=1 WHERE id IN (${placeholders})`).run(...ids);
100
+ }
101
+
102
+ // Summary for daily sync (aggregated, no notes/descriptions — privacy safe)
103
+ export function getDailySummary() {
104
+ return db().prepare(`
105
+ SELECT category, model, client, SUM(tokens) as tokens, SUM(cost) as cost, COUNT(*) as n
106
+ FROM events WHERE synced=0
107
+ GROUP BY category, model, client
108
+ `).all();
109
+ }
110
+
111
+ export function markAllSynced() {
112
+ db().prepare('UPDATE events SET synced=1 WHERE synced=0').run();
113
+ }
package/lib/sync.js ADDED
@@ -0,0 +1,72 @@
1
+ import { createHash } from 'crypto';
2
+ import { loadConfig } from './config.js';
3
+ import { getDailySummary, markAllSynced } from './store.js';
4
+
5
+ // Generate a dedup key so the server can ignore duplicate syncs
6
+ function idempKey(userId, row) {
7
+ const date = new Date().toISOString().slice(0, 10);
8
+ return createHash('sha256').update(`${userId}:${row.category}:${row.model}:${row.client}:${row.tokens}:${date}`).digest('hex').slice(0, 16);
9
+ }
10
+
11
+ // Sync modes:
12
+ // off — nothing leaves your machine
13
+ // daily — aggregated summary, no descriptions, no notes
14
+ // live — each event sent as it happens (still no raw descriptions)
15
+
16
+ export async function syncDaily() {
17
+ const config = loadConfig();
18
+ if (!config.user_id || config.stream === 'off') return { synced: 0 };
19
+
20
+ const summary = getDailySummary();
21
+ if (!summary.length) return { synced: 0 };
22
+
23
+ // Convert summary rows into batch events (aggregated = privacy safe)
24
+ const events = summary.map(row => ({
25
+ category: row.category,
26
+ model: row.model,
27
+ client: row.client,
28
+ tokens: row.tokens,
29
+ cost: row.cost,
30
+ idempotency_key: idempKey(config.user_id, row),
31
+ }));
32
+
33
+ try {
34
+ const res = await fetch(config.api_url + '/api/report/batch', {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify({
38
+ user_id: config.user_id,
39
+ events,
40
+ country: config.country || null,
41
+ }),
42
+ });
43
+ const data = await res.json();
44
+ if (data.ok) {
45
+ markAllSynced();
46
+ return { synced: events.length };
47
+ }
48
+ } catch {}
49
+ return { synced: 0 };
50
+ }
51
+
52
+ export async function syncEvent(event) {
53
+ const config = loadConfig();
54
+ if (!config.user_id || config.stream !== 'live') return false;
55
+
56
+ try {
57
+ await fetch(config.api_url + '/api/report', {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({
61
+ user_id: config.user_id,
62
+ category: event.category,
63
+ model: event.model,
64
+ client: event.client,
65
+ tokens: event.tokens,
66
+ cost: event.cost,
67
+ country: config.country || null,
68
+ }),
69
+ });
70
+ return true;
71
+ } catch { return false; }
72
+ }
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "codeslop",
3
+ "version": "0.1.0",
4
+ "description": "Track every dollar your AI agent lights on fire. Works with Claude Code, Cursor, Windsurf, Copilot, Aider, OpenCode. codeslop.org",
5
+ "bin": {
6
+ "codeslop": "./bin/wasted.js"
7
+ },
8
+ "type": "module",
9
+ "keywords": ["ai", "tokens", "waste", "slop", "codeslop", "llm", "claude", "cursor", "copilot", "windsurf", "opencode", "aider"],
10
+ "license": "MIT",
11
+ "files": ["bin/", "lib/"],
12
+ "dependencies": {
13
+ "better-sqlite3": "^11.6.0"
14
+ }
15
+ }