tokenjam 0.3.4__tar.gz → 0.4.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 (296) hide show
  1. {tokenjam-0.3.4 → tokenjam-0.4.0}/.gitignore +4 -0
  2. {tokenjam-0.3.4 → tokenjam-0.4.0}/CLAUDE.md +64 -17
  3. {tokenjam-0.3.4 → tokenjam-0.4.0}/PKG-INFO +6 -2
  4. {tokenjam-0.3.4 → tokenjam-0.4.0}/README.md +4 -0
  5. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/installation.md +15 -0
  6. tokenjam-0.4.0/docs/internal/lens-vendor-versions.md +22 -0
  7. {tokenjam-0.3.4 → tokenjam-0.4.0}/pyproject.toml +10 -2
  8. {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/package.json +1 -1
  9. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/test_api.py +148 -0
  10. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/test_cli.py +100 -0
  11. tokenjam-0.4.0/tests/manual-new-release-tests.md +271 -0
  12. tokenjam-0.4.0/tests/manual-pre-release-runner.md +119 -0
  13. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/manual-pre-release-testing.md +63 -0
  14. tokenjam-0.4.0/tests/manual-pre-release-v0.4.0.md +291 -0
  15. tokenjam-0.4.0/tests/results/manual-pre-release-v0.4.0-20260619T202233Z.md +301 -0
  16. tokenjam-0.4.0/tests/results/manual-pre-release-v0.4.0-20260619T205108Z.md +293 -0
  17. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_cmd_tokenmaxx.py +21 -2
  18. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_cost.py +52 -2
  19. tokenjam-0.4.0/tests/unit/test_cost_compare_framing.py +87 -0
  20. tokenjam-0.4.0/tests/unit/test_framing.py +249 -0
  21. tokenjam-0.4.0/tests/unit/test_lens_ui_regression.py +123 -0
  22. tokenjam-0.4.0/tests/unit/test_no_tracked_dev_secrets.py +31 -0
  23. tokenjam-0.4.0/tests/unit/test_onboard_hygiene.py +39 -0
  24. tokenjam-0.4.0/tests/unit/test_onboard_plan.py +65 -0
  25. tokenjam-0.4.0/tests/unit/test_optimize_recoverable.py +209 -0
  26. tokenjam-0.4.0/tests/unit/test_optimize_reuse.py +248 -0
  27. tokenjam-0.4.0/tests/unit/test_reuse_skeleton.py +106 -0
  28. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_ui_offline.py +11 -1
  29. tokenjam-0.4.0/tokenjam/__init__.py +6 -0
  30. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/app.py +4 -1
  31. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/budget.py +7 -0
  32. tokenjam-0.4.0/tokenjam/api/routes/cost.py +125 -0
  33. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/cost_compare.py +18 -0
  34. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/optimize.py +38 -14
  35. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/traces.py +2 -1
  36. tokenjam-0.4.0/tokenjam/api/routes/version.py +20 -0
  37. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_cost.py +118 -53
  38. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_onboard.py +23 -3
  39. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_optimize.py +152 -89
  40. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_report.py +97 -7
  41. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_tokenmaxx.py +3 -30
  42. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_uninstall.py +15 -1
  43. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/main.py +5 -1
  44. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/api_backend.py +5 -0
  45. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/cost.py +15 -5
  46. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/db.py +17 -11
  47. tokenjam-0.4.0/tokenjam/core/export/reuse_report.py +418 -0
  48. tokenjam-0.4.0/tokenjam/core/export/reuse_skeleton.py +75 -0
  49. tokenjam-0.4.0/tokenjam/core/framing.py +333 -0
  50. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/models.py +2 -0
  51. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/__init__.py +6 -0
  52. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/cache_efficacy.py +60 -0
  53. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/model_downgrade.py +11 -0
  54. tokenjam-0.4.0/tokenjam/core/optimize/analyzers/plan_reuse.py +302 -0
  55. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/prompt_bloat.py +69 -0
  56. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/workflow_restructure.py +27 -2
  57. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/runner.py +46 -6
  58. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/types.py +66 -1
  59. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/mcp/server.py +14 -1
  60. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/pricing/models.toml +36 -0
  61. tokenjam-0.4.0/tokenjam/ui/index.html +2284 -0
  62. tokenjam-0.4.0/tokenjam/ui/vendor/uplot.css +2 -0
  63. tokenjam-0.4.0/tokenjam/ui/vendor/uplot.js +6 -0
  64. tokenjam-0.3.4/tests/manual-new-release-tests.md +0 -180
  65. tokenjam-0.3.4/tokenjam/__init__.py +0 -1
  66. tokenjam-0.3.4/tokenjam/api/routes/cost.py +0 -43
  67. tokenjam-0.3.4/tokenjam/ui/index.html +0 -1313
  68. {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/CODEOWNERS +0 -0
  69. {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  70. {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  71. {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/ISSUE_TEMPLATE/integration_request.md +0 -0
  72. {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/pull_request_template.md +0 -0
  73. {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/workflows/ci.yml +0 -0
  74. {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/workflows/publish-npm.yml +0 -0
  75. {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/workflows/publish-pypi.yml +0 -0
  76. {tokenjam-0.3.4 → tokenjam-0.4.0}/AGENTS.md +0 -0
  77. {tokenjam-0.3.4 → tokenjam-0.4.0}/CHANGELOG.md +0 -0
  78. {tokenjam-0.3.4 → tokenjam-0.4.0}/CONTRIBUTING.md +0 -0
  79. {tokenjam-0.3.4 → tokenjam-0.4.0}/LICENSE +0 -0
  80. {tokenjam-0.3.4 → tokenjam-0.4.0}/Makefile +0 -0
  81. {tokenjam-0.3.4 → tokenjam-0.4.0}/SECURITY.md +0 -0
  82. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/alerts.md +0 -0
  83. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/architecture.md +0 -0
  84. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/backfill/helicone.md +0 -0
  85. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/backfill/langfuse.md +0 -0
  86. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/backfill/otlp.md +0 -0
  87. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/backfill/overview.md +0 -0
  88. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/claude-code-integration.md +0 -0
  89. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/cli-reference.md +0 -0
  90. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/configuration.md +0 -0
  91. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/export.md +0 -0
  92. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/framework-support.md +0 -0
  93. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/internal/specs/.gitkeep +0 -0
  94. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/nemoclaw-integration.md +0 -0
  95. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/openclaw.md +0 -0
  96. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/optimize/cache.md +0 -0
  97. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/optimize/downsize.md +0 -0
  98. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/optimize/script.md +0 -0
  99. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/optimize/trim.md +0 -0
  100. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/policy/overview.md +0 -0
  101. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/python-sdk.md +0 -0
  102. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/screenshots/tj-alerts.png +0 -0
  103. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/screenshots/tj-budget.png +0 -0
  104. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/screenshots/tj-cost.png +0 -0
  105. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/screenshots/tj-status.png +0 -0
  106. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/screenshots/tj-traces.png +0 -0
  107. {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/typescript-sdk.md +0 -0
  108. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/README.md +0 -0
  109. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/alerts_and_drift/_shared.py +0 -0
  110. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/alerts_and_drift/budget_breach_demo.py +0 -0
  111. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/alerts_and_drift/drift_demo.py +0 -0
  112. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/alerts_and_drift/sensitive_actions_demo.py +0 -0
  113. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/rag_pipeline.py +0 -0
  114. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/research_team.py +0 -0
  115. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/router_agent.py +0 -0
  116. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/sample_docs/agent_patterns.txt +0 -0
  117. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/sample_docs/cost_management.txt +0 -0
  118. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/sample_docs/observability.txt +0 -0
  119. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/sample_docs/safety.txt +0 -0
  120. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/openclaw/README.md +0 -0
  121. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_framework/autogen_agent.py +0 -0
  122. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_framework/crewai_agent.py +0 -0
  123. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_framework/langchain_agent.py +0 -0
  124. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_framework/langgraph_agent.py +0 -0
  125. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_framework/llamaindex_agent.py +0 -0
  126. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_provider/anthropic_agent.py +0 -0
  127. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_provider/bedrock_agent.py +0 -0
  128. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_provider/gemini_agent.py +0 -0
  129. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_provider/litellm_agent.py +0 -0
  130. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_provider/openai_agent.py +0 -0
  131. {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_provider/openai_agents_sdk_agent.py +0 -0
  132. {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/hallucination-drift/BLOG.md +0 -0
  133. {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/hallucination-drift/README.md +0 -0
  134. {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/hallucination-drift/scenario.py +0 -0
  135. {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/retry-loop/BLOG.md +0 -0
  136. {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/retry-loop/README.md +0 -0
  137. {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/retry-loop/scenario.py +0 -0
  138. {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/surprise-cost/BLOG.md +0 -0
  139. {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/surprise-cost/README.md +0 -0
  140. {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/surprise-cost/scenario.py +0 -0
  141. {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/README.md +0 -0
  142. {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/package-lock.json +0 -0
  143. {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/client.test.ts +0 -0
  144. {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/client.ts +0 -0
  145. {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/index.ts +0 -0
  146. {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/semconv.test.ts +0 -0
  147. {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/semconv.ts +0 -0
  148. {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/span-builder.test.ts +0 -0
  149. {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/span-builder.ts +0 -0
  150. {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/types.ts +0 -0
  151. {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/tsconfig.json +0 -0
  152. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/__init__.py +0 -0
  153. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/__init__.py +0 -0
  154. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/email_agent_budget_breach.py +0 -0
  155. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/email_agent_drift.py +0 -0
  156. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/email_agent_loop.py +0 -0
  157. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/email_agent_normal.py +0 -0
  158. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/mock_llm.py +0 -0
  159. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/test_mock_scenarios.py +0 -0
  160. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/conftest.py +0 -0
  161. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/e2e/__init__.py +0 -0
  162. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/e2e/conftest.py +0 -0
  163. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/e2e/test_real_llm.py +0 -0
  164. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/factories.py +0 -0
  165. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/fixtures/helicone_real_response.json +0 -0
  166. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/fixtures/langfuse_real_response.json +0 -0
  167. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/fixtures/otlp_sample.json +0 -0
  168. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/__init__.py +0 -0
  169. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/test_db.py +0 -0
  170. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/test_demos.py +0 -0
  171. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/test_full_pipeline.py +0 -0
  172. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/test_logs_api.py +0 -0
  173. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/synthetic/__init__.py +0 -0
  174. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/synthetic/test_alert_rules.py +0 -0
  175. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/synthetic/test_cost_tracking.py +0 -0
  176. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/synthetic/test_drift_detection.py +0 -0
  177. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/synthetic/test_ingest.py +0 -0
  178. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/synthetic/test_schema_validation.py +0 -0
  179. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/toy_agent/toy_agent.py +0 -0
  180. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/__init__.py +0 -0
  181. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_alerts.py +0 -0
  182. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_backfill.py +0 -0
  183. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_cache_efficacy.py +0 -0
  184. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_cache_recommend.py +0 -0
  185. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_cmd_policy.py +0 -0
  186. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_cmd_stop.py +0 -0
  187. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_compare.py +0 -0
  188. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_config.py +0 -0
  189. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_config_secret_divergence.py +0 -0
  190. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_demo_env.py +0 -0
  191. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_demo_scenarios.py +0 -0
  192. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_drift.py +0 -0
  193. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_export_claude_code.py +0 -0
  194. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_formatting.py +0 -0
  195. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_ingest_helicone.py +0 -0
  196. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_ingest_langfuse.py +0 -0
  197. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_ingest_otlp.py +0 -0
  198. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_litellm_client.py +0 -0
  199. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_litellm_integration.py +0 -0
  200. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_logs_converter.py +0 -0
  201. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_mcp_server.py +0 -0
  202. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_models.py +0 -0
  203. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_onboard_codex.py +0 -0
  204. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_onboard_daemon.py +0 -0
  205. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_openclaw_ingest.py +0 -0
  206. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_optimize.py +0 -0
  207. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_pricing_override.py +0 -0
  208. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_prompt_bloat.py +0 -0
  209. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_spans_stats_repair.py +0 -0
  210. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_time_parse.py +0 -0
  211. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_transport_401.py +0 -0
  212. {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_workflow_restructure.py +0 -0
  213. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/__init__.py +0 -0
  214. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/deps.py +0 -0
  215. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/middleware.py +0 -0
  216. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/__init__.py +0 -0
  217. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/agents.py +0 -0
  218. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/alerts.py +0 -0
  219. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/drift.py +0 -0
  220. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/logs.py +0 -0
  221. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/metrics.py +0 -0
  222. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/otlp.py +0 -0
  223. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/spans.py +0 -0
  224. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/status.py +0 -0
  225. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/tools.py +0 -0
  226. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/__init__.py +0 -0
  227. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_alerts.py +0 -0
  228. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_backfill.py +0 -0
  229. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_budget.py +0 -0
  230. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_demo.py +0 -0
  231. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_doctor.py +0 -0
  232. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_drift.py +0 -0
  233. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_export.py +0 -0
  234. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_mcp.py +0 -0
  235. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_policy.py +0 -0
  236. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_serve.py +0 -0
  237. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_status.py +0 -0
  238. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_stop.py +0 -0
  239. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_tools.py +0 -0
  240. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_traces.py +0 -0
  241. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/__init__.py +0 -0
  242. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/alerts.py +0 -0
  243. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/backfill.py +0 -0
  244. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/config.py +0 -0
  245. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/drift.py +0 -0
  246. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/export/__init__.py +0 -0
  247. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/export/claude_code.py +0 -0
  248. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/ingest.py +0 -0
  249. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/ingest_adapters/__init__.py +0 -0
  250. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/ingest_adapters/helicone.py +0 -0
  251. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/ingest_adapters/langfuse.py +0 -0
  252. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/ingest_adapters/otlp.py +0 -0
  253. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/README.md +0 -0
  254. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/__init__.py +0 -0
  255. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/budget_projection.py +0 -0
  256. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/cache_recommend.py +0 -0
  257. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/registry.py +0 -0
  258. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/pricing.py +0 -0
  259. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/retention.py +0 -0
  260. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/schema_validator.py +0 -0
  261. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/demo/__init__.py +0 -0
  262. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/demo/env.py +0 -0
  263. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/mcp/__init__.py +0 -0
  264. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/otel/__init__.py +0 -0
  265. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/otel/exporters.py +0 -0
  266. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/otel/otlp_parsing.py +0 -0
  267. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/otel/provider.py +0 -0
  268. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/otel/semconv.py +0 -0
  269. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/py.typed +0 -0
  270. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/__init__.py +0 -0
  271. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/agent.py +0 -0
  272. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/bootstrap.py +0 -0
  273. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/client.py +0 -0
  274. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/http_exporter.py +0 -0
  275. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/__init__.py +0 -0
  276. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/anthropic.py +0 -0
  277. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/autogen.py +0 -0
  278. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/base.py +0 -0
  279. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/bedrock.py +0 -0
  280. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/crewai.py +0 -0
  281. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/gemini.py +0 -0
  282. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/langchain.py +0 -0
  283. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/langgraph.py +0 -0
  284. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/litellm.py +0 -0
  285. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/llamaindex.py +0 -0
  286. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/nemoclaw.py +0 -0
  287. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/openai.py +0 -0
  288. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/openai_agents_sdk.py +0 -0
  289. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/transport.py +0 -0
  290. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/ui/vendor/htm.js +0 -0
  291. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/ui/vendor/preact-hooks.js +0 -0
  292. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/ui/vendor/preact.js +0 -0
  293. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/utils/__init__.py +0 -0
  294. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/utils/formatting.py +0 -0
  295. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/utils/ids.py +0 -0
  296. {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/utils/time_parse.py +0 -0
@@ -50,3 +50,7 @@ tj.toml
50
50
  # Superpowers skill output (internal planning artifacts, not for OSS)
51
51
  docs/superpowers/
52
52
  .gstack/
53
+
54
+ # Per-release pre-release test logs are committed (for reference) — see
55
+ # tests/results/. They're small markdown files, one per release run, and
56
+ # serve as a record of what was verified at release time.
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## Project Overview
6
6
 
7
- `tj` (TokenJam) is a local-first, OTel-native observability CLI for AI agents. No cloud backend, no signup. It captures telemetry from agent runtimes, stores it in a local DuckDB database, and exposes a CLI + local REST API for querying. Install via `pip install tokenjam`, run via `tj <subcommand>`. Requires Python >=3.10.
7
+ `tj` (TokenJam) is a local-first, OTel-native **cost-optimization layer** for AI agents (with a full observability stack underneath). No cloud backend, no signup. It captures telemetry from agent runtimes, stores it in a local DuckDB database, and runs four named analyzers (`downsize` / `cache` / `script` / `trim`) that surface cost-saving candidates from real usage — plus a CLI, local REST API, web UI, and MCP server for querying. Install via `pipx install tokenjam` (recommended — sidesteps PEP 668 on Homebrew Python and Debian 12+/Ubuntu 24+) or `pip install tokenjam` in a venv. Run via `tj <subcommand>`. Requires Python >=3.10.
8
8
 
9
9
  ## Build & Development
10
10
 
@@ -14,7 +14,7 @@ pip install -e ".[dev]"
14
14
 
15
15
  # Linting and type checking
16
16
  ruff check tokenjam/ # line-length=100, target py310
17
- mypy tokenjam/ # strict mode
17
+ mypy tokenjam/ # partial config (not --strict; see [tool.mypy] in pyproject.toml)
18
18
 
19
19
  # Tests (CI runs all except e2e)
20
20
  pytest tests/unit/ tests/synthetic/ tests/agents/ tests/integration/
@@ -37,6 +37,27 @@ cd sdk-ts && npm install && npm test
37
37
  ```
38
38
 
39
39
 
40
+ ## Working with concurrent agents
41
+
42
+ When more than one agent is editing this repo in parallel, **each agent must operate in its own git worktree**. A single working directory shares one `HEAD`, so two `git commit` calls from different agents land on whichever branch was checked out last — leading to commits leaking into the wrong PR. We've hit this multiple times.
43
+
44
+ Spin up a per-task worktree before starting:
45
+ ```bash
46
+ git worktree add ../tokenjam-<task> main
47
+ cd ../tokenjam-<task>
48
+ git checkout -b feat/<task>
49
+ ```
50
+
51
+ When the PR merges and the branch is deleted, prune the worktree:
52
+ ```bash
53
+ git worktree remove ../tokenjam-<task>
54
+ ```
55
+
56
+ Symptom of a missed worktree: `git log` shows a commit on a branch you didn't intend (because another agent's `HEAD` was the checked-out one when your `git commit` ran). If you see this, do **not** force-push — rebase the stray commit off your branch first, and only force-push if you own every commit being rewritten.
57
+
58
+ `.tj/config.toml` is intentionally untracked (see PR #145 + Critical Rule 20) and gets mutated at runtime by `tj onboard` / `tj serve` regenerating the local `ingest_secret`. Don't `git add` it back. The CI test `tests/unit/test_no_tracked_dev_secrets.py` guards against this.
59
+
60
+
40
61
  ## Architecture
41
62
 
42
63
  ### Data Flow
@@ -61,16 +82,25 @@ Post-ingest hooks run synchronously after each span is written to DB:
61
82
  - **`tokenjam/core/db.py`**: `StorageBackend` protocol + `DuckDBBackend` + `InMemoryBackend` (for tests) + migration runner. Migrations are `(version, sql)` tuples in a `MIGRATIONS` list — never modify existing ones, only append. **Note:** `StorageBackend` doesn't cover every query. Some callers (e.g. `CostEngine`, `cmd_status`) access `db.conn` directly for queries not in the protocol (cost updates, active session lookups). Helper `_row_to_session()` is used to convert raw DuckDB rows.
62
83
  - **`tokenjam/core/ingest.py`**: `IngestPipeline` (central hub), `SpanSanitizer` (rejects oversized/malformed spans), `strip_captured_content()`. Post-ingest hooks (cost, alerts, schema) are optional and error-tolerant — hook failures are logged, never propagated.
63
84
  - **`tokenjam/core/pricing.py`**: `ModelRates` (frozen dataclass), `load_pricing_table()` (LRU-cached), `get_rates(provider, model)`. Falls back to default rates for unknown models.
64
- - **`tokenjam/core/cost.py`**: `calculate_cost()` (pure function, rounds to 8dp) + `CostEngine` (post-ingest hook that updates `spans.cost_usd` and `sessions.total_cost_usd` via `db.conn` — see db.py note). Pricing loaded from `pricing/models.toml`.
85
+ - **`tokenjam/core/cost.py`**: `calculate_cost()` (pure function, rounds to 8dp) + `CostEngine` (post-ingest hook that updates `spans.cost_usd` and `sessions.total_cost_usd` via `db.conn` — see db.py note). Pricing loaded from `tokenjam/pricing/models.toml`. **Cache-read vs cache-write are separate fields** on `NormalizedSpan` (`cache_tokens` = read, `cache_write_tokens` = create); they bill at different rates and `calculate_cost` charges each at its own rate. The early-return no-op guard checks all four token counts (input/output/cache_read/cache_write) — see PR #90 and PR #92 for the cache-only-span and cache-write-on-live-path fixes.
65
86
  - **`tokenjam/core/alerts.py`**: `AlertEngine` with 13 alert types, `CooldownTracker` (in-memory, per agent+type, resets on restart), `AlertDispatcher` routing to 6 channel types (stdout, file, ntfy, webhook, Discord, Telegram). `AlertEngine.fire()` is the external entry point for other modules (SchemaValidator, DriftDetector) to fire alerts. Suppressed alerts are still persisted to DB but not dispatched to channels. Hardcoded thresholds: retry loop fires at 4+ identical tool calls in last 6 spans; failure rate fires at >20% errors in last 20 spans (checked every 5th error); session duration default 3600s. Stdout and file channels always include full detail regardless of `include_captured_content` config.
66
87
  - **`tokenjam/core/drift.py`**: `DriftDetector` — Z-score based behavioral drift detection, fires at session end.
67
- - **`tokenjam/core/optimize/`**: Package powering `tj optimize` and the `get_optimize_report` MCP tool. Public API re-exported from `__init__.py`: `build_report()` (orchestrator), `report_to_dict()`, `ANALYZER_REGISTRY`, `ANALYZER_ORDER`, plus result dataclasses. Architecture: `registry.py` holds the `@register("name")` decorator and `ANALYZER_REGISTRY` dict; `runner.py` defines `ANALYZER_ORDER` and orchestrates execution; `types.py` holds `AnalyzerContext` + result dataclasses + `MODEL_DOWNGRADE_CAVEAT`. Individual analyzers live in `analyzers/`, each as a single file registering via `@register`: `model_downgrade.py` (structural candidates — input < 5K tokens AND output < 500 tokens AND tool_calls ≤ 5; never claims quality equivalence, caveat baked into dataclass default), `budget_projection.py` (per-provider cycle spend vs `[budget.<provider>]` ceiling; only fires when budget > 0), `cache_efficacy.py`, `cache_recommend.py`, `prompt_bloat.py`, `workflow_restructure.py`. Analyzers receive an `AnalyzerContext` and operate on `db.conn` directly. To add a new analyzer: drop a file under `analyzers/`, decorate with `@register("name")`, append to `ANALYZER_ORDER` if ordering matters — `cmd_optimize --finding` choices auto-derive from the registry.
88
+ - **`tokenjam/core/optimize/`**: Package powering `tj optimize` and the `get_optimize_report` MCP tool. Public API re-exported from `__init__.py`: `build_report()` (orchestrator), `report_to_dict()`, `ANALYZER_REGISTRY`, `ANALYZER_ORDER`, plus result dataclasses. Architecture: `registry.py` holds the `@register("name")` decorator and `ANALYZER_REGISTRY` dict; `runner.py` defines `ANALYZER_ORDER` and orchestrates execution; `types.py` holds `AnalyzerContext` + result dataclasses + `MODEL_DOWNGRADE_CAVEAT`. Individual analyzers live in `analyzers/`, each as a single file registering via `@register`. **Registry strings (the user-facing names) and file names are decoupled**:
89
+ - `model_downgrade.py` → `@register("downsize")` — structural candidates (input < 5K tokens AND output < 500 tokens AND tool_calls ≤ 5; never claims quality equivalence, caveat baked into dataclass default)
90
+ - `budget_projection.py` → `@register("budget-projection")` — per-provider cycle spend vs `[budget.<provider>]` ceiling; only fires when budget > 0
91
+ - `cache_efficacy.py` → `@register("cache")` — current cache-read efficacy per (provider, model)
92
+ - `cache_recommend.py` → `@register("cache-recommend")` — Anthropic-only structural prefix detection for `cache_control` placement
93
+ - `workflow_restructure.py` → `@register("script")` — `(tool_name, arg_shape)` cluster detection for deterministic-script candidates
94
+ - `prompt_bloat.py` → `@register("trim")` — LLMLingua-2 token-significance classification (requires `tokenjam[bloat]` extra)
95
+ Analyzers receive an `AnalyzerContext` and operate on `db.conn` directly. To add a new analyzer: drop a file under `analyzers/`, decorate with `@register("name")`, append to `ANALYZER_ORDER` if ordering matters — `cmd_optimize`'s positional `findings` Click choices auto-derive from the registry.
96
+ **Recoverable-savings contract** (issues #111/#122): every *savings* analyzer's result dataclass carries `estimated_recoverable_usd` / `estimated_recoverable_tokens` / `estimate_basis` / `estimate_confidence` (`"heuristic"`). All four are on **one time basis — recoverable over the analyzed window** (`downsize` keeps a separate `monthly_savings_usd` for its CLI projection line, but `estimated_recoverable_usd` is the window figure so Overview tiles are comparable). `cache-recommend` and `budget-projection` deliberately carry **no** recoverable field (not savings analyzers); the Overview waste band is registry-driven off the presence of `estimated_recoverable_usd`, so a future analyzer (e.g. reuse) appears with no UI change. `report_to_dict`/`report_from_dict` round-trip these fields. Honesty discipline (Critical Rule 14) is mandatory — every estimate is "estimated recoverable", never "saves you".
68
97
  - **`tokenjam/core/ingest_adapters/`**: Third-party trace-export adapters that normalize external payloads (`langfuse.py`, `helicone.py`, `otlp.py`) into `NormalizedSpan` for ingest. Each is reachable as a `tj backfill <name>` subcommand and accepts `--source-url` (live API) or `--source-file` (offline JSON dump). Adapters write deterministic span IDs derived from the source's identifiers so re-runs are idempotent. `otlp.py` shares span-mapping logic with the live `POST /api/v1/spans` route via `tokenjam/otel/otlp_parsing.py`.
69
98
  - **`tokenjam/core/export/`**: Routing-config snippet generators for `tj optimize --export-config`. Currently `claude_code.py` emits a JSONC fragment under a `tokenjam.routing_recommendations` namespace with honest-framing caveat comments baked in. Writes to `~/.config/tokenjam/exports/`; never touches `~/.claude/settings.json` or other external configs (no `--apply` flag — Claude Code doesn't currently honor TokenJam routing keys, so auto-writing would change nothing and erode trust).
70
99
  - **`tokenjam/core/backfill.py`**: Parses Claude Code on-disk session JSONL files into `NormalizedSpan`s. Cost is recomputed from `pricing/models.toml` because the on-disk format has no `cost_usd`. The parser tolerates the dated `claude-<family>-<ver>-YYYYMMDD` model-name suffixes Anthropic ships (handled by `core/pricing.py.get_rates()`, which strips the trailing 8-digit date suffix when no exact pricing match exists). Idempotency relies on deterministic span IDs derived from `(session_id, message uuid)` / `(session_id, tool_use id)`.
71
100
  - **`tokenjam/core/schema_validator.py`**: Validates tool outputs against declared or genson-inferred JSON Schema. Only fires on `gen_ai.tool.call` spans with `gen_ai.tool.output` in attributes. Schema priority: 1) declared file from agent config `output_schema`, 2) inferred schema from `DriftBaseline.output_schema_inferred`. Caches schemas in-memory per agent.
72
101
  - **`tokenjam/core/models.py`**: All domain dataclasses — `NormalizedSpan`, `SessionRecord`, `Alert`, `DriftBaseline`, filter types, etc. `NormalizedSpan` carries `billing_account` (provider-only: `anthropic` / `openai` / `google` / `bedrock` / `local.ollama`). `SessionRecord` carries `plan_tier` (api / pro / max_5x / max_20x / plus / team / enterprise / local / unknown) plus a derived `pricing_mode` property (`local` / `subscription` / `api` / `unknown`). Spans inherit plan via the session FK — analyzers JOIN through `SessionRecord` when they need plan context. See [`docs/architecture.md`](docs/architecture.md) → "OTel semconv extensions" for the full derivation rules.
73
102
  - **`tokenjam/core/config.py`**: `TjConfig` dataclass tree, TOML loading/writing, config file discovery. `ProviderBudget` carries an optional `plan` field (set by `tj onboard`'s plan-tier prompt) that `IngestPipeline._build_or_update_session` reads to populate `SessionRecord.plan_tier` at session creation. `CaptureConfig` has four fine-grained content-capture toggles (`prompts` / `completions` / `tool_inputs` / `tool_outputs`); `strip_captured_content()` in `core/ingest.py` enforces them at the single ingest-pipeline gate.
103
+ - **`tokenjam/core/framing.py`**: **Single source of truth for plan-tier-aware rendering** (issue #110). `compute_framing(config, window_summary, by_provider_breakdown) -> Framing` decides whether dollar figures are shown verbatim (`api`), suppressed for token-share framing (`subscription`), shown as tokens-only (`local`), or shown with an "may overstate" qualifier (`unknown`). Plus `render_dollar()` / `render_savings()` (UI-facing compact formatters), and the shared helpers `pricing_mode_for` / `dominant_plan` / `config_declared_plan` (with the #106 global-config fallback) / `plan_tier_mix`. **Consumed by both the CLI (`cmd_optimize`, `cmd_tokenmaxx`) and the REST API** (which emits `Framing.to_dict()` as the `framing` block) — neither re-derives the rules. This module *reads* plan-tier/pricing-mode; the canonical derivation still lives on `SessionRecord.pricing_mode` + `SUBSCRIPTION_PLAN_TIERS` (semconv). When adding a dollar-bearing surface, consume this — do not re-implement the suppression rules.
74
104
  - **`tokenjam/sdk/agent.py`**: `@watch()` decorator creates session spans only. `record_llm_call()` and `record_tool_call()` create child spans for manual instrumentation. LLM call spans from provider clients require `patch_anthropic()`, `patch_openai()`, etc.
75
105
  - **`tokenjam/sdk/transport.py`**: `HttpTransport` — buffers up to 1000 spans, retries with exponential backoff (3 attempts, 2s base). Used when `tj serve` runs as a separate process.
76
106
  - **`tokenjam/sdk/bootstrap.py`**: `ensure_initialised()` — lazy, thread-safe, idempotent bootstrap of config -> DB -> IngestPipeline -> TracerProvider. Called automatically by `@watch()` and all `patch_*()` functions. Registers atexit flush.
@@ -79,10 +109,10 @@ Post-ingest hooks run synchronously after each span is written to DB:
79
109
  - **`tokenjam/otel/exporters.py`**: Prometheus metric reader setup via `build_prometheus_exporter()`.
80
110
  - **`tokenjam/otel/otlp_parsing.py`**: Shared OTLP JSON → `NormalizedSpan` parser. Two callers: `api/routes/spans.py` (live `POST /api/v1/spans`) and `core/ingest_adapters/otlp.py` (`tj backfill otlp`). Keep parsing in this one place — the live receive path and the backfill adapter must agree on attribute extraction, billing_account derivation, and timestamp handling.
81
111
  - **`tokenjam/otel/semconv.py`**: `GenAIAttributes`, `TjAttributes` (includes `BILLING_ACCOUNT` and `PLAN_TIER`), `VALID_PLAN_TIERS` and `SUBSCRIPTION_PLAN_TIERS` frozensets — OTel GenAI semantic convention constants plus tj-specific extensions.
82
- - **`tokenjam/api/app.py`**: FastAPI app factory. `tj serve` starts it with uvicorn. Accepts `db`, `config`, `ingest_pipeline` for testability. Registers all routers under `/api/v1` plus `/metrics`.
112
+ - **`tokenjam/api/app.py`**: FastAPI app factory (OpenAPI title `"TokenJam Lens"`). `tj serve` starts it with uvicorn. Accepts `db`, `config`, `ingest_pipeline` for testability. Registers all routers under `/api/v1` plus `/metrics`, `/health`, and the SPA at `/`. **`index.html` is read into a module string once at `create_app()` time** (`_index_html`) — so editing `tokenjam/ui/index.html` requires a `tj serve` restart to take effect; tests read the file from disk directly and aren't affected. Mounts `/ui/vendor` as `StaticFiles`.
83
113
  - **`tokenjam/api/middleware.py`**: `IngestAuthMiddleware` — protects `POST /api/v1/spans` with Bearer token. Returns `JSONResponse(401)` directly (not `HTTPException`, which doesn't propagate from `BaseHTTPMiddleware.dispatch`).
84
114
  - **`tokenjam/api/deps.py`**: `require_api_key` — FastAPI dependency for optional API key auth on GET endpoints. Only enforced when `api.auth.enabled = true` in config.
85
- - **`tokenjam/api/routes/`**: One file per resource — `spans.py` (OTLP JSON ingest), `traces.py`, `cost.py`, `tools.py`, `alerts.py`, `drift.py`, `metrics.py` (Prometheus text format from DB queries).
115
+ - **`tokenjam/api/routes/`**: One file per resource — `spans.py` (OTLP JSON ingest), `traces.py`, `cost.py`, `cost_compare.py`, `tools.py`, `alerts.py`, `drift.py`, `optimize.py`, `budget.py`, `status.py`, `agents.py`, `metrics.py` (Prometheus text format from DB queries), `version.py` (unauthenticated `GET /health` → `{"status":"ok","version":...}` mounted with no prefix, plus `GET /api/v1/version`; the version is derived at runtime via `importlib.metadata.version("tokenjam")` — no hardcoded literal). **The dollar-bearing read routes (`/cost`, `/cost/compare`, `/optimize`, `/budget`) each return a `framing` block** (see `core/framing.py`) so the web UI renders plan-tier-aware figures without re-deriving the rules in JS. `/optimize` takes `?fast=true` to skip the expensive Trim analyzer (returns `skipped_analyzers`) for the polling Overview; `/cost` returns a window-bucketed `series` for the chart (see Web UI below). **Concurrency caveat:** the sync (`def`) read routes run in Starlette's threadpool and share the daemon's single DuckDB connection, which is not safe for concurrent use — fan-out callers (the Overview) must fetch sequentially. See issue #124.
86
116
  - **`tokenjam/mcp/server.py`**: FastMCP stdio server exposing observability data to Claude Code. Uses either a read-only DuckDB connection or HTTP proxy to `tj serve`. Initialized via `init()` from `cmd_mcp.py`.
87
117
  - **`tokenjam/cli/main.py`**: Root Click group with global options (`--config`, `--json`, `--no-color`, `--db`, `--agent`, `-v`). Registers all subcommands.
88
118
 
@@ -92,11 +122,12 @@ Post-ingest hooks run synchronously after each span is written to DB:
92
122
 
93
123
  - **`tj demo [scenario]`** (`cmd_demo.py`) — runs Agent Incident Library scenarios (zero-config, no API keys). `tj demo` lists all; `tj demo retry-loop` runs one.
94
124
  - **`tj doctor`** (`cmd_doctor.py`) — health checks (config, DB, secrets, webhooks, drift readiness, schema-vs-capture consistency). Exit 0 = ok, 1 = warnings, 2 = errors.
95
- - **`tj optimize`** (`cmd_optimize.py`) — six analyzers, registry-driven: `model-downgrade`, `budget-projection`, `cache-efficacy`, `cache-recommend`, `workflow-restructure`, `prompt-bloat`. Flags: `--since 30d`, `--finding <name>` (repeatable; choices auto-derive from `ANALYZER_REGISTRY` at click decoration time), `--budget <provider>`, `--budget-usd <amount>`, `--compare <period>` (window-cost diff vs prior period; accepts `previous` / `last-week` / `last-month` / `last-7d` / `last-30d` / `YYYY-MM-DD:YYYY-MM-DD`), `--export-config <target>` (writes a routing snippet — currently `claude-code` — under `~/.config/tokenjam/exports/`; no `--apply` flag by design). Plan-tier-aware rendering: subscription users see "implied API value" framing and token-share savings (never dollar "spend"); local users see token-only framing; unknown-plan users see dollar figures suppressed with a `tj onboard --reconfigure` hint. Opens the live DB read-only so it works alongside a running `tj serve`.
125
+ - **`tj optimize`** (`cmd_optimize.py`) — six analyzers, registry-driven. **Analyzers are positional args** (not `--finding <name>`): `tj optimize downsize cache trim` runs three; bare `tj optimize` runs all. Registered names: `downsize`, `cache`, `cache-recommend`, `script`, `trim`, `budget-projection`. Flags: `--since 30d`, `--budget <provider>`, `--budget-usd <amount>`, `--compare <period>` (window-cost diff vs prior period; accepts `previous` / `last-week` / `last-month` / `last-7d` / `last-30d` / `YYYY-MM-DD:YYYY-MM-DD`), `--export-config <target>` (writes a routing snippet — currently `claude-code` — under `~/.config/tokenjam/exports/`; no `--apply` flag by design). Plan-tier-aware rendering: subscription users see "implied API value" framing and token-share savings (never dollar "spend"); local users see token-only framing; unknown-plan users see dollar figures suppressed with a `tj onboard --reconfigure` hint. Works alongside a running `tj serve` via the `/api/v1/optimize` HTTP fallback when the DuckDB write lock is held by the daemon.
126
+ - **`tj tokenmaxx`** (`cmd_tokenmaxx.py`) — shareable spend-tier command. Reads last 30 days of usage, classifies into a 6-tier ladder (Sipper / Moderator / Maxxer / SuperMaxxer / MegaMaxxer / GigaMaxxer) using the multiplier vs the user's declared subscription plan as the primary classifier, with absolute USD/mo thresholds as the API-user fallback. Output is a bordered Panel designed for screenshotting. Plan-aware: shows the multiplier line only when the user has `[budget.<provider>] plan = "max_5x"` (or pro / max_20x / plus) configured — the declared-plan lookup uses `core/framing.config_declared_plan`, which falls back to the global `~/.config/tj/config.toml` when the active project config has no `[budget]` section (issue #106). The companion landing page is `tokenjam.dev/tokenmaxxing`. Designed to never exit without an actionable next step — pairs the tier callout with the downsize savings figure inline.
96
127
  - **`tj cost`** (`cmd_cost.py`) — cost breakdown by `--group-by agent|model|day|tool`. Same `--compare <period>` flag as `tj optimize` for window-over-window diffs (▲/▼ indicators, per-agent and per-model top-shifts, dollar + token deltas).
97
128
  - **`tj backfill <source>`** (`cmd_backfill.py`) — ingest historical telemetry from external sources. Subcommands: `claude-code` (parses `~/.claude/projects/*.jsonl`, auto-invoked at the end of `tj onboard --claude-code`), `langfuse` (live API or JSON dump), `helicone` (live API or JSON dump), `otlp` (raw OTLP JSON via URL or file — reuses the same parser as the live `POST /api/v1/spans` route). All idempotent via deterministic span IDs.
98
- - **`tj onboard`** (`cmd_onboard.py`) — `--claude-code` and `--codex` flags trigger integration-specific flows. Prompts for plan tier (api / pro / max_5x / max_20x for Anthropic; api / plus / team / enterprise for OpenAI) and writes it to `[budget.<provider>] plan = "..."`. Supports `--reconfigure` to re-prompt against an existing config, and `--plan <tier>` for non-interactive use. Does NOT auto-write a default `usd = 200` cycle ceiling — subscription users get only the `plan` field; API users are explicitly asked whether they want a self-imposed ceiling.
99
- - **`tj report`** (`cmd_report.py`) — generates standalone HTML visualizations of analyzer findings (e.g. `tj report --bloat [<agent_id>]` renders the prompt-bloat analyzer's per-token significance). Writes to `~/.cache/tokenjam/reports/` (override via `TOKENJAM_REPORT_DIR`) and opens in the default browser.
129
+ - **`tj onboard`** (`cmd_onboard.py`) — `--claude-code` and `--codex` flags trigger integration-specific flows (writing to the **global** config). All paths — including plain `tj onboard` — prompt for plan tier (api / pro / max_5x / max_20x for Anthropic; api / plus / team / enterprise for OpenAI) and write it to `[budget.<provider>] plan = "..."`; `--plan <tier>` sets it non-interactively (issue #4). The plain path is Claude-first: its interactive prompt offers the Anthropic tiers, and an OpenAI-only `--plan` (plus/team/enterprise) is routed to `[budget.openai]`. Supports `--reconfigure` to re-prompt against an existing config. Does NOT auto-write a default `usd = 200` cycle ceiling — subscription users get only the `plan` field; API users are explicitly asked whether they want a self-imposed ceiling.
130
+ - **`tj report`** (`cmd_report.py`) — generates standalone HTML visualizations of analyzer findings. Currently `tj report --trim [<agent_id>]` renders the Trim analyzer's per-token significance (was `--bloat` pre-0.3.1, renamed alongside the analyzer's registry string). Writes to `~/.cache/tokenjam/reports/` (override via `TOKENJAM_REPORT_DIR`) and opens in the default browser.
100
131
  - **`tj policy list`** (`cmd_policy.py`) — read-only preview of the unified policy surface. Consolidates existing `[alerts]`, `[alerts.channels]`, `[defaults.budget]`, `[budget.<provider>]`, per-agent `budget`/`drift`/`sensitive_actions`/`output_schema`, and `[capture]` config into one table; each row carries its source TOML section. Supports `--json`. `tj policy add | edit | apply | remove | test` are intentionally absent this sprint — the unified config migration is next sprint's work. `policy` is in `no_db_commands` in `cli/main.py` so it doesn't open the DB. Rich source-section strings (`[budget.anthropic]`, `[[alerts.channels]]`) must be passed through `rich.markup.escape()` before rendering — otherwise Rich consumes them as style tags.
101
132
 
102
133
  All commands support `--json` for machine-readable output. Commands that query alerts use exit code 1 if active (unacknowledged, unsuppressed) alerts exist.
@@ -121,6 +152,18 @@ For `GET /api/v1/drift`, if `agent_id` is missing, return `JSONResponse(status_c
121
152
 
122
153
  Integration tests use `httpx.AsyncClient` with `httpx.ASGITransport(app=app)` against `InMemoryBackend`. Synthetic alert tests use `unittest.mock.MagicMock` for the DB — you must explicitly set up `db.get_recent_spans.return_value` before calling `engine.evaluate()`, and silence channels with `engine.dispatcher.channels = []`.
123
154
 
155
+ ### Web UI ("TokenJam Lens")
156
+
157
+ `tokenjam/ui/index.html` is the served dashboard — a **single-file Preact + htm SPA** (no build step, no TypeScript, no client-side router). "TokenJam Lens" is the **brand only**: it appears in `<title>`, the sidebar wordmark, and the OpenAPI title, but never in module names, route paths, or config keys. Screens: **Overview** (the default landing route — a triage front door), Status, Traces, Cost, Alerts, Drift, Optimize, Budget.
158
+
159
+ - **Offline-first (Critical Rule 18):** every JS/CSS dep is vendored under `tokenjam/ui/vendor/` — Preact + hooks + htm (ESM via `<script type="importmap">`) and **uPlot** (vendored IIFE global `uPlot` + CSS, pinned in `docs/internal/lens-vendor-versions.md`). No render-time external HTTP. `tests/unit/test_ui_offline.py` enforces this; clickable `<a href>` links are the only allowed external URLs.
160
+ - **Single compute path:** the UI reads everything from the REST API and **never re-implements analysis, aggregation, or plan-tier framing in JS** — it consumes the `framing` block (see `core/framing.py`). If the UI needs a number, extend the endpoint; don't compute it client-side.
161
+ - **URL is the source of truth for filters:** state lives in the hash + query params (`#/cost?since=7d&group_by=model`); `getRoute()` parses it, `navigate()` writes it back omitting defaults. Window vocabulary matches the CLI (`1h`/`24h`/`7d`/`30d`/`90d` + `YYYY-MM-DD:YYYY-MM-DD`). The default landing route is Overview (empty hash → `getRoute()` returns `overview`; do **not** re-introduce a render-time `location.hash = ...` redirect — it raced the first render, issue #132).
162
+ - **Charts:** `SpendChart` wraps uPlot, reads CSS custom properties (`--chart-1..5`) so it re-themes, and has a cursor tooltip. The spend chart spans the **full selected window** with zero-fill: `/api/v1/cost` returns a window-bucketed `series` (hourly buckets for ≤2-day windows, daily otherwise; epoch-second `bucket` keys) plus `series_bucket` + `window_start`/`window_end`, and the UI builds a continuous grid + pins the x-scale to the window (issues #133/#136).
163
+ - **Run-rate** is a single linear figure projected to the end of the current calendar cycle (`daily_rate × days-remaining`), captioned "not a forecast". The forecasting boundary is deliberate: linear run-rate only — no EWMA, seasonality, or anomaly detection.
164
+ - **Polling:** the Overview auto-refreshes every 30s only while the tab is visible (`document.visibilityState`) and **fetches its endpoints sequentially** (the daemon's single DuckDB connection isn't concurrency-safe — see the REST API caveat / #124). Detail screens refresh on user action.
165
+ - **Testing the UI (no JS runner in CI):** the Python `test` job can't run JS, so UI fixes are guarded by **static-grep regression tests** in `tests/unit/test_lens_ui_regression.py` (assert buggy patterns are *gone* and new helpers are present) plus `test_ui_offline.py`. When iterating locally, validate syntax with `node --check` on the extracted `<script type="module">` block, and verify visually by running `tj serve` (or a seeded `create_app` + uvicorn on an alt port) and screenshotting with headless Chrome — there is intentionally no Playwright/Cypress.
166
+
124
167
  ### Session Continuity
125
168
 
126
169
  When a span has a `conversation_id` matching an existing session, it's attributed to that session (even across process restarts). New `conversation_id` = new session.
@@ -139,11 +182,14 @@ When a span has a `conversation_id` matching an existing session, it's attribute
139
182
  10. **Use semconv constants** — reference `GenAIAttributes` and `TjAttributes` from `tokenjam/otel/semconv.py` instead of hardcoding OTel attribute name strings.
140
183
  11. **OTel TracerProvider is global and set-once** — `trace.set_tracer_provider()` only works once per process. In tests, set the provider once at module level (not per-test in a fixture) and clear spans between tests. Use a custom `_CollectingExporter(SpanExporter)` since `InMemorySpanExporter` is not available in the installed OTel version. See `tests/agents/test_mock_scenarios.py` for the SDK test pattern and `tests/integration/test_full_pipeline.py` for the pipeline pattern.
141
184
  12. **New SDK integrations must call `ensure_initialised()`** — every `patch_*()` convenience function must call `from tokenjam.sdk.bootstrap import ensure_initialised; ensure_initialised()` before installing hooks. This lazily bootstraps the TracerProvider + IngestPipeline on first use.
142
- 13. **PyPI package name is `tokenjam`, not `ocw`** — `pip install tokenjam` is the correct install command. The CLI command is `tj` and the Python package directory is `tokenjam/`. The published package name on PyPI is `tokenjam`. Never write `pip install ocw` in docs, examples, or comments.
143
- 14. **`tj optimize` output must never claim quality equivalence** — the model-downgrade finding flags structural candidates only. Every user-visible string says "looks like" / "candidate" / "review before switching" — never "safe to downgrade" or "would have worked." The `MODEL_DOWNGRADE_CAVEAT` constant lives on `DowngradeFinding` as a dataclass default so it can't be removed by accident; it must also appear in human-readable CLI output. The same honesty discipline applies to all other analyzers — `cache-efficacy` ("you're getting X% of available caching"), `cache-recommend` (Anthropic-only, structural prefix detection), `workflow-restructure` ("structural shape matches", "review before replacing with a script"), `prompt-bloat` ("predicted low-significance regions; review before editing"). `tj optimize --export-config` snippets bake the caveat block into the JSONC output as comments.
185
+ 13. **PyPI package name is `tokenjam`, not `ocw`** — the package on PyPI is `tokenjam`. The CLI command is `tj`. The Python package directory is `tokenjam/`. **Recommended install: `pipx install tokenjam`** (sidesteps PEP 668 on Homebrew Python and Debian 12+/Ubuntu 24+). `pip install tokenjam` works inside a clean venv but fails on system Python with a misleading externally-managed-environment error. Never write `pip install ocw` in docs, examples, or comments.
186
+ 14. **`tj optimize` output must never claim quality equivalence** — the `downsize` finding flags structural candidates only. Every user-visible string says "looks like" / "candidate" / "review before switching" — never "safe to downgrade" or "would have worked." The `MODEL_DOWNGRADE_CAVEAT` constant lives on `DowngradeFinding` as a dataclass default so it can't be removed by accident; it must also appear in human-readable CLI output. The same honesty discipline applies to all other analyzers — `cache` ("you're getting X% of available caching"), `cache-recommend` (Anthropic-only, structural prefix detection), `script` ("structural shape matches", "review before replacing with a script"), `trim` ("predicted low-significance regions; review before editing"). `tj optimize --export-config` snippets bake the caveat block into the JSONC output as comments.
144
187
  15. **Version bump on release** — both `pyproject.toml` (`version = "X.Y.Z"`) and `sdk-ts/package.json` (`"version": "X.Y.Z"`) must be bumped to the new version before creating a GitHub release. The publish workflows (`publish-pypi.yml`, `publish-npm.yml`) trigger on `release published` events and will fail with 403 if the version already exists on PyPI/npm.
145
- 16. **New optimize analyzers self-register** — drop a `.py` file under `tokenjam/core/optimize/analyzers/` with a function decorated `@register("name")` taking `AnalyzerContext`. Auto-discovery in `analyzers/__init__.py` walks the directory at import time. `cmd_optimize.py`'s `--finding` choices read from `ANALYZER_REGISTRY.keys()` at click decoration — no edits needed there. If your analyzer depends on (or is depended on by) another, append it to `ANALYZER_ORDER` in `runner.py` at the right position. Wave-2 analyzers attach their findings to `OptimizeReport.findings[name]` (generic dict); the older `model-downgrade` / `budget-projection` analyzers retain typed slots on `OptimizeReport` for backwards compat with `cmd_optimize` and the MCP server.
188
+ 16. **New optimize analyzers self-register** — drop a `.py` file under `tokenjam/core/optimize/analyzers/` with a function decorated `@register("name")` taking `AnalyzerContext`. Auto-discovery in `analyzers/__init__.py` walks the directory at import time. `cmd_optimize.py`'s positional `findings` Click choices read from `ANALYZER_REGISTRY.keys()` at decoration — no edits needed there. If your analyzer depends on (or is depended on by) another, append it to `ANALYZER_ORDER` in `runner.py` at the right position. Wave-2 analyzers attach their findings to `OptimizeReport.findings[name]` (generic dict); the older `downsize` (registered name; file is `model_downgrade.py`) and `budget-projection` analyzers retain typed slots on `OptimizeReport` for backwards compat with `cmd_optimize` and the MCP server.
146
189
  17. **OTLP parsing has one home** — `tokenjam/otel/otlp_parsing.py`. Both the live `POST /api/v1/spans` route and the `tj backfill otlp` adapter import `parse_otlp_span` and `extract_resource_attrs` from there. If you need to extend OTLP attribute extraction, do it once in that module; do not copy-paste into either caller.
190
+ 18. **Web UI must work fully offline** — `tokenjam/ui/index.html` is the served dashboard ("TokenJam Lens"; see Architecture → Web UI). It is intentionally a single-file SPA with **zero external HTTP loads at render time**. Preact + hooks + htm + **uPlot** are vendored under `tokenjam/ui/vendor/` (ESM via `<script type="importmap">`; uPlot as a plain `<script>` IIFE global); fonts use system-font fallbacks (no Google Fonts); the favicon is inlined as a `data:` URL. The FastAPI app mounts `/ui/vendor` as `StaticFiles`. The `tests/unit/test_ui_offline.py` regression test asserts no render-time external URLs exist anywhere outside `<a href>` (clickable links to github.com are fine — they only fetch on click) and that vendored CSS has no external `url()`. If you add a CDN font, script, or stylesheet, that test will fail. Vendor the asset locally instead. See issue #87 + PR #88.
191
+ 19. **Analyzer registry names ≠ file names** — registry strings (`downsize`, `cache`, `script`, `trim`) are decoupled from Python module filenames (`model_downgrade.py`, `cache_efficacy.py`, `workflow_restructure.py`, `prompt_bloat.py`). The 0.3.1 rename only changed `@register("...")` strings; file names stayed for git-blame continuity. When grepping for an analyzer, search both the registry string AND the older file-name keyword.
192
+ 20. **`.tj/config.toml` is untracked and must stay that way** — the file contains a live per-install `ingest_secret` and is regenerated by `tj onboard` / `tj serve`. It was committed in error from v0.2.0 through v0.3.5 (leaked secret in git history; see PR #145 + issue #141 finding #6). `.gitignore` covers it, and `tests/unit/test_no_tracked_dev_secrets.py` fails CI if it's re-added to the index. If you see `.tj/config.toml` in your `git status` as modified or new, that's expected — just don't `git add` it.
147
193
 
148
194
  ## Config
149
195
 
@@ -217,7 +263,7 @@ If a version already exists on PyPI or npm, the publish workflow fails with 403
217
263
 
218
264
  ## Packaging
219
265
 
220
- Build system is hatchling. The `pyproject.toml` requires `[tool.hatch.build.targets.wheel] packages = ["tj"]` because the package name (`tokenjam`) differs from the directory name (`tj`). Without this, `pip install -e .` fails.
266
+ Build system is hatchling. `[tool.hatch.build.targets.wheel] packages = ["tokenjam"]` the package directory is `tokenjam/` (matching the PyPI name); only the *CLI command* is `tj` (`[project.scripts] tj = "tokenjam.cli.main:cli"`). Non-`.py` assets under the package ship in the wheel automatically — this is how the vendored UI (`tokenjam/ui/index.html`, `tokenjam/ui/vendor/*`) and `tokenjam/pricing/models.toml` reach users.
221
267
 
222
268
  Key runtime dependency: `pytz` is required by DuckDB for `TIMESTAMPTZ` column handling — it's listed explicitly in `dependencies` because DuckDB doesn't declare it on all platforms.
223
269
 
@@ -233,10 +279,11 @@ Key runtime dependency: `pytz` is required by DuckDB for `TIMESTAMPTZ` column ha
233
279
  - **[docs/installation.md](docs/installation.md)** — base install vs optional extras matrix. Documents `tokenjam[bloat]` (the ~2GB torch + transformers extra used by the Trim analyzer), framework adapter extras (`[langchain]` / `[crewai]` / `[autogen]` / `[litellm]`), and the MCP / dev extras.
234
280
  - **[docs/configuration.md](docs/configuration.md)** — full TOML config surface plus the "Content capture and privacy" section explaining the four `[capture]` toggles and how they interact with `alerts.include_captured_content`.
235
281
  - **Optimize product pages** — one per user-facing product, all under `docs/optimize/`:
236
- - [`downsize.md`](docs/optimize/downsize.md) — model-downgrade candidate flagging (internal: `model-downgrade`)
237
- - [`cache.md`](docs/optimize/cache.md) — `cache-efficacy` (current caching ratio) + `cache-recommend` (Anthropic-only breakpoint suggestions)
238
- - [`script.md`](docs/optimize/script.md) — `workflow-restructure` clustering by `(tool_name, arg_shape)` signature
239
- - [`trim.md`](docs/optimize/trim.md) — LLMLingua-2 token-significance classifier (`prompt-bloat`), install + capture requirements, performance numbers
282
+ - [`downsize.md`](docs/optimize/downsize.md) — cheaper-model candidate flagging (registry: `downsize`, file: `model_downgrade.py`)
283
+ - [`cache.md`](docs/optimize/cache.md) — `cache` (current caching ratio) + `cache-recommend` (Anthropic-only breakpoint suggestions)
284
+ - [`script.md`](docs/optimize/script.md) — `script` clustering by `(tool_name, arg_shape)` signature (file: `workflow_restructure.py`)
285
+ - [`trim.md`](docs/optimize/trim.md) — LLMLingua-2 token-significance classifier (`trim`, file: `prompt_bloat.py`), install + capture requirements, performance numbers
286
+ - **[AGENTS.md](AGENTS.md)** — codebase conventions for contributors (referenced from the top-level README).
240
287
  - **Backfill adapters** — `docs/backfill/overview.md` lists the four sources (`claude-code` / `langfuse` / `helicone` / `otlp`) with the partnership-posture framing; per-adapter pages document modes (URL / file), field mapping, idempotency, and v1 limitations.
241
288
  - **[docs/policy/overview.md](docs/policy/overview.md)** — read-only preview of the unified policy surface (`tj policy list`). Notes that the `add` / `edit` / `apply` subcommands and the underlying `[policy]` config migration land next sprint.
242
289
  - **Internal specs** — `docs/internal/specs/` is reserved for canonical specs that production code references at long-term. Currently empty (sprint specs have been cleaned up after merge); add new ones here when a feature needs a stable, code-referenced source of truth.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tokenjam
3
- Version: 0.3.4
3
+ Version: 0.4.0
4
4
  Summary: TokenJam — local-first OTel-native observability for Autonomous AI agents
5
5
  Project-URL: Homepage, https://opencla.watch
6
6
  Project-URL: Repository, https://github.com/Metabuilder-Labs/openclawwatch
@@ -23,6 +23,7 @@ Requires-Dist: apscheduler>=3.10
23
23
  Requires-Dist: click>=8.1
24
24
  Requires-Dist: duckdb>=0.10
25
25
  Requires-Dist: fastapi>=0.110
26
+ Requires-Dist: fastmcp>=0.2
26
27
  Requires-Dist: genson>=1.2
27
28
  Requires-Dist: httpx>=0.27
28
29
  Requires-Dist: jsonschema>=4.0
@@ -53,7 +54,6 @@ Requires-Dist: langchain>=0.2; extra == 'langchain'
53
54
  Provides-Extra: litellm
54
55
  Requires-Dist: litellm>=1.40; extra == 'litellm'
55
56
  Provides-Extra: mcp
56
- Requires-Dist: fastmcp; extra == 'mcp'
57
57
  Description-Content-Type: text/markdown
58
58
 
59
59
  <div align="center">
@@ -154,6 +154,8 @@ tj onboard --claude-code
154
154
  tj optimize # cost-saving candidates from your actual usage
155
155
  ```
156
156
 
157
+ To upgrade later: `pipx upgrade tokenjam` (then `tj stop && tj serve &` to reload the daemon, and `tj --version` to verify). See [docs/installation.md](docs/installation.md#upgrading).
158
+
157
159
  For any Python agent:
158
160
 
159
161
  ```python
@@ -259,6 +261,8 @@ tj serve # start the web UI + REST API
259
261
  **Shipped in 0.3.x:** Downsize · Cache · Script · Trim · Claude Code + Codex onboarding · MCP server · Web UI · Backfill adapters (Langfuse, Helicone, OTLP) · Period comparison · Routing-config export · Read-only policy preview
260
262
 
261
263
  **Up next:**
264
+ - [ ] **[TokenJam Lens](https://github.com/Metabuilder-Labs/tokenjam/milestone/1)** — local dashboard rebrand: new Overview triage front-door, Optimize detail tab, real spend-over-time charts, cross-screen drill-through
265
+ - [ ] **[Reuse analyzer](https://github.com/Metabuilder-Labs/tokenjam/milestone/2)** — fifth analyzer: detects clusters of sessions with repeated planning, exports reviewable skeleton templates you can convert into slash commands or scripts
262
266
  - [ ] `tj policy add | edit | apply` — unified rule surface
263
267
  - [ ] `tj replay` — replay captured sessions against new model versions
264
268
  - [ ] TypeScript framework patches (LangChain JS, OpenAI Agents SDK)
@@ -96,6 +96,8 @@ tj onboard --claude-code
96
96
  tj optimize # cost-saving candidates from your actual usage
97
97
  ```
98
98
 
99
+ To upgrade later: `pipx upgrade tokenjam` (then `tj stop && tj serve &` to reload the daemon, and `tj --version` to verify). See [docs/installation.md](docs/installation.md#upgrading).
100
+
99
101
  For any Python agent:
100
102
 
101
103
  ```python
@@ -201,6 +203,8 @@ tj serve # start the web UI + REST API
201
203
  **Shipped in 0.3.x:** Downsize · Cache · Script · Trim · Claude Code + Codex onboarding · MCP server · Web UI · Backfill adapters (Langfuse, Helicone, OTLP) · Period comparison · Routing-config export · Read-only policy preview
202
204
 
203
205
  **Up next:**
206
+ - [ ] **[TokenJam Lens](https://github.com/Metabuilder-Labs/tokenjam/milestone/1)** — local dashboard rebrand: new Overview triage front-door, Optimize detail tab, real spend-over-time charts, cross-screen drill-through
207
+ - [ ] **[Reuse analyzer](https://github.com/Metabuilder-Labs/tokenjam/milestone/2)** — fifth analyzer: detects clusters of sessions with repeated planning, exports reviewable skeleton templates you can convert into slash commands or scripts
204
208
  - [ ] `tj policy add | edit | apply` — unified rule surface
205
209
  - [ ] `tj replay` — replay captured sessions against new model versions
206
210
  - [ ] TypeScript framework patches (LangChain JS, OpenAI Agents SDK)
@@ -74,6 +74,21 @@ If you run `tj optimize trim` without the extra installed, the analyzer self-reg
74
74
 
75
75
  See [`docs/optimize/trim.md`](optimize/trim.md) for performance numbers, capture requirements, and what the analyzer actually reports.
76
76
 
77
+ ## Upgrading
78
+
79
+ ```bash
80
+ pipx upgrade tokenjam # if you installed via pipx (recommended)
81
+ pip install --upgrade tokenjam # if you're in a pip + venv setup
82
+ ```
83
+
84
+ After upgrading:
85
+
86
+ 1. Restart the daemon to pick up the new code: `tj stop && tj serve &`
87
+ 2. DB migrations apply automatically on the next `tj` invocation — no manual step required
88
+ 3. Verify with `tj --version`
89
+
90
+ PyPI's CDN occasionally lags ~1–2 min after a release. If `pipx upgrade` reports "already at the latest version" but the reported `tj --version` is older than what's on the [releases page](https://github.com/Metabuilder-Labs/tokenjam/releases), wait a minute and retry.
91
+
77
92
  ## TypeScript SDK
78
93
 
79
94
  ```bash
@@ -0,0 +1,22 @@
1
+ # Vendored front-end libraries
2
+
3
+ The local web UI (`tokenjam/ui/index.html`) is offline-first (CLAUDE.md Critical
4
+ Rule 18): every JS/CSS dependency is vendored under `tokenjam/ui/vendor/` and
5
+ served by the FastAPI `/ui/vendor` StaticFiles mount. No CDN loads at render
6
+ time. One line per vendored library below.
7
+
8
+ | Library | Version | Files | License | Notes |
9
+ |---|---|---|---|---|
10
+ | Preact | (as vendored) | `preact.js`, `preact-hooks.js` | MIT | ESM, via importmap |
11
+ | htm | (as vendored) | `htm.js` | Apache-2.0 | ESM, via importmap |
12
+ | uPlot | 1.6.32 | `uplot.js`, `uplot.css` | MIT | IIFE global `uPlot`, loaded via plain `<script>` (issue #112) |
13
+
14
+ ## Bump procedure
15
+
16
+ 1. Download the new release's `dist/` files from the upstream repo
17
+ (uPlot: `dist/uPlot.iife.min.js` + `dist/uPlot.min.css`).
18
+ 2. Replace the file under `tokenjam/ui/vendor/`, keeping the version-pin header
19
+ comment at the top.
20
+ 3. Update the version in the table above.
21
+ 4. Run `pytest tests/unit/test_ui_offline.py` — it asserts no render-time
22
+ external URLs and that the vendored files exist and ship in the wheel.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tokenjam"
7
- version = "0.3.4"
7
+ version = "0.4.0"
8
8
  description = "TokenJam — local-first OTel-native observability for Autonomous AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -41,6 +41,11 @@ dependencies = [
41
41
  "httpx>=0.27",
42
42
  "apscheduler>=3.10",
43
43
  "websockets>=12.0",
44
+ # fastmcp ships in the base install (was in the [mcp] extra) so `tj mcp`
45
+ # works on a fresh `pipx install tokenjam` without requiring users to
46
+ # remember the extra. Claude Code's MCP integration is now a primary
47
+ # use case rather than an opt-in. Issue #101.
48
+ "fastmcp>=0.2",
44
49
  ]
45
50
 
46
51
  [project.urls]
@@ -54,7 +59,10 @@ crewai = ["crewai>=0.28"]
54
59
  autogen = ["pyautogen>=0.2"]
55
60
  litellm = ["litellm>=1.40"]
56
61
  dev = ["pytest", "pytest-asyncio", "httpx", "ruff", "mypy"]
57
- mcp = ["fastmcp"]
62
+ # Kept as a no-op extra for back-compat — `pipx install 'tokenjam[mcp]'` still
63
+ # works, just installs the same fastmcp that's now in the base dependencies.
64
+ # Documented in `docs/installation.md` so users know they no longer need it.
65
+ mcp = []
58
66
  # Trim analyzer (`tj optimize --finding prompt-bloat`). LLMLingua-2 pulls in
59
67
  # PyTorch and transformers, ~2GB total. Kept optional so the base install
60
68
  # stays small — most users don't run the bloat analyzer.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenjam/sdk",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "TypeScript SDK for TokenJam — local-first observability for AI agents",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -238,6 +238,154 @@ async def test_get_cost_returns_aggregated_rows(client):
238
238
  assert "total_cost_usd" in data
239
239
 
240
240
 
241
+ async def test_trace_detail_includes_cache_write_tokens(db, client):
242
+ """The traces API exposes cache_write_tokens so per-span cost reconciles
243
+ from the displayed columns (#17 — it was the ~91% cost driver, hidden)."""
244
+ sp = make_llm_span(agent_id="a", model="claude-opus-4-8", provider="anthropic",
245
+ input_tokens=2, output_tokens=465, cache_tokens=243597,
246
+ cache_write_tokens=209000, cost_usd=1.4423)
247
+ db.insert_span(sp)
248
+ resp = await client.get(f"/api/v1/traces/{sp.trace_id}")
249
+ assert resp.status_code == 200
250
+ span = resp.json()["spans"][0]
251
+ assert span["cache_tokens"] == 243597
252
+ assert span["cache_write_tokens"] == 209000
253
+
254
+
255
+ async def test_cost_rows_carry_cache_tokens(db, client):
256
+ """`/api/v1/cost` rows + totals include cache-read and cache-write (#17)."""
257
+ sp = make_llm_span(agent_id="a", model="claude-opus-4-8", provider="anthropic",
258
+ input_tokens=2, output_tokens=465, cache_tokens=243597,
259
+ cache_write_tokens=209000, cost_usd=1.4423)
260
+ db.insert_span(sp)
261
+ data = (await client.get("/api/v1/cost?group_by=model")).json()
262
+ assert data["rows"][0]["cache_tokens"] == 243597
263
+ assert data["rows"][0]["cache_write_tokens"] == 209000
264
+ assert data["total_cache_write_tokens"] == 209000
265
+
266
+
267
+ async def test_cost_includes_window_series_for_chart(client):
268
+ """/api/v1/cost carries a window-bucketed series (per bucket+agent+model)
269
+ plus the bucket size and window bounds so the chart can span the full
270
+ selected window with zero-fill (#113/#133)."""
271
+ await _ingest_sample_span(client)
272
+ resp = await client.get("/api/v1/cost", params={"since": "7d", "group_by": "day"})
273
+ assert resp.status_code == 200
274
+ data = resp.json()
275
+ assert "series" in data and isinstance(data["series"], list)
276
+ assert data["series_bucket"] in ("hour", "day")
277
+ assert isinstance(data["window_start"], int) and isinstance(data["window_end"], int)
278
+ assert data["window_end"] >= data["window_start"]
279
+ if data["series"]:
280
+ item = data["series"][0]
281
+ assert {"bucket", "agent_id", "model", "cost_usd",
282
+ "input_tokens", "output_tokens"} <= set(item)
283
+ assert isinstance(item["bucket"], int) # epoch seconds
284
+
285
+
286
+ async def test_cost_series_buckets_hourly_for_short_window(client):
287
+ """A ≤2-day window buckets hourly so 24h renders with hourly ticks (#133)."""
288
+ await _ingest_sample_span(client)
289
+ resp = await client.get("/api/v1/cost", params={"since": "24h"})
290
+ assert resp.json()["series_bucket"] == "hour"
291
+
292
+
293
+ _FRAMING_KEYS = {
294
+ "pricing_mode", "plan_tier", "plan_label", "plan_monthly_usd",
295
+ "subscription_share_pct", "api_share_pct", "display_rule", "qualifier_text",
296
+ }
297
+
298
+
299
+ async def test_cost_response_includes_framing_block(client):
300
+ await _ingest_sample_span(client)
301
+ resp = await client.get("/api/v1/cost")
302
+ assert resp.status_code == 200
303
+ framing = resp.json()["framing"]
304
+ assert _FRAMING_KEYS <= set(framing)
305
+
306
+
307
+ async def test_optimize_response_includes_framing_block(client):
308
+ await _ingest_sample_span(client)
309
+ resp = await client.get("/api/v1/optimize?since=30d")
310
+ assert resp.status_code == 200
311
+ data = resp.json()
312
+ if data.get("error") == "no_data":
313
+ pytest.skip("no spans landed for optimize in this fixture")
314
+ assert _FRAMING_KEYS <= set(data["framing"])
315
+
316
+
317
+ async def test_optimize_response_always_carries_downgrade_key(client):
318
+ """The downsize typed slot must always be present (null when no candidates)
319
+ so the UI can always render a Downsize section (#126)."""
320
+ await _ingest_sample_span(client)
321
+ resp = await client.get("/api/v1/optimize?since=30d")
322
+ data = resp.json()
323
+ if data.get("error") == "no_data":
324
+ pytest.skip("no spans landed for optimize in this fixture")
325
+ assert "downgrade" in data # present even when null
326
+
327
+
328
+ async def test_root_serves_lens_title(client):
329
+ """Brand pass (#114): the served dashboard <title> is 'TokenJam Lens'."""
330
+ resp = await client.get("/")
331
+ assert resp.status_code == 200
332
+ assert "<title>TokenJam Lens</title>" in resp.text
333
+
334
+
335
+ async def test_optimize_chain_framing_and_recoverable_fields(client):
336
+ """The framing block (#110) + per-finding recoverable fields (#111) are
337
+ both present on /api/v1/optimize — validates the chain Overview relies on."""
338
+ await _ingest_sample_span(client)
339
+ resp = await client.get("/api/v1/optimize?since=30d")
340
+ data = resp.json()
341
+ if data.get("error") == "no_data":
342
+ pytest.skip("no spans landed for optimize in this fixture")
343
+ assert _FRAMING_KEYS <= set(data["framing"])
344
+ # The savings analyzers carry the recoverable contract fields (#111).
345
+ # cache-recommend / budget-projection intentionally do not.
346
+ findings = data.get("findings") or {}
347
+ for name in ("cache", "script", "trim"):
348
+ if name in findings:
349
+ assert "estimated_recoverable_usd" in findings[name]
350
+ assert "estimate_basis" in findings[name]
351
+
352
+
353
+ async def test_optimize_fast_skips_trim(client):
354
+ """fast=true skips the expensive Trim analyzer and reports it (#114)."""
355
+ await _ingest_sample_span(client)
356
+ resp = await client.get("/api/v1/optimize?since=30d&fast=true")
357
+ assert resp.status_code == 200
358
+ data = resp.json()
359
+ if data.get("error") == "no_data":
360
+ pytest.skip("no spans landed for optimize in this fixture")
361
+ assert "trim" in data.get("skipped_analyzers", [])
362
+ assert "trim" not in (data.get("findings") or {})
363
+
364
+
365
+ async def test_budget_framing_reflects_configured_subscription_plan(db):
366
+ """The budget surface has no window, so framing falls back to the
367
+ declared plan in config (#110)."""
368
+ from tokenjam.core.config import ProviderBudget
369
+
370
+ cfg = TjConfig(
371
+ version="1",
372
+ security=SecurityConfig(ingest_secret=INGEST_SECRET),
373
+ api=ApiConfig(auth=ApiAuthConfig(enabled=False)),
374
+ )
375
+ cfg.budgets["anthropic"] = ProviderBudget(plan="max_5x")
376
+ pipeline = IngestPipeline(db=db, config=cfg)
377
+ app = create_app(config=cfg, db=db, ingest_pipeline=pipeline)
378
+ transport = httpx.ASGITransport(app=app)
379
+ async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
380
+ resp = await c.get("/api/v1/budget")
381
+ assert resp.status_code == 200
382
+ framing = resp.json()["framing"]
383
+ assert framing["pricing_mode"] == "subscription"
384
+ assert framing["plan_tier"] == "max_5x"
385
+ assert framing["plan_label"] == "Max 5x plan"
386
+ assert framing["display_rule"] == "suppress_dollars_for_subscription_share"
387
+
388
+
241
389
  async def test_get_alerts_returns_list(client):
242
390
  resp = await client.get("/api/v1/alerts")
243
391
  assert resp.status_code == 200