meterhouse-rotor 0.2.2__tar.gz
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.
- meterhouse_rotor-0.2.2/PKG-INFO +312 -0
- meterhouse_rotor-0.2.2/README.md +283 -0
- meterhouse_rotor-0.2.2/meterhouse/__init__.py +111 -0
- meterhouse_rotor-0.2.2/meterhouse/__main__.py +4 -0
- meterhouse_rotor-0.2.2/meterhouse/account.py +203 -0
- meterhouse_rotor-0.2.2/meterhouse/cli.py +345 -0
- meterhouse_rotor-0.2.2/meterhouse/config.py +141 -0
- meterhouse_rotor-0.2.2/meterhouse/daemon.py +219 -0
- meterhouse_rotor-0.2.2/meterhouse/health.py +86 -0
- meterhouse_rotor-0.2.2/meterhouse/identity.py +84 -0
- meterhouse_rotor-0.2.2/meterhouse/logging_setup.py +60 -0
- meterhouse_rotor-0.2.2/meterhouse/parser.py +313 -0
- meterhouse_rotor-0.2.2/meterhouse/pricing.py +66 -0
- meterhouse_rotor-0.2.2/meterhouse/reports.py +69 -0
- meterhouse_rotor-0.2.2/meterhouse/scanner.py +108 -0
- meterhouse_rotor-0.2.2/meterhouse/store.py +281 -0
- meterhouse_rotor-0.2.2/meterhouse/sync.py +146 -0
- meterhouse_rotor-0.2.2/meterhouse/ws_client.py +175 -0
- meterhouse_rotor-0.2.2/meterhouse_rotor.egg-info/PKG-INFO +312 -0
- meterhouse_rotor-0.2.2/meterhouse_rotor.egg-info/SOURCES.txt +35 -0
- meterhouse_rotor-0.2.2/meterhouse_rotor.egg-info/dependency_links.txt +1 -0
- meterhouse_rotor-0.2.2/meterhouse_rotor.egg-info/entry_points.txt +2 -0
- meterhouse_rotor-0.2.2/meterhouse_rotor.egg-info/requires.txt +12 -0
- meterhouse_rotor-0.2.2/meterhouse_rotor.egg-info/top_level.txt +1 -0
- meterhouse_rotor-0.2.2/pyproject.toml +49 -0
- meterhouse_rotor-0.2.2/setup.cfg +4 -0
- meterhouse_rotor-0.2.2/tests/test_account.py +288 -0
- meterhouse_rotor-0.2.2/tests/test_config.py +56 -0
- meterhouse_rotor-0.2.2/tests/test_daemon.py +84 -0
- meterhouse_rotor-0.2.2/tests/test_health.py +50 -0
- meterhouse_rotor-0.2.2/tests/test_identity.py +25 -0
- meterhouse_rotor-0.2.2/tests/test_parser.py +124 -0
- meterhouse_rotor-0.2.2/tests/test_pricing.py +28 -0
- meterhouse_rotor-0.2.2/tests/test_prompts.py +159 -0
- meterhouse_rotor-0.2.2/tests/test_scanner.py +93 -0
- meterhouse_rotor-0.2.2/tests/test_sdk.py +25 -0
- meterhouse_rotor-0.2.2/tests/test_store.py +70 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meterhouse-rotor
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Rotor — the Meterhouse metering agent. Reads Claude Code usage on this machine and reports it to your Meterhouse dashboard.
|
|
5
|
+
Author: Meterhouse
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/Aniruth-Sakthivel/claude-code-uusage
|
|
8
|
+
Project-URL: Repository, https://github.com/Aniruth-Sakthivel/claude-code-uusage
|
|
9
|
+
Project-URL: Documentation, https://github.com/Aniruth-Sakthivel/claude-code-uusage#readme
|
|
10
|
+
Keywords: meterhouse,rotor,claude-code,agent,usage,sync,dashboard
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: Other/Proprietary License
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Provides-Extra: sync
|
|
22
|
+
Requires-Dist: httpx>=0.27; extra == "sync"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
25
|
+
Provides-Extra: build
|
|
26
|
+
Requires-Dist: pyinstaller>=6.0; extra == "build"
|
|
27
|
+
Provides-Extra: realtime
|
|
28
|
+
Requires-Dist: websockets>=12.0; extra == "realtime"
|
|
29
|
+
|
|
30
|
+
# Rotor — Install & Scan Guide
|
|
31
|
+
|
|
32
|
+
**Rotor** is the Meterhouse metering agent: the part that sits on each machine
|
|
33
|
+
and turns as work happens. It scans Claude Code's local transcript files, stores
|
|
34
|
+
usage in a local SQLite database, and (optionally) syncs it to the central
|
|
35
|
+
server. **Scanning works fully offline — no server required.**
|
|
36
|
+
|
|
37
|
+
Installed as `meterhouse-rotor`; the command it provides is `meterhouse`.
|
|
38
|
+
|
|
39
|
+
> **Tracked activity ≠ official quota.** All numbers are token counts parsed from
|
|
40
|
+
> local transcripts — an estimate, not your Claude Max/Pro billing or quota.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 1. Prerequisites
|
|
45
|
+
|
|
46
|
+
- **Python 3.10+** (check with `python --version`)
|
|
47
|
+
- **Claude Code** installed and used at least once on this machine, so transcripts
|
|
48
|
+
exist under `~/.claude/projects/` (Windows: `%USERPROFILE%\.claude\projects\`).
|
|
49
|
+
|
|
50
|
+
The agent uses **only the Python standard library** — there is nothing to
|
|
51
|
+
`pip install` for scanning.
|
|
52
|
+
|
|
53
|
+
> For **central mode** (sending usage to the dashboard) you install the
|
|
54
|
+
> `meterhouse` command from the repo, which needs **git** on `PATH` — see §7.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 2. Install
|
|
59
|
+
|
|
60
|
+
Clone the repository and move into the agent folder:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
git clone <your-repo-url> meterhouse
|
|
64
|
+
cd meterhouse\agent
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You can run it three ways:
|
|
68
|
+
|
|
69
|
+
**A. Install from PyPI (public release)** — once published, users can install:
|
|
70
|
+
```powershell
|
|
71
|
+
pip install meterhouse-rotor
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**B. No install (simplest)** — run it as a module from the `agent/` folder:
|
|
75
|
+
```powershell
|
|
76
|
+
python -m meterhouse --help
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**C. Install the `meterhouse` command locally** (so you can run it from anywhere):
|
|
80
|
+
```powershell
|
|
81
|
+
pip install -e .
|
|
82
|
+
meterhouse --help
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Both install options are equivalent for command usage; the rest of this guide uses `python -m meterhouse`.
|
|
86
|
+
|
|
87
|
+
> The package is now public-ready as `meterhouse-rotor`. If the package has already been published on PyPI, use the `pip install meterhouse-rotor` command above.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 3. Use the agent as a Python SDK
|
|
92
|
+
|
|
93
|
+
The package now exposes a simple SDK interface via `meterhouse.Agent`.
|
|
94
|
+
|
|
95
|
+
### Install the package
|
|
96
|
+
|
|
97
|
+
```powershell
|
|
98
|
+
pip install -e .
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Example usage
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from meterhouse import Agent
|
|
105
|
+
|
|
106
|
+
agent = Agent(display_name="PC-01")
|
|
107
|
+
agent.register(
|
|
108
|
+
server_url="http://127.0.0.1:8000",
|
|
109
|
+
api_key="cfk_...",
|
|
110
|
+
display_name="PC-01",
|
|
111
|
+
)
|
|
112
|
+
print(agent.scan())
|
|
113
|
+
print(agent.sync())
|
|
114
|
+
print(agent.health())
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The SDK also supports running the daemon programmatically:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
agent = Agent(display_name="PC-01")
|
|
121
|
+
agent.daemon()
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 4. Run a scan
|
|
127
|
+
|
|
128
|
+
Give this machine a name (once), then scan:
|
|
129
|
+
|
|
130
|
+
```powershell
|
|
131
|
+
python -m meterhouse identity --display-name PC-01
|
|
132
|
+
python -m meterhouse scan
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Example output:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
[NEW] C:\Users\you\.claude\projects\my-proj\<uuid>.jsonl (+266 events)
|
|
139
|
+
...
|
|
140
|
+
scan complete: new=18 updated=0 skipped=0 events+=1787
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The scan is **incremental and idempotent**:
|
|
144
|
+
- Unchanged files are skipped; changed files are read only from where they left off.
|
|
145
|
+
- Running it again processes nothing new (`events+=0`) — it never double-counts.
|
|
146
|
+
|
|
147
|
+
Run `scan` whenever you want fresh data (or schedule it — see §6).
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 4. View your usage
|
|
152
|
+
|
|
153
|
+
```powershell
|
|
154
|
+
python -m meterhouse today # today's tokens by model
|
|
155
|
+
python -m meterhouse week # last 7 days
|
|
156
|
+
python -m meterhouse stats # all-time totals, by model, top projects
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`stats` example:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
Meterhouse - all-time tracked usage
|
|
163
|
+
Sessions: 13
|
|
164
|
+
Input tokens: 6.2M Output tokens: 1.1M Cache read: 280M ...
|
|
165
|
+
By model: claude-opus-4-8 ... claude-sonnet-5 ...
|
|
166
|
+
Top projects: Github/hotel-demo ...
|
|
167
|
+
(estimate only - not official Max/Pro quota)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 5. Where your data lives
|
|
173
|
+
|
|
174
|
+
| What | Default location | Override with |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| Usage database | `~/.claude/meterhouse/usage.db` | `METERHOUSE_DB` |
|
|
177
|
+
| Machine identity/config | `~/.claude/meterhouse/agent.json` | `METERHOUSE_CONFIG` |
|
|
178
|
+
| Transcripts it reads (read-only) | `~/.claude/projects/**/*.jsonl` | auto-discovered |
|
|
179
|
+
|
|
180
|
+
Example with a custom DB path (PowerShell):
|
|
181
|
+
```powershell
|
|
182
|
+
$env:METERHOUSE_DB = "D:\data\usage.db"
|
|
183
|
+
python -m meterhouse scan
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The agent **never modifies Claude Code's files** and never stores prompts,
|
|
187
|
+
responses, or source code — only token counts and metadata.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 6. Automatic scanning (Windows Task Scheduler)
|
|
192
|
+
|
|
193
|
+
Run a scan every 15 minutes without thinking about it (one line):
|
|
194
|
+
|
|
195
|
+
```powershell
|
|
196
|
+
schtasks /Create /SC MINUTE /MO 15 /TN "Meterhouse Scan" /TR "python -m meterhouse scan --quiet" /ST 00:00
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
If you installed via the "Connect PC" flow (§7) or `deploy/install.ps1`, the
|
|
200
|
+
task is instead named **"Meterhouse Scan+Sync"** and also pushes data to the
|
|
201
|
+
central server every 15 minutes.
|
|
202
|
+
|
|
203
|
+
### Stop the agent from scanning
|
|
204
|
+
|
|
205
|
+
Remove whichever scheduled task applies to how you installed:
|
|
206
|
+
|
|
207
|
+
```powershell
|
|
208
|
+
schtasks /Delete /TN "Meterhouse Scan" /F # local scan-only task
|
|
209
|
+
schtasks /Delete /TN "Meterhouse Scan+Sync" /F # Connect PC / install.ps1 task
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
To pause it instead of deleting it (keeps run history, easy to re-enable):
|
|
213
|
+
|
|
214
|
+
```powershell
|
|
215
|
+
schtasks /Change /TN "Meterhouse Scan+Sync" /DISABLE
|
|
216
|
+
schtasks /Change /TN "Meterhouse Scan+Sync" /ENABLE # resume later
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
> `schtasks /TR` does not go through `cmd.exe`, so a raw `scan && sync`
|
|
220
|
+
> command line will not chain correctly — it's run via a small
|
|
221
|
+
> `meterhouse-scan-sync.cmd` wrapper batch file instead. If you set the task
|
|
222
|
+
> up by hand, point `/TR` at a `.cmd` wrapper rather than an inline `&&`.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 7. Send data to the central server (optional)
|
|
227
|
+
|
|
228
|
+
Local scanning is enough for one machine. To feed a central dashboard:
|
|
229
|
+
|
|
230
|
+
1. Sign in to the web app and open **Connect PC** (or ask your admin for an API key).
|
|
231
|
+
2. The web app generates a one-line setup command for the machine you want to track.
|
|
232
|
+
This is the command you run in PowerShell on that PC.
|
|
233
|
+
3. The command installs the agent, registers the machine with the server, scans local
|
|
234
|
+
Claude Code transcripts, and syncs the results back to the dashboard.
|
|
235
|
+
|
|
236
|
+
If you only have the website link, that is enough. The site does not scan your PC
|
|
237
|
+
from the browser; it only generates the install/connect command and gives you the
|
|
238
|
+
server URL and API key to use.
|
|
239
|
+
|
|
240
|
+
```powershell
|
|
241
|
+
pip install meterhouse-rotor
|
|
242
|
+
meterhouse register --server https://YOUR-API-URL --api-key cfk_... --display-name PC-01
|
|
243
|
+
meterhouse scan
|
|
244
|
+
meterhouse sync
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Or use the Windows installer from the repo (`deploy/install.ps1`), which does
|
|
248
|
+
all four steps above and sets up the recurring scan+sync task automatically.
|
|
249
|
+
|
|
250
|
+
> `--server` must be the **API** server (e.g. `http://localhost:8000` in dev),
|
|
251
|
+
> not the dashboard's frontend URL (`http://localhost:5173/...`). Pointing it
|
|
252
|
+
> at the frontend URL will fail to register.
|
|
253
|
+
|
|
254
|
+
The agent connects by calling the server at the given API URL and authenticating
|
|
255
|
+
with the supplied API key. After registration, it reads local transcript files,
|
|
256
|
+
aggregates token events, and sends only usage metadata to the dashboard.
|
|
257
|
+
|
|
258
|
+
An administrator can also create an **API key** under Admin → Agent API keys, then share the key and API URL with each user.
|
|
259
|
+
|
|
260
|
+
`sync` only sends events the server hasn't seen; if the server is down it simply
|
|
261
|
+
retries next time (nothing is lost, nothing is double-counted). The dashboard
|
|
262
|
+
shows a system as **"Never synced"** until the first successful `sync` call —
|
|
263
|
+
registering alone, or running `scan` without `sync`, is not enough.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 8. Troubleshooting
|
|
268
|
+
|
|
269
|
+
| Symptom | Fix |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `scan complete: new=0 ... events+=0` on first run | No transcripts found. Confirm `%USERPROFILE%\.claude\projects\` exists and you've used Claude Code. |
|
|
272
|
+
| `python` not found | Install Python 3.10+ and ensure it's on `PATH` (`py -3` also works on Windows). |
|
|
273
|
+
| Want a clean re-scan | Delete the usage DB (`%USERPROFILE%\.claude\meterhouse\usage.db`) and run `scan` again. |
|
|
274
|
+
| `sync` says "Central mode not configured" | Run `register` first with `--server` and `--api-key`. |
|
|
275
|
+
| `sync` fails / offline | Expected when the server is unreachable; it retries on the next run. |
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Command reference
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
python -m meterhouse scan [--display-name NAME] [--quiet]
|
|
283
|
+
python -m meterhouse today | week | stats
|
|
284
|
+
python -m meterhouse identity [--display-name NAME] [--set-display-name NAME]
|
|
285
|
+
python -m meterhouse register --server URL --api-key KEY [--display-name NAME]
|
|
286
|
+
python -m meterhouse sync [--quiet]
|
|
287
|
+
python -m meterhouse heartbeat
|
|
288
|
+
python -m meterhouse account [show | enable | disable]
|
|
289
|
+
python -m meterhouse --version
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Claude account reporting (optional, off by default)
|
|
293
|
+
|
|
294
|
+
`account` controls whether this machine also reports which Claude
|
|
295
|
+
subscription it is signed into, so an admin can see who is on which plan and
|
|
296
|
+
how much of its rate limit is used.
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
python -m meterhouse account show # print the exact payload - sends nothing
|
|
300
|
+
python -m meterhouse account enable
|
|
301
|
+
python -m meterhouse account disable
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Enabled, the agent reads a fixed allowlist of fields from `~/.claude.json`:
|
|
305
|
+
account UUID, email, display name, organisation, plan tier, and the cached
|
|
306
|
+
rate-limit percentages. **OAuth tokens and credentials are never read**, and
|
|
307
|
+
`.credentials.json` is never opened. See `meterhouse/account.py` for the
|
|
308
|
+
allowlist and `tests/test_account.py` for the tests that enforce it.
|
|
309
|
+
|
|
310
|
+
Equivalent env var: `METERHOUSE_ACCOUNT_REPORTING=true`.
|
|
311
|
+
|
|
312
|
+
Run the test suite with `pip install pytest && python -m pytest`.
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Rotor — Install & Scan Guide
|
|
2
|
+
|
|
3
|
+
**Rotor** is the Meterhouse metering agent: the part that sits on each machine
|
|
4
|
+
and turns as work happens. It scans Claude Code's local transcript files, stores
|
|
5
|
+
usage in a local SQLite database, and (optionally) syncs it to the central
|
|
6
|
+
server. **Scanning works fully offline — no server required.**
|
|
7
|
+
|
|
8
|
+
Installed as `meterhouse-rotor`; the command it provides is `meterhouse`.
|
|
9
|
+
|
|
10
|
+
> **Tracked activity ≠ official quota.** All numbers are token counts parsed from
|
|
11
|
+
> local transcripts — an estimate, not your Claude Max/Pro billing or quota.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Prerequisites
|
|
16
|
+
|
|
17
|
+
- **Python 3.10+** (check with `python --version`)
|
|
18
|
+
- **Claude Code** installed and used at least once on this machine, so transcripts
|
|
19
|
+
exist under `~/.claude/projects/` (Windows: `%USERPROFILE%\.claude\projects\`).
|
|
20
|
+
|
|
21
|
+
The agent uses **only the Python standard library** — there is nothing to
|
|
22
|
+
`pip install` for scanning.
|
|
23
|
+
|
|
24
|
+
> For **central mode** (sending usage to the dashboard) you install the
|
|
25
|
+
> `meterhouse` command from the repo, which needs **git** on `PATH` — see §7.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Install
|
|
30
|
+
|
|
31
|
+
Clone the repository and move into the agent folder:
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
git clone <your-repo-url> meterhouse
|
|
35
|
+
cd meterhouse\agent
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
You can run it three ways:
|
|
39
|
+
|
|
40
|
+
**A. Install from PyPI (public release)** — once published, users can install:
|
|
41
|
+
```powershell
|
|
42
|
+
pip install meterhouse-rotor
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**B. No install (simplest)** — run it as a module from the `agent/` folder:
|
|
46
|
+
```powershell
|
|
47
|
+
python -m meterhouse --help
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**C. Install the `meterhouse` command locally** (so you can run it from anywhere):
|
|
51
|
+
```powershell
|
|
52
|
+
pip install -e .
|
|
53
|
+
meterhouse --help
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Both install options are equivalent for command usage; the rest of this guide uses `python -m meterhouse`.
|
|
57
|
+
|
|
58
|
+
> The package is now public-ready as `meterhouse-rotor`. If the package has already been published on PyPI, use the `pip install meterhouse-rotor` command above.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 3. Use the agent as a Python SDK
|
|
63
|
+
|
|
64
|
+
The package now exposes a simple SDK interface via `meterhouse.Agent`.
|
|
65
|
+
|
|
66
|
+
### Install the package
|
|
67
|
+
|
|
68
|
+
```powershell
|
|
69
|
+
pip install -e .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Example usage
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from meterhouse import Agent
|
|
76
|
+
|
|
77
|
+
agent = Agent(display_name="PC-01")
|
|
78
|
+
agent.register(
|
|
79
|
+
server_url="http://127.0.0.1:8000",
|
|
80
|
+
api_key="cfk_...",
|
|
81
|
+
display_name="PC-01",
|
|
82
|
+
)
|
|
83
|
+
print(agent.scan())
|
|
84
|
+
print(agent.sync())
|
|
85
|
+
print(agent.health())
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The SDK also supports running the daemon programmatically:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
agent = Agent(display_name="PC-01")
|
|
92
|
+
agent.daemon()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 4. Run a scan
|
|
98
|
+
|
|
99
|
+
Give this machine a name (once), then scan:
|
|
100
|
+
|
|
101
|
+
```powershell
|
|
102
|
+
python -m meterhouse identity --display-name PC-01
|
|
103
|
+
python -m meterhouse scan
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Example output:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
[NEW] C:\Users\you\.claude\projects\my-proj\<uuid>.jsonl (+266 events)
|
|
110
|
+
...
|
|
111
|
+
scan complete: new=18 updated=0 skipped=0 events+=1787
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The scan is **incremental and idempotent**:
|
|
115
|
+
- Unchanged files are skipped; changed files are read only from where they left off.
|
|
116
|
+
- Running it again processes nothing new (`events+=0`) — it never double-counts.
|
|
117
|
+
|
|
118
|
+
Run `scan` whenever you want fresh data (or schedule it — see §6).
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 4. View your usage
|
|
123
|
+
|
|
124
|
+
```powershell
|
|
125
|
+
python -m meterhouse today # today's tokens by model
|
|
126
|
+
python -m meterhouse week # last 7 days
|
|
127
|
+
python -m meterhouse stats # all-time totals, by model, top projects
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`stats` example:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
Meterhouse - all-time tracked usage
|
|
134
|
+
Sessions: 13
|
|
135
|
+
Input tokens: 6.2M Output tokens: 1.1M Cache read: 280M ...
|
|
136
|
+
By model: claude-opus-4-8 ... claude-sonnet-5 ...
|
|
137
|
+
Top projects: Github/hotel-demo ...
|
|
138
|
+
(estimate only - not official Max/Pro quota)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 5. Where your data lives
|
|
144
|
+
|
|
145
|
+
| What | Default location | Override with |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| Usage database | `~/.claude/meterhouse/usage.db` | `METERHOUSE_DB` |
|
|
148
|
+
| Machine identity/config | `~/.claude/meterhouse/agent.json` | `METERHOUSE_CONFIG` |
|
|
149
|
+
| Transcripts it reads (read-only) | `~/.claude/projects/**/*.jsonl` | auto-discovered |
|
|
150
|
+
|
|
151
|
+
Example with a custom DB path (PowerShell):
|
|
152
|
+
```powershell
|
|
153
|
+
$env:METERHOUSE_DB = "D:\data\usage.db"
|
|
154
|
+
python -m meterhouse scan
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The agent **never modifies Claude Code's files** and never stores prompts,
|
|
158
|
+
responses, or source code — only token counts and metadata.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 6. Automatic scanning (Windows Task Scheduler)
|
|
163
|
+
|
|
164
|
+
Run a scan every 15 minutes without thinking about it (one line):
|
|
165
|
+
|
|
166
|
+
```powershell
|
|
167
|
+
schtasks /Create /SC MINUTE /MO 15 /TN "Meterhouse Scan" /TR "python -m meterhouse scan --quiet" /ST 00:00
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
If you installed via the "Connect PC" flow (§7) or `deploy/install.ps1`, the
|
|
171
|
+
task is instead named **"Meterhouse Scan+Sync"** and also pushes data to the
|
|
172
|
+
central server every 15 minutes.
|
|
173
|
+
|
|
174
|
+
### Stop the agent from scanning
|
|
175
|
+
|
|
176
|
+
Remove whichever scheduled task applies to how you installed:
|
|
177
|
+
|
|
178
|
+
```powershell
|
|
179
|
+
schtasks /Delete /TN "Meterhouse Scan" /F # local scan-only task
|
|
180
|
+
schtasks /Delete /TN "Meterhouse Scan+Sync" /F # Connect PC / install.ps1 task
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
To pause it instead of deleting it (keeps run history, easy to re-enable):
|
|
184
|
+
|
|
185
|
+
```powershell
|
|
186
|
+
schtasks /Change /TN "Meterhouse Scan+Sync" /DISABLE
|
|
187
|
+
schtasks /Change /TN "Meterhouse Scan+Sync" /ENABLE # resume later
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
> `schtasks /TR` does not go through `cmd.exe`, so a raw `scan && sync`
|
|
191
|
+
> command line will not chain correctly — it's run via a small
|
|
192
|
+
> `meterhouse-scan-sync.cmd` wrapper batch file instead. If you set the task
|
|
193
|
+
> up by hand, point `/TR` at a `.cmd` wrapper rather than an inline `&&`.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 7. Send data to the central server (optional)
|
|
198
|
+
|
|
199
|
+
Local scanning is enough for one machine. To feed a central dashboard:
|
|
200
|
+
|
|
201
|
+
1. Sign in to the web app and open **Connect PC** (or ask your admin for an API key).
|
|
202
|
+
2. The web app generates a one-line setup command for the machine you want to track.
|
|
203
|
+
This is the command you run in PowerShell on that PC.
|
|
204
|
+
3. The command installs the agent, registers the machine with the server, scans local
|
|
205
|
+
Claude Code transcripts, and syncs the results back to the dashboard.
|
|
206
|
+
|
|
207
|
+
If you only have the website link, that is enough. The site does not scan your PC
|
|
208
|
+
from the browser; it only generates the install/connect command and gives you the
|
|
209
|
+
server URL and API key to use.
|
|
210
|
+
|
|
211
|
+
```powershell
|
|
212
|
+
pip install meterhouse-rotor
|
|
213
|
+
meterhouse register --server https://YOUR-API-URL --api-key cfk_... --display-name PC-01
|
|
214
|
+
meterhouse scan
|
|
215
|
+
meterhouse sync
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Or use the Windows installer from the repo (`deploy/install.ps1`), which does
|
|
219
|
+
all four steps above and sets up the recurring scan+sync task automatically.
|
|
220
|
+
|
|
221
|
+
> `--server` must be the **API** server (e.g. `http://localhost:8000` in dev),
|
|
222
|
+
> not the dashboard's frontend URL (`http://localhost:5173/...`). Pointing it
|
|
223
|
+
> at the frontend URL will fail to register.
|
|
224
|
+
|
|
225
|
+
The agent connects by calling the server at the given API URL and authenticating
|
|
226
|
+
with the supplied API key. After registration, it reads local transcript files,
|
|
227
|
+
aggregates token events, and sends only usage metadata to the dashboard.
|
|
228
|
+
|
|
229
|
+
An administrator can also create an **API key** under Admin → Agent API keys, then share the key and API URL with each user.
|
|
230
|
+
|
|
231
|
+
`sync` only sends events the server hasn't seen; if the server is down it simply
|
|
232
|
+
retries next time (nothing is lost, nothing is double-counted). The dashboard
|
|
233
|
+
shows a system as **"Never synced"** until the first successful `sync` call —
|
|
234
|
+
registering alone, or running `scan` without `sync`, is not enough.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 8. Troubleshooting
|
|
239
|
+
|
|
240
|
+
| Symptom | Fix |
|
|
241
|
+
|---|---|
|
|
242
|
+
| `scan complete: new=0 ... events+=0` on first run | No transcripts found. Confirm `%USERPROFILE%\.claude\projects\` exists and you've used Claude Code. |
|
|
243
|
+
| `python` not found | Install Python 3.10+ and ensure it's on `PATH` (`py -3` also works on Windows). |
|
|
244
|
+
| Want a clean re-scan | Delete the usage DB (`%USERPROFILE%\.claude\meterhouse\usage.db`) and run `scan` again. |
|
|
245
|
+
| `sync` says "Central mode not configured" | Run `register` first with `--server` and `--api-key`. |
|
|
246
|
+
| `sync` fails / offline | Expected when the server is unreachable; it retries on the next run. |
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Command reference
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
python -m meterhouse scan [--display-name NAME] [--quiet]
|
|
254
|
+
python -m meterhouse today | week | stats
|
|
255
|
+
python -m meterhouse identity [--display-name NAME] [--set-display-name NAME]
|
|
256
|
+
python -m meterhouse register --server URL --api-key KEY [--display-name NAME]
|
|
257
|
+
python -m meterhouse sync [--quiet]
|
|
258
|
+
python -m meterhouse heartbeat
|
|
259
|
+
python -m meterhouse account [show | enable | disable]
|
|
260
|
+
python -m meterhouse --version
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Claude account reporting (optional, off by default)
|
|
264
|
+
|
|
265
|
+
`account` controls whether this machine also reports which Claude
|
|
266
|
+
subscription it is signed into, so an admin can see who is on which plan and
|
|
267
|
+
how much of its rate limit is used.
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
python -m meterhouse account show # print the exact payload - sends nothing
|
|
271
|
+
python -m meterhouse account enable
|
|
272
|
+
python -m meterhouse account disable
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Enabled, the agent reads a fixed allowlist of fields from `~/.claude.json`:
|
|
276
|
+
account UUID, email, display name, organisation, plan tier, and the cached
|
|
277
|
+
rate-limit percentages. **OAuth tokens and credentials are never read**, and
|
|
278
|
+
`.credentials.json` is never opened. See `meterhouse/account.py` for the
|
|
279
|
+
allowlist and `tests/test_account.py` for the tests that enforce it.
|
|
280
|
+
|
|
281
|
+
Equivalent env var: `METERHOUSE_ACCOUNT_REPORTING=true`.
|
|
282
|
+
|
|
283
|
+
Run the test suite with `pip install pytest && python -m pytest`.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Meterhouse local agent.
|
|
2
|
+
|
|
3
|
+
Original software (not derived from any third-party project). Scans Claude Code
|
|
4
|
+
JSONL transcripts on the local machine, stores a centralization-ready usage
|
|
5
|
+
record in local SQLite, and (later) syncs it to the central API.
|
|
6
|
+
|
|
7
|
+
The JSONL layout it reads is Claude Code's own on-disk data format, independently
|
|
8
|
+
observed from real transcripts (see docs/UPSTREAM_AUDIT.md for the format).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.2"
|
|
14
|
+
|
|
15
|
+
from .cli import build_parser, main as cli_main
|
|
16
|
+
from .config import AgentConfig
|
|
17
|
+
from .daemon import run_daemon
|
|
18
|
+
from .health import HealthState
|
|
19
|
+
from .identity import (
|
|
20
|
+
Identity,
|
|
21
|
+
default_config_path,
|
|
22
|
+
load_identity,
|
|
23
|
+
save_identity,
|
|
24
|
+
)
|
|
25
|
+
from .scanner import discover_files, scan as scan_files
|
|
26
|
+
from .store import Store, default_db_path
|
|
27
|
+
from .sync import SyncClient, sync_store
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Agent",
|
|
31
|
+
"cli_main",
|
|
32
|
+
"build_parser",
|
|
33
|
+
"AgentConfig",
|
|
34
|
+
"HealthState",
|
|
35
|
+
"Identity",
|
|
36
|
+
"load_identity",
|
|
37
|
+
"save_identity",
|
|
38
|
+
"default_config_path",
|
|
39
|
+
"discover_files",
|
|
40
|
+
"scan_files",
|
|
41
|
+
"Store",
|
|
42
|
+
"default_db_path",
|
|
43
|
+
"SyncClient",
|
|
44
|
+
"sync_store",
|
|
45
|
+
"run_daemon",
|
|
46
|
+
"__version__",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Agent:
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
config_path: str | None = None,
|
|
54
|
+
db_path: str | None = None,
|
|
55
|
+
display_name: str | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
self.config_path = config_path
|
|
58
|
+
self.db_path = db_path or str(default_db_path())
|
|
59
|
+
self.identity = load_identity(config_path=config_path, display_name=display_name)
|
|
60
|
+
self.config = AgentConfig.load()
|
|
61
|
+
|
|
62
|
+
def register(
|
|
63
|
+
self,
|
|
64
|
+
server_url: str,
|
|
65
|
+
api_key: str,
|
|
66
|
+
display_name: str | None = None,
|
|
67
|
+
ws_url: str | None = None,
|
|
68
|
+
) -> dict:
|
|
69
|
+
self.identity.server_url = server_url.rstrip("/")
|
|
70
|
+
self.identity.api_key = api_key
|
|
71
|
+
if display_name:
|
|
72
|
+
self.identity.display_name = display_name
|
|
73
|
+
save_identity(self.identity, self.config_path)
|
|
74
|
+
|
|
75
|
+
if ws_url:
|
|
76
|
+
self.config.ws_enabled = True
|
|
77
|
+
self.config.ws_url = ws_url
|
|
78
|
+
self.config.save()
|
|
79
|
+
|
|
80
|
+
client = SyncClient(self.identity.server_url, self.identity.api_key)
|
|
81
|
+
return client.register(
|
|
82
|
+
self.identity.display_name,
|
|
83
|
+
self.identity.hostname,
|
|
84
|
+
self.identity.agent_version,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def scan(self, verbose: bool = False, project_dirs: list[str] | None = None) -> dict:
|
|
88
|
+
return scan_files(
|
|
89
|
+
system_id=self.identity.system_id,
|
|
90
|
+
db_path=self.db_path,
|
|
91
|
+
project_dirs=project_dirs,
|
|
92
|
+
verbose=verbose,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def sync(self, verbose: bool = False) -> dict:
|
|
96
|
+
if not self.identity.server_url or not self.identity.api_key:
|
|
97
|
+
raise RuntimeError(
|
|
98
|
+
"Central mode not configured. Call register() with server_url and api_key first."
|
|
99
|
+
)
|
|
100
|
+
store = Store(self.db_path)
|
|
101
|
+
try:
|
|
102
|
+
client = SyncClient(self.identity.server_url, self.identity.api_key)
|
|
103
|
+
return sync_store(store, client, verbose=verbose)
|
|
104
|
+
finally:
|
|
105
|
+
store.close()
|
|
106
|
+
|
|
107
|
+
def health(self) -> HealthState | None:
|
|
108
|
+
return HealthState.load()
|
|
109
|
+
|
|
110
|
+
def daemon(self, display_name: str | None = None) -> None:
|
|
111
|
+
run_daemon(display_name=display_name, db_path=self.db_path)
|