Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
},
"metadata": {
"description": "Ultra-compressed Ukrainian mode for Claude Code. −65-90% tokens, 0% accuracy loss.",
"version": "2.3.0"
"version": "2.4.0"
},
"plugins": [
{
"name": "cavemenko",
"source": "./",
"description": "Ultra-compressed Ukrainian mode. −65-90% tokens, auto-detect lang, custom abbr, context-aware compression.",
"version": "2.3.0",
"version": "2.4.0",
"author": {
"name": "ruslanlap",
"url": "https://github.com/ruslanlap"
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cavemenko",
"version": "2.3.0",
"version": "2.4.0",
"description": "Ultra-compressed Ukrainian mode for Claude Code. −65-90% tokens, 0% accuracy loss. Auto-detect language, custom abbreviations, token counter, context-aware compression.",
"author": {
"name": "ruslanlap",
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ hermes skill install cavemenko # або скопіювати SKILL.md у ~/.h

Рівень: `/cavemenko lite|full|ultra`, вимкнути — `звичайний режим`.

Щоб не набирати `/cavemenko` щоразу, додай в `~/.hermes/config.yaml`:

```yaml
skills:
auto_load:
- cavemenko
```

Реальні цифри в Hermes читаються з його власного обліку (`state.db`):

```bash
python3 integrations/hermes/cavemenko-stats.py --last 5
```

Деталі — [integrations/hermes/README.md](integrations/hermes/README.md).

</details>

<details>
Expand Down
42 changes: 42 additions & 0 deletions integrations/hermes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Hermes integration

Hermes loads cavemenko as a skill. Three parts:

## 1. Install the skill

```bash
# copy the ruleset into a Hermes skill
mkdir -p ~/.hermes/skills/productivity/cavemenko
cp skills/cavemenko/SKILL.md ~/.hermes/skills/productivity/cavemenko/
```

Or point Hermes at the repo directly — the skill is one `SKILL.md`, no build step.

## 2. Auto-load it

`~/.hermes/config.yaml`:

```yaml
skills:
auto_load:
- cavemenko
```

Loaded every session, so no `/cavemenko` needed. Levels: `/cavemenko lite|full|ultra`, off via `звичайний режим`.

## 3. Real stats

Hermes already bills every call into `state.db` (table `session_model_usage`), so token counts need no estimating:

```bash
python3 integrations/hermes/cavemenko-stats.py # latest session
python3 integrations/hermes/cavemenko-stats.py --last 5 # per-session table
```

Prints output/input/cache-read tokens, API calls, average tokens per call — all provider-reported.

**It does not compute a savings percentage.** There is no baseline session to compare against, and a made-up ratio is worse than none. Compare two sessions with `--last 5` and read the ratio yourself.

Override the DB path with `HERMES_STATE_DB=/path/to/state.db` if it is not at `/opt/data/state.db`.

The Claude Code `Stop` hook (`hooks/cavemenko-stats.js`) is a separate path for that host; this script is the Hermes equivalent and reads the same underlying idea from Hermes' own accounting rather than counting transcript characters.
80 changes: 80 additions & 0 deletions integrations/hermes/cavemenko-stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""cavemenko stats for Hermes — real numbers from Hermes' own token accounting.

Hermes already bills every call into /opt/data/state.db (table
session_model_usage: output_tokens, input_tokens, cache_read_tokens).
There is no need to estimate anything: this reads the provider-reported
counts. Nothing here is inferred, extrapolated or invented.

Usage:
cavemenko-stats.py # latest session
cavemenko-stats.py --all # per-session table
cavemenko-stats.py --last N # last N sessions
"""
import os
import sqlite3
import sys
import time

DB = os.environ.get("HERMES_STATE_DB", "/opt/data/state.db")


def load(db):
if not os.path.exists(db):
print(f"no Hermes state db at {db} — nothing measured yet")
return None
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
rows = con.execute(
"""
SELECT session_id, model,
SUM(input_tokens), SUM(output_tokens), SUM(cache_read_tokens),
SUM(api_call_count), MAX(last_seen)
FROM session_model_usage
GROUP BY session_id, model

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Aggregate all models for each requested session

When a Hermes session uses more than one model, grouping by both session_id and model creates multiple rows for that session. The default path then reports only the most recently used model's counters, while --last N may return the same session several times and omit older requested sessions. This makes the advertised per-session token totals incomplete; aggregate by session or select all model rows belonging to each chosen session.

Useful? React with 👍 / 👎.

ORDER BY MAX(last_seen) DESC
Comment on lines +33 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Latest session omits other models' tokens

When a session uses multiple models, main reports only the most recently used model's tokens as the latest session. load returns separate rows per model, so --last N also counts model rows instead of sessions.

Learn more

The query returns one row for each session-and-model pair, ordered by each model's latest use. The default display then takes only the first row, so it omits calls made to any other model during that session. The table slices those same rows, so a session can take multiple slots in --last N.

Example: Session A uses model X for 10 output tokens and model Y for 20; session B uses only X. With A's Y call newest, the default prints 20 rather than A's total of 30. --last 2 can show A twice and omit B.

Recommended fix: Select the latest N distinct session IDs first. Aggregate token and call counts across all models for each session when printing session-level totals. If model-level detail is needed, render it beneath each selected session without counting it against N.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

"""
).fetchall()
con.close()
return rows


def main():
args = sys.argv[1:]
rows = load(DB)
if not rows:
return 0

if "--all" in args or "--last" in args:
n = 10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return every row for --all

When the database contains more than ten session/model rows, --all still initializes n to 10 and the loop slices to rows[:n], so the command silently prints only ten entries despite being documented as the unrestricted per-session table. Use the full result set for --all and apply a limit only for --last.

Useful? React with 👍 / 👎.

if "--last" in args:
try:
n = int(args[args.index("--last") + 1])
except (IndexError, ValueError):
pass
print(f"{'session':28} {'model':26} {'calls':>6} {'out':>9} {'in':>10} {'cache_read':>12}")
for sid, model, inp, out, cache, calls, last in rows[:n]:
Comment on lines +47 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 All-session view silently drops older sessions

With more than ten usage rows, --all prints only ten and silently drops older sessions. n stays at its default of ten unless --last is present.

Learn more

The table path is shared by --all and --last, but both pass through a slice using n, which defaults to 10. The --all option never updates that limit, so it does not show the complete history.

Example: A database has 12 usage rows. Running --all shows ten rows, while the two oldest rows disappear without a warning.

Recommended fix: Keep the --all path unlimited; apply the numeric slice only when --last is requested. Apply that slice to distinct sessions rather than model rows.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

print(f"{sid[:28]:28} {model[:26]:26} {calls:>6} {out:>9,} {inp:>10,} {cache:>12,}")
return 0

if not rows:
return 0
sid, model, inp, out, cache, calls, last = rows[0]
when = time.strftime("%Y-%m-%d %H:%M", time.localtime(last)) if last else "unknown"
print("cavemenko — виміряно в Hermes (provider-reported, не оцінка)")
print(f" session {sid}")
print(f" model {model}")
print(f" остання {when}")
print(f" api calls {calls}")
print(f" output {out:,} токенів")
print(f" input {inp:,} токенів")
print(f" cache read {cache:,} токенів")
if out:
print(f" avg out {out // max(calls, 1):,} токенів на виклик")
print()
print(" Економія порівнюється з іншою сесією — подивись --last 5.")
print(" Без базової сесії відсоток не рахується: краще 0, ніж вигадка.")
return 0


if __name__ == "__main__":
sys.exit(main())
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cavemenko",
"version": "2.3.0",
"version": "2.4.0",
"description": "Ultra-compressed Ukrainian mode for Claude Code",
"license": "MIT",
"author": "ruslanlap",
Expand Down
37 changes: 37 additions & 0 deletions tests/hermes-integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');

const SCRIPT = path.join(__dirname, '..', 'integrations', 'hermes', 'cavemenko-stats.py');

// Hermes integration: the stats script must exist, be executable Python, and
// never claim a savings percentage without a baseline.
describe('Hermes integration', () => {
test('stats script exists and is valid Python', () => {
expect(fs.existsSync(SCRIPT)).toBe(true);
const src = fs.readFileSync(SCRIPT, 'utf8');
// A syntax error here is the failure mode we care about: a broken script
// that only fails at runtime on the user's machine.
execFileSync('python3', ['-c', `compile(open(${JSON.stringify(SCRIPT)}).read(), 'x', 'exec')`]);
expect(src).toContain('session_model_usage');
});

test('reads Hermes state db, not a transcript', () => {
const src = fs.readFileSync(SCRIPT, 'utf8');
expect(src).toContain('state.db');
expect(src).toContain('output_tokens');
});

test('declines to invent a savings percentage', () => {
const src = fs.readFileSync(SCRIPT, 'utf8');
expect(src).toMatch(/не рахується|краще 0, ніж вигадка/);
});

test('has an integration README', () => {
const readme = path.join(__dirname, '..', 'integrations', 'hermes', 'README.md');
expect(fs.existsSync(readme)).toBe(true);
const text = fs.readFileSync(readme, 'utf8');
expect(text).toContain('auto_load');
expect(text).toContain('cavemenko-stats.py');
});
});
Loading