agenttelemetry-app 2.2.0__tar.gz → 2.3.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 (61) hide show
  1. {agenttelemetry_app-2.2.0/agenttelemetry_app.egg-info → agenttelemetry_app-2.3.0}/PKG-INFO +33 -18
  2. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/README.md +32 -17
  3. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/__init__.py +1 -1
  4. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/accounts.py +3 -3
  5. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0/agenttelemetry_app.egg-info}/PKG-INFO +33 -18
  6. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/dashboard.py +25 -15
  7. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/index.html +4 -2
  8. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/native.py +99 -0
  9. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/native_activity.py +7 -3
  10. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/parser.py +36 -13
  11. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/pyproject.toml +1 -1
  12. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/release_notes.py +3 -1
  13. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/session_names.py +28 -12
  14. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/core.js +7 -1
  15. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/views.js +9 -3
  16. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/LICENSE +0 -0
  17. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/MANIFEST.in +0 -0
  18. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/__main__.py +0 -0
  19. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/agenttelemetry_app.egg-info/SOURCES.txt +0 -0
  20. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/agenttelemetry_app.egg-info/dependency_links.txt +0 -0
  21. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/agenttelemetry_app.egg-info/entry_points.txt +0 -0
  22. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/agenttelemetry_app.egg-info/top_level.txt +0 -0
  23. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/antigravity.py +0 -0
  24. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/chart.umd.min.js +0 -0
  25. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/cli.py +0 -0
  26. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/cursor_data.py +0 -0
  27. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/diagnostics.py +0 -0
  28. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/docs/antigravity.md +0 -0
  29. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/docs/native-activity.md +0 -0
  30. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/docs/python-package.md +0 -0
  31. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/docs/release-notes.md +0 -0
  32. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/editor_snapshot.py +0 -0
  33. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/ledger_backups.py +0 -0
  34. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/local_servers.py +0 -0
  35. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/local_servers_windows.py +0 -0
  36. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/manifest.json +0 -0
  37. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/session_timeline.py +0 -0
  38. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/setup.cfg +0 -0
  39. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/setup.py +0 -0
  40. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/app.css +0 -0
  41. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/apple-touch-icon.png +0 -0
  42. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/charts.js +0 -0
  43. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/icon-192.png +0 -0
  44. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/icon-512.png +0 -0
  45. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/maintenance.js +0 -0
  46. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/release-notes.js +0 -0
  47. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/timeline.js +0 -0
  48. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/LICENSE.antigravity.txt +0 -0
  49. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/LICENSE.hermes.txt +0 -0
  50. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/LICENSE.openclaw.txt +0 -0
  51. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/LICENSE.simple-icons.txt +0 -0
  52. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/README.md +0 -0
  53. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/antigravity.svg +0 -0
  54. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/claude.svg +0 -0
  55. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/copilot.svg +0 -0
  56. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/cursor.svg +0 -0
  57. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/hermes.svg +0 -0
  58. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/openai.svg +0 -0
  59. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/openclaw.svg +0 -0
  60. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/static/tool-icons/opencode.svg +0 -0
  61. {agenttelemetry_app-2.2.0 → agenttelemetry_app-2.3.0}/sw.js +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenttelemetry-app
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: A private, local dashboard for AI coding agent usage
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/uttamdeb/agent-telemetry
@@ -47,6 +47,12 @@ Covers **Claude Code · Claude Desktop · Codex · GitHub Copilot · Cursor · A
47
47
  [Browser dashboard (Python or source)](#quick-start) ·
48
48
  [Product page](https://agenttelemetry-app.vercel.app/)
49
49
 
50
+ **AgentTelemetry Pro (optional).** Everything above stays free, open source and local. [AgentTelemetry
51
+ Pro](https://agenttelemetry-app.vercel.app/#pro) is a separate add-on with a sign-in: your dashboard in any browser or
52
+ on your phone; chat with your running Claude Code and Codex sessions from the web, your phone or the Pro app on Mac
53
+ and Windows; answering Claude Code's permission prompts from your phone; sessions that message each other across
54
+ your devices; Optimize; and the local MCP bridge. See [Free vs Pro](https://agenttelemetry-app.vercel.app/#pro).
55
+
50
56
  ## Download the app
51
57
 
52
58
  Choose the installer for your platform from the release's **Assets** section.
@@ -70,9 +76,9 @@ These builds are unsigned/unnotarized. macOS may require **System Settings → P
70
76
  Security → Open Anyway**; Windows SmartScreen may require **More info → Run anyway**.
71
77
  [Product and install page](https://agenttelemetry-app.vercel.app/) ·
72
78
  [All releases](https://github.com/uttamdeb/agent-telemetry/releases) ·
73
- [What's new in 2.2.0](docs/releases/v2.2.0.md) ·
79
+ [What's new in 2.3.0](docs/releases/v2.3.0.md) ·
74
80
  [SHA-256 checksums in release assets](https://github.com/uttamdeb/agent-telemetry/releases/latest) ·
75
- [PyPI package 2.2.0](https://pypi.org/project/agenttelemetry-app/2.2.0/)
81
+ [PyPI package 2.3.0](https://pypi.org/project/agenttelemetry-app/2.3.0/)
76
82
 
77
83
  ---
78
84
 
@@ -386,12 +392,16 @@ anything is saved. Close the session in its original tool before confirming, and
386
392
  that tool if needed. Token, cost, prompt, tool-call and compaction records stay intact.
387
393
  Previous title metadata is backed up privately in
388
394
  `AgentTelemetryNative/session-title-backups/`, separately from the usage ledger.
389
- This supports local Claude Code transcripts, Codex through its installed CLI’s
395
+ This supports local Claude Code transcripts, Codex through its
390
396
  [official name API](https://learn.chatgpt.com/docs/app-server), and OpenCode’s SQLite
391
- session stores. Other formats, remote sessions and deleted logs cannot be edited.
392
- Recognized local Claude subagent transcripts instead offer **Name in AgentTelemetry**.
393
- This saves a private display name on this device without editing Claude's transcript,
394
- native subagent name, or parent session. The preview states this scope explicitly.
397
+ session stores. Codex's program is found wherever its apps install it (the ChatGPT app,
398
+ the Codex editor extension, the Codex desktop app on Windows, or a Codex CLI), with no
399
+ need for it on your PATH. Remote sessions and deleted logs cannot be renamed.
400
+ Recognized local Claude subagent transcripts, and sessions from tools whose own title
401
+ can't be edited safely (Copilot, Cursor, Claude Desktop, Antigravity, Hermes, OpenClaw),
402
+ instead offer **Name in AgentTelemetry**. This saves a private display name on this
403
+ device without editing the tool's files, Claude's transcript, native subagent name, or
404
+ parent session. The preview states this scope explicitly.
395
405
  These names live in `AgentTelemetryNative/session-display-names.json`, outside the
396
406
  usage ledger, summaries, and device sharing. Enter the source title again to remove
397
407
  a display-name override. See [subagent name behavior](docs/subagent-sessions.md).
@@ -479,9 +489,10 @@ dollars**, so cost is always derived. Rates live in `parser.py → PRICING` as
479
489
  `(input, output, cache_write_5m, cache_write_1h, cache_read)` per 1M tokens; edit freely
480
490
  (recomputed on each request, no re-parse needed).
481
491
 
482
- - **Anthropic** rates are current list prices (Opus 5.5 $4/$20, Opus 5 & 4.x $5/$25, Sonnet 5
483
- & 5.5 $2/$10, Sonnet 4.x $3/$15, Haiku 4.5 $1/$5; cache write 1.25×/2× input for 5-min/1-hour, cache
484
- read 0.1× — 0.05× on Opus 5.5). **OpenAI** GPT-5.4/5.5/5.6/6 are verified from OpenAI docs;
492
+ - **Anthropic** rates are current list prices (Fable & Mythos 5.2 $8/$40, Fable & Mythos 5/5.1
493
+ $10/$50, Opus 5.5 $4/$20, Opus 5 & 4.x $5/$25, Sonnet 5 & 5.5 $2/$10, Sonnet 4.x $3/$15, Haiku
494
+ 4.5 $1/$5; cache write 1.25×/2× input for 5-min/1-hour, cache read 0.1× — 0.05× on Opus 5.5 and
495
+ Sonnet 5.5, 0.025× on Fable & Mythos 5.1/5.2). **OpenAI** GPT-5.4/5.5/5.6/6 are verified from OpenAI docs;
485
496
  older/other models are estimates.
486
497
  - **Haiku 5.5** uses [Anthropic's published rates](https://platform.claude.com/docs/en/about-claude/pricing):
487
498
  $0.10/$0.50 per million input/output tokens up to 100K prompt tokens, $0.50/$2.50
@@ -490,7 +501,8 @@ dollars**, so cost is always derived. Rates live in `parser.py → PRICING` as
490
501
  message counts stay unchanged. Older archives/peers and other tools without that
491
502
  detail use the short-prompt rate.
492
503
  - **Price changes are date-aware.** When a vendor cuts a price (OpenAI's GPT-5.6 cuts of
493
- 2026-07-30 and 2026-08-21), usage from before the change keeps the rate it had then —
504
+ 2026-07-30 and 2026-08-21, Anthropic's Sonnet 5.5 cache-read cut of 2026-10-07), usage
505
+ from before the change keeps the rate it had then —
494
506
  see `parser.py → PRICE_HISTORY`.
495
507
  - **These are API-equivalent values.** If you're on a subscription (Claude Max/Pro, Codex,
496
508
  Copilot), you don't pay per token — the $ is "what this would cost at API rates."
@@ -512,12 +524,15 @@ dollars**, so cost is always derived. Rates live in `parser.py → PRICING` as
512
524
  when it runs through supported tools, at the Standard rate for prompts up to 200K
513
525
  tokens. Live, TTS, image and embedding models are not priced, and nor are the higher Pro rate
514
526
  for longer prompts or audio input. The Gemini CLI itself writes no token counts to read.
515
- - **Gemini 4 Argon** is recognized across the supported tools and uses Google's
516
- [announced introductory rates](https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-4-argon/):
517
- $2 input, $10 output and $0.10 cached input per 1M tokens. Access is still limited to
518
- trusted testers, and Google has not listed a public API model ID. These are API-equivalent
519
- estimates for Argon usage found in logs. The later $4/$20 rate has no effective date yet
520
- and is not applied ahead of time; cache storage charges are not modeled.
527
+ - **Gemini 4 Argon and Gemini 4 Carbon** (`gemini-4-argon`, `gemini-4-carbon`) are recognized
528
+ across the supported tools and use Google's [published rates](https://aistudio.google.com/docs/pricing):
529
+ $2 input, $10 output and $0.10 cached input per 1M tokens through November 30, 2026. Google
530
+ doubles them to $4, $20 and $0.20 on December 1, 2026. The higher rate is not applied ahead
531
+ of time: it goes in once it takes effect, and earlier usage keeps the rate it had then.
532
+ Cache storage charges are not modeled.
533
+ - **Gemini 4 Hydrogen** (`gemini-4-hydrogen`) uses Google's paid rate: $1.50 input, $7.50 output
534
+ and $0.075 cached input per 1M tokens. Its free tier can't be told apart in logs, so usage
535
+ is shown at the paid rate.
521
536
 
522
537
  ## Note on log retention
523
538
 
@@ -23,6 +23,12 @@ Covers **Claude Code · Claude Desktop · Codex · GitHub Copilot · Cursor · A
23
23
  [Browser dashboard (Python or source)](#quick-start) ·
24
24
  [Product page](https://agenttelemetry-app.vercel.app/)
25
25
 
26
+ **AgentTelemetry Pro (optional).** Everything above stays free, open source and local. [AgentTelemetry
27
+ Pro](https://agenttelemetry-app.vercel.app/#pro) is a separate add-on with a sign-in: your dashboard in any browser or
28
+ on your phone; chat with your running Claude Code and Codex sessions from the web, your phone or the Pro app on Mac
29
+ and Windows; answering Claude Code's permission prompts from your phone; sessions that message each other across
30
+ your devices; Optimize; and the local MCP bridge. See [Free vs Pro](https://agenttelemetry-app.vercel.app/#pro).
31
+
26
32
  ## Download the app
27
33
 
28
34
  Choose the installer for your platform from the release's **Assets** section.
@@ -46,9 +52,9 @@ These builds are unsigned/unnotarized. macOS may require **System Settings → P
46
52
  Security → Open Anyway**; Windows SmartScreen may require **More info → Run anyway**.
47
53
  [Product and install page](https://agenttelemetry-app.vercel.app/) ·
48
54
  [All releases](https://github.com/uttamdeb/agent-telemetry/releases) ·
49
- [What's new in 2.2.0](docs/releases/v2.2.0.md) ·
55
+ [What's new in 2.3.0](docs/releases/v2.3.0.md) ·
50
56
  [SHA-256 checksums in release assets](https://github.com/uttamdeb/agent-telemetry/releases/latest) ·
51
- [PyPI package 2.2.0](https://pypi.org/project/agenttelemetry-app/2.2.0/)
57
+ [PyPI package 2.3.0](https://pypi.org/project/agenttelemetry-app/2.3.0/)
52
58
 
53
59
  ---
54
60
 
@@ -362,12 +368,16 @@ anything is saved. Close the session in its original tool before confirming, and
362
368
  that tool if needed. Token, cost, prompt, tool-call and compaction records stay intact.
363
369
  Previous title metadata is backed up privately in
364
370
  `AgentTelemetryNative/session-title-backups/`, separately from the usage ledger.
365
- This supports local Claude Code transcripts, Codex through its installed CLI’s
371
+ This supports local Claude Code transcripts, Codex through its
366
372
  [official name API](https://learn.chatgpt.com/docs/app-server), and OpenCode’s SQLite
367
- session stores. Other formats, remote sessions and deleted logs cannot be edited.
368
- Recognized local Claude subagent transcripts instead offer **Name in AgentTelemetry**.
369
- This saves a private display name on this device without editing Claude's transcript,
370
- native subagent name, or parent session. The preview states this scope explicitly.
373
+ session stores. Codex's program is found wherever its apps install it (the ChatGPT app,
374
+ the Codex editor extension, the Codex desktop app on Windows, or a Codex CLI), with no
375
+ need for it on your PATH. Remote sessions and deleted logs cannot be renamed.
376
+ Recognized local Claude subagent transcripts, and sessions from tools whose own title
377
+ can't be edited safely (Copilot, Cursor, Claude Desktop, Antigravity, Hermes, OpenClaw),
378
+ instead offer **Name in AgentTelemetry**. This saves a private display name on this
379
+ device without editing the tool's files, Claude's transcript, native subagent name, or
380
+ parent session. The preview states this scope explicitly.
371
381
  These names live in `AgentTelemetryNative/session-display-names.json`, outside the
372
382
  usage ledger, summaries, and device sharing. Enter the source title again to remove
373
383
  a display-name override. See [subagent name behavior](docs/subagent-sessions.md).
@@ -455,9 +465,10 @@ dollars**, so cost is always derived. Rates live in `parser.py → PRICING` as
455
465
  `(input, output, cache_write_5m, cache_write_1h, cache_read)` per 1M tokens; edit freely
456
466
  (recomputed on each request, no re-parse needed).
457
467
 
458
- - **Anthropic** rates are current list prices (Opus 5.5 $4/$20, Opus 5 & 4.x $5/$25, Sonnet 5
459
- & 5.5 $2/$10, Sonnet 4.x $3/$15, Haiku 4.5 $1/$5; cache write 1.25×/2× input for 5-min/1-hour, cache
460
- read 0.1× — 0.05× on Opus 5.5). **OpenAI** GPT-5.4/5.5/5.6/6 are verified from OpenAI docs;
468
+ - **Anthropic** rates are current list prices (Fable & Mythos 5.2 $8/$40, Fable & Mythos 5/5.1
469
+ $10/$50, Opus 5.5 $4/$20, Opus 5 & 4.x $5/$25, Sonnet 5 & 5.5 $2/$10, Sonnet 4.x $3/$15, Haiku
470
+ 4.5 $1/$5; cache write 1.25×/2× input for 5-min/1-hour, cache read 0.1× — 0.05× on Opus 5.5 and
471
+ Sonnet 5.5, 0.025× on Fable & Mythos 5.1/5.2). **OpenAI** GPT-5.4/5.5/5.6/6 are verified from OpenAI docs;
461
472
  older/other models are estimates.
462
473
  - **Haiku 5.5** uses [Anthropic's published rates](https://platform.claude.com/docs/en/about-claude/pricing):
463
474
  $0.10/$0.50 per million input/output tokens up to 100K prompt tokens, $0.50/$2.50
@@ -466,7 +477,8 @@ dollars**, so cost is always derived. Rates live in `parser.py → PRICING` as
466
477
  message counts stay unchanged. Older archives/peers and other tools without that
467
478
  detail use the short-prompt rate.
468
479
  - **Price changes are date-aware.** When a vendor cuts a price (OpenAI's GPT-5.6 cuts of
469
- 2026-07-30 and 2026-08-21), usage from before the change keeps the rate it had then —
480
+ 2026-07-30 and 2026-08-21, Anthropic's Sonnet 5.5 cache-read cut of 2026-10-07), usage
481
+ from before the change keeps the rate it had then —
470
482
  see `parser.py → PRICE_HISTORY`.
471
483
  - **These are API-equivalent values.** If you're on a subscription (Claude Max/Pro, Codex,
472
484
  Copilot), you don't pay per token — the $ is "what this would cost at API rates."
@@ -488,12 +500,15 @@ dollars**, so cost is always derived. Rates live in `parser.py → PRICING` as
488
500
  when it runs through supported tools, at the Standard rate for prompts up to 200K
489
501
  tokens. Live, TTS, image and embedding models are not priced, and nor are the higher Pro rate
490
502
  for longer prompts or audio input. The Gemini CLI itself writes no token counts to read.
491
- - **Gemini 4 Argon** is recognized across the supported tools and uses Google's
492
- [announced introductory rates](https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-4-argon/):
493
- $2 input, $10 output and $0.10 cached input per 1M tokens. Access is still limited to
494
- trusted testers, and Google has not listed a public API model ID. These are API-equivalent
495
- estimates for Argon usage found in logs. The later $4/$20 rate has no effective date yet
496
- and is not applied ahead of time; cache storage charges are not modeled.
503
+ - **Gemini 4 Argon and Gemini 4 Carbon** (`gemini-4-argon`, `gemini-4-carbon`) are recognized
504
+ across the supported tools and use Google's [published rates](https://aistudio.google.com/docs/pricing):
505
+ $2 input, $10 output and $0.10 cached input per 1M tokens through November 30, 2026. Google
506
+ doubles them to $4, $20 and $0.20 on December 1, 2026. The higher rate is not applied ahead
507
+ of time: it goes in once it takes effect, and earlier usage keeps the rate it had then.
508
+ Cache storage charges are not modeled.
509
+ - **Gemini 4 Hydrogen** (`gemini-4-hydrogen`) uses Google's paid rate: $1.50 input, $7.50 output
510
+ and $0.075 cached input per 1M tokens. Its free tier can't be told apart in logs, so usage
511
+ is shown at the paid rate.
497
512
 
498
513
  ## Note on log retention
499
514
 
@@ -1,3 +1,3 @@
1
1
  """AgentTelemetry's local dashboard package."""
2
2
 
3
- __version__ = "2.2.0"
3
+ __version__ = "2.3.0"
@@ -148,8 +148,8 @@ def _environment(source):
148
148
 
149
149
 
150
150
  def _codex_read():
151
- executable = shutil.which("codex")
152
- if not executable: raise ValueError("Codex CLI is unavailable on the dashboard's PATH.")
151
+ executable = N.codex_executable()
152
+ if not executable: raise ValueError("Codex's command-line program wasn't found on this device.")
153
153
  # This child exposes only read methods. Never start/resume a thread/model turn.
154
154
  process = subprocess.Popen([executable, "app-server", "--listen", "stdio://", "-c", "analytics.enabled=false"],
155
155
  stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
@@ -183,7 +183,7 @@ def _codex_read():
183
183
  if not isinstance(result, dict): raise ValueError("Codex returned an unsupported account response.")
184
184
  return result
185
185
  try:
186
- request(1, "initialize", {"clientInfo": {"name": "agent_telemetry", "title": "AgentTelemetry", "version": "2.2.0"}})
186
+ request(1, "initialize", {"clientInfo": {"name": "agent_telemetry", "title": "AgentTelemetry", "version": "2.3.0"}})
187
187
  process.stdin.write('{"method":"initialized","params":{}}\n'); process.stdin.flush()
188
188
  before = request(2, "account/read", {"refreshToken": False}).get("account")
189
189
  limits = request(3, "account/rateLimits/read", {}) if isinstance(before, dict) and before.get("type") == "chatgpt" else {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenttelemetry-app
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: A private, local dashboard for AI coding agent usage
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/uttamdeb/agent-telemetry
@@ -47,6 +47,12 @@ Covers **Claude Code · Claude Desktop · Codex · GitHub Copilot · Cursor · A
47
47
  [Browser dashboard (Python or source)](#quick-start) ·
48
48
  [Product page](https://agenttelemetry-app.vercel.app/)
49
49
 
50
+ **AgentTelemetry Pro (optional).** Everything above stays free, open source and local. [AgentTelemetry
51
+ Pro](https://agenttelemetry-app.vercel.app/#pro) is a separate add-on with a sign-in: your dashboard in any browser or
52
+ on your phone; chat with your running Claude Code and Codex sessions from the web, your phone or the Pro app on Mac
53
+ and Windows; answering Claude Code's permission prompts from your phone; sessions that message each other across
54
+ your devices; Optimize; and the local MCP bridge. See [Free vs Pro](https://agenttelemetry-app.vercel.app/#pro).
55
+
50
56
  ## Download the app
51
57
 
52
58
  Choose the installer for your platform from the release's **Assets** section.
@@ -70,9 +76,9 @@ These builds are unsigned/unnotarized. macOS may require **System Settings → P
70
76
  Security → Open Anyway**; Windows SmartScreen may require **More info → Run anyway**.
71
77
  [Product and install page](https://agenttelemetry-app.vercel.app/) ·
72
78
  [All releases](https://github.com/uttamdeb/agent-telemetry/releases) ·
73
- [What's new in 2.2.0](docs/releases/v2.2.0.md) ·
79
+ [What's new in 2.3.0](docs/releases/v2.3.0.md) ·
74
80
  [SHA-256 checksums in release assets](https://github.com/uttamdeb/agent-telemetry/releases/latest) ·
75
- [PyPI package 2.2.0](https://pypi.org/project/agenttelemetry-app/2.2.0/)
81
+ [PyPI package 2.3.0](https://pypi.org/project/agenttelemetry-app/2.3.0/)
76
82
 
77
83
  ---
78
84
 
@@ -386,12 +392,16 @@ anything is saved. Close the session in its original tool before confirming, and
386
392
  that tool if needed. Token, cost, prompt, tool-call and compaction records stay intact.
387
393
  Previous title metadata is backed up privately in
388
394
  `AgentTelemetryNative/session-title-backups/`, separately from the usage ledger.
389
- This supports local Claude Code transcripts, Codex through its installed CLI’s
395
+ This supports local Claude Code transcripts, Codex through its
390
396
  [official name API](https://learn.chatgpt.com/docs/app-server), and OpenCode’s SQLite
391
- session stores. Other formats, remote sessions and deleted logs cannot be edited.
392
- Recognized local Claude subagent transcripts instead offer **Name in AgentTelemetry**.
393
- This saves a private display name on this device without editing Claude's transcript,
394
- native subagent name, or parent session. The preview states this scope explicitly.
397
+ session stores. Codex's program is found wherever its apps install it (the ChatGPT app,
398
+ the Codex editor extension, the Codex desktop app on Windows, or a Codex CLI), with no
399
+ need for it on your PATH. Remote sessions and deleted logs cannot be renamed.
400
+ Recognized local Claude subagent transcripts, and sessions from tools whose own title
401
+ can't be edited safely (Copilot, Cursor, Claude Desktop, Antigravity, Hermes, OpenClaw),
402
+ instead offer **Name in AgentTelemetry**. This saves a private display name on this
403
+ device without editing the tool's files, Claude's transcript, native subagent name, or
404
+ parent session. The preview states this scope explicitly.
395
405
  These names live in `AgentTelemetryNative/session-display-names.json`, outside the
396
406
  usage ledger, summaries, and device sharing. Enter the source title again to remove
397
407
  a display-name override. See [subagent name behavior](docs/subagent-sessions.md).
@@ -479,9 +489,10 @@ dollars**, so cost is always derived. Rates live in `parser.py → PRICING` as
479
489
  `(input, output, cache_write_5m, cache_write_1h, cache_read)` per 1M tokens; edit freely
480
490
  (recomputed on each request, no re-parse needed).
481
491
 
482
- - **Anthropic** rates are current list prices (Opus 5.5 $4/$20, Opus 5 & 4.x $5/$25, Sonnet 5
483
- & 5.5 $2/$10, Sonnet 4.x $3/$15, Haiku 4.5 $1/$5; cache write 1.25×/2× input for 5-min/1-hour, cache
484
- read 0.1× — 0.05× on Opus 5.5). **OpenAI** GPT-5.4/5.5/5.6/6 are verified from OpenAI docs;
492
+ - **Anthropic** rates are current list prices (Fable & Mythos 5.2 $8/$40, Fable & Mythos 5/5.1
493
+ $10/$50, Opus 5.5 $4/$20, Opus 5 & 4.x $5/$25, Sonnet 5 & 5.5 $2/$10, Sonnet 4.x $3/$15, Haiku
494
+ 4.5 $1/$5; cache write 1.25×/2× input for 5-min/1-hour, cache read 0.1× — 0.05× on Opus 5.5 and
495
+ Sonnet 5.5, 0.025× on Fable & Mythos 5.1/5.2). **OpenAI** GPT-5.4/5.5/5.6/6 are verified from OpenAI docs;
485
496
  older/other models are estimates.
486
497
  - **Haiku 5.5** uses [Anthropic's published rates](https://platform.claude.com/docs/en/about-claude/pricing):
487
498
  $0.10/$0.50 per million input/output tokens up to 100K prompt tokens, $0.50/$2.50
@@ -490,7 +501,8 @@ dollars**, so cost is always derived. Rates live in `parser.py → PRICING` as
490
501
  message counts stay unchanged. Older archives/peers and other tools without that
491
502
  detail use the short-prompt rate.
492
503
  - **Price changes are date-aware.** When a vendor cuts a price (OpenAI's GPT-5.6 cuts of
493
- 2026-07-30 and 2026-08-21), usage from before the change keeps the rate it had then —
504
+ 2026-07-30 and 2026-08-21, Anthropic's Sonnet 5.5 cache-read cut of 2026-10-07), usage
505
+ from before the change keeps the rate it had then —
494
506
  see `parser.py → PRICE_HISTORY`.
495
507
  - **These are API-equivalent values.** If you're on a subscription (Claude Max/Pro, Codex,
496
508
  Copilot), you don't pay per token — the $ is "what this would cost at API rates."
@@ -512,12 +524,15 @@ dollars**, so cost is always derived. Rates live in `parser.py → PRICING` as
512
524
  when it runs through supported tools, at the Standard rate for prompts up to 200K
513
525
  tokens. Live, TTS, image and embedding models are not priced, and nor are the higher Pro rate
514
526
  for longer prompts or audio input. The Gemini CLI itself writes no token counts to read.
515
- - **Gemini 4 Argon** is recognized across the supported tools and uses Google's
516
- [announced introductory rates](https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-4-argon/):
517
- $2 input, $10 output and $0.10 cached input per 1M tokens. Access is still limited to
518
- trusted testers, and Google has not listed a public API model ID. These are API-equivalent
519
- estimates for Argon usage found in logs. The later $4/$20 rate has no effective date yet
520
- and is not applied ahead of time; cache storage charges are not modeled.
527
+ - **Gemini 4 Argon and Gemini 4 Carbon** (`gemini-4-argon`, `gemini-4-carbon`) are recognized
528
+ across the supported tools and use Google's [published rates](https://aistudio.google.com/docs/pricing):
529
+ $2 input, $10 output and $0.10 cached input per 1M tokens through November 30, 2026. Google
530
+ doubles them to $4, $20 and $0.20 on December 1, 2026. The higher rate is not applied ahead
531
+ of time: it goes in once it takes effect, and earlier usage keeps the rate it had then.
532
+ Cache storage charges are not modeled.
533
+ - **Gemini 4 Hydrogen** (`gemini-4-hydrogen`) uses Google's paid rate: $1.50 input, $7.50 output
534
+ and $0.075 cached input per 1M tokens. Its free tier can't be told apart in logs, so usage
535
+ is shown at the paid rate.
521
536
 
522
537
  ## Note on log retention
523
538
 
@@ -661,7 +661,8 @@ def build_native_activity(observed_at=None):
661
661
  local_items = [(path_by_object.get(id(aggregate), aggregate.get("path", "")), aggregate)
662
662
  for aggregate in unique]
663
663
  snapshot = NA.project(local_items, observed_at=observed_at,
664
- identity=SN.identity, local_device=local_device)
664
+ identity=SN.identity, local_device=local_device,
665
+ display_scope=SN.display_scope)
665
666
  names = {}
666
667
  if NA.has_display_aliases(snapshot):
667
668
  try:
@@ -863,10 +864,11 @@ def set_session_name(body):
863
864
  aggregate, session = matches[0]
864
865
  path = aggregate["path"]
865
866
  original = session.get("title") or session.get("project") or session_id
866
- display_only = source == "claude" and bool(session.get("subagent"))
867
+ subagent = bool(session.get("subagent"))
868
+ display_only = SN.display_scope(source, path, subagent)
867
869
  if body.get("action") == "preview":
868
870
  if display_only:
869
- return SN.prepare_display(device, source, path, session_id, body.get("name"), original)
871
+ return SN.prepare_display(device, source, path, session_id, body.get("name"), original, subagent)
870
872
  return SN.prepare(device, source, path, session_id, body.get("name"), original)
871
873
  if body.get("action") != "rename":
872
874
  raise ValueError("Review the name-change preview before saving.")
@@ -962,14 +964,16 @@ def build_editor_snapshot(query):
962
964
  except ValueError:
963
965
  names, alias_error = {}, True
964
966
  for path, agg in items if names else ():
965
- if (str(path).startswith("peer:") or agg.get("source") != "claude" or not agg.get("subagent")
966
- or agg.get("_device") not in (None, "", local)):
967
+ if str(path).startswith("peer:") or agg.get("_device") not in (None, "", local):
967
968
  continue
968
969
  for session in agg.get("sessions", []):
970
+ if not SN.display_scope(agg.get("source"), agg.get("path", ""),
971
+ bool(session.get("subagent") or agg.get("subagent"))):
972
+ continue
969
973
  native_id = SN.identity(agg, session)
970
974
  if not native_id:
971
975
  continue
972
- alias = SN.display_name(names, local, "claude", agg.get("path", ""), native_id)
976
+ alias = SN.display_name(names, local, agg.get("source"), agg.get("path", ""), native_id)
973
977
  if alias:
974
978
  aliases[(agg.get("path", ""), native_id)] = alias
975
979
  result = ES.build_snapshot(items, local, query, refresh_sync()["revision"],
@@ -1623,10 +1627,11 @@ def _build_payload_locked():
1623
1627
  s2["account_metadata"] = A.session_metadata(agg)
1624
1628
  available, reason = SN.availability(source, agg.get("path", ""), remote)
1625
1629
  s2["rename_id"] = _session_identity(agg, session)
1626
- if not s2["rename_id"]:
1627
- available, reason = False, "The full native session identity is unavailable."
1628
- if source == "claude" and session.get("subagent"):
1629
- available, reason = SN.display_availability(source, agg.get("path", ""), True, remote)
1630
+ subagent = bool(session.get("subagent"))
1631
+ if SN.display_scope(source, agg.get("path", ""), subagent):
1632
+ # Claude subagents and tools whose own title can't be edited safely are
1633
+ # named in AgentTelemetry only; their source files are never written.
1634
+ available, reason = SN.display_availability(source, agg.get("path", ""), subagent, remote)
1630
1635
  if not remote:
1631
1636
  s2["rename_scope"] = "display"
1632
1637
  if display_name_error:
@@ -1636,6 +1641,8 @@ def _build_payload_locked():
1636
1641
  s2["original_title"] = session.get("title") or session.get("project") or s2["rename_id"]
1637
1642
  s2["title"] = alias
1638
1643
  s2["display_alias"] = True
1644
+ if not s2["rename_id"]:
1645
+ available, reason = False, "The full native session identity is unavailable."
1639
1646
  if session.get("retained"):
1640
1647
  available, reason = False, "This session was deleted in its tool. Its telemetry is preserved."
1641
1648
  s2["rename_available"], s2["rename_reason"] = available, reason
@@ -1808,21 +1815,24 @@ def _build_payload_locked():
1808
1815
  "pricing_long_context": {m: list(P.price_of(m, long_context=True))
1809
1816
  for m in set(P.LONG_CONTEXT_PRICING) | set(model_meta)
1810
1817
  if P.price_of(m, long_context=True) != P.price_of(m)},
1811
- "pricing_note": ("Anthropic costs use current list pricing (Fable 5 $10/$50, Opus 5.5 "
1818
+ "pricing_note": ("Anthropic costs use current list pricing (Fable & Mythos 5.2 $8/$40, "
1819
+ "Fable & Mythos 5/5.1 $10/$50, Opus 5.5 "
1812
1820
  "$4/$20, Opus 5 & 4.x $5/$25, Sonnet 5 & 5.5 $2/$10, Sonnet 4.x "
1813
1821
  "$3/$15, Haiku 4.5 $1/$5 per Mtok). Haiku 5.5 uses $0.10/$0.50 "
1814
1822
  "up to 100K prompt tokens and $0.50/$2.50 above 100K, including cache "
1815
1823
  "reads/writes in each request's threshold; available Claude logs retain "
1816
1824
  "that split, while older archives/peers and other tools without request "
1817
1825
  "detail use the short-prompt rate. Cache write is billed at 1.25x (5-min) "
1818
- "/ 2x (1-hour) input and cache read at 0.1x (0.05x on Opus 5.5, 0.025x on Fable 5.1); OpenAI/"
1826
+ "/ 2x (1-hour) input and cache read at 0.1x (0.05x on Opus 5.5 and on "
1827
+ "Sonnet 5.5 from 2026-10-07, 0.025x on Fable & Mythos 5.1/5.2); OpenAI/"
1819
1828
  "Codex/Copilot/Cursor prices are estimates. Codex GPT-5.4/5.5/5.6/6 use "
1820
1829
  "verified OpenAI list rates (e.g. GPT-6 Sol $2/$10, cached $0.20; GPT-6.1 Sol cached $0.10), each "
1821
1830
  "day priced at the rate in force then — OpenAI cut GPT-5.6 prices on "
1822
1831
  "2026-07-30 and 2026-08-21; a >272K-input surcharge is not modeled, so "
1823
- "heavy-context sessions may be higher. Gemini 4 Argon uses Google's "
1824
- "announced introductory rates ($2 input/$10 output, cached input $0.10 "
1825
- "per Mtok). Other Gemini text models use Standard rates for prompts "
1832
+ "heavy-context sessions may be higher. Gemini 4 Argon and Carbon use "
1833
+ "Google's rates through 2026-11-30 ($2 input/$10 output, cached input "
1834
+ "$0.10 per Mtok); Gemini 4 Hydrogen is $1.50/$7.50, cached $0.075. "
1835
+ "Other Gemini text models use Standard rates for prompts "
1826
1836
  "up to 200K tokens. "
1827
1837
  "Actual "
1828
1838
  "billing may differ. Claude Code/Desktop & Codex can run on either "
@@ -45,7 +45,9 @@
45
45
  <symbol id="i-storage" viewBox="0 0 16 16"><ellipse cx="8" cy="3.9" rx="5.2" ry="1.9"/><path d="M2.8 3.9v8.2c0 1.1 2.3 1.9 5.2 1.9s5.2-.8 5.2-1.9V3.9M2.8 8c0 1.1 2.3 1.9 5.2 1.9s5.2-.8 5.2-1.9"/></symbol>
46
46
  <symbol id="i-servers" viewBox="0 0 16 16"><rect x="2.3" y="2.2" width="11.4" height="4.1" rx="1.2"/><rect x="2.3" y="9.7" width="11.4" height="4.1" rx="1.2"/><path d="M4.5 4.2h.1M4.5 11.7h.1M7.2 4.2h4.2M7.2 11.7h4.2"/></symbol>
47
47
  <symbol id="i-refresh" viewBox="0 0 16 16"><path d="M13.2 8a5.2 5.2 0 1 1-1.6-3.8"/><path d="M13.2 2.6v2.9h-2.9"/></symbol>
48
- <symbol id="i-theme" viewBox="0 0 16 16"><circle cx="8" cy="8" r="5.6"/><path d="M8 2.4v11.2a5.6 5.6 0 0 0 0-11.2z" fill="currentColor" stroke="none"/></symbol>
48
+ <symbol id="i-theme-auto" viewBox="0 0 16 16"><circle cx="8" cy="8" r="5.6"/><path d="M8 2.4v11.2a5.6 5.6 0 0 0 0-11.2z" fill="currentColor" stroke="none"/></symbol>
49
+ <symbol id="i-theme-light" viewBox="0 0 16 16"><circle cx="8" cy="8" r="2.7"/><path d="M8 1.6v1.5M8 12.9v1.5M1.6 8h1.5M12.9 8h1.5M3.5 3.5l1.1 1.1M11.4 11.4l1.1 1.1M3.5 12.5l1.1-1.1M11.4 4.6l1.1-1.1"/></symbol>
50
+ <symbol id="i-theme-dark" viewBox="0 0 16 16"><path d="M13.3 9.7A5.6 5.6 0 0 1 6.3 2.7a5.6 5.6 0 1 0 7 7z"/></symbol>
49
51
  <symbol id="i-settings" viewBox="0 0 16 16"><path d="M12.76 6.86 L14.34 7.15 L14.34 8.85 L12.76 9.14 L12.18 10.56 L13.09 11.88 L11.88 13.09 L10.56 12.18 L9.14 12.76 L8.85 14.34 L7.15 14.34 L6.86 12.76 L5.44 12.18 L4.12 13.09 L2.91 11.88 L3.82 10.56 L3.24 9.14 L1.66 8.85 L1.66 7.15 L3.24 6.86 L3.82 5.44 L2.91 4.12 L4.12 2.91 L5.44 3.82 L6.86 3.24 L7.15 1.66 L8.85 1.66 L9.14 3.24 L10.56 3.82 L11.88 2.91 L13.09 4.12 L12.18 5.44Z" stroke-linejoin="round"/><circle cx="8" cy="8" r="2.1"/></symbol>
50
52
  <symbol id="i-sidebar" viewBox="0 0 16 16"><rect x="2" y="2.6" width="12" height="10.8" rx="1.8"/><path d="M6.2 2.6v10.8"/><path d="M4 5.4h.6M4 7.4h.6M4 9.4h.6"/></symbol>
51
53
  <symbol id="i-filter" viewBox="0 0 16 16"><path d="M2.3 3.2h11.4L9.3 8.5v4.3l-2.6 1.3V8.5z"/></symbol>
@@ -93,7 +95,7 @@
93
95
  </button>
94
96
  <div class="side-actions">
95
97
  <button class="icon-btn" id="refreshBtn" title="Refresh now (r)" aria-label="Refresh now"><svg class="ic"><use href="#i-refresh"/></svg></button>
96
- <button class="icon-btn" id="themeBtn" title="Theme (t)" aria-label="Theme"><svg class="ic"><use href="#i-theme"/></svg></button>
98
+ <button class="icon-btn" id="themeBtn" title="Theme: Auto (t)" aria-label="Theme: Auto. Change theme"><svg class="ic"><use href="#i-theme-auto"/></svg></button>
97
99
  <button class="icon-btn" id="settingsBtn" title="Settings" aria-label="Settings"><svg class="ic"><use href="#i-settings"/></svg></button>
98
100
  </div>
99
101
  <div class="side-note coverage-meta" id="coverage"></div>
@@ -3,7 +3,9 @@ import json
3
3
  import ntpath
4
4
  import os
5
5
  from pathlib import Path
6
+ import platform
6
7
  import posixpath
8
+ import re
7
9
  import subprocess
8
10
  import sys
9
11
  import threading
@@ -12,6 +14,103 @@ import uuid
12
14
 
13
15
  _lock = threading.Lock()
14
16
  REFRESH_INTERVALS = (0, 15, 60, 300, 900)
17
+ _CODEX_FOUND = {"at": None, "path": None}
18
+ _CODEX_TTL = 60
19
+ _EDITOR_DIR = re.compile(r"\.(vscode|cursor|windsurf|antigravity)")
20
+ _CODEX_EXTENSION = re.compile(r"openai\.chatgpt-(\d+(?:\.\d+)*)")
21
+
22
+
23
+ def codex_candidates(env=None, home=None, system=None, machine=None):
24
+ """Where Codex's command-line program may be, in the Pro connector's order.
25
+
26
+ A service started by the macOS or Windows app does not see the user's shell
27
+ PATH, and Codex's own apps never put their copy on it: the ChatGPT app keeps
28
+ one inside its bundle, each editor extension one per version, and the Codex
29
+ desktop app on Windows one per build. Paths only; nothing is started here.
30
+ """
31
+ env = os.environ if env is None else env
32
+ home = str(Path.home()) if home is None else home
33
+ system = sys.platform if system is None else system
34
+ windows = system == "win32"
35
+ join = ntpath.join if windows else posixpath.join
36
+ exe = "codex.exe" if windows else "codex"
37
+ machine = (platform.machine() if machine is None else machine).lower()
38
+ arm = machine in ("arm64", "aarch64")
39
+ found = []
40
+ override = env.get("CODEX_BIN")
41
+ if override:
42
+ found.append(override)
43
+ path_key = next((k for k in env if k.lower() == "path"), None) if windows else "PATH"
44
+ for folder in (env.get(path_key) or "").split(";" if windows else ":"):
45
+ if folder:
46
+ found.append(join(folder, exe))
47
+ if windows:
48
+ # The Codex desktop app's own copy, newest build first; it is never on the saved PATH.
49
+ root = join(env.get("LOCALAPPDATA") or join(home, "AppData", "Local"), "OpenAI", "Codex", "bin")
50
+ builds = []
51
+ try:
52
+ names = os.listdir(root)
53
+ except OSError:
54
+ names = []
55
+ for name in names:
56
+ path = join(root, name, exe)
57
+ try:
58
+ builds.append((os.stat(path).st_mtime, path))
59
+ except OSError:
60
+ continue
61
+ found += [path for _, path in sorted(builds, reverse=True)]
62
+ target = ("windows" if windows else "macos" if system == "darwin" else "linux") + (
63
+ "-aarch64" if arm else "-x86_64")
64
+ extensions = []
65
+ try:
66
+ editors = sorted(name for name in os.listdir(home) if _EDITOR_DIR.match(name))
67
+ except OSError:
68
+ editors = []
69
+ for editor in editors:
70
+ try:
71
+ names = os.listdir(join(home, editor, "extensions"))
72
+ except OSError:
73
+ continue
74
+ for name in names:
75
+ match = _CODEX_EXTENSION.match(name)
76
+ if match:
77
+ version = tuple(int(part) for part in match.group(1).split("."))
78
+ extensions.append((version, join(home, editor, "extensions", name, "bin", target, exe)))
79
+ found += [path for _, path in sorted(extensions, key=lambda item: item[0], reverse=True)]
80
+ if windows:
81
+ # An npm global install puts only codex.cmd on PATH; its real program is in a platform package.
82
+ package = "codex-win32-arm64" if arm else "codex-win32-x64"
83
+ binary = join("vendor", "aarch64-pc-windows-msvc" if arm else "x86_64-pc-windows-msvc", "bin", exe)
84
+ prefix = join(env.get("APPDATA") or join(home, "AppData", "Roaming"), "npm")
85
+ found += [join(prefix, "node_modules", "@openai", "codex", "node_modules", "@openai", package, binary),
86
+ join(prefix, "node_modules", "@openai", package, binary)]
87
+ else:
88
+ if system == "darwin":
89
+ app = join("Applications", "ChatGPT.app", "Contents", "Resources", "codex-cli",
90
+ "CodexCLI.app", "Contents", "MacOS", "codex")
91
+ found += ["/" + app, join(home, app)]
92
+ # Where a CLI install usually goes; a service's PATH often lacks them.
93
+ found += [join(folder, exe) for folder in ("/opt/homebrew/bin", "/usr/local/bin",
94
+ join(home, ".local", "bin"))]
95
+ unique = []
96
+ for path in found:
97
+ if path not in unique:
98
+ unique.append(path)
99
+ return unique
100
+
101
+
102
+ def codex_executable():
103
+ """The first runnable Codex program (see codex_candidates), or None. Cached for a minute."""
104
+ now = time.monotonic()
105
+ with _lock:
106
+ cached, at = _CODEX_FOUND["path"], _CODEX_FOUND["at"]
107
+ if at is not None and now - at < _CODEX_TTL and (cached is None or os.path.isfile(cached)):
108
+ return cached
109
+ path = next((candidate for candidate in codex_candidates()
110
+ if os.path.isfile(candidate) and os.access(candidate, os.X_OK)), None)
111
+ with _lock:
112
+ _CODEX_FOUND.update(at=now, path=path)
113
+ return path
15
114
 
16
115
 
17
116
  def _project_path(value):
@@ -235,7 +235,7 @@ def is_local_live_aggregate(aggregate, local_device=None) -> bool:
235
235
 
236
236
 
237
237
  def project(items: Iterable, *, observed_at=None, identity: Optional[Callable] = None,
238
- local_device: Optional[str] = None) -> dict:
238
+ local_device: Optional[str] = None, display_scope: Optional[Callable] = None) -> dict:
239
239
  """Build the bounded public snapshot and private alias references.
240
240
 
241
241
  ``items`` is the local ``(path, aggregate)`` list after the dashboard's
@@ -305,7 +305,11 @@ def project(items: Iterable, *, observed_at=None, identity: Optional[Callable] =
305
305
  if safe_availability:
306
306
  row["usage_availability"] = safe_availability
307
307
  alias = None
308
- if source == "claude" and subagent and isinstance(path, str):
308
+ # A private AgentTelemetry-only name: Claude subagents, and tools whose own
309
+ # title is never edited (display_scope decides; without it, Claude children only).
310
+ scoped = (display_scope(source, path, subagent) if callable(display_scope)
311
+ else source == "claude" and subagent)
312
+ if scoped and isinstance(path, str):
309
313
  alias_id = source_id[len("native:"):] if source_id.startswith("native:") else source_id
310
314
  alias = (local_device, source, path, alias_id)
311
315
  entry = {"row": row, "_alias": alias, "_sort": end}
@@ -330,7 +334,7 @@ def has_display_aliases(snapshot) -> bool:
330
334
 
331
335
 
332
336
  def apply_display_aliases(snapshot, names, display_name: Callable) -> dict:
333
- """Apply private Claude child names only after the session passed eligibility."""
337
+ """Apply private AgentTelemetry-only names only after the session passed eligibility."""
334
338
  public = []
335
339
  for row in snapshot.get("sessions", []):
336
340
  result = {key: value for key, value in row.items() if key != "_display_alias"}
@@ -200,23 +200,27 @@ EDITOR_LABEL = {
200
200
  # (input, output, cache_write_5m, cache_write_1h, cache_read)
201
201
  # Anthropic prices verified against the current model table (Opus 4.x = $5/$25,
202
202
  # NOT the old $15/$75 — Opus pricing dropped with 4.5). Cache write = 1.25x input
203
- # (5-min TTL) / 2x input (1-hour TTL); cache read = 0.1x input, EXCEPT Opus 5.5
204
- # (0.05x) and Fable/Mythos 5.1 (0.025x) — so the cache_read slot is written out per
205
- # row, never derived from input. Most OpenAI rows have no cache-write tier
203
+ # (5-min TTL) / 2x input (1-hour TTL); cache read = 0.1x input, EXCEPT Opus 5.5 and
204
+ # Sonnet 5.5 (0.05x) and Fable/Mythos 5.1 and 5.2 (0.025x) — so the cache_read slot is
205
+ # written out per row, never derived from input. Most OpenAI rows have no cache-write tier
206
206
  # (cw5/cw1 = 0, billed at the input rate); cached input goes in the cache_read slot.
207
207
  # Codex/Copilot/Cursor are subscription-billed, so their $ is an API-equivalent
208
208
  # estimate, not an actual charge. Costs are computed at request time — edit
209
209
  # freely, no re-parse needed.
210
210
  # ---------------------------------------------------------------------------
211
211
  PRICING = {
212
- # Anthropic Claude 5 family — verified against platform.claude.com pricing (2026-09-29).
212
+ # Anthropic Claude 5 family — verified against platform.claude.com pricing (2026-11-10).
213
+ # 5.2 (launched 2026-11-09) is $8/$40 with 5.1's 0.025x cache read ($0.20).
214
+ "Claude Fable 5.2": (8, 40, 10, 16, 0.20),
215
+ "Claude Mythos 5.2": (8, 40, 10, 16, 0.20),
213
216
  # 5.1 keeps 5's $10/$50 but cuts cache reads to 0.025x input ($0.25, not $1).
214
217
  "Claude Fable 5.1": (10, 50, 12.5, 20, 0.25),
215
218
  "Claude Mythos 5.1": (10, 50, 12.5, 20, 0.25),
216
219
  "Claude Fable 5": (10, 50, 12.5, 20, 1.0),
217
220
  "Claude Mythos 5": (10, 50, 12.5, 20, 1.0),
218
- # Sonnet 5.5 lists the same $2/$10 as Sonnet 5, with the usual 0.1x cache read.
219
- "Claude Sonnet 5.5": (2, 10, 2.5, 4, 0.20),
221
+ # Sonnet 5.5 lists the same $2/$10 as Sonnet 5. Its cache read was cut from 0.1x to
222
+ # 0.05x ($0.10) on 2026-10-07 (Anthropic's release notes); the old rate is in PRICE_HISTORY.
223
+ "Claude Sonnet 5.5": (2, 10, 2.5, 4, 0.10),
220
224
  # Sonnet 5's $2/$10 launch "intro" rate became its standard price — Anthropic
221
225
  # cancelled the $3/$15 increase scheduled for 2026-09-01, so there's no date split.
222
226
  "Claude Sonnet 5": (2, 10, 2.5, 4, 0.20),
@@ -311,6 +315,15 @@ PRICING = {
311
315
  # this tuple into PRICE_HISTORY as [("2026-11-30", ...)] and put (4, 20, 0, 0, 0.20)
312
316
  # here. Implicit caching only; cache storage is not modeled.
313
317
  "Gemini 4 Argon": (2.00, 10.00, 0, 0, 0.10),
318
+ # Gemini 4 Carbon — gemini-4-carbon; verified against aistudio.google.com/docs/pricing
319
+ # (2026-11-12): the same rate card as Argon, $2 / $10 / $0.10 cached "through November
320
+ # 30, 2026", then $4 / $20 / $0.20 "starting December 1, 2026". Move it on that date
321
+ # exactly as Argon above, never before. Implicit caching only.
322
+ "Gemini 4 Carbon": (2.00, 10.00, 0, 0, 0.10),
323
+ # Gemini 4 Hydrogen — gemini-4-hydrogen; verified against aistudio.google.com/docs/pricing
324
+ # (2026-11-13): $1.50 / $7.50 / $0.075 cached, one rate (no prompt-size tier, no dated
325
+ # change). Implicit caching only.
326
+ "Gemini 4 Hydrogen": (1.50, 7.50, 0, 0, 0.075),
314
327
  # 3.6 to 3.8 Flash are a promo "through December 31, 2026" and double on 2027-01-01:
315
328
  # then move these tuples into PRICE_HISTORY dated 2026-12-31 and put the doubled
316
329
  # ones here ($1.50 / $7.50, cache read $0.15).
@@ -346,6 +359,9 @@ PRICE_HISTORY = {
346
359
  # "Starting today", 2026-08-21: Sol -20% "for the next 3 months"
347
360
  # (community.openai.com/t/1391726).
348
361
  "GPT-5.6 Sol": [("2026-08-20", (5, 30, 0, 0, 0.50))],
362
+ # 2026-10-07: Sonnet 5.5 cache reads $0.20 -> $0.10; nothing else changed
363
+ # (platform.claude.com release notes).
364
+ "Claude Sonnet 5.5": [("2026-10-06", (2, 10, 2.5, 4, 0.20))],
349
365
  }
350
366
 
351
367
 
@@ -353,9 +369,12 @@ PRICE_HISTORY = {
353
369
  # model name so they show as their own row and price without a new record field.
354
370
  FAST_SUFFIX = " (fast)"
355
371
  US_SUFFIX = " (US)"
356
- # Fast mode's own rate card (platform.claude.com pricing, 2026-09): the cache
357
- # multipliers apply on top of the fast input rate, same as standard speed.
372
+ # Fast mode's own rate card (platform.claude.com pricing, 2026-09; Fable/Mythos 5.2
373
+ # added 2026-11-10): the cache multipliers apply on top of the fast input rate, same as
374
+ # standard speed.
358
375
  FAST_PRICING = {
376
+ "Claude Fable 5.2": (16, 80, 20, 32, 0.40),
377
+ "Claude Mythos 5.2": (16, 80, 20, 32, 0.40),
359
378
  "Claude Opus 5.5": (8, 40, 10, 16, 0.40),
360
379
  "Claude Opus 5": (10, 50, 12.5, 20, 1.0),
361
380
  "Claude Opus 4.8": (10, 50, 12.5, 20, 1.0),
@@ -465,8 +484,11 @@ def _canonicalize(name):
465
484
  if "raptor" in low:
466
485
  return "Raptor Mini"
467
486
 
468
- # Claude — "Claude Sonnet 4.5", "claude-sonnet-4-5", "claude-3.5-sonnet"
469
- tier = next((t.capitalize() for t in ("opus", "sonnet", "haiku") if t in low), None)
487
+ # Claude — "Claude Sonnet 4.5", "claude-sonnet-4-5", "claude-3.5-sonnet", "claude-fable-5-2".
488
+ # Fable and Mythos were missing here, so OpenCode, Hermes and Cursor ids for them priced at $0.
489
+ if "claude" in low and "mythos" in low and "preview" in low:
490
+ return "Claude Mythos Preview"
491
+ tier = next((t.capitalize() for t in ("opus", "sonnet", "haiku", "fable", "mythos") if t in low), None)
470
492
  if "claude" in low and tier:
471
493
  nums = re.findall(r"\d+(?:\.\d+)?", low)
472
494
  ver = ""
@@ -476,13 +498,14 @@ def _canonicalize(name):
476
498
  ver = nums[0]
477
499
  return f"Claude {tier} {ver}".strip()
478
500
 
479
- # Gemini — "gemini-3.1-pro-preview", "Google: Gemini 4 Argon", "gemini-4-argon"
501
+ # Gemini — "gemini-3.1-pro-preview", "Google: Gemini 4 Argon", "gemini-4-hydrogen"
480
502
  # → one display name. Preview/date/effort suffixes drop; Live, TTS, image, embedding and
481
503
  # the like are different products with their own rates, so they keep their own spelling.
482
- mgm = re.match(r"(?:models/)?gemini-?\s*(\d+(?:\.\d+)?)-?\s*(argon|pro|flash[\s\-]*lite|flash|computer-use)\b(.*)$", low)
504
+ mgm = re.match(r"(?:models/)?gemini-?\s*(\d+(?:\.\d+)?)-?\s*(argon|carbon|hydrogen|pro|flash[\s\-]*lite|flash|computer-use)\b(.*)$", low)
483
505
  if mgm and not re.search(r"live|tts|image|audio|embed|transcribe|translate|robot|omni",
484
506
  mgm.group(3)):
485
- kind = {"argon": "Argon", "pro": "Pro", "flash": "Flash", "computer-use": "Computer Use"}.get(
507
+ kind = {"argon": "Argon", "carbon": "Carbon", "hydrogen": "Hydrogen", "pro": "Pro",
508
+ "flash": "Flash", "computer-use": "Computer Use"}.get(
486
509
  mgm.group(2), "Flash-Lite")
487
510
  return f"Gemini {mgm.group(1)} {kind}"
488
511
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agenttelemetry-app"
7
- version = "2.2.0"
7
+ version = "2.3.0"
8
8
  description = "A private, local dashboard for AI coding agent usage"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -498,7 +498,9 @@ def _fetch_release(url: str, required_version: Optional[str] = None):
498
498
 
499
499
  def _placeholder(version: Optional[str], tag_hint: Optional[str] = None) -> dict:
500
500
  tag = tag_hint if tag_hint and _tag_version(tag_hint) and _tag_version(tag_hint)[0] == version else None
501
- url = _tag_url(tag) if tag else ""
501
+ # Releases are tagged vX.Y.Z, but a packaged app knows only its bare version
502
+ # ("2.2.0"), and /releases/tag/2.2.0 is a 404.
503
+ url = _tag_url("v" + tag.lstrip("v")) if tag else ""
502
504
  label = "AgentTelemetry " + (version or "release")
503
505
  return {
504
506
  "key": tag or version,
@@ -6,7 +6,6 @@ import os
6
6
  from pathlib import Path, PurePosixPath
7
7
  import queue
8
8
  import re
9
- import shutil
10
9
  import sqlite3
11
10
  import subprocess
12
11
  import threading
@@ -49,13 +48,29 @@ def claude_child_identity(path):
49
48
  return ""
50
49
 
51
50
 
51
+ def display_scope(source, path, subagent=False):
52
+ """Whether this session's name is kept only in AgentTelemetry.
53
+
54
+ Claude subagents, and every tool whose own title can't be edited safely
55
+ (Copilot, Cursor, Claude Desktop, Antigravity, Hermes, OpenClaw, OpenCode's
56
+ older JSON storage): their source files are never written for a name.
57
+ """
58
+ if source == "claude": return bool(subagent)
59
+ if source == "opencode": return not str(path).endswith(".db")
60
+ return source not in SUPPORTED
61
+
62
+
52
63
  def display_availability(source, path, subagent=False, remote=False):
53
64
  if remote: return False, "Name this session on its source device."
54
- if source != "claude" or not subagent or not claude_child_identity(path):
65
+ if source == "claude" and (not subagent or not claude_child_identity(path)):
55
66
  return False, "This Claude child transcript layout is not supported for a local display name."
67
+ if not display_scope(source, path, subagent):
68
+ return False, "This session is renamed in its own tool."
56
69
  if not os.path.isfile(path):
57
70
  return False, "The original log is unavailable; its saved telemetry is preserved."
58
- return True, "Names this subagent only in AgentTelemetry; Claude and its parent session stay unchanged."
71
+ if source == "claude":
72
+ return True, "Names this subagent only in AgentTelemetry; Claude and its parent session stay unchanged."
73
+ return True, "Names this session only in AgentTelemetry; the tool keeps its own title."
59
74
 
60
75
 
61
76
  def display_names():
@@ -134,11 +149,11 @@ def _write_display_names(names):
134
149
  except FileNotFoundError: pass
135
150
 
136
151
 
137
- def prepare_display(device, source, path, session_id, name, original_title):
152
+ def prepare_display(device, source, path, session_id, name, original_title, subagent=True):
138
153
  name = validate_name(name)
139
- ok, reason = display_availability(source, path, True)
154
+ ok, reason = display_availability(source, path, subagent)
140
155
  if not ok: raise ValueError(reason)
141
- if session_id != claude_child_identity(path):
156
+ if source == "claude" and session_id != claude_child_identity(path):
142
157
  raise ValueError("The Claude child identity does not match its discovered transcript.")
143
158
  names = display_names()
144
159
  stat = os.stat(path)
@@ -162,7 +177,7 @@ def commit_display(ticket, device, source, path, session_id, confirmed):
162
177
  plan = _plans.pop(ticket, None) if isinstance(ticket, str) else None
163
178
  if (plan is None or plan.get("scope") != "display" or time.monotonic() - plan["at"] > 300
164
179
  or [plan[k] for k in ("device", "source", "path", "id")] != [device, source, path, session_id]
165
- or session_id != claude_child_identity(path)):
180
+ or (source == "claude" and session_id != claude_child_identity(path))):
166
181
  raise ValueError("Display-name preview expired or the source changed. Review it again.")
167
182
  stat = os.stat(path)
168
183
  if ([stat.st_dev, stat.st_ino] != plan["file_identity"]
@@ -239,8 +254,9 @@ def availability(source, path, remote=False):
239
254
  try: uuid.UUID(Path(path).stem)
240
255
  except ValueError:
241
256
  return False, "This Claude transcript has no verified session UUID."
242
- if source == "codex" and not shutil.which("codex"):
243
- return False, "Codex's local CLI is required to use its official rename API."
257
+ if source == "codex" and not N.codex_executable():
258
+ return False, ("Codex's command-line program wasn't found. Install the ChatGPT app, "
259
+ "the Codex editor extension or the Codex CLI to rename Codex sessions.")
244
260
  if source == "codex":
245
261
  try: _codex_home(path)
246
262
  except ValueError as error: return False, str(error)
@@ -257,9 +273,9 @@ def _codex_home(path):
257
273
  def _codex_rpc(method, params, home=None):
258
274
  if method not in ("thread/read", "thread/name/set"):
259
275
  raise ValueError("Only Codex title metadata methods are permitted.")
260
- executable = shutil.which("codex")
276
+ executable = N.codex_executable()
261
277
  if not executable:
262
- raise ValueError("Codex CLI is unavailable.")
278
+ raise ValueError("Codex's command-line program wasn't found.")
263
279
  # A dedicated local metadata connection: never start/resume a thread or turn.
264
280
  environment = os.environ.copy()
265
281
  environment.pop("AGENT_TELEMETRY_CONTROL_TOKEN", None)
@@ -291,7 +307,7 @@ def _codex_rpc(method, params, home=None):
291
307
  return message.get("result", {})
292
308
  try:
293
309
  send({"id": 1, "method": "initialize", "params": {"clientInfo": {
294
- "name": "agent_telemetry", "title": "AgentTelemetry", "version": "2.2.0"}}})
310
+ "name": "agent_telemetry", "title": "AgentTelemetry", "version": "2.3.0"}}})
295
311
  receive(1); send({"method": "initialized", "params": {}})
296
312
  send({"id": 2, "method": method, "params": params})
297
313
  return receive(2)
@@ -574,7 +574,13 @@ function applyTheme(mode){
574
574
  matchMedia("(prefers-color-scheme: dark)").matches);
575
575
  root.dataset.resolved = dark?"dark":"light";
576
576
  try{ localStorage.setItem("aiu.theme", mode); }catch(e){}
577
- document.getElementById("themeBtn").title = "Theme: "+mode+" (t)";
577
+ // The button shows the mode in use: a sun for Light, a moon for Dark, half and half for Auto (follow the system).
578
+ const shown = mode==="light"||mode==="dark" ? mode : "auto";
579
+ const name = {auto:"Auto", light:"Light", dark:"Dark"}[shown];
580
+ const btn=document.getElementById("themeBtn");
581
+ btn.title = "Theme: "+name+" (t)";
582
+ btn.setAttribute("aria-label", "Theme: "+name+". Change theme");
583
+ btn.querySelector("use").setAttribute("href", "#i-theme-"+shown);
578
584
  if(RAW) renderAll();
579
585
  }
580
586
  function cycleTheme(){
@@ -1498,6 +1498,12 @@ function renameButton(s,text=false){
1498
1498
  '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="m10.5 2.5 3 3-7.5 7.5-4 1 1-4zM9 4l3 3"/></svg>'}</button>`;
1499
1499
  }
1500
1500
  let sessionRenameTarget=null, sessionRenameSaving=false;
1501
+ // What stays as it was when a name is kept in AgentTelemetry only: a Claude subagent's
1502
+ // transcript and parent, or another tool's own title (its files are never written).
1503
+ function displayNameKeeps(source){
1504
+ return source==='claude'?'Claude and the parent session stay unchanged.'
1505
+ :`${(SRC[source]||{label:source}).label} keeps its own title; its files are not changed.`;
1506
+ }
1501
1507
  function openSessionRename(identity){
1502
1508
  if(sessionRenameSaving) return;
1503
1509
  const s=RAW.sessions.find(x=>sessionIdentity(x)===identity); if(!s||s.rename_available===false) return;
@@ -1505,7 +1511,7 @@ function openSessionRename(identity){
1505
1511
  const display=s.rename_scope==='display';
1506
1512
  document.getElementById('sessionNameHeading').textContent=display?'Name in AgentTelemetry':'Rename session';
1507
1513
  document.getElementById('sessionNameHelp').textContent=display
1508
- ?'This display name is saved only in AgentTelemetry on this device. Claude and the parent session stay unchanged. You will review the change before saving.'
1514
+ ?`This display name is saved only in AgentTelemetry on this device. ${displayNameKeeps(s.source)} You will review the change before saving.`
1509
1515
  :'Renaming changes the original tool’s saved session name. You’ll review a warning before it is saved.';
1510
1516
  const input=document.getElementById('sessionNameInput');
1511
1517
  input.value=s.title||s.project||s.id;
@@ -1530,7 +1536,7 @@ async function saveSessionName(name){
1530
1536
  if(!await askConfirm({title:display?'Name in AgentTelemetry?':'Rename in '+(SRC[target.source]||{label:target.source}).label+'?',ok:display?'Save display name':'Rename in tool',
1531
1537
  body:display?`<p>This changes only the local display name in <b>AgentTelemetry</b>.</p>
1532
1538
  <p><b>${esc(preview.current_name)}</b> → <b>${esc(preview.name)}</b></p>
1533
- <div class="modal-note">Claude’s transcript, native subagent name, and parent session stay unchanged. The private name is not included in the usage ledger or device sharing.</div>
1539
+ <div class="modal-note">${target.source==='claude'?'Claude’s transcript, native subagent name, and parent session stay unchanged.':esc(displayNameKeeps(target.source))} The private name is not included in the usage ledger or device sharing.</div>
1534
1540
  <p>Target: <code>${esc(preview.target)}</code></p>`:
1535
1541
  `<p>This changes the name in the <b>original coding tool</b>, not only AgentTelemetry.</p>
1536
1542
  <p><b>${esc(preview.current_name)}</b> → <b>${esc(preview.name)}</b></p>
@@ -1657,7 +1663,7 @@ function openSessionDetails(s,related=false){
1657
1663
  ${sessionUsageDetails(s)}
1658
1664
  ${cursorDetailStatusDetails(s)}
1659
1665
  ${sessionUndatedUsageDetails(s)}
1660
- ${row("Name scope",s.rename_scope==='display'?'AgentTelemetry only — Claude and its parent session are unchanged.':null)}
1666
+ ${row("Name scope",s.rename_scope==='display'?(s.source==='claude'?'AgentTelemetry only — Claude and its parent session are unchanged.':`AgentTelemetry only — ${esc((SRC[s.source]||{label:s.source}).label)} keeps its own title.`):null)}
1661
1667
  ${row("Source title",s.display_alias?esc(s.original_title):null)}
1662
1668
  ${sessionDelegationDetails(s)}
1663
1669
  ${row("Device",multiDevice()?esc(deviceName(s.device||RAW.device.id)):null)}