@klars/agentobs 0.2.0 → 0.2.2

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 CHANGED
@@ -36,16 +36,31 @@ account, no cloud, no telemetry.
36
36
  ```bash
37
37
  npm install -g @klars/agentobs
38
38
  agentobs init
39
+ agentobs import
39
40
  ```
40
41
 
41
- `init` prints a hook configuration block. Paste it into `~/.claude/settings.json`
42
- (or a project's `.claude/settings.json`), then:
42
+ `import` reads Claude Code's own session transcripts from `~/.claude/projects/`
43
+ and backfills everything you have already done — no configuration, no hooks.
44
+ Then:
43
45
 
44
46
  ```bash
45
47
  agentobs dashboard
46
48
  ```
47
49
 
48
- Run Claude Code as usual. Tool calls appear in the dashboard within seconds.
50
+ That is the fastest path to real data, and the one to try first.
51
+
52
+ ### Live capture (optional)
53
+
54
+ `import` is after-the-fact. To record calls **as they happen** — and to let
55
+ guardrails actually block them — add the hook configuration that
56
+ `agentobs init` prints to `~/.claude/settings.json`, then restart Claude Code.
57
+
58
+ > **If hooks record nothing:** this has been observed on at least one Windows
59
+ > install, where Claude Code did not invoke the configured command at all — a
60
+ > plain two-line `.cmd` file also never fired, so it is not specific to
61
+ > AgentObs. Check by running any tool and then `agentobs stats --today`. If it
62
+ > stays at zero, keep using `agentobs import`, which needs no hooks; only
63
+ > guardrail *blocking* depends on them.
49
64
 
50
65
  ---
51
66
 
@@ -85,6 +100,7 @@ Everything lives in `~/.agentobs/`. Uninstalling is `rm -rf ~/.agentobs`.
85
100
 
86
101
  ```
87
102
  agentobs init Set up ~/.agentobs and print the hook config
103
+ agentobs import [--days n] [--all] Import Claude Code transcripts (no hooks needed)
88
104
  agentobs dashboard [--port] [--host] Serve the dashboard (default 127.0.0.1:4300)
89
105
  agentobs stats [--today] [--since] Print totals in the terminal
90
106
  agentobs run -- <command...> Observe any command (coarse detail)
@@ -148,22 +164,37 @@ Two deliberate behaviours worth knowing:
148
164
 
149
165
  ## Agent support
150
166
 
151
- | Agent | How | Detail |
152
- | --------------- | -------------------------- | ------------------------------------------------- |
153
- | **Claude Code** | Native hooks | **Rich** — every tool call, plus policy enforcement |
154
- | Any CLI agent | `agentobs run -- <cmd>` | **Coarse** duration and exit code only |
155
- | Custom / in-house | `agentobs watch <file>` | **Rich**, if it writes JSONL |
167
+ | Agent | How | Detail | Needs setup? |
168
+ | --- | --- | --- | --- |
169
+ | **Claude Code** | `agentobs import` | **Rich** — every tool call, tokens, cost | **No** |
170
+ | **Claude Code** | Native hooks | **Rich**, live, and can *block* calls | Yes — hook config |
171
+ | Any CLI agent | `agentobs run -- <cmd>` | **Coarse** duration and exit code only | No |
172
+ | Custom / in-house | `agentobs watch <file>` | **Rich**, if it writes JSONL | No |
173
+
174
+ `import` and hooks read the same underlying data. The difference is timing:
175
+ hooks see a call *before* it runs, which is what makes blocking possible;
176
+ `import` reads the transcript afterwards. If you only want observability,
177
+ `import` is enough and needs no configuration.
156
178
 
157
179
  The dashboard labels coarse sessions as `coarse` rather than implying detail it
158
- does not have.
180
+ does not have, and `agentobs stats` explains why a coarse-only range shows zero
181
+ tool calls.
159
182
 
160
183
  ### A note on cost accuracy
161
184
 
162
- Claude Code's `PostToolUse` hook payload carries **no token or cost fields**.
163
- AgentObs therefore reads token usage from the session transcript at
164
- `SessionEnd`, which makes **session-level cost accurate** but leaves
165
- **per-tool-call cost blank** for hook-sourced data. It does not divide a total
166
- across calls to manufacture a number.
185
+ Claude Code's `PostToolUse` hook payload carries **no token or cost fields**,
186
+ so token usage comes from the session transcript at `SessionEnd` for hooks,
187
+ or directly via `agentobs import`. That makes **session-level cost accurate**
188
+ while leaving **per-tool-call cost blank**: usage is reported per assistant
189
+ message, not per tool call, and dividing a total across calls would be a
190
+ manufactured number.
191
+
192
+ **Cache tokens dominate a long session.** A cached conversation replays its
193
+ whole context on every turn, so `cache_read` can reach hundreds of millions of
194
+ tokens in a single session. AgentObs tracks cache reads (billed at 0.1x input)
195
+ and cache writes (1.25x) separately from fresh tokens, and `agentobs import`
196
+ prints the four lines separately — a single unexplained total looks like a bug
197
+ when the cache line legitimately dwarfs everything else.
167
198
 
168
199
  Model prices live in `~/.agentobs/pricing.json` and are yours to edit. A model
169
200
  missing from that file shows cost as `—`, never `$0.00`.
@@ -135,14 +135,19 @@ export async function importTranscript(db, file) {
135
135
  const message = (row.message ?? {});
136
136
  const usage = message.usage;
137
137
  if (usage && typeof usage === 'object') {
138
- // Only genuinely fresh input tokens go in tokensIn. Cache writes and
139
- // reads are tracked separately because they bill at different rates,
140
- // and because a cache read replays the whole context every turn - the
141
- // headline token count must not include the same tokens hundreds of
142
- // times.
143
- result.tokensIn += usage.input_tokens ?? 0;
138
+ // Fresh input = uncached input + cache writes. A cache write is real
139
+ // new content being sent for the first time (just stored for reuse), so
140
+ // excluding it made "tokens in" absurd: 24K in against 4.6M out, when
141
+ // real agent usage is heavily input-weighted. It is still tracked
142
+ // separately for costing, since it bills at 1.25x.
143
+ //
144
+ // Cache *reads* stay out of this total: they replay the entire context
145
+ // on every turn, so counting them would report the same tokens hundreds
146
+ // of times (2.4 billion across three sessions).
147
+ const cacheWrite = usage.cache_creation_input_tokens ?? 0;
148
+ result.tokensIn += (usage.input_tokens ?? 0) + cacheWrite;
144
149
  result.tokensOut += usage.output_tokens ?? 0;
145
- result.cacheWriteTokens += usage.cache_creation_input_tokens ?? 0;
150
+ result.cacheWriteTokens += cacheWrite;
146
151
  // cache_read is the whole conversation context replayed on every single
147
152
  // message, so it re-counts the same tokens on each turn - summing it
148
153
  // reported 413 million tokens for one session. It is tracked separately
@@ -186,6 +191,8 @@ export async function importTranscript(db, file) {
186
191
  }
187
192
  if (!sessionStarted)
188
193
  return result;
194
+ // tokensIn already contains the cache-write tokens, so they are costed once
195
+ // at the base rate here and only topped up by the extra 0.25x premium.
189
196
  const baseCost = computeCost(result.model, result.tokensIn, result.tokensOut);
190
197
  const readCost = computeCost(result.model, result.cacheReadTokens, 0);
191
198
  const writeCost = computeCost(result.model, result.cacheWriteTokens, 0);
@@ -194,7 +201,7 @@ export async function importTranscript(db, file) {
194
201
  ? null
195
202
  : baseCost +
196
203
  (readCost ?? 0) * CACHE_READ_RATE +
197
- (writeCost ?? 0) * CACHE_WRITE_RATE;
204
+ (writeCost ?? 0) * (CACHE_WRITE_RATE - 1);
198
205
  // Session totals come from the transcript's own usage blocks, which are
199
206
  // authoritative - the per-call rows have no tokens to sum.
200
207
  db.prepare(`UPDATE sessions
@@ -191,10 +191,10 @@ function renderTools(rows) {
191
191
  body.replaceChildren();
192
192
  if (rows.length === 0) {
193
193
  const tr = document.createElement('tr');
194
- const msg =
195
- state.summary && state.summary.coarse_sessions > 0
196
- ? 'Coarse sessions record no tool calls — connect the Claude Code hook for per-tool detail.'
197
- : 'No tool calls recorded yet.';
194
+ const msg =
195
+ state.summary && state.summary.coarse_sessions > 0
196
+ ? 'Coarse sessions record no tool calls — connect the Claude Code hook for per-tool detail.'
197
+ : 'No tool calls recorded yet.';
198
198
  tr.append(Object.assign(cell(msg, 'empty'), { colSpan: 5 }));
199
199
  body.append(tr);
200
200
  return;
@@ -630,6 +630,16 @@ function renderDelta(el, current, previous, { goodWhenUp = true } = {}) {
630
630
  return;
631
631
  }
632
632
  const change = ((current - previous) / previous) * 100;
633
+ // A tiny previous period produces a huge, meaningless percentage - "+2300%"
634
+ // says nothing except that yesterday was nearly empty. Cap the display so
635
+ // the chip stays informative rather than theatrical.
636
+ if (Math.abs(change) > 999) {
637
+ el.className = `delta ${change > 0 === goodWhenUp ? 'delta-good' : 'delta-bad'}`;
638
+ el.textContent = `${change > 0 ? '▲' : '▼'} vs ${previous}`;
639
+ el.title = `Previous period had only ${previous}; a percentage would be misleading`;
640
+ el.removeAttribute('hidden');
641
+ return;
642
+ }
633
643
  if (!Number.isFinite(change) || Math.abs(change) < 0.5) {
634
644
  // Below half a percent is noise; a chip there implies a signal that
635
645
  // isn't real.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klars/agentobs",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Observability and control layer for AI coding agents - see every tool call, token, and dollar your agents spend, and stop them before they do something risky.",
5
5
  "license": "MIT",
6
6
  "author": "Klars AI",