claude-session-logger 0.1.0__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.
Files changed (29) hide show
  1. claude_session_logger-0.1.0/LICENSE +21 -0
  2. claude_session_logger-0.1.0/PKG-INFO +458 -0
  3. claude_session_logger-0.1.0/README.md +437 -0
  4. claude_session_logger-0.1.0/pyproject.toml +40 -0
  5. claude_session_logger-0.1.0/setup.cfg +4 -0
  6. claude_session_logger-0.1.0/src/claude_session_logger/__init__.py +3 -0
  7. claude_session_logger-0.1.0/src/claude_session_logger/_setup.py +176 -0
  8. claude_session_logger-0.1.0/src/claude_session_logger/classify.py +73 -0
  9. claude_session_logger-0.1.0/src/claude_session_logger/cli.py +336 -0
  10. claude_session_logger-0.1.0/src/claude_session_logger/cost.py +28 -0
  11. claude_session_logger-0.1.0/src/claude_session_logger/db.py +333 -0
  12. claude_session_logger-0.1.0/src/claude_session_logger/identity.py +23 -0
  13. claude_session_logger-0.1.0/src/claude_session_logger/memory.py +296 -0
  14. claude_session_logger-0.1.0/src/claude_session_logger/prices.json +5 -0
  15. claude_session_logger-0.1.0/src/claude_session_logger/resolve.py +21 -0
  16. claude_session_logger-0.1.0/src/claude_session_logger/transcript.py +89 -0
  17. claude_session_logger-0.1.0/src/claude_session_logger.egg-info/PKG-INFO +458 -0
  18. claude_session_logger-0.1.0/src/claude_session_logger.egg-info/SOURCES.txt +27 -0
  19. claude_session_logger-0.1.0/src/claude_session_logger.egg-info/dependency_links.txt +1 -0
  20. claude_session_logger-0.1.0/src/claude_session_logger.egg-info/entry_points.txt +4 -0
  21. claude_session_logger-0.1.0/src/claude_session_logger.egg-info/requires.txt +5 -0
  22. claude_session_logger-0.1.0/src/claude_session_logger.egg-info/top_level.txt +1 -0
  23. claude_session_logger-0.1.0/tests/test_classify.py +78 -0
  24. claude_session_logger-0.1.0/tests/test_cli.py +338 -0
  25. claude_session_logger-0.1.0/tests/test_cost.py +28 -0
  26. claude_session_logger-0.1.0/tests/test_db.py +416 -0
  27. claude_session_logger-0.1.0/tests/test_resolve.py +41 -0
  28. claude_session_logger-0.1.0/tests/test_setup.py +60 -0
  29. claude_session_logger-0.1.0/tests/test_transcript.py +54 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Keith Fajardo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,458 @@
1
+ Metadata-Version: 2.4
2
+ Name: claude-session-logger
3
+ Version: 0.1.0
4
+ Summary: Log Claude Code sessions and shared issues to DuckDB + MotherDuck
5
+ Author-email: Keith Fajardo <contact@keithfajardo.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/keith-fajardo/claude-session-logger
8
+ Project-URL: Repository, https://github.com/keith-fajardo/claude-session-logger
9
+ Keywords: claude,claude-code,duckdb,motherduck,logging
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: duckdb==1.5.3
17
+ Requires-Dist: pyyaml>=6
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7.0; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # claude-session-logger
23
+
24
+ Captures every Claude Code session into a local DuckDB file and syncs it to a
25
+ cloud DuckDB (MotherDuck), so you can trace sessions over time — cost, tokens,
26
+ duration, project, session type — plus on-demand structured log entries
27
+ (findings, tasks, decisions, issues, notes) you ask Claude to record mid-session.
28
+
29
+ Local DuckDB is the source of truth. The cloud copy is a synced mirror. Sessions
30
+ are logged automatically at session end; log entries are written on demand.
31
+
32
+ ## How it works
33
+
34
+ ```
35
+ Claude Code session ends
36
+
37
+
38
+ SessionEnd hook (~/.claude/settings.json)
39
+ │ passes session_id + transcript_path + cwd on stdin (JSON)
40
+
41
+ log_session.py record --no-sync
42
+ │ 1. parse transcript JSONL → aggregate tokens by model, derive metadata
43
+ │ 2. compute cost from prices.json (incl. cache-write/read rates)
44
+ │ 3. UPSERT one row into LOCAL sessions table (synced = FALSE)
45
+
46
+ launchctl kickstart …flush (fires asynchronously, off the hot path)
47
+
48
+ LaunchAgent → log_session.py flush
49
+ │ push all synced = FALSE rows → MotherDuck, mark synced = TRUE
50
+
51
+ sessions.duckdb ──sync──▶ md:claude_sessions (cloud)
52
+ ```
53
+
54
+ Session end writes locally and returns fast (`--no-sync`); the cloud push runs
55
+ out-of-band via a launchd LaunchAgent. This keeps session exit instant and avoids
56
+ the script blocking on a slow/offline network. On the next `SessionStart`, the
57
+ flush agent is kicked again to drain anything still queued, and a banner of recent
58
+ sessions is printed.
59
+
60
+ ### Concurrency
61
+
62
+ DuckDB allows **one** read-write process **or many** read-only processes per file.
63
+ Concurrent Claude sessions plus the flush LaunchAgent contend for the write lock,
64
+ so `db.connect()` retries with exponential backoff on a transient
65
+ `Conflicting lock is held` error. The startup banner (`recent`) connects
66
+ **read-only** so it coexists with a writer and degrades silently if the file is
67
+ missing, locked past retries, or has no tables yet.
68
+
69
+ ## Files
70
+
71
+ Code is a pip package under `src/claude_session_logger/`. Installing exposes three
72
+ console commands: **`claude-session-logger`** (`cli.py`),
73
+ **`claude-session-logger-setup`** (`_setup.py`), and **`claude-memory-sync`**
74
+ (`memory.py`, the separate `.md` memory system).
75
+
76
+ | File (`src/claude_session_logger/`) | Purpose |
77
+ |---|---|
78
+ | `cli.py` | CLI entry point. Subcommands: `init`, `record`, `log`, `resolve`, `list`, `show`, `recent`, `inbox`, `flush`. |
79
+ | `db.py` | DuckDB connect (with lock retry), schema DDL + idempotent migrations, CRUD helpers, token resolution + self-heal, bidirectional `sync()`, inbox/meta helpers. |
80
+ | `transcript.py` | Parse a session transcript `.jsonl` → tokens by model, models, timestamps, skills, message/tool counts, metadata. |
81
+ | `cost.py` | Compute USD cost from per-model token usage and `prices.json`. |
82
+ | `classify.py` | Infer `session_type` from skills used + project/cwd. |
83
+ | `identity.py` | Resolve the current username (`$CLAUDE_LOGGER_USER` → `git config user.name`) for `created_by`/`updated_by`. |
84
+ | `resolve.py` | Resolve the active `session_id` from the newest `.jsonl` in the cwd's project dir. |
85
+ | `memory.py` | `.md` file-memory ↔ MotherDuck (`md:claude_memory`); `push`/`pull`/`sync`/`search`/`get`/`hook`. |
86
+ | `_setup.py` | Post-install wiring: LaunchAgent + idempotent hook merge + token check. |
87
+ | `prices.json` | Editable model → `{input, output, cache_write, cache_read}` per-1M-token rates. |
88
+ | `sessions.duckdb` | Local buffer / source of truth (created at `~/.claude/claude-session-logger/`, gitignored). |
89
+ | `CLAUDE.md` / `DESIGN.md` | Contributor guide (incl. security guardrails) / as-built architecture. |
90
+ | `tests/` | pytest suite for each module + the CLI + setup. |
91
+
92
+ > Legacy note: examples below use `claude-session-logger <cmd>`; the equivalent
93
+ > `python3 log_session.py <cmd>` still works from a source checkout.
94
+
95
+ ## Setup
96
+
97
+ 1. **Install** (pulls in DuckDB, pinned to 1.5.3 — MotherDuck's max):
98
+
99
+ ```bash
100
+ pip install . # or `pip install -e .` for a dev/editable install
101
+ ```
102
+
103
+ This puts three commands on your PATH: `claude-session-logger`,
104
+ `claude-session-logger-setup`, `claude-memory-sync`.
105
+
106
+ **Quick path:** after the token step below, run **`claude-session-logger-setup`**
107
+ — it creates the local schema, writes the flush LaunchAgent, and merges the hooks
108
+ into `~/.claude/settings.json` (idempotent; backs up `settings.json.bak`; migrates
109
+ old script-path hooks). Steps 3–5 below are what it automates, for reference.
110
+
111
+ 2. **MotherDuck token** — cloud sync resolves a token from, in order: the
112
+ `motherduck_token` env var, `MOTHERDUCK_TOKEN`, then the file
113
+ `~/.config/motherduck/token` (chmod 600). That path is **MotherDuck's own**
114
+ credential store — the `motherduck` CLI / device-auth writes it, so the scripts,
115
+ the flush LaunchAgent, and official tooling all read **one** file (no second
116
+ copy to drift out of sync on rotation). The file matters because **launchd does
117
+ not inherit your shell exports** — the flush LaunchAgent relies on it, so
118
+ without it sync would fall back to interactive browser device-auth (and hang).
119
+ If a stale shell/uppercase token outranks a freshly rotated file token, the
120
+ ATTACH self-heals by retrying once with the file token. Without any token, local
121
+ writes still work and sync raises a clean error (logged to `sync.log`, retried
122
+ later).
123
+
124
+ 3. **Create the schema:**
125
+
126
+ ```bash
127
+ claude-session-logger init
128
+ ```
129
+
130
+ 4. **Wire the hooks** into `~/.claude/settings.json` (merge — don't replace
131
+ existing hooks). `claude-session-logger-setup` writes these for you; shown here
132
+ for reference (commands invoke `python -m claude_session_logger.cli …` so
133
+ launchd's minimal PATH still resolves the package):
134
+
135
+ ```jsonc
136
+ "SessionStart": [
137
+ { "hooks": [
138
+ { "type": "command", "command": "python3 -m claude_session_logger.cli recent" },
139
+ { "type": "command", "command": "python3 -m claude_session_logger.cli inbox --count" },
140
+ { "type": "command", "command": "launchctl kickstart gui/$(id -u)/com.keithfajardo.claude-session-logger.flush" },
141
+ { "type": "command", "command": "python3 -m claude_session_logger.memory pull 2>&1 || true" }
142
+ ]}
143
+ ],
144
+ "PostToolUse": [
145
+ { "matcher": "Write|Edit", "hooks": [
146
+ { "type": "command", "command": "python3 -m claude_session_logger.memory hook 2>/dev/null || true", "async": true }
147
+ ]}
148
+ ],
149
+ "SessionEnd": [
150
+ { "hooks": [
151
+ { "type": "command", "command": "python3 -m claude_session_logger.cli record --no-sync; launchctl kickstart gui/$(id -u)/com.keithfajardo.claude-session-logger.flush" }
152
+ ]}
153
+ ]
154
+ ```
155
+
156
+ The `inbox --count` line prints the shared-issue badge (silent when nothing is
157
+ unseen).
158
+
159
+ 5. **LaunchAgent** (`~/Library/LaunchAgents/com.keithfajardo.claude-session-logger.flush.plist`)
160
+ runs `claude-session-logger flush` when kickstarted, pushing queued rows to the
161
+ cloud off the session-end hot path. Since `flush` does a **bidirectional**
162
+ `log_entries` merge, this is also what pulls other users' resolves/reopens down
163
+ to this device. The plist embeds **no token** — the code resolves it from the
164
+ token file in-process.
165
+
166
+ 6. **Username (optional)** — set `CLAUDE_LOGGER_USER` in the hook environment to
167
+ control the name recorded in `created_by`/`updated_by`. If unset, it falls back
168
+ to `git config user.name`. Needed only if you share the issue table.
169
+
170
+ ## Usage
171
+
172
+ ```bash
173
+ # Create local + cloud schema (idempotent)
174
+ python3 log_session.py init
175
+
176
+ # Record a session (reads hook JSON on stdin). --no-sync = local write only.
177
+ python3 log_session.py record [--no-sync]
178
+
179
+ # Flush queued (synced = FALSE) rows to the cloud
180
+ python3 log_session.py flush
181
+
182
+ # Print the most recent sessions banner (read-only)
183
+ python3 log_session.py recent [--limit 3]
184
+
185
+ # Inbox of shared-issue changes by other users
186
+ python3 log_session.py inbox # list changes, clear the badge
187
+ python3 log_session.py inbox --count # badge only (read-only; used by SessionStart)
188
+ ```
189
+
190
+ ### On-demand log entries
191
+
192
+ Tell Claude in plain language and it runs the right command. session_id
193
+ auto-resolves from the active transcript, so this works from **any** repo.
194
+
195
+ **What to say → what runs:**
196
+
197
+ | Say | Runs |
198
+ |---|---|
199
+ | "log an issue: \<title\> — \<details\>" | `log --category issue --status open …` |
200
+ | "log a finding: \<title\>" | `log --category finding --status open …` |
201
+ | "log a task: \<title\>" | `log --category task --status open …` |
202
+ | "log a decision: \<title\>" | `log --category decision --status info …` |
203
+ | "log a note: \<title\>" | `log --category note --status info …` |
204
+ | "resolve the \<words\> issue" | `resolve --title-match "<words>" --status resolved` |
205
+ | "resolve \<id\> [because …]" | `resolve --id <id> --status resolved [--note …]` |
206
+ | "set \<id\> to blocked / in progress" | `resolve --id <id> --status <status>` |
207
+ | "list open issues" / "what did I log" | `list [--category …] [--open] …` |
208
+ | "check my inbox" / "what changed" | `inbox` (lists others' changes, clears the badge) |
209
+
210
+ Default status: issue/finding/task → `open`; decision/note → `info`. Add "… status
211
+ blocked" (etc.) to override. The headline goes in `--title`, extra context in
212
+ `--body`.
213
+
214
+ ```bash
215
+ # Add an entry. session_id auto-resolved from the active transcript if omitted.
216
+ # Prints the new id: "logged issue (open) id=<uuid>" — keep it to resolve later.
217
+ python3 log_session.py log \
218
+ --category issue --status open \
219
+ --title "staging model double-counts refunds" \
220
+ --body "details…" [--session-id <id>]
221
+
222
+ # Resolve / update an existing entry (appends a note to the body if given).
223
+ # By words — no UUID needed; must match exactly one entry, else it lists candidates.
224
+ python3 log_session.py resolve --title-match "double-counts refunds" --status resolved \
225
+ [--note "fixed in PR #42"]
226
+ # Or by exact id
227
+ python3 log_session.py resolve --id <entry-id> --status resolved [--note "fixed in PR #42"]
228
+
229
+ # List entries, newest first (by updated_at, falling back to logged_at).
230
+ # Use this to grab an id when a title match is ambiguous.
231
+ python3 log_session.py list [--category issue] [--status open] [--open] [--limit N]
232
+
233
+ # Show ONE entry's full body (read-only). By id / id prefix, or title words.
234
+ # Use this instead of a hand-written duckdb query — `list` omits the body.
235
+ python3 log_session.py show <id-or-prefix>
236
+ python3 log_session.py show --title-match "double-counts refunds"
237
+ ```
238
+
239
+ `--open` excludes `status = 'resolved'` entries. `show` accepts a short id prefix
240
+ (e.g. `b045dd70`) and prints the id/type/status/title/timestamps/body; if a title
241
+ match is ambiguous it lists the candidates instead. `resolve` takes **either**
242
+ `--title-match` (resolve by words; errors if it matches 0 or >1 entries) **or**
243
+ `--id` (exact).
244
+
245
+ **Categories:** `finding | task | decision | issue | note`
246
+ **Statuses:** `open | in_progress | blocked | resolved | info`
247
+
248
+ ## Sharing the issue table (multi-user)
249
+
250
+ The `log_entries` table is **shared and bidirectional**: install the logger on a
251
+ second device/account and both people see and resolve the same issues. `sessions`
252
+ stay **private** (push-only) — only `log_entries` is pulled back down.
253
+
254
+ **Identity.** Every entry records who created/changed it. Username resolves from
255
+ `$CLAUDE_LOGGER_USER` → `git config user.name` → `"unknown"`:
256
+ - `created_by` — who logged the entry.
257
+ - `updated_by` — who last changed its status (e.g. who resolved it).
258
+
259
+ **Sync model.** `flush`/`record` run a last-write-wins merge (newest
260
+ `updated_at` wins) of `log_entries` between local and `md:claude_sessions`. So
261
+ Jane can resolve an issue Keith opened; on Keith's next sync the resolve lands
262
+ locally with `updated_by = "Jane"`. Concurrent edits to the *same* entry: latest
263
+ timestamp wins (low-contention assumption — like the rest of the system).
264
+
265
+ ### Inbox — change notifications
266
+
267
+ When someone else resolves or reopens an issue, you get a count badge at session
268
+ start, then read the details on demand.
269
+
270
+ ```bash
271
+ # Badge (wired into the SessionStart hook): read-only, prints e.g.
272
+ # 📬 2 shared issue update(s) — run: …log_session.py inbox
273
+ # Silent when nothing is unseen. Never blocks; reads local state only.
274
+ python3 log_session.py inbox --count
275
+
276
+ # Inbox view: lists changes made by others since you last looked,
277
+ # then advances the "seen" watermark (clears the badge).
278
+ python3 log_session.py inbox
279
+ ```
280
+
281
+ A change counts as unseen if `COALESCE(updated_at, logged_at)` is newer than the
282
+ local watermark **and** the actor (`updated_by`, else `created_by`) isn't you. The
283
+ watermark lives in a local-only `meta` table — it is **not** synced, so "seen" is
284
+ per-device. The badge reads whatever the last sync pulled down (kept current by
285
+ the flush LaunchAgent), so it never makes a network call on the session-start hot
286
+ path.
287
+
288
+ > Real-time push (email/Slack) is intentionally out of scope — the badge + inbox
289
+ > is a pull-and-diff, no extra infrastructure.
290
+
291
+ ## Pulling memory (local & remote)
292
+
293
+ "Memory" spans **two separate systems**. Know which one you want before pulling.
294
+
295
+ | | System A — Session memory (**this repo**) | System B — File-based memory (sibling) |
296
+ |---|---|---|
297
+ | What | Sessions + on-demand log entries | Markdown memory files Claude writes |
298
+ | Local store | `~/.claude/claude-session-logger/sessions.duckdb` | `~/.claude/projects/<slug>/memory/*.md` |
299
+ | Remote store | MotherDuck `md:claude_sessions` | MotherDuck `md:claude_memory` |
300
+ | Tool | `log_session.py` (this repo) | `~/.claude/scripts/memory_sync.py` |
301
+
302
+ ### System A — session memory (this repo)
303
+
304
+ **Pull local** — read straight from `sessions.duckdb`:
305
+
306
+ ```bash
307
+ # On-demand log entries (findings/tasks/decisions/issues/notes), newest first
308
+ python3 log_session.py list [--category finding] [--status open] [--open] [--limit N]
309
+
310
+ # Recent sessions banner
311
+ python3 log_session.py recent [--limit 5]
312
+
313
+ # Or query the local file directly
314
+ duckdb ~/.claude/claude-session-logger/sessions.duckdb \
315
+ "SELECT session_date, project, cost_usd, total_tokens FROM sessions ORDER BY ended_at DESC LIMIT 10"
316
+ ```
317
+
318
+ **Pull remote** — query the MotherDuck mirror (needs `MOTHERDUCK_TOKEN`):
319
+
320
+ ```bash
321
+ duckdb "md:?motherduck_token=$MOTHERDUCK_TOKEN" \
322
+ "SELECT category, status, title, logged_at
323
+ FROM claude_sessions.log_entries
324
+ WHERE title ILIKE '%refund%' ORDER BY logged_at DESC"
325
+ ```
326
+
327
+ Local is the source of truth; remote is the synced mirror, useful from another
328
+ device. Both hold the same `sessions` + `log_entries` tables. `sessions` syncs
329
+ **up only** (private); `log_entries` syncs **both ways** (shared — see
330
+ [Sharing the issue table](#sharing-the-issue-table-multi-user)).
331
+
332
+ ### System B — file-based memory (`memory_sync.py`)
333
+
334
+ Separate system: the `.md` memory files Claude maintains per project, mirrored to
335
+ MotherDuck `md:claude_memory` for cross-device access. It lives in
336
+ `~/.claude/scripts/`, **not** in this repo. **Always use the script — never
337
+ hand-write inline `duckdb` queries (token-wasteful, fragile).**
338
+
339
+ ```bash
340
+ # Pull remote — grep memory by content (current project). Matching lines only.
341
+ python3 ~/.claude/scripts/memory_sync.py search "<thing>"
342
+ python3 ~/.claude/scripts/memory_sync.py search "<thing>" --project-key <repo-name>
343
+ python3 ~/.claude/scripts/memory_sync.py search "<thing>" --all-projects # every project
344
+ python3 ~/.claude/scripts/memory_sync.py search "<thing>" --full # whole files
345
+
346
+ # Pull remote — print one file's full content (name substring match)
347
+ python3 ~/.claude/scripts/memory_sync.py get <name-fragment> [--project-key <repo-name>]
348
+
349
+ # Refresh local .md files from remote (newer wins), or reconcile both ways
350
+ python3 ~/.claude/scripts/memory_sync.py pull
351
+ python3 ~/.claude/scripts/memory_sync.py sync
352
+ ```
353
+
354
+ `search`/`get` are read-only (no local write). Workflow: `search` to find the file
355
+ + matching lines, then `get` only if the full file is needed. Local `.md` files
356
+ are read directly by the Claude Code harness; `pull`/`sync` keep them current
357
+ across devices.
358
+
359
+ ## Schema
360
+
361
+ Two synced tables plus a local-only `meta` table. The `synced` column is **local
362
+ only** (tracks what still needs pushing); it is carried into the cloud copy but
363
+ only meaningful locally.
364
+
365
+ ### `sessions` (one row per session, auto at session end)
366
+
367
+ `session_id` (PK), `project`, `cwd`, `session_date`, `started_at`, `ended_at`,
368
+ `duration_min`, `models`, `input_tokens`, `output_tokens`, `cache_write_tokens`,
369
+ `cache_read_tokens`, `total_tokens`, `cost_usd`, `message_count`,
370
+ `tool_call_count`, `skills_used`, `git_branch`, `cc_version`, `title`,
371
+ `session_type`, `created_by`, `synced`.
372
+
373
+ ### `log_entries` (on-demand, shared)
374
+
375
+ `id` (PK, UUID), `session_id`, `logged_at`, `category`, `status`, `title`, `body`,
376
+ `synced`, `updated_at` (NULL until first resolve/update), `created_by`,
377
+ `updated_by`. `create_tables()` runs idempotent `ADD COLUMN IF NOT EXISTS`
378
+ migrations (`updated_at`, `created_by`, `updated_by`) for older DBs.
379
+
380
+ ### `meta` (local only — not synced)
381
+
382
+ `key` (PK), `value`. Holds the per-device `inbox_watermark` (last time you cleared
383
+ the inbox). Never created cloud-side.
384
+
385
+ All timestamps are stored in UTC.
386
+
387
+ > **Column order note:** sync uses **explicit column lists**, never `SELECT *`.
388
+ > `ADD COLUMN` appends physically at the end, so a migrated table and a freshly
389
+ > created one can have different column *positions* — positional `SELECT *` across
390
+ > local↔cloud would silently misalign (e.g. map `created_by` onto `synced`).
391
+
392
+ ## Cost model
393
+
394
+ Per assistant message, token usage is accumulated per model
395
+ (`input`, `output`, `cache_creation` → `cache_write`, `cache_read`), de-duplicated
396
+ by message uuid/id. Cost:
397
+
398
+ ```
399
+ cost_usd = Σ_model ( input·p.input + output·p.output
400
+ + cache_write·p.cache_write + cache_read·p.cache_read ) / 1e6
401
+ ```
402
+
403
+ Rates live in `prices.json` (per 1M tokens). Seeded values:
404
+
405
+ | model | input | output | cache_write | cache_read |
406
+ |---|---|---|---|---|
407
+ | claude-opus-4-8 | 5.00 | 25.00 | 6.25 | 0.50 |
408
+ | claude-sonnet-4-6 | 3.00 | 15.00 | 3.75 | 0.30 |
409
+ | claude-haiku-4-5 | 1.00 | 5.00 | 1.25 | 0.10 |
410
+
411
+ An unknown model contributes 0 cost and is logged to `sync.log` so it can be added.
412
+
413
+ ## `session_type` inference (customizable)
414
+
415
+ Built-in defaults (`classify.py`), first match wins:
416
+
417
+ 1. skill mentions `dbt`/`kimball`, or project/cwd mentions dbt → `dbt`
418
+ 2. skill `systematic-debugging` → `debugging`
419
+ 3. skill `deep-research`/`brainstorming`/`research` → `research`
420
+ 4. skill `writing-plans`/`executing-plans` → `planning`
421
+ 5. else → `general`
422
+
423
+ **Add your own** without touching code: drop a YAML file at
424
+ `~/.claude/claude-session-logger/session_types.yaml` (copy
425
+ `session_types.example.yaml`). Your rules are checked **before** the defaults, so they
426
+ can add a new type or override a built-in one:
427
+
428
+ ```yaml
429
+ - type: infra
430
+ skills: [terraform, k8s] # case-insensitive substrings of the session's skills
431
+ path: [ops/, infra/] # …or of "<project> <cwd>"
432
+ ```
433
+
434
+ Loading is hook-safe — a missing file, malformed YAML, or absent PyYAML silently
435
+ falls back to the defaults, so session-end never breaks.
436
+
437
+ ## Reliability
438
+
439
+ - **Cloud unreachable / token missing** → local write succeeds, sync fails silently,
440
+ logged to `sync.log`, retried on the next `flush`.
441
+ - **Lock contention** → `connect()` retries with backoff; the read-only banner
442
+ degrades silently.
443
+ - **Malformed transcript line** → skipped (counted, not fatal).
444
+
445
+ ## Tests
446
+
447
+ ```bash
448
+ python3 -m pytest
449
+ ```
450
+
451
+ ## Limitations
452
+
453
+ - **Single active session per directory** assumption: `resolve.py` picks the
454
+ newest-mtime `.jsonl` in the cwd's project dir. Concurrent sessions in the same
455
+ directory can mis-attribute an on-demand log entry.
456
+ - No per-message conversation storage (by design — avoids dumping file contents /
457
+ secrets into the cloud).
458
+ - Hooks and the LaunchAgent path are macOS / launchd specific.