@agentstrack/collector 0.1.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/LICENSE +202 -0
  3. package/README.md +779 -0
  4. package/dist/adapters/account.d.ts +22 -0
  5. package/dist/adapters/account.js +142 -0
  6. package/dist/adapters/account.js.map +1 -0
  7. package/dist/adapters/claude.d.ts +49 -0
  8. package/dist/adapters/claude.js +259 -0
  9. package/dist/adapters/claude.js.map +1 -0
  10. package/dist/adapters/codex.d.ts +64 -0
  11. package/dist/adapters/codex.js +350 -0
  12. package/dist/adapters/codex.js.map +1 -0
  13. package/dist/adapters/opencode.d.ts +79 -0
  14. package/dist/adapters/opencode.js +338 -0
  15. package/dist/adapters/opencode.js.map +1 -0
  16. package/dist/adapters/types.d.ts +97 -0
  17. package/dist/adapters/types.js +20 -0
  18. package/dist/adapters/types.js.map +1 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +450 -0
  21. package/dist/cli.js.map +1 -0
  22. package/dist/commands/service.d.ts +3 -0
  23. package/dist/commands/service.js +86 -0
  24. package/dist/commands/service.js.map +1 -0
  25. package/dist/config.d.ts +67 -0
  26. package/dist/config.js +97 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/daemon.d.ts +57 -0
  29. package/dist/daemon.js +368 -0
  30. package/dist/daemon.js.map +1 -0
  31. package/dist/git/commits.d.ts +34 -0
  32. package/dist/git/commits.js +85 -0
  33. package/dist/git/commits.js.map +1 -0
  34. package/dist/git/repo.d.ts +36 -0
  35. package/dist/git/repo.js +141 -0
  36. package/dist/git/repo.js.map +1 -0
  37. package/dist/index.d.ts +12 -0
  38. package/dist/index.js +12 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/privacy/mode.d.ts +11 -0
  41. package/dist/privacy/mode.js +17 -0
  42. package/dist/privacy/mode.js.map +1 -0
  43. package/dist/privacy/paths.d.ts +11 -0
  44. package/dist/privacy/paths.js +30 -0
  45. package/dist/privacy/paths.js.map +1 -0
  46. package/dist/privacy/pipeline.d.ts +26 -0
  47. package/dist/privacy/pipeline.js +112 -0
  48. package/dist/privacy/pipeline.js.map +1 -0
  49. package/dist/privacy/redact.d.ts +29 -0
  50. package/dist/privacy/redact.js +58 -0
  51. package/dist/privacy/redact.js.map +1 -0
  52. package/dist/queue/spool.d.ts +47 -0
  53. package/dist/queue/spool.js +152 -0
  54. package/dist/queue/spool.js.map +1 -0
  55. package/dist/queue/tailer.d.ts +25 -0
  56. package/dist/queue/tailer.js +48 -0
  57. package/dist/queue/tailer.js.map +1 -0
  58. package/dist/schema.d.ts +76 -0
  59. package/dist/schema.js +46 -0
  60. package/dist/schema.js.map +1 -0
  61. package/dist/sessions/title.d.ts +2 -0
  62. package/dist/sessions/title.js +11 -0
  63. package/dist/sessions/title.js.map +1 -0
  64. package/dist/transport/client.d.ts +76 -0
  65. package/dist/transport/client.js +80 -0
  66. package/dist/transport/client.js.map +1 -0
  67. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,779 @@
1
+ # AgentsTrack Collector
2
+
3
+ **See where your AI coding agents actually spend your time and your tokens — without shipping your code anywhere.**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@agentstrack/collector.svg)](https://www.npmjs.com/package/@agentstrack/collector)
6
+ [![CI](https://github.com/agentstrack/collector/actions/workflows/ci.yml/badge.svg)](https://github.com/agentstrack/collector/actions/workflows/ci.yml)
7
+ [![License: Apache 2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
8
+ [![Node](https://img.shields.io/badge/node-%3E%3D24-brightgreen.svg)](https://nodejs.org)
9
+
10
+ `@agentstrack/collector` turns the records Claude Code, Codex and OpenCode already keep on your
11
+ machine into a normalized event stream:
12
+
13
+ - **Tokens and cost** — input, cached input, cache creation, output and reasoning tokens, normalized across every agent. OpenCode's real settled provider cost comes through as `REPORTED`, not an estimate.
14
+ - **Which account paid** — a stable, opaque account key per session, so a personal login and a work one never merge into one bill.
15
+ - **What actually happened** — tool calls, commands, files changed with line counts, and the commits a session produced.
16
+ - **Nothing you didn't agree to** — prompts and code are discarded on your machine, before anything is queued for upload.
17
+
18
+ It **reads what the agent already wrote** — append-only logs for Claude Code and Codex, a read-only
19
+ SQLite query for OpenCode. It does not install hooks, it does not wrap your agent, and it **never
20
+ writes to `~/.claude/settings.json`, `~/.codex/hooks.json` or OpenCode's database**. Uninstalling is `npm rm -g` plus deleting one
21
+ directory; nothing about your agent setup changes.
22
+
23
+ **You do not have to take that on faith.** It is Apache-2.0 and this is the whole of it — the part
24
+ that runs on your machine and reads your files. Prompts and code are dropped locally, before the
25
+ upload queue, and `agentstrack sync --dry-run --print` shows you the literal JSON that would be sent
26
+ before anything is: [Verify it yourself](#verify-it-yourself).
27
+
28
+ ---
29
+
30
+ ## Quick start
31
+
32
+ ```bash
33
+ npm install -g @agentstrack/collector # requires Node >= 24
34
+
35
+ agentstrack login at_live_xxxxxxxx_xxxxxxxx # key from Settings → API keys
36
+ agentstrack start # installs a login service and starts collecting
37
+ agentstrack status # confirm it is working
38
+ ```
39
+
40
+ ```console
41
+ $ agentstrack login at_test_0123456789abcdef_EXAMPLEonly_not_a_real_key_00000…
42
+ ✓ Logged in and registered this device.
43
+ Collector: 3f9a1e6c-1c4b-4c7e-9d0f-2a5b8c1d7e40
44
+ Privacy: analytics
45
+ Config: /Users/you/.agentstrack/config.yaml
46
+
47
+ Next: agentstrack start
48
+ ```
49
+
50
+ ```console
51
+ $ agentstrack status
52
+ AgentsTrack collector v0.1.0
53
+ Logged in: yes
54
+ API: https://api.agentstrack.ai
55
+ Collector: 3f9a1e6c-1c4b-4c7e-9d0f-2a5b8c1d7e40
56
+ Privacy: analytics
57
+ Queue depth: 0
58
+ Service: installed
59
+ Running: no
60
+
61
+ Agents
62
+ ✓ claude_code 134 transcripts
63
+ ✓ codex 13 transcripts
64
+ ```
65
+
66
+ `Running: no` with `Service: installed` is normal — `Running` tracks a **foreground** collector
67
+ (`agentstrack start --foreground`), which is the only mode that writes a pidfile. The background
68
+ service is supervised by launchd/systemd; `Service: installed` is the line that matters for it.
69
+
70
+ **Your existing history is picked up automatically.** On its first pass the collector reads every
71
+ agent transcript modified in the last 7 days, from byte zero. There is no separate backfill step.
72
+
73
+ Nothing showing up? Run `agentstrack doctor`.
74
+
75
+ ---
76
+
77
+ ## What it collects, and what it never does
78
+
79
+ | ✅ It collects | ❌ It never does |
80
+ |---|---|
81
+ | Session ids, agent name and version, event timestamps | Read your source tree, or open any file other than agent transcripts |
82
+ | Token counts: input, cached input, cache creation, output, reasoning | Upload prompt text — unless you explicitly set `privacy.mode: full` |
83
+ | Model id, provider, stop reason | Upload file contents or diffs — unless you explicitly set `privacy.code_content: full` |
84
+ | Tool names, tool call ids, success/failure | Send tool output or command output |
85
+ | Shell commands, with secrets redacted locally | Send a command that still contains a matched credential |
86
+ | File paths, **relative to the project root by default** | Send absolute paths — which leak your username and your clients' names — unless you opt in |
87
+ | Lines added/removed per edit, computed locally from the tool input | Send the lines themselves |
88
+ | Git branch, commit SHA, additions/deletions/files changed | Send your git remote URL (only a SHA-256 of it) or commit messages and diffs |
89
+ | Locally generated session titles (`analytics` mode and above) | Send the prompt those titles were derived from |
90
+ | Your hostname, OS, arch and each detected agent's version — **once, at registration** | Store the raw hostname server-side (it is kept only as a SHA-256) |
91
+ | | Install hooks or modify `~/.claude/settings.json` / `~/.codex/hooks.json` |
92
+ | | Send `organization_id` or `user_id` — they are not in the wire format at all |
93
+ | | Watch your keyboard, your screen, or any process on your machine |
94
+
95
+ **Two facts worth repeating.**
96
+
97
+ 1. **`organization_id` and `user_id` do not exist in the event envelope.** The server derives both
98
+ from your API key. A collector cannot name its own tenant, by construction — and `npm test`
99
+ asserts it — see [`docs/EVENT_SCHEMA.md`](./docs/EVENT_SCHEMA.md).
100
+ 2. **Privacy is enforced here, before upload — not on the server.** In `metadata` mode there is no
101
+ content to leak, because it was discarded on your laptop.
102
+
103
+ ### Verify it yourself
104
+
105
+ Do not take the table above on trust. **`agentstrack sync --dry-run --print` prints the exact,
106
+ post-redaction JSON that would be uploaded, and sends nothing.** It is the single most useful command
107
+ in the tool: the privacy claims are checkable on your own machine, against your own sessions, before
108
+ a single byte leaves it.
109
+
110
+ ```console
111
+ $ agentstrack sync --dry-run
112
+ 10 event(s) would be sent to https://api.agentstrack.ai:
113
+
114
+ 3 tool.started
115
+ 2 model.response
116
+ 1 user.prompted
117
+ 1 file.read
118
+ 1 tool.completed
119
+ 1 file.changed
120
+ 1 command.executed
121
+
122
+ Re-run with --print to see the full event bodies.
123
+ ```
124
+
125
+ ```console
126
+ $ agentstrack sync --dry-run --print
127
+ 10 event(s) would be sent to https://api.agentstrack.ai:
128
+
129
+ {
130
+ "occurred_at": "2026-08-26T10:00:00.000Z",
131
+ "session_id": "06f3470f-d924-4552-b3ee-3f8924286cec",
132
+ "agent": "claude_code",
133
+ "agent_version": "2.1.241",
134
+ "event_type": "user.prompted",
135
+ "payload": {
136
+ "prompt_chars": 81,
137
+ "derived_title": "Add tests for the cost calculator",
138
+ "repo": {
139
+ "branch": "feature/pricing"
140
+ }
141
+ },
142
+ "event_id": "fbaa5b45-7172-4b52-93c8-565aa281d51d",
143
+ "schema_version": 1,
144
+ "collector_id": "3f9a1e6c-1c4b-4c7e-9d0f-2a5b8c1d7e40"
145
+ }
146
+
147
+ ```
148
+
149
+ That is a real `user.prompted` event in the default `analytics` mode. Read what is **not** there:
150
+ no `prompt_text` — the 81-character prompt it was derived from was discarded on the machine — no
151
+ `organization_id`, no `user_id`, no `project_path`. Set `privacy.prompts: never` and re-run, and
152
+ `derived_title` disappears too.
153
+
154
+ Two companion commands:
155
+
156
+ ```bash
157
+ agentstrack config --show-effective # the policy actually in force, defaults included
158
+ agentstrack doctor --json # structured diagnostics, safe to paste into an issue
159
+ ```
160
+
161
+ ---
162
+
163
+ ## How it works
164
+
165
+ ```text
166
+ ~/.claude/projects/<slug>/<session-uuid>.jsonl
167
+ ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl
168
+ │ ~/.local/share/opencode/opencode.db
169
+ │ append-only files │
170
+ ▼ ▼
171
+ ┌────────────────────────┐ ┌────────────────────────┐
172
+ │ tailer │ │ db poller │ same 5s cycle
173
+ │ (read-only, resumable) │ │ (READ-ONLY, no writes) │ time_updated cursors
174
+ │ (path,inode,offset) cp │ │ in spool.db meta │ in spool.db meta
175
+ └───────────┬────────────┘ └───────────┬────────────┘
176
+ └───────────────┬───────────────────┘
177
+
178
+ ┌────────────────────────┐
179
+ │ privacy pipeline │ mode-based content strip
180
+ │ │ → 15 built-in secret rules + org rules
181
+ │ │ → path normalization
182
+ │ │ → raw content DISCARDED HERE
183
+ └───────────┬────────────┘
184
+
185
+ ┌────────────────────────┐
186
+ │ ~/.agentstrack/ │ SQLite (WAL) — durable across crash,
187
+ │ spool.db │ restart and reboot
188
+ └───────────┬────────────┘
189
+
190
+ ┌────────────────────────┐
191
+ │ batched HTTPS + gzip │ POST /v1/events/batch, 100 events / 30s
192
+ │ exponential backoff │ idempotent on event_id
193
+ └────────────────────────┘
194
+ ```
195
+
196
+ **Offline is a normal state, not an error.** With no network, a blocked VPN or a dead API, the
197
+ collector keeps parsing and keeps spooling; when the network returns it drains oldest-first. Bodies
198
+ over 1 KB are gzipped.
199
+
200
+ **Restart is safe.** File read offsets live in the same SQLite database as the queue, keyed by
201
+ `(path, inode)`. A restart resumes mid-file. If a file is replaced (new inode) or truncated (offset
202
+ past the end), it is re-read from the start rather than silently skipped.
203
+
204
+ **A line the agent is still writing is never consumed.** The tailer advances its checkpoint only as
205
+ far as the **last complete newline**; a partial trailing line is left unread and picked up whole on
206
+ the next pass, once the agent has terminated it. This matters because the collector reads live
207
+ sessions: without it, every mid-write read would emit half a JSON object *and* orphan its remainder,
208
+ so both halves would fail to parse and that event would be lost. Byte offsets are computed from the
209
+ raw buffer, not from decoded text, so a multi-byte character cannot desynchronise the position
210
+ either.
211
+
212
+ **Backpressure is handled.** A `413` halves the batch size and the collector recovers it on the next
213
+ success. A `5xx`, a timeout, a `408` or a `429` is retried with jittered exponential backoff (1s
214
+ base, capped at 5 minutes). A `4xx` that is none of those means the server will never accept the
215
+ batch: **the attempt counter is incremented for the events in that batch only, and one of them is
216
+ deleted once it reaches `upload.max_retries` (default 8)**. It is not parked and it does not come
217
+ back — a poison event must not be able to block the queue forever.
218
+
219
+ Two properties of that deletion are worth stating explicitly, because both are easy to get wrong:
220
+
221
+ - **It is scoped to the failing batch.** An event sitting elsewhere in the spool cannot be destroyed
222
+ by a batch it was not part of.
223
+ - **At least one attempt is always allowed.** `max_retries: 0` is clamped to `1`; a value of zero
224
+ would otherwise match every never-attempted row and empty the whole queue on the first failure.
225
+
226
+ ---
227
+
228
+ ## Privacy
229
+
230
+ ### The three modes
231
+
232
+ | | `metadata` | `analytics` *(default)* | `full` |
233
+ |---|---|---|---|
234
+ | Token counts, timings, models, costs | ✅ | ✅ | ✅ |
235
+ | Tool names and outcomes | ✅ | ✅ | ✅ |
236
+ | Commands (secret-redacted) | ✅ | ✅ | ✅ |
237
+ | File paths, line counts | ✅ | ✅ | ✅ |
238
+ | Git branch / SHA / remote **hash** | ✅ | ✅ | ✅ |
239
+ | Locally derived session titles | ❌ stripped | ✅ | ✅ |
240
+ | Error messages | ❌ stripped | ✅ | ✅ |
241
+ | Prompt text | ❌ never | ❌ never | ⚠️ only with `privacy.prompts: full` |
242
+ | File contents / diffs | ❌ never | ❌ never | ⚠️ only with `privacy.code_content: full` |
243
+
244
+ `analytics` is the honest middle: the title is computed **on your machine** from the prompt, and then
245
+ the prompt is deleted. The server receives `"fix flaky auth test"`, never the 900 words you typed.
246
+
247
+ If even the title is too much, `privacy.prompts: never` drops that too: in `analytics` it strips
248
+ `derived_title`, so nothing derived from a prompt leaves the machine, without giving up token, tool
249
+ and cost analytics the way `metadata` does. `metadata` already drops the title by mode.
250
+
251
+ `never` means never, in every mode — including `full`, where it strips both `prompt_text` and
252
+ `derived_title`. Setting it is the strongest prompt-privacy guarantee available without dropping to
253
+ `metadata`.
254
+
255
+ `full` is opt-in twice over: setting `mode: full` alone changes nothing about prompts or code —
256
+ you must also set `privacy.prompts: full` and `privacy.code_content: full`. Nothing in the product
257
+ nags you to.
258
+
259
+ ### Org policy is a ceiling, never a floor
260
+
261
+ Your organization's default mode arrives in the `POST /v1/collector/register` response at login, and
262
+ is re-read from `GET /v1/collector/config` on every daemon start. **A local setting that is stricter
263
+ always wins.** An org set to `full` cannot widen a laptop configured for `metadata`; an org set to
264
+ `metadata` does clamp a laptop asking for `full`. The clamp is one shared function so login and the
265
+ daemon cannot drift.
266
+
267
+ ### Built-in secret redaction
268
+
269
+ Every free-text field that survives the mode strip (`prompt_text`, `derived_title`, `message`,
270
+ `command`) passes through these 15 rules, most-specific first, on your machine:
271
+
272
+ | Rule | Catches |
273
+ |---|---|
274
+ | `anthropic_key` | `sk-ant-…` |
275
+ | `openai_key` | `sk-…`, `sk-proj-…` |
276
+ | `github_token` | `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_` |
277
+ | `github_pat` | `github_pat_…` |
278
+ | `slack_token` | `xoxb-`, `xoxa-`, `xoxp-`, `xoxr-`, `xoxs-` |
279
+ | `stripe_key` | `sk_live_`, `sk_test_`, `rk_live_`, `rk_test_` |
280
+ | `aws_access_key` | `AKIA…`, `ASIA…` |
281
+ | `google_api_key` | `AIza…` |
282
+ | `agentstrack_key` | our own `at_live_…` / `at_test_…` keys |
283
+ | `private_key` | any `-----BEGIN … PRIVATE KEY-----` block |
284
+ | `jwt` | three-segment `eyJ…` tokens |
285
+ | `bearer_header` | `Bearer <token>` → `Bearer [REDACTED]` |
286
+ | `basic_auth_url` | `https://user:pw@host` → `https://[REDACTED]@host` |
287
+ | `env_assignment` | `*SECRET*=`, `*TOKEN*=`, `*PASSWORD*=`, `*PASSWD*=`, `*APIKEY*=`, `*API_KEY*=`, `*ACCESS_KEY*=`, `*PRIVATE_KEY*=` |
288
+ | `generic_hex_secret` | bare hex strings of 40+ characters |
289
+
290
+ A match is replaced in place, and most rules substitute `[REDACTED:rule_name]`. Four do not:
291
+ `bearer_header` → `Bearer [REDACTED]` and `basic_auth_url` → `scheme://[REDACTED]@host` keep the
292
+ surrounding syntax so the shape of the command survives, `env_assignment` → `NAME=[REDACTED]` keeps
293
+ the variable name, and `generic_hex_secret` substitutes the shorter `[REDACTED:hex]`. Your
294
+ organization can add patterns server-side; a malformed org pattern is skipped rather than breaking
295
+ the collector.
296
+
297
+ Redaction is defence in depth, not the primary control. The primary control is that in `metadata`
298
+ and `analytics` modes the content is **deleted locally** and never enters the pipeline at all.
299
+
300
+ ### How file paths are handled
301
+
302
+ With `file_paths: relative` (the default):
303
+
304
+ | Actual path | Uploaded as |
305
+ |---|---|
306
+ | `/Users/dana/work/api/src/auth.ts` (project root `/Users/dana/work/api`) | `src/auth.ts` |
307
+ | `/Users/dana/.zshrc` | `~/.zshrc` |
308
+ | `/etc/nginx/sites-enabled/default` | `…/sites-enabled/default` |
309
+
310
+ The project root itself (`repo.project_path`) is **dropped entirely** in `never` and `relative`
311
+ modes — it is only transmitted if you opt into `file_paths: absolute`. Repositories are correlated
312
+ by `remote_hash`, a SHA-256 of the normalized remote URL, not by path.
313
+
314
+ ### Excluding a project entirely
315
+
316
+ ```yaml
317
+ privacy:
318
+ excluded_projects:
319
+ - "~/work/client-under-nda"
320
+ - "/Users/me/personal"
321
+ ```
322
+
323
+ Prefix match on the session's working directory, `~` expands. An excluded project produces **no
324
+ events at all** — not even counts. Edit the YAML and restart the collector.
325
+
326
+ ---
327
+
328
+ ## Commands
329
+
330
+ | Command | What it does |
331
+ |---|---|
332
+ | `agentstrack login <api-key>` | Register this device, store the key, apply the org privacy ceiling |
333
+ | `agentstrack logout [--purge]` | Remove the stored key; `--purge` also deletes the unsent spool |
334
+ | `agentstrack start [-f]` | Install + start the login service, or run in this terminal with `-f` |
335
+ | `agentstrack stop` | Stop the collector and remove its service unit |
336
+ | `agentstrack status [--json]` | Health, queue depth, detected agents |
337
+ | `agentstrack doctor [--json]` | Diagnose setup problems; `--json` is what bug reports want |
338
+ | `agentstrack config [--path\|--show-effective]` | Print the config (API key masked) |
339
+ | `agentstrack sync [--dry-run [--print]]` | Upload queued events now, or show what would be sent |
340
+ | `agentstrack service <install\|uninstall>` | Manage the login service without starting a collector |
341
+
342
+ ### `login`
343
+
344
+ ```
345
+ agentstrack login <api-key> [--api-url <url>] [--label <name>]
346
+ ```
347
+
348
+ The key is a **positional argument** — there is no interactive prompt and no environment variable.
349
+ `--api-url` points at a self-hosted instance; `--label` names this machine in the dashboard.
350
+ Registration is idempotent on (user, hostname hash), so re-running `login` on the same machine reuses
351
+ the existing collector instead of fragmenting its history. The config file is written mode `600`, in
352
+ a directory created mode `700`.
353
+
354
+ The registration payload is `hostname`, `label`, `os`, `arch`, the collector's own version, and one
355
+ entry per configured agent: `{ agent, version }`. The **agent version is the real one**, read out of
356
+ a transcript the agent already wrote (`2.1.247`, say, from Claude Code's `version` field). Where an
357
+ adapter cannot cheaply establish a version at detection time the field is simply **omitted** rather
358
+ than filled with a placeholder, so a missing version in the dashboard means "not reported", never
359
+ "not detected".
360
+
361
+ Both shipping adapters report a real version. Claude Code reads it from a transcript
362
+ (e.g. `2.1.247`); Codex reads `session_meta.cli_version` from its newest rollout
363
+ (e.g. `0.149.0-alpha.4.3`). The same value is attached to every event as `agent_version`.
364
+
365
+ If your local privacy mode is stricter than the org's, login says so and keeps yours:
366
+
367
+ ```
368
+ Privacy: metadata (your local setting; org allows analytics)
369
+ ```
370
+
371
+ ### `start` / `stop`
372
+
373
+ `agentstrack start` writes a **launchd** agent on macOS (`~/Library/LaunchAgents/ai.agentstrack.collector.plist`)
374
+ or a **systemd user unit** on Linux (`~/.config/systemd/user/agentstrack.service`), loads it, and
375
+ returns. Neither needs root. `-f` / `--foreground` runs in the terminal instead — best for a first
376
+ run, and the only mode where `status` reports `Running: yes`.
377
+
378
+ `agentstrack stop` removes the service unit *and* signals a foreground collector. There is no
379
+ "stop but keep the unit"; use `agentstrack service install` to put it back.
380
+
381
+ ### `doctor`
382
+
383
+ ```console
384
+ $ agentstrack doctor
385
+ Configuration
386
+ ✓ config exists at /Users/you/.agentstrack/config.yaml
387
+ ✓ API key present
388
+ ✓ device registered
389
+
390
+ Agents
391
+ ✓ claude_code transcripts found
392
+ 225 file(s) modified in the last 7 days
393
+ ✓ codex transcripts found
394
+ 3 file(s) modified in the last 7 days
395
+
396
+ Connectivity
397
+ ✗ API reachable at https://api.agentstrack.ai
398
+ fetch failed
399
+
400
+ Queue
401
+ ✓ queue depth 0
402
+ log: /Users/you/.agentstrack/collector.log
403
+
404
+ 1 problem(s) found.
405
+ ```
406
+
407
+ Exit code is non-zero when there is a problem, so it works in a monitoring check.
408
+ `agentstrack doctor --json` prints the structured form, which is what the bug template asks for:
409
+
410
+ ```json
411
+ {
412
+ "version": "0.1.0",
413
+ "node": "v22.22.0",
414
+ "platform": "darwin-arm64",
415
+ "configured": true,
416
+ "logged_in": true,
417
+ "collector_id": "3f9a1e6c-1c4b-4c7e-9d0f-2a5b8c1d7e40",
418
+ "privacy_mode": "analytics",
419
+ "api_url": "https://api.agentstrack.ai",
420
+ "api": { "reachable": true, "privacy_mode": "analytics" },
421
+ "queue_depth": 0,
422
+ "running": false,
423
+ "service_installed": true,
424
+ "agents": [
425
+ { "agent": "claude_code", "installed": true, "healthy": true, "files_tracked": 134, "note": null },
426
+ { "agent": "codex", "installed": true, "healthy": true, "files_tracked": 13, "note": null }
427
+ ],
428
+ "log_path": "/Users/you/.agentstrack/collector.log"
429
+ }
430
+ ```
431
+
432
+ It contains no API key, no prompt, no path inside a project — it is safe to paste into an issue.
433
+
434
+ ### `config`
435
+
436
+ `agentstrack config` prints the file as it is on disk with the key masked. `--path` prints the path
437
+ only. `--show-effective` prints the config **after every default is applied** — the policy the
438
+ collector actually runs with:
439
+
440
+ ```console
441
+ $ agentstrack config --show-effective
442
+ api_url: https://api.agentstrack.ai
443
+ privacy:
444
+ mode: analytics
445
+ prompts: local_summary_only
446
+ code_content: never
447
+ file_paths: relative
448
+ shell_arguments: redact_secrets
449
+ excluded_projects: []
450
+ tracking:
451
+ idle_timeout_seconds: 120
452
+ git_metadata: true
453
+ process_metrics: true
454
+ agents:
455
+ - claude_code
456
+ - codex
457
+ - opencode
458
+ upload:
459
+ batch_size: 100
460
+ interval_seconds: 30
461
+ max_retries: 8
462
+ ```
463
+
464
+ There is no `config set` — edit the YAML. An invalid config is a **hard error**, never a silent
465
+ fallback to defaults, so a typo cannot quietly widen your privacy mode.
466
+
467
+ ### `sync`
468
+
469
+ Drains the spool once and exits — useful after a network outage, or from a cron job on a machine
470
+ where you would rather not run a daemon. It does **not** take a time window; the daemon's own scan is
471
+ what reads new transcript lines.
472
+
473
+ ```console
474
+ $ agentstrack sync
475
+ ✓ Queue is already empty.
476
+
477
+ $ agentstrack sync --dry-run
478
+ ✓ Nothing queued — nothing would be sent.
479
+ ```
480
+
481
+ With a backlog it prints `Uploading <n> queued events…` and then either `✓ Uploaded <n> events.` or,
482
+ if some remain, a warning naming the log — and exits non-zero, so it is safe to run from cron.
483
+
484
+ `--dry-run` sends nothing at all and prints a breakdown by event type; `--print` adds the full
485
+ post-redaction JSON body of each one — see [Verify it yourself](#verify-it-yourself). Both inspect
486
+ the head of the queue, up to `upload.batch_size` events, so `100 event(s) would be sent` on a large
487
+ backlog means "the next batch", not "the whole spool" — `agentstrack status` reports the true depth.
488
+
489
+ ---
490
+
491
+ ## Configuration
492
+
493
+ `~/.agentstrack/config.yaml`, mode `600` because it holds an API key. Set `AGENTSTRACK_HOME` to
494
+ relocate the whole directory (config, spool, log, pidfile). Every key has a default — an empty file
495
+ is a valid config. This is the complete set:
496
+
497
+ ```yaml
498
+ # --- Connection ---------------------------------------------------------
499
+ api_url: https://api.agentstrack.ai # change for a self-hosted instance
500
+ api_key: at_live_xxxxxxxxxxxxxxxx_xxxx # written by `agentstrack login`. Never commit.
501
+ collector_id: 3f9a1e6c-… # assigned by the server at registration
502
+
503
+ # --- Privacy ------------------------------------------------------------
504
+ privacy:
505
+ # metadata | analytics (default) | full — see the table above
506
+ mode: analytics
507
+
508
+ # never | local_summary_only (default) | full
509
+ # `full` is what keeps `mode: full` from uploading prompt text unless you also
510
+ # ask for it here. `never` suppresses prompt text in every mode, and in
511
+ # `analytics` it additionally drops the locally derived `derived_title`, so
512
+ # nothing derived from a prompt leaves the machine at all. Under `mode: full`
513
+ # it does NOT drop `derived_title` — see the privacy section.
514
+ prompts: local_summary_only
515
+
516
+ # never (default) | full
517
+ # `never` drops file contents and diffs in EVERY mode, so opting into `full`
518
+ # prompts does not silently opt into shipping source code.
519
+ code_content: never
520
+
521
+ # never | relative (default) | absolute
522
+ # relative: paths relative to the project root; `~` for home; last two
523
+ # segments for anything else. The project root is only sent under `absolute`.
524
+ file_paths: relative
525
+
526
+ # never | redact_secrets (default) | full
527
+ # `never` truncates a command to its first whitespace token. The other two
528
+ # keep the command line; secret redaction is applied either way.
529
+ shell_arguments: redact_secrets
530
+
531
+ # Prefix match on the session's working directory. `~` expands.
532
+ # An excluded project produces no events of any kind.
533
+ excluded_projects: []
534
+
535
+ # --- Tracking -----------------------------------------------------------
536
+ tracking:
537
+ # Read by the server, not by the collector — see "Roadmap". 30–3600.
538
+ idle_timeout_seconds: 120
539
+
540
+ # Read git branch, project root and a HASH of the remote; poll `git log` for
541
+ # commits made during a session. `false` means no git process is ever spawned.
542
+ git_metadata: true
543
+
544
+ # Not implemented yet — see "Roadmap".
545
+ process_metrics: true
546
+
547
+ # Which adapters to run. Removing one stops it being read entirely.
548
+ agents:
549
+ - claude_code
550
+ - codex
551
+
552
+ # --- Upload -------------------------------------------------------------
553
+ upload:
554
+ batch_size: 100 # events per request. 1–500 (server caps at 500).
555
+ interval_seconds: 30 # seconds between flushes. 5–600.
556
+ max_retries: 8 # non-retryable failures before an event is DELETED. 0–20;
557
+ # 0 is clamped to 1, since "zero attempts allowed" would
558
+ # match every queued event on the first failure.
559
+ ```
560
+
561
+ Environment overrides: `AGENTSTRACK_HOME` (all local state), `CLAUDE_CONFIG_DIR` (default
562
+ `~/.claude`), `CODEX_HOME` (default `~/.codex`), `OPENCODE_DATA_DIR` (default
563
+ `$XDG_DATA_HOME/opencode`, falling back to `~/.local/share/opencode`) and `OPENCODE_DB` (the
564
+ database filename or an absolute path — the same override OpenCode itself honours).
565
+
566
+ ---
567
+
568
+ ## Supported agents
569
+
570
+ | Agent | Status | Reads |
571
+ |---|---|---|
572
+ | **Claude Code** | ✅ Stable | `~/.claude/projects/<slug>/<session-uuid>.jsonl` |
573
+ | **Codex** | ✅ Stable | `~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl` |
574
+ | **OpenCode** | ✅ Stable | `~/.local/share/opencode/opencode.db` — SQLite, opened **read-only** |
575
+ | Gemini CLI · Cursor · Cline · Copilot CLI | 🗓 Planned | ids reserved in the schema, no adapter yet |
576
+
577
+ Per-signal honesty — the adapters do not produce identical data, because the agents do not record
578
+ identical things:
579
+
580
+ | Signal | Claude Code | Codex | OpenCode |
581
+ |---|---|---|---|
582
+ | Session start | ➖ inferred from the first event | ✅ from `session_meta` | ✅ from the `session` row |
583
+ | Session end | ➖ inferred | ➖ inferred | ✅ on archive or compaction |
584
+ | Prompts / titles | ✅ | ✅ | ✅ (OpenCode names its own sessions) |
585
+ | Per-response token usage | ✅ `model.response` with full cache breakdown | ➖ cumulative snapshots only | ➖ cumulative per-session totals only |
586
+ | **Provider-reported cost** | ➖ estimated from a rate card | ➖ estimated | ✅ **`REPORTED` — the real settled charge** |
587
+ | Turn boundaries | ➖ not logged | ✅ `agent.turn.started` / `.ended` | ➖ not emitted |
588
+ | Tool calls | ✅ | ✅ (incl. `custom_tool_call`) | ✅ terminal event with a real duration |
589
+ | Commands | ✅ from `Bash` tool input | ✅ with exit code and duration | ✅ from the `bash` tool's input |
590
+ | File changes + line counts | ✅ from `Edit`/`Write`/`NotebookEdit` inputs | ✅ parsed from the `apply_patch` body | ✅ from `edit`/`write` tool inputs |
591
+ | File reads | ✅ from the `Read` tool | ➖ heuristic, from `cat`/`head`/`tail`/`sed`/`nl`/`bat`/`less` | ✅ from the `read` tool |
592
+ | Agent errors | ➖ not logged | ✅ `error` | ➖ not emitted |
593
+ | Commits | ✅ (from `git log`, not the transcript) | ✅ (same) | ✅ (same) |
594
+ | Plan / subscription type | ➖ | ✅ `plan_type` | ➖ |
595
+ | Account attribution | ✅ from `~/.claude.json` | ➖ no account file | ✅ from `account.json`, per provider |
596
+
597
+ Every adapter is read-only. Adapter formats drift between agent releases: an unparseable line is
598
+ skipped, never fatal to the file.
599
+
600
+ ### OpenCode is a live database, not a log
601
+
602
+ OpenCode keeps sessions, messages and message parts in SQLite — the same file its UI is writing to
603
+ while you work. So this adapter does not tail; it polls, on the same 5-second cycle as the tailer,
604
+ and it takes deliberate care not to be the reason your editor stutters or your history breaks:
605
+
606
+ - opened `readonly` **and** `fileMustExist`, with `PRAGMA query_only` — a bug here cannot write,
607
+ migrate or create anything;
608
+ - `PRAGMA busy_timeout` so a concurrent OpenCode write makes us wait briefly instead of failing;
609
+ - one short query at a time, then the handle is closed. No long transactions, ever.
610
+
611
+ Because `(path, inode, offset)` means nothing to a database, resumption uses three `time_updated`
612
+ cursors in the collector's own spool. A first-ever run reaches back 7 days, the same horizon the
613
+ tailer uses for transcripts.
614
+
615
+ ### Which account did this?
616
+
617
+ One machine often drives several accounts. Each session carries a stable, opaque `account.key`
618
+ (Claude Code's `accountUuid`; OpenCode's `<serviceID>:<accountId>`) so their costs never merge. The
619
+ readable half — email, organization name — is PII and is stripped in `metadata` mode, where sessions
620
+ still split correctly but the account shows as opaque.
621
+
622
+ **The credential is never read.** OpenCode's `account.json` stores a live API key next to the account
623
+ id; only `id` and `serviceID` are touched, and `auth.json` is never opened at all.
624
+
625
+ **Live events only.** These files record who is signed in *now* and are rewritten on account switch,
626
+ so events that predate the collector's start carry **no** account rather than today's — a
627
+ retroactive guess would look exactly like a fact.
628
+
629
+ Want an agent that is not here? Open an
630
+ [agent support request](https://github.com/agentstrack/collector/issues/new?template=agent_support.yml),
631
+ or write it — see [CONTRIBUTING.md](./CONTRIBUTING.md).
632
+
633
+ ---
634
+
635
+ ## Self-hosting
636
+
637
+ The collector speaks plain REST over HTTPS. Point it anywhere:
638
+
639
+ ```bash
640
+ agentstrack login <api-key> --api-url https://agentstrack.internal.example.com
641
+ ```
642
+
643
+ Or set `api_url` in `config.yaml` and restart. These are the only endpoints it calls:
644
+
645
+ | Method | Path | When |
646
+ |---|---|---|
647
+ | `POST` | `/v1/collector/register` | `login`, and once on daemon start if `collector_id` is missing |
648
+ | `GET` | `/v1/collector/config` | `doctor`, and each daemon start — org privacy ceiling + redaction rules. Not called by `login`: the register response already carries the ceiling |
649
+ | `POST` | `/v1/collector/health` | Every 60s while running — queue depth, version, detected agents |
650
+ | `POST` | `/v1/events/batch` | Every `upload.interval_seconds`, or when the queue reaches `batch_size` |
651
+
652
+ Authentication is `Authorization: Bearer <api_key>` on every request. Batch bodies over 1 KB are
653
+ gzipped (`content-encoding: gzip`).
654
+
655
+ ---
656
+
657
+ ## Troubleshooting
658
+
659
+ **Start here: `agentstrack doctor`.** It checks every failure mode below.
660
+
661
+ ### No sessions appearing
662
+
663
+ 1. `agentstrack status` — is `Service: installed` (or `Running: yes` for a foreground run), and does
664
+ each agent show a transcript count above zero?
665
+ 2. Do the transcripts exist and are they recent? The collector only reads files modified in the last
666
+ **7 days**:
667
+ ```bash
668
+ ls -lt ~/.claude/projects/*/*.jsonl | head
669
+ find ~/.codex/sessions -name '*.jsonl' -mtime -7 | head
670
+ ```
671
+ 3. Is the project on your exclusion list? `agentstrack config --show-effective | grep -A3 excluded`
672
+ 4. Is the agent enabled under `tracking.agents`?
673
+ 5. Is anything queued but stuck? `agentstrack sync --dry-run` shows the head of the queue.
674
+
675
+ ### The queue is not draining
676
+
677
+ `agentstrack status` shows `Queue depth` climbing. Check `agentstrack doctor`, then the log:
678
+
679
+ | Symptom | Cause | Fix |
680
+ |---|---|---|
681
+ | `401` in the log | Key revoked or wrong | `agentstrack login <new-key>` |
682
+ | `403` | Key lacks ingest permission | Issue a new key |
683
+ | `fetch failed`, `ETIMEDOUT` | Network, VPN or proxy | Set `HTTPS_PROXY`; events keep spooling meanwhile |
684
+ | `Server rejected the batch as too large` | Batch above the server's limit | Automatic — batch size halves and recovers |
685
+ | `Batch permanently rejected: … (dropped N)` | Non-retryable `4xx` | N events **in that batch** hit `max_retries` and were deleted. Nothing outside the batch is touched. Check the API version matches the collector's schema. |
686
+
687
+ Retryable failures never lose anything: `spool.db` is durable across restarts and reboots, and
688
+ draining resumes automatically.
689
+
690
+ ### Permission errors
691
+
692
+ ```
693
+ EACCES: permission denied, open '/Users/you/.claude/projects/…/abc.jsonl'
694
+ ```
695
+
696
+ The collector runs as **you** and needs read access to the agent log directories plus read/write on
697
+ `~/.agentstrack`. It never needs root — do not run it with `sudo`, since a root-owned spool is the
698
+ usual cause of this error showing up later.
699
+
700
+ ```bash
701
+ ls -ld ~/.agentstrack ~/.claude/projects ~/.codex/sessions
702
+ ```
703
+
704
+ `~/.agentstrack` is created mode `700`, `config.yaml` and `spool.db` (with its `-wal` / `-shm`
705
+ files) mode `600` — the collector sets those itself, so you should not have to. If an older install
706
+ or a `sudo` run left them wider, this puts them back:
707
+
708
+ ```bash
709
+ chmod 700 ~/.agentstrack && chmod 600 ~/.agentstrack/config.yaml ~/.agentstrack/spool.db*
710
+ ```
711
+
712
+ On macOS, if your agent directories sit under Documents or Desktop, grant your terminal (and, for the
713
+ service, `node`) Full Disk Access in System Settings → Privacy & Security.
714
+
715
+ ### Reading the log
716
+
717
+ ```bash
718
+ tail -f ~/.agentstrack/collector.log
719
+ grep -i "error\|failed\|rejected" ~/.agentstrack/collector.log | tail -20
720
+ ```
721
+
722
+ The log records counts, queue depths and `event_id`s — never payloads, prompts, code or keys. That is
723
+ what makes it safe to attach to an issue. Please attach `agentstrack doctor --json` too.
724
+
725
+ ### Complete reset
726
+
727
+ ```bash
728
+ agentstrack stop
729
+ rm -rf ~/.agentstrack # config, spool, log, pidfile — all local state
730
+ agentstrack login <api-key>
731
+ agentstrack start
732
+ ```
733
+
734
+ ---
735
+
736
+ ## Roadmap
737
+
738
+ Honest list of things that are **not** in 0.1.0, so you do not go looking for them.
739
+ [ROADMAP.md](./ROADMAP.md) has the same list with the design constraints and what "help wanted"
740
+ means for each.
741
+
742
+ - **Backfill window control** (`sync --since 30d`). Today the daemon reads whatever was modified in the last 7 days, and that window is not configurable.
743
+ - **`config get` / `config set` / `config edit`** — edit the YAML by hand for now.
744
+ - **`--verbose` logging** and per-run agent selection (`start --agent codex`); use `tracking.agents`.
745
+ - **Local time accounting.** Human-active / agent-active / idle windows are derived server-side from the event stream; `tracking.idle_timeout_seconds` is parsed by the collector but not used by it.
746
+ - **Process metrics.** `tracking.process_metrics` is accepted and ignored.
747
+ - **`session.ended`, `heartbeat`, `model.request` and `git.branch_changed`** are in the schema but no adapter emits them yet.
748
+ - **Local task classification** (`task_category`) — the field exists in the schema; the collector only derives a title.
749
+ - **Windows.** The service installer covers launchd and systemd only; `--foreground` works anywhere Node 24+ does.
750
+ - **Content-derived `event_id`.** Ids are random per enqueue, so retrying a batch is safe but re-reading a truncated transcript would create duplicates.
751
+ - **`MultiEdit`.** The Claude Code adapter derives file changes from `Edit`, `Write`, `NotebookEdit` and `Read`; a `MultiEdit` call is still recorded as `tool.started`/`tool.completed`, but produces no `file.changed` events and no line counts.
752
+
753
+ ---
754
+
755
+ ## Contributing
756
+
757
+ The single highest-value contribution is **a new agent adapter**, and it is smaller than it sounds:
758
+ one file implementing three methods — `detect()`, `health()`, `normalize()` — plus a redacted fixture
759
+ and a test. There is deliberately no `installHooks()`; if an agent cannot be observed by reading files
760
+ it already writes, open an issue before writing code.
761
+
762
+ [CONTRIBUTING.md](./CONTRIBUTING.md) walks the whole thing: dev setup is `npm install && npm test`,
763
+ and running against a local API is one environment variable.
764
+
765
+ - 🐛 [Bug report](https://github.com/agentstrack/collector/issues/new?template=bug_report.yml)
766
+ - 💡 [Feature request](https://github.com/agentstrack/collector/issues/new?template=feature_request.yml)
767
+ - 🤖 [Request an agent](https://github.com/agentstrack/collector/issues/new?template=agent_support.yml)
768
+ - 🙋 [Getting help](./SUPPORT.md) — where a question goes versus a bug versus a vulnerability
769
+ - 🗺 [Roadmap](./ROADMAP.md) — the known gaps, and which are good first issues
770
+ - 🏛 [Governance](./GOVERNANCE.md) — how decisions get made and how to become a maintainer
771
+ - 🔒 [Security policy](./SECURITY.md) — report vulnerabilities privately
772
+ - 📜 [Code of Conduct](./CODE_OF_CONDUCT.md)
773
+
774
+ ## License
775
+
776
+ [Apache License 2.0](./LICENSE) © AgentsTrack contributors.
777
+
778
+ The collector is open source and always will be. It is the part that runs on your machine and reads
779
+ your files — you should be able to audit every line of it.