carmoji 0.1.0 → 0.2.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/README.md +24 -7
- package/carmoji.js +150 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,22 +39,30 @@ big puppy eyes when the agent is waiting for your approval.
|
|
|
39
39
|
|
|
40
40
|
Coding in *this* repo works out of the box — the hooks ship in
|
|
41
41
|
[.claude/settings.json](../.claude/settings.json). To get reactions in
|
|
42
|
-
every project
|
|
42
|
+
every project:
|
|
43
43
|
|
|
44
44
|
```sh
|
|
45
|
-
npx carmoji claude
|
|
45
|
+
npx carmoji install-claude
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
merges the hooks into `~/.claude/settings.json`, preserving everything
|
|
49
|
+
already there (a backup is saved to `settings.json.bak` first, reruns are
|
|
50
|
+
no-ops, and a file that doesn't parse is refused untouched). Prefer to do
|
|
51
|
+
it by hand? `npx carmoji claude-config` prints the snippet instead.
|
|
52
|
+
|
|
48
53
|
## Codex
|
|
49
54
|
|
|
50
55
|
```sh
|
|
51
|
-
npx carmoji codex
|
|
56
|
+
npx carmoji install-codex
|
|
52
57
|
```
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
sets the `notify` program in `~/.codex/config.toml` (backup to
|
|
60
|
+
`config.toml.bak`; an existing notify line is replaced with a printed
|
|
61
|
+
warning). `npx carmoji codex-config` prints the line if you'd rather paste
|
|
62
|
+
it yourself. Codex's notify surface only reports completed turns, so the
|
|
63
|
+
pet celebrates finished work but doesn't see individual edits (tail
|
|
64
|
+
`~/.codex/sessions/` is a possible future upgrade — see
|
|
65
|
+
[docs/ROADMAP.md](../docs/ROADMAP.md)).
|
|
58
66
|
|
|
59
67
|
## Try it without a coding agent
|
|
60
68
|
|
|
@@ -91,6 +99,15 @@ dot while agents are live, and GPS pauses to save battery. Prompt snippets
|
|
|
91
99
|
and permission messages (first ~140 chars) are sent to the phone as brief
|
|
92
100
|
toasts; they stay on your LAN and are only accepted by the paired device.
|
|
93
101
|
|
|
102
|
+
## Plan-window usage
|
|
103
|
+
|
|
104
|
+
The buddy dashboard's corner shows your rolling **5-hour and 7-day token
|
|
105
|
+
totals** ("5H 2.5M / 7D 13.7M"), summed from the local Claude Code
|
|
106
|
+
transcripts the same way community usage tools do (input + output +
|
|
107
|
+
cache-write tokens; cache reads excluded). It's your real consumption, not
|
|
108
|
+
an official percentage of plan quota — there's no public API for that.
|
|
109
|
+
Recomputed at turn boundaries with a 5-minute cache so hooks stay fast.
|
|
110
|
+
|
|
94
111
|
## Answer permissions from the phone (Claude Code, opt-in)
|
|
95
112
|
|
|
96
113
|
```sh
|
package/carmoji.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import {
|
|
16
16
|
readFileSync, writeFileSync, mkdirSync,
|
|
17
17
|
statSync, openSync, readSync, closeSync,
|
|
18
|
+
copyFileSync, existsSync, readdirSync,
|
|
18
19
|
} from 'node:fs';
|
|
19
20
|
import { spawn } from 'node:child_process';
|
|
20
21
|
import { homedir } from 'node:os';
|
|
@@ -222,6 +223,65 @@ function summarizeToolInput(payload) {
|
|
|
222
223
|
return String(text).replace(/\s+/g, ' ').slice(0, 140);
|
|
223
224
|
}
|
|
224
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Rolling 5-hour / 7-day token totals summed from the local Claude Code
|
|
228
|
+
* transcripts — the same data community usage tools read. Counts input,
|
|
229
|
+
* output, and cache-write tokens (cache reads are nearly free and would
|
|
230
|
+
* drown the numbers). Cached for 5 minutes because a full scan reads a
|
|
231
|
+
* week of JSONL; only turn boundaries may recompute — per-tool hooks just
|
|
232
|
+
* reuse the cache so they stay fast.
|
|
233
|
+
*/
|
|
234
|
+
function claudeUsage(allowRecompute) {
|
|
235
|
+
const cachePath = join(dirname(CONFIG_PATH), 'usage-cache.json');
|
|
236
|
+
try {
|
|
237
|
+
const cached = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
238
|
+
if (!allowRecompute || Date.now() - cached.computedAt < 300_000) {
|
|
239
|
+
return cached.usage;
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
if (!allowRecompute) return undefined;
|
|
243
|
+
}
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
const h5cut = now - 5 * 3600e3;
|
|
246
|
+
const d7cut = now - 7 * 86400e3;
|
|
247
|
+
let h5 = 0;
|
|
248
|
+
let d7 = 0;
|
|
249
|
+
const stack = [join(homedir(), '.claude', 'projects')];
|
|
250
|
+
while (stack.length) {
|
|
251
|
+
const dir = stack.pop();
|
|
252
|
+
let entries;
|
|
253
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
254
|
+
for (const entry of entries) {
|
|
255
|
+
const path = join(dir, entry.name);
|
|
256
|
+
if (entry.isDirectory()) { stack.push(path); continue; }
|
|
257
|
+
if (!entry.name.endsWith('.jsonl')) continue;
|
|
258
|
+
try {
|
|
259
|
+
if (statSync(path).mtimeMs < d7cut) continue;
|
|
260
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
261
|
+
if (!line.includes('"usage"')) continue;
|
|
262
|
+
try {
|
|
263
|
+
const record = JSON.parse(line);
|
|
264
|
+
const usage = record.message?.usage;
|
|
265
|
+
if (!usage) continue;
|
|
266
|
+
const ts = Date.parse(record.timestamp);
|
|
267
|
+
if (!(ts >= d7cut)) continue;
|
|
268
|
+
const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0)
|
|
269
|
+
+ (usage.cache_creation_input_tokens || 0);
|
|
270
|
+
d7 += tokens;
|
|
271
|
+
if (ts >= h5cut) h5 += tokens;
|
|
272
|
+
} catch { /* skip malformed lines */ }
|
|
273
|
+
}
|
|
274
|
+
} catch { /* unreadable file */ }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const usage = { h5, d7 };
|
|
278
|
+
try {
|
|
279
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
280
|
+
writeFileSync(cachePath, JSON.stringify({ computedAt: now, usage }));
|
|
281
|
+
} catch { /* cache is best-effort */ }
|
|
282
|
+
return usage;
|
|
283
|
+
}
|
|
284
|
+
|
|
225
285
|
/** Map a Claude Code hook payload to a wire event, or null to ignore. */
|
|
226
286
|
function eventFromClaudeHook(payload) {
|
|
227
287
|
switch (payload.hook_event_name) {
|
|
@@ -290,6 +350,80 @@ const TEST_EVENTS = {
|
|
|
290
350
|
end: { event: 'session_end' },
|
|
291
351
|
};
|
|
292
352
|
|
|
353
|
+
/** Copy `path` to `path.bak` before modifying it; returns the backup path. */
|
|
354
|
+
function backupFile(path) {
|
|
355
|
+
const bak = `${path}.bak`;
|
|
356
|
+
copyFileSync(path, bak);
|
|
357
|
+
return bak;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const CLAUDE_HOOK_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse',
|
|
361
|
+
'PostToolUse', 'Notification', 'Stop', 'SessionEnd'];
|
|
362
|
+
|
|
363
|
+
/** Merge the CarMoji hooks into ~/.claude/settings.json, keeping a backup. */
|
|
364
|
+
function installClaude() {
|
|
365
|
+
const path = join(homedir(), '.claude', 'settings.json');
|
|
366
|
+
let settings = {};
|
|
367
|
+
if (existsSync(path)) {
|
|
368
|
+
try {
|
|
369
|
+
settings = JSON.parse(readFileSync(path, 'utf8'));
|
|
370
|
+
} catch {
|
|
371
|
+
console.error(`${path} is not valid JSON — fix it first (nothing was changed).`);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (typeof settings.hooks !== 'object' || settings.hooks === null) settings.hooks = {};
|
|
376
|
+
|
|
377
|
+
let added = 0;
|
|
378
|
+
for (const name of CLAUDE_HOOK_EVENTS) {
|
|
379
|
+
const entries = Array.isArray(settings.hooks[name]) ? settings.hooks[name]
|
|
380
|
+
: (settings.hooks[name] = []);
|
|
381
|
+
if (JSON.stringify(entries).includes('carmoji')) continue; // already ours
|
|
382
|
+
entries.push({ hooks: [{ type: 'command', command: HOOK_COMMAND, timeout: 5 }] });
|
|
383
|
+
added++;
|
|
384
|
+
}
|
|
385
|
+
if (added === 0) {
|
|
386
|
+
console.log(`CarMoji hooks already installed in ${path}`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const bak = existsSync(path) ? backupFile(path) : null;
|
|
390
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
391
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + '\n');
|
|
392
|
+
console.log(`Added CarMoji hooks to ${added} event(s) in ${path}`);
|
|
393
|
+
if (bak) console.log(`Backup saved to ${bak}`);
|
|
394
|
+
console.log('New Claude Code sessions will pick them up.');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Set the CarMoji notify program in ~/.codex/config.toml, keeping a backup. */
|
|
398
|
+
function installCodex() {
|
|
399
|
+
const path = join(homedir(), '.codex', 'config.toml');
|
|
400
|
+
const notifyLine = RUNNING_FROM_PACKAGE
|
|
401
|
+
? 'notify = ["npx", "-y", "carmoji", "codex-notify"]'
|
|
402
|
+
: `notify = ["node", "${SCRIPT_PATH}", "codex-notify"]`;
|
|
403
|
+
|
|
404
|
+
let text = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
405
|
+
const lines = text.split('\n');
|
|
406
|
+
const notifyIndex = lines.findIndex((line) => /^\s*notify\s*=/.test(line));
|
|
407
|
+
|
|
408
|
+
if (notifyIndex >= 0 && lines[notifyIndex].includes('carmoji')) {
|
|
409
|
+
console.log(`CarMoji notify already installed in ${path}`);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const bak = text ? backupFile(path) : null;
|
|
413
|
+
if (notifyIndex >= 0) {
|
|
414
|
+
console.log(`Replacing existing notify: ${lines[notifyIndex].trim()}`);
|
|
415
|
+
lines[notifyIndex] = notifyLine;
|
|
416
|
+
text = lines.join('\n');
|
|
417
|
+
} else {
|
|
418
|
+
// notify is a top-level key, so it must sit before any [section].
|
|
419
|
+
text = `${notifyLine}\n${text}`;
|
|
420
|
+
}
|
|
421
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
422
|
+
writeFileSync(path, text.endsWith('\n') ? text : `${text}\n`);
|
|
423
|
+
console.log(`Installed CarMoji notify in ${path}`);
|
|
424
|
+
if (bak) console.log(`Backup saved to ${bak}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
293
427
|
function claudeConfigSnippet() {
|
|
294
428
|
const entry = [{ hooks: [{ type: 'command', command: HOOK_COMMAND, timeout: 5 }] }];
|
|
295
429
|
const hooks = {};
|
|
@@ -384,6 +518,10 @@ async function main() {
|
|
|
384
518
|
: payload.hook_event_name === 'Notification' ? payload.message
|
|
385
519
|
: undefined;
|
|
386
520
|
if (detail) message.detail = String(detail).replace(/\s+/g, ' ').slice(0, 140);
|
|
521
|
+
// Plan-window usage: recompute only at turn boundaries.
|
|
522
|
+
const recompute = ['SessionStart', 'Stop'].includes(payload.hook_event_name);
|
|
523
|
+
const usage = claudeUsage(recompute);
|
|
524
|
+
if (usage) message.usage = usage;
|
|
387
525
|
await send(config, message);
|
|
388
526
|
} catch {
|
|
389
527
|
// Never surface errors into the coding session.
|
|
@@ -423,6 +561,8 @@ async function main() {
|
|
|
423
561
|
const message = { source: 'test', session, ...event };
|
|
424
562
|
if (tokens !== undefined) message.tokens = Number(tokens);
|
|
425
563
|
if (project) message.project = project;
|
|
564
|
+
const usage = claudeUsage(true);
|
|
565
|
+
if (usage) message.usage = usage;
|
|
426
566
|
const result = await send(config, message);
|
|
427
567
|
console.log(result === 'ok' ? 'Sent!' : `Failed: ${result}`);
|
|
428
568
|
process.exit(result === 'ok' ? 0 : 1);
|
|
@@ -440,8 +580,17 @@ async function main() {
|
|
|
440
580
|
break;
|
|
441
581
|
}
|
|
442
582
|
|
|
583
|
+
case 'install-claude':
|
|
584
|
+
installClaude();
|
|
585
|
+
break;
|
|
586
|
+
|
|
587
|
+
case 'install-codex':
|
|
588
|
+
installCodex();
|
|
589
|
+
break;
|
|
590
|
+
|
|
443
591
|
case 'claude-config':
|
|
444
592
|
console.log('Merge this into the project’s .claude/settings.json (or ~/.claude/settings.json for everywhere):\n');
|
|
593
|
+
console.log('(Or let `carmoji install-claude` do it for you, with a backup.)\n');
|
|
445
594
|
console.log(claudeConfigSnippet());
|
|
446
595
|
break;
|
|
447
596
|
|
|
@@ -468,7 +617,7 @@ async function main() {
|
|
|
468
617
|
break;
|
|
469
618
|
|
|
470
619
|
default:
|
|
471
|
-
console.log('carmoji bridge — usage: pair <code> [ip[:port]] | discover | test <event> | hook [--gate] | codex-notify | claude-config | codex-config | gate-config');
|
|
620
|
+
console.log('carmoji bridge — usage: pair <code> [ip[:port]] | discover | install-claude | install-codex | test <event> | hook [--gate] | codex-notify | claude-config | codex-config | gate-config');
|
|
472
621
|
}
|
|
473
622
|
}
|
|
474
623
|
|