tokenjam 0.5.2__tar.gz → 0.5.4__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 (499) hide show
  1. tokenjam-0.5.4/.dockerignore +40 -0
  2. tokenjam-0.5.4/.github/actions/docker-publish/action.yml +71 -0
  3. tokenjam-0.5.4/.github/workflows/docker-publish.yml +36 -0
  4. tokenjam-0.5.4/.github/workflows/publish-npm.yml +69 -0
  5. {tokenjam-0.5.2 → tokenjam-0.5.4}/.gitignore +2 -0
  6. {tokenjam-0.5.2 → tokenjam-0.5.4}/CLAUDE.md +37 -12
  7. tokenjam-0.5.4/Dockerfile +44 -0
  8. {tokenjam-0.5.2 → tokenjam-0.5.4}/PKG-INFO +126 -77
  9. tokenjam-0.5.4/README.md +335 -0
  10. tokenjam-0.5.4/docs/agent-capability-matrix.md +46 -0
  11. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/alerts.md +2 -2
  12. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/architecture.md +4 -4
  13. tokenjam-0.5.4/docs/assets/tj-quickstart-hero.png +0 -0
  14. tokenjam-0.5.4/docs/claude-code-integration.md +129 -0
  15. tokenjam-0.5.4/docs/cli-reference.md +451 -0
  16. tokenjam-0.5.4/docs/first-hour.md +54 -0
  17. tokenjam-0.5.4/docs/getting-started.md +120 -0
  18. tokenjam-0.5.4/docs/harness-integration.md +105 -0
  19. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/installation.md +36 -0
  20. tokenjam-0.5.4/docs/internal/close-the-loop.md +87 -0
  21. tokenjam-0.5.4/docs/internal/specs/map-board-direction.md +62 -0
  22. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/optimize/cache.md +1 -0
  23. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/optimize/downsize.md +1 -0
  24. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/optimize/script.md +1 -0
  25. tokenjam-0.5.4/docs/optimize/subagent.md +73 -0
  26. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/optimize/trim.md +1 -0
  27. {tokenjam-0.5.2 → tokenjam-0.5.4}/incidents/retry-loop/scenario.py +8 -1
  28. tokenjam-0.5.4/npm-wrapper/README.md +40 -0
  29. tokenjam-0.5.4/npm-wrapper/bin/tj.js +77 -0
  30. tokenjam-0.5.4/npm-wrapper/package.json +25 -0
  31. {tokenjam-0.5.2 → tokenjam-0.5.4}/pyproject.toml +2 -1
  32. {tokenjam-0.5.2 → tokenjam-0.5.4}/sdk-ts/package.json +1 -1
  33. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/factories.py +159 -11
  34. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_analytics.py +27 -0
  35. tokenjam-0.5.4/tests/integration/test_api.py +2914 -0
  36. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_cli.py +759 -9
  37. tokenjam-0.5.4/tests/integration/test_context_serve.py +204 -0
  38. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_cost_charts.py +2 -2
  39. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_db.py +35 -6
  40. tokenjam-0.5.4/tests/integration/test_storage_backend_parity.py +455 -0
  41. tokenjam-0.5.4/tests/integration/test_summarize_cli.py +417 -0
  42. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/manual-new-release-tests.md +22 -12
  43. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/manual-pre-release-testing.md +15 -14
  44. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/synthetic/test_alert_rules.py +88 -6
  45. tokenjam-0.5.4/tests/synthetic/test_async_hooks_concurrency.py +365 -0
  46. tokenjam-0.5.4/tests/synthetic/test_cost_tracking.py +232 -0
  47. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/synthetic/test_ingest.py +238 -8
  48. tokenjam-0.5.4/tests/unit/test_async_hooks.py +131 -0
  49. tokenjam-0.5.4/tests/unit/test_backfill.py +1311 -0
  50. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_budget_cap_policy.py +14 -8
  51. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_cmd_cost.py +9 -4
  52. tokenjam-0.5.4/tests/unit/test_cmd_hook.py +154 -0
  53. tokenjam-0.5.4/tests/unit/test_cmd_pricing.py +109 -0
  54. tokenjam-0.5.4/tests/unit/test_cmd_resume_brief.py +81 -0
  55. tokenjam-0.5.4/tests/unit/test_cmd_tokenmaxx.py +285 -0
  56. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_compare.py +1 -1
  57. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_config.py +51 -0
  58. tokenjam-0.5.4/tests/unit/test_context_diagnostic.py +540 -0
  59. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_cost.py +161 -19
  60. tokenjam-0.5.4/tests/unit/test_db_close_backfill.py +84 -0
  61. tokenjam-0.5.4/tests/unit/test_db_helpers.py +201 -0
  62. tokenjam-0.5.4/tests/unit/test_distill.py +321 -0
  63. tokenjam-0.5.4/tests/unit/test_doctor_first_signal.py +109 -0
  64. tokenjam-0.5.4/tests/unit/test_doctor_paths.py +35 -0
  65. tokenjam-0.5.4/tests/unit/test_doctor_statusline_wiring.py +76 -0
  66. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_drift.py +42 -0
  67. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_formatting.py +15 -7
  68. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_framing.py +46 -0
  69. tokenjam-0.5.4/tests/unit/test_harness_setup.py +93 -0
  70. tokenjam-0.5.4/tests/unit/test_humanize.py +62 -0
  71. tokenjam-0.5.4/tests/unit/test_lens_ui_regression.py +1858 -0
  72. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_litellm_integration.py +56 -0
  73. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_logs_converter.py +21 -0
  74. tokenjam-0.5.4/tests/unit/test_loop.py +156 -0
  75. tokenjam-0.5.4/tests/unit/test_loop_api.py +161 -0
  76. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_mcp_server.py +335 -6
  77. tokenjam-0.5.4/tests/unit/test_method_capture.py +193 -0
  78. tokenjam-0.5.4/tests/unit/test_method_spine.py +320 -0
  79. tokenjam-0.5.4/tests/unit/test_models.py +193 -0
  80. tokenjam-0.5.4/tests/unit/test_onboard_cap_hook.py +80 -0
  81. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_onboard_daemon.py +69 -0
  82. tokenjam-0.5.4/tests/unit/test_onboard_detect.py +215 -0
  83. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_onboard_first_run.py +8 -4
  84. tokenjam-0.5.4/tests/unit/test_onboard_resume_hook.py +89 -0
  85. tokenjam-0.5.4/tests/unit/test_onboard_stack_detection.py +74 -0
  86. tokenjam-0.5.4/tests/unit/test_onboard_statusline.py +101 -0
  87. tokenjam-0.5.4/tests/unit/test_onboard_verify.py +162 -0
  88. tokenjam-0.5.4/tests/unit/test_onboard_verify_cli.py +141 -0
  89. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_optimize.py +62 -0
  90. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_optimize_recoverable.py +4 -0
  91. tokenjam-0.5.4/tests/unit/test_optimize_savings_ci.py +218 -0
  92. tokenjam-0.5.4/tests/unit/test_optimize_subagent.py +109 -0
  93. tokenjam-0.5.4/tests/unit/test_opus_quota_audit.py +202 -0
  94. tokenjam-0.5.4/tests/unit/test_output_cap.py +228 -0
  95. tokenjam-0.5.4/tests/unit/test_phases.py +107 -0
  96. tokenjam-0.5.4/tests/unit/test_ping.py +137 -0
  97. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_pricing_override.py +80 -5
  98. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_provider_attribution_194.py +4 -4
  99. tokenjam-0.5.4/tests/unit/test_provider_content_capture.py +319 -0
  100. tokenjam-0.5.4/tests/unit/test_provider_run_id.py +50 -0
  101. tokenjam-0.5.4/tests/unit/test_quickstart.py +305 -0
  102. tokenjam-0.5.4/tests/unit/test_resume_brief.py +221 -0
  103. tokenjam-0.5.4/tests/unit/test_runlink.py +51 -0
  104. tokenjam-0.5.4/tests/unit/test_savings_log.py +92 -0
  105. tokenjam-0.5.4/tests/unit/test_schema_self_heal.py +189 -0
  106. tokenjam-0.5.4/tests/unit/test_session_label.py +194 -0
  107. tokenjam-0.5.4/tests/unit/test_session_subagents.py +53 -0
  108. tokenjam-0.5.4/tests/unit/test_sessionmap.py +185 -0
  109. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_spans_stats_repair.py +74 -1
  110. tokenjam-0.5.4/tests/unit/test_status_sdk.py +224 -0
  111. tokenjam-0.5.4/tests/unit/test_statusline.py +228 -0
  112. tokenjam-0.5.4/tests/unit/test_summarize_apply.py +180 -0
  113. tokenjam-0.5.4/tests/unit/test_summarize_candidates.py +219 -0
  114. tokenjam-0.5.4/tests/unit/test_summarize_catalog.py +50 -0
  115. tokenjam-0.5.4/tests/unit/test_summarize_delivery.py +415 -0
  116. tokenjam-0.5.4/tests/unit/test_summarize_detect.py +57 -0
  117. tokenjam-0.5.4/tests/unit/test_summarize_estimate.py +24 -0
  118. tokenjam-0.5.4/tests/unit/test_summarize_session.py +288 -0
  119. tokenjam-0.5.4/tests/unit/test_summarize_wrap.py +142 -0
  120. tokenjam-0.5.4/tests/unit/test_transcript.py +898 -0
  121. tokenjam-0.5.4/tests/unit/test_usage.py +99 -0
  122. tokenjam-0.5.4/tests/unit/test_workmap.py +210 -0
  123. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/app.py +8 -0
  124. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/middleware.py +3 -1
  125. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/analytics.py +45 -3
  126. tokenjam-0.5.4/tokenjam/api/routes/context.py +102 -0
  127. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/logs.py +22 -1
  128. tokenjam-0.5.4/tokenjam/api/routes/loop.py +167 -0
  129. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/optimize.py +17 -1
  130. tokenjam-0.5.4/tokenjam/api/routes/runs.py +256 -0
  131. tokenjam-0.5.4/tokenjam/api/routes/sessions.py +1874 -0
  132. tokenjam-0.5.4/tokenjam/api/routes/status.py +402 -0
  133. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_backfill.py +28 -6
  134. tokenjam-0.5.4/tokenjam/cli/cmd_context.py +358 -0
  135. tokenjam-0.5.4/tokenjam/cli/cmd_doctor.py +777 -0
  136. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_drift.py +2 -1
  137. tokenjam-0.5.4/tokenjam/cli/cmd_hook.py +166 -0
  138. tokenjam-0.5.4/tokenjam/cli/cmd_loop.py +281 -0
  139. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_mcp.py +14 -2
  140. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_onboard.py +814 -59
  141. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_optimize.py +386 -60
  142. tokenjam-0.5.4/tokenjam/cli/cmd_otel.py +37 -0
  143. tokenjam-0.5.4/tokenjam/cli/cmd_ping.py +223 -0
  144. tokenjam-0.5.4/tokenjam/cli/cmd_pricing.py +73 -0
  145. tokenjam-0.5.4/tokenjam/cli/cmd_quickstart.py +298 -0
  146. tokenjam-0.5.4/tokenjam/cli/cmd_quota_audit.py +323 -0
  147. tokenjam-0.5.4/tokenjam/cli/cmd_resume_brief.py +228 -0
  148. tokenjam-0.5.4/tokenjam/cli/cmd_savings.py +89 -0
  149. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_serve.py +8 -0
  150. tokenjam-0.5.4/tokenjam/cli/cmd_session_end.py +61 -0
  151. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_status.py +109 -31
  152. tokenjam-0.5.4/tokenjam/cli/cmd_statusline.py +149 -0
  153. tokenjam-0.5.4/tokenjam/cli/cmd_summarize.py +355 -0
  154. tokenjam-0.5.4/tokenjam/cli/cmd_tokenmaxx.py +354 -0
  155. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_uninstall.py +8 -0
  156. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/main.py +46 -2
  157. tokenjam-0.5.4/tokenjam/cli/onboard_detect.py +241 -0
  158. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/alerts.py +100 -21
  159. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/api_backend.py +29 -3
  160. tokenjam-0.5.4/tokenjam/core/backfill.py +873 -0
  161. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/config.py +122 -0
  162. tokenjam-0.5.4/tokenjam/core/context_diagnostic.py +977 -0
  163. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/cost.py +41 -72
  164. tokenjam-0.5.4/tokenjam/core/db.py +1907 -0
  165. tokenjam-0.5.4/tokenjam/core/distill.py +322 -0
  166. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/drift.py +9 -0
  167. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/framing.py +87 -0
  168. tokenjam-0.5.4/tokenjam/core/harness_setup.py +164 -0
  169. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/ingest.py +248 -5
  170. tokenjam-0.5.4/tokenjam/core/loop.py +336 -0
  171. tokenjam-0.5.4/tokenjam/core/method_capture.py +132 -0
  172. tokenjam-0.5.4/tokenjam/core/method_spine.py +335 -0
  173. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/models.py +95 -5
  174. tokenjam-0.5.4/tokenjam/core/onboard_verify.py +168 -0
  175. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/optimize/__init__.py +8 -0
  176. tokenjam-0.5.4/tokenjam/core/optimize/analyzers/model_downgrade.py +422 -0
  177. tokenjam-0.5.4/tokenjam/core/optimize/analyzers/subagent_rightsizing.py +210 -0
  178. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/optimize/runner.py +33 -1
  179. tokenjam-0.5.4/tokenjam/core/optimize/stats.py +80 -0
  180. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/optimize/types.py +71 -0
  181. tokenjam-0.5.4/tokenjam/core/output_cap.py +281 -0
  182. tokenjam-0.5.4/tokenjam/core/phases.py +125 -0
  183. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/pricing.py +132 -10
  184. tokenjam-0.5.4/tokenjam/core/resume_brief.py +483 -0
  185. tokenjam-0.5.4/tokenjam/core/runlink.py +92 -0
  186. tokenjam-0.5.4/tokenjam/core/savings_log.py +145 -0
  187. tokenjam-0.5.4/tokenjam/core/session_timeline.py +185 -0
  188. tokenjam-0.5.4/tokenjam/core/sessionmap.py +188 -0
  189. tokenjam-0.5.4/tokenjam/core/summarize/__init__.py +28 -0
  190. tokenjam-0.5.4/tokenjam/core/summarize/agent_files.toml +36 -0
  191. tokenjam-0.5.4/tokenjam/core/summarize/apply.py +106 -0
  192. tokenjam-0.5.4/tokenjam/core/summarize/backup.py +79 -0
  193. tokenjam-0.5.4/tokenjam/core/summarize/candidates.py +358 -0
  194. tokenjam-0.5.4/tokenjam/core/summarize/catalog.py +91 -0
  195. tokenjam-0.5.4/tokenjam/core/summarize/delivery.py +256 -0
  196. tokenjam-0.5.4/tokenjam/core/summarize/detect.py +111 -0
  197. tokenjam-0.5.4/tokenjam/core/summarize/estimate.py +29 -0
  198. tokenjam-0.5.4/tokenjam/core/summarize/session.py +273 -0
  199. tokenjam-0.5.4/tokenjam/core/summarize/wrap.py +196 -0
  200. tokenjam-0.5.4/tokenjam/core/transcript.py +1083 -0
  201. tokenjam-0.5.4/tokenjam/core/usage.py +113 -0
  202. tokenjam-0.5.4/tokenjam/core/workmap.py +417 -0
  203. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/mcp/server.py +349 -19
  204. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/otel/otlp_parsing.py +5 -1
  205. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/otel/provider.py +21 -2
  206. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/otel/semconv.py +27 -0
  207. tokenjam-0.5.4/tokenjam/pricing/models.toml +253 -0
  208. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/bootstrap.py +15 -1
  209. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/_request_capture.py +88 -0
  210. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/anthropic.py +16 -1
  211. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/bedrock.py +54 -1
  212. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/gemini.py +19 -1
  213. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/litellm.py +34 -27
  214. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/openai.py +13 -1
  215. tokenjam-0.5.4/tokenjam/ui/index.html +6877 -0
  216. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/utils/formatting.py +13 -11
  217. tokenjam-0.5.4/tokenjam/utils/humanize.py +44 -0
  218. tokenjam-0.5.4/tokenjam/utils/signatures.py +31 -0
  219. tokenjam-0.5.2/.github/workflows/publish-npm.yml +0 -35
  220. tokenjam-0.5.2/README.md +0 -286
  221. tokenjam-0.5.2/docs/claude-code-integration.md +0 -81
  222. tokenjam-0.5.2/docs/cli-reference.md +0 -164
  223. tokenjam-0.5.2/tests/integration/test_api.py +0 -724
  224. tokenjam-0.5.2/tests/synthetic/test_cost_tracking.py +0 -261
  225. tokenjam-0.5.2/tests/unit/test_backfill.py +0 -599
  226. tokenjam-0.5.2/tests/unit/test_cmd_tokenmaxx.py +0 -163
  227. tokenjam-0.5.2/tests/unit/test_db_helpers.py +0 -35
  228. tokenjam-0.5.2/tests/unit/test_lens_ui_regression.py +0 -912
  229. tokenjam-0.5.2/tests/unit/test_models.py +0 -111
  230. tokenjam-0.5.2/tokenjam/api/routes/status.py +0 -109
  231. tokenjam-0.5.2/tokenjam/cli/cmd_doctor.py +0 -396
  232. tokenjam-0.5.2/tokenjam/cli/cmd_tokenmaxx.py +0 -333
  233. tokenjam-0.5.2/tokenjam/core/backfill.py +0 -563
  234. tokenjam-0.5.2/tokenjam/core/db.py +0 -1076
  235. tokenjam-0.5.2/tokenjam/core/optimize/analyzers/model_downgrade.py +0 -223
  236. tokenjam-0.5.2/tokenjam/pricing/models.toml +0 -124
  237. tokenjam-0.5.2/tokenjam/ui/index.html +0 -3533
  238. {tokenjam-0.5.2 → tokenjam-0.5.4}/.github/CODEOWNERS +0 -0
  239. {tokenjam-0.5.2 → tokenjam-0.5.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  240. {tokenjam-0.5.2 → tokenjam-0.5.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  241. {tokenjam-0.5.2 → tokenjam-0.5.4}/.github/ISSUE_TEMPLATE/integration_request.md +0 -0
  242. {tokenjam-0.5.2 → tokenjam-0.5.4}/.github/pull_request_template.md +0 -0
  243. {tokenjam-0.5.2 → tokenjam-0.5.4}/.github/scripts/archive_traffic.py +0 -0
  244. {tokenjam-0.5.2 → tokenjam-0.5.4}/.github/workflows/ci.yml +0 -0
  245. {tokenjam-0.5.2 → tokenjam-0.5.4}/.github/workflows/publish-pypi.yml +0 -0
  246. {tokenjam-0.5.2 → tokenjam-0.5.4}/.github/workflows/traffic-archive.yml +0 -0
  247. {tokenjam-0.5.2 → tokenjam-0.5.4}/AGENTS.md +0 -0
  248. {tokenjam-0.5.2 → tokenjam-0.5.4}/CODE_OF_CONDUCT.md +0 -0
  249. {tokenjam-0.5.2 → tokenjam-0.5.4}/CONTRIBUTING.md +0 -0
  250. {tokenjam-0.5.2 → tokenjam-0.5.4}/LICENSE +0 -0
  251. {tokenjam-0.5.2 → tokenjam-0.5.4}/Makefile +0 -0
  252. {tokenjam-0.5.2 → tokenjam-0.5.4}/SECURITY.md +0 -0
  253. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/backfill/helicone.md +0 -0
  254. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/backfill/langfuse.md +0 -0
  255. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/backfill/otlp.md +0 -0
  256. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/backfill/overview.md +0 -0
  257. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/brand/README.md +0 -0
  258. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/brand/tokenjam-banner.png +0 -0
  259. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/brand/tokenjam-banner.svg +0 -0
  260. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/brand/tokenjam-icon.png +0 -0
  261. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/brand/tokenjam-icon.svg +0 -0
  262. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/brand/tokenjam-wordmark.png +0 -0
  263. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/brand/tokenjam-wordmark.svg +0 -0
  264. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/configuration.md +0 -0
  265. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/export.md +0 -0
  266. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/framework-support.md +0 -0
  267. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/internal/lens-vendor-versions.md +0 -0
  268. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/internal/release-smoke-checklist.md +0 -0
  269. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/internal/specs/.gitkeep +0 -0
  270. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/nemoclaw-integration.md +0 -0
  271. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/openclaw.md +0 -0
  272. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/optimize/reuse.md +0 -0
  273. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/policy/overview.md +0 -0
  274. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/proxy/overview.md +0 -0
  275. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/python-sdk.md +0 -0
  276. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/screenshots/tj-budget.png +0 -0
  277. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/screenshots/tj-cost.png +0 -0
  278. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/screenshots/tj-dashboard-leaderboard.png +0 -0
  279. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/screenshots/tj-dashboard-tools.png +0 -0
  280. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/screenshots/tj-dashboard.png +0 -0
  281. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/screenshots/tj-status.png +0 -0
  282. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/screenshots/tj-traces.png +0 -0
  283. {tokenjam-0.5.2 → tokenjam-0.5.4}/docs/typescript-sdk.md +0 -0
  284. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/README.md +0 -0
  285. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/alerts_and_drift/_shared.py +0 -0
  286. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/alerts_and_drift/budget_breach_demo.py +0 -0
  287. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/alerts_and_drift/drift_demo.py +0 -0
  288. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/alerts_and_drift/sensitive_actions_demo.py +0 -0
  289. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/multi/rag_pipeline.py +0 -0
  290. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/multi/research_team.py +0 -0
  291. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/multi/router_agent.py +0 -0
  292. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/multi/sample_docs/agent_patterns.txt +0 -0
  293. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/multi/sample_docs/cost_management.txt +0 -0
  294. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/multi/sample_docs/observability.txt +0 -0
  295. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/multi/sample_docs/safety.txt +0 -0
  296. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/openclaw/README.md +0 -0
  297. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/single_framework/autogen_agent.py +0 -0
  298. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/single_framework/crewai_agent.py +0 -0
  299. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/single_framework/langchain_agent.py +0 -0
  300. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/single_framework/langgraph_agent.py +0 -0
  301. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/single_framework/llamaindex_agent.py +0 -0
  302. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/single_provider/anthropic_agent.py +0 -0
  303. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/single_provider/bedrock_agent.py +0 -0
  304. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/single_provider/gemini_agent.py +0 -0
  305. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/single_provider/litellm_agent.py +0 -0
  306. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/single_provider/openai_agent.py +0 -0
  307. {tokenjam-0.5.2 → tokenjam-0.5.4}/examples/single_provider/openai_agents_sdk_agent.py +0 -0
  308. {tokenjam-0.5.2 → tokenjam-0.5.4}/growth/README.md +0 -0
  309. {tokenjam-0.5.2 → tokenjam-0.5.4}/incidents/hallucination-drift/BLOG.md +0 -0
  310. {tokenjam-0.5.2 → tokenjam-0.5.4}/incidents/hallucination-drift/README.md +0 -0
  311. {tokenjam-0.5.2 → tokenjam-0.5.4}/incidents/hallucination-drift/scenario.py +0 -0
  312. {tokenjam-0.5.2 → tokenjam-0.5.4}/incidents/retry-loop/BLOG.md +0 -0
  313. {tokenjam-0.5.2 → tokenjam-0.5.4}/incidents/retry-loop/README.md +0 -0
  314. {tokenjam-0.5.2 → tokenjam-0.5.4}/incidents/surprise-cost/BLOG.md +0 -0
  315. {tokenjam-0.5.2 → tokenjam-0.5.4}/incidents/surprise-cost/README.md +0 -0
  316. {tokenjam-0.5.2 → tokenjam-0.5.4}/incidents/surprise-cost/scenario.py +0 -0
  317. {tokenjam-0.5.2 → tokenjam-0.5.4}/sdk-ts/README.md +0 -0
  318. {tokenjam-0.5.2 → tokenjam-0.5.4}/sdk-ts/package-lock.json +0 -0
  319. {tokenjam-0.5.2 → tokenjam-0.5.4}/sdk-ts/src/client.test.ts +0 -0
  320. {tokenjam-0.5.2 → tokenjam-0.5.4}/sdk-ts/src/client.ts +0 -0
  321. {tokenjam-0.5.2 → tokenjam-0.5.4}/sdk-ts/src/index.ts +0 -0
  322. {tokenjam-0.5.2 → tokenjam-0.5.4}/sdk-ts/src/semconv.test.ts +0 -0
  323. {tokenjam-0.5.2 → tokenjam-0.5.4}/sdk-ts/src/semconv.ts +0 -0
  324. {tokenjam-0.5.2 → tokenjam-0.5.4}/sdk-ts/src/span-builder.test.ts +0 -0
  325. {tokenjam-0.5.2 → tokenjam-0.5.4}/sdk-ts/src/span-builder.ts +0 -0
  326. {tokenjam-0.5.2 → tokenjam-0.5.4}/sdk-ts/src/types.ts +0 -0
  327. {tokenjam-0.5.2 → tokenjam-0.5.4}/sdk-ts/tsconfig.json +0 -0
  328. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/__init__.py +0 -0
  329. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/agent-post-release-runner.md +0 -0
  330. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/agent-post-release-v0.4.0.md +0 -0
  331. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/agent-pre-release-runner.md +0 -0
  332. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/agent-pre-release-v0.4.0.md +0 -0
  333. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/agent-pre-release-v0.4.1.md +0 -0
  334. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/agents/__init__.py +0 -0
  335. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/agents/email_agent_budget_breach.py +0 -0
  336. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/agents/email_agent_drift.py +0 -0
  337. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/agents/email_agent_loop.py +0 -0
  338. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/agents/email_agent_normal.py +0 -0
  339. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/agents/mock_llm.py +0 -0
  340. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/agents/test_mock_scenarios.py +0 -0
  341. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/conftest.py +0 -0
  342. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/e2e/__init__.py +0 -0
  343. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/e2e/conftest.py +0 -0
  344. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/e2e/test_real_llm.py +0 -0
  345. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/fixtures/helicone_real_response.json +0 -0
  346. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/fixtures/langfuse_real_response.json +0 -0
  347. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/fixtures/otlp_sample.json +0 -0
  348. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/__init__.py +0 -0
  349. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_cli_api_framing_parity.py +0 -0
  350. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_cmd_proxy.py +0 -0
  351. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_cmd_route.py +0 -0
  352. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_cost_framing_window.py +0 -0
  353. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_demos.py +0 -0
  354. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_first_run_roundtrip_239.py +0 -0
  355. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_full_pipeline.py +0 -0
  356. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_litellm_enrichment.py +0 -0
  357. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_logs_api.py +0 -0
  358. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_policy_api.py +0 -0
  359. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_proxy_app.py +0 -0
  360. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_proxy_policy.py +0 -0
  361. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/integration/test_sdk_config_discovery.py +0 -0
  362. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/results/agent-post-release-v0.4.0-20260619T221412Z.md +0 -0
  363. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/results/agent-pre-release-v0.4.0-20260619T202233Z.md +0 -0
  364. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/results/agent-pre-release-v0.4.0-20260619T205108Z.md +0 -0
  365. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/results/agent-pre-release-v0.4.1-20260620T002546Z.md +0 -0
  366. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/synthetic/__init__.py +0 -0
  367. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/synthetic/test_drift_detection.py +0 -0
  368. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/synthetic/test_request_capture.py +0 -0
  369. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/synthetic/test_schema_validation.py +0 -0
  370. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/toy_agent/toy_agent.py +0 -0
  371. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/__init__.py +0 -0
  372. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_alerts.py +0 -0
  373. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_cache_efficacy.py +0 -0
  374. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_cache_recommend.py +0 -0
  375. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_cmd_policy.py +0 -0
  376. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_cmd_stop.py +0 -0
  377. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_config_secret_divergence.py +0 -0
  378. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_cost_compare_framing.py +0 -0
  379. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_cycle.py +0 -0
  380. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_demo_env.py +0 -0
  381. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_demo_packaging.py +0 -0
  382. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_demo_scenarios.py +0 -0
  383. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_export_claude_code.py +0 -0
  384. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_export_route_generators.py +0 -0
  385. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_ingest_helicone.py +0 -0
  386. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_ingest_langfuse.py +0 -0
  387. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_ingest_otlp.py +0 -0
  388. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_litellm_client.py +0 -0
  389. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_mcp_policy.py +0 -0
  390. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_no_tracked_dev_secrets.py +0 -0
  391. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_onboard_codex.py +0 -0
  392. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_onboard_hygiene.py +0 -0
  393. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_onboard_plan.py +0 -0
  394. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_openclaw_ingest.py +0 -0
  395. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_optimize_reuse.py +0 -0
  396. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_plan_tier_apply.py +0 -0
  397. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_policy_engine.py +0 -0
  398. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_prompt_bloat.py +0 -0
  399. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_proxy_audit.py +0 -0
  400. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_proxy_gate.py +0 -0
  401. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_reuse_skeleton.py +0 -0
  402. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_time_parse.py +0 -0
  403. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_transport_401.py +0 -0
  404. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_ui_offline.py +0 -0
  405. {tokenjam-0.5.2 → tokenjam-0.5.4}/tests/unit/test_workflow_restructure.py +0 -0
  406. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/__init__.py +0 -0
  407. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/__init__.py +0 -0
  408. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/deps.py +0 -0
  409. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/__init__.py +0 -0
  410. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/agents.py +0 -0
  411. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/alerts.py +0 -0
  412. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/budget.py +0 -0
  413. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/cost.py +0 -0
  414. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/cost_compare.py +0 -0
  415. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/drift.py +0 -0
  416. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/metrics.py +0 -0
  417. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/otlp.py +0 -0
  418. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/policy.py +0 -0
  419. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/reuse.py +0 -0
  420. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/spans.py +0 -0
  421. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/tools.py +0 -0
  422. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/traces.py +0 -0
  423. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/api/routes/version.py +0 -0
  424. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/__init__.py +0 -0
  425. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/banner.py +0 -0
  426. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_alerts.py +0 -0
  427. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_budget.py +0 -0
  428. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_cost.py +0 -0
  429. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_demo.py +0 -0
  430. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_export.py +0 -0
  431. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_policy.py +0 -0
  432. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_proxy.py +0 -0
  433. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_report.py +0 -0
  434. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_route.py +0 -0
  435. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_stop.py +0 -0
  436. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_tools.py +0 -0
  437. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/cmd_traces.py +0 -0
  438. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/cli/home.py +0 -0
  439. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/__init__.py +0 -0
  440. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/cycle.py +0 -0
  441. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/export/__init__.py +0 -0
  442. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/export/ccr.py +0 -0
  443. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/export/claude_code.py +0 -0
  444. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/export/common.py +0 -0
  445. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/export/litellm.py +0 -0
  446. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/export/reuse_report.py +0 -0
  447. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/export/reuse_skeleton.py +0 -0
  448. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/ingest_adapters/__init__.py +0 -0
  449. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/ingest_adapters/helicone.py +0 -0
  450. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/ingest_adapters/langfuse.py +0 -0
  451. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/ingest_adapters/otlp.py +0 -0
  452. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/optimize/README.md +0 -0
  453. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/optimize/analyzers/__init__.py +0 -0
  454. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/optimize/analyzers/budget_projection.py +0 -0
  455. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/optimize/analyzers/cache_efficacy.py +0 -0
  456. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/optimize/analyzers/cache_recommend.py +0 -0
  457. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/optimize/analyzers/plan_reuse.py +0 -0
  458. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/optimize/analyzers/prompt_bloat.py +0 -0
  459. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/optimize/analyzers/workflow_restructure.py +0 -0
  460. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/optimize/registry.py +0 -0
  461. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/retention.py +0 -0
  462. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/core/schema_validator.py +0 -0
  463. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/demo/__init__.py +0 -0
  464. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/demo/env.py +0 -0
  465. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/mcp/__init__.py +0 -0
  466. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/otel/__init__.py +0 -0
  467. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/otel/exporters.py +0 -0
  468. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/proxy/__init__.py +0 -0
  469. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/proxy/app.py +0 -0
  470. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/proxy/audit.py +0 -0
  471. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/proxy/engine.py +0 -0
  472. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/proxy/gate.py +0 -0
  473. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/proxy/observer.py +0 -0
  474. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/proxy/recommend.py +0 -0
  475. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/proxy/server.py +0 -0
  476. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/proxy/wiring.py +0 -0
  477. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/py.typed +0 -0
  478. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/__init__.py +0 -0
  479. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/agent.py +0 -0
  480. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/client.py +0 -0
  481. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/http_exporter.py +0 -0
  482. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/__init__.py +0 -0
  483. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/autogen.py +0 -0
  484. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/base.py +0 -0
  485. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/crewai.py +0 -0
  486. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/langchain.py +0 -0
  487. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/langgraph.py +0 -0
  488. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/llamaindex.py +0 -0
  489. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/nemoclaw.py +0 -0
  490. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/integrations/openai_agents_sdk.py +0 -0
  491. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/sdk/transport.py +0 -0
  492. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/ui/vendor/htm.js +0 -0
  493. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/ui/vendor/preact-hooks.js +0 -0
  494. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/ui/vendor/preact.js +0 -0
  495. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/ui/vendor/uplot.css +0 -0
  496. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/ui/vendor/uplot.js +0 -0
  497. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/utils/__init__.py +0 -0
  498. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/utils/ids.py +0 -0
  499. {tokenjam-0.5.2 → tokenjam-0.5.4}/tokenjam/utils/time_parse.py +0 -0
@@ -0,0 +1,40 @@
1
+ # VCS
2
+ .git
3
+ .gitignore
4
+
5
+ # CI / Deployment config
6
+ .github
7
+ Dockerfile
8
+ .dockerignore
9
+
10
+ # Local TokenJam runtime state (untracked; .tj/config.toml holds a live ingest_secret)
11
+ .tj
12
+ .tj-test-data
13
+ *.duckdb
14
+
15
+ # Python caches / build artifacts
16
+ __pycache__
17
+ *.py[cod]
18
+ *.egg-info
19
+ .eggs
20
+ build
21
+ dist
22
+ .mypy_cache
23
+ .ruff_cache
24
+ .pytest_cache
25
+
26
+ # Test + docs (not needed in the image; incidents/ IS needed and is not ignored)
27
+ tests
28
+ docs
29
+ examples
30
+ growth
31
+ traffic
32
+
33
+ # TypeScript SDK (independent package)
34
+ sdk-ts/node_modules
35
+ sdk-ts/dist
36
+
37
+ # Editor / OS cruft
38
+ .DS_Store
39
+ .vscode
40
+ .idea
@@ -0,0 +1,71 @@
1
+ name: Docker publish
2
+ description: Log in to a registry, build a Docker image, and push it with computed tags.
3
+
4
+ inputs:
5
+ registry:
6
+ description: Container registry to push to.
7
+ required: false
8
+ default: ghcr.io
9
+ username:
10
+ description: Registry username (e.g. github.actor).
11
+ required: true
12
+ password:
13
+ description: Registry password or token (e.g. secrets.GITHUB_TOKEN).
14
+ required: true
15
+ image:
16
+ description: Fully-qualified image name without tag (e.g. ghcr.io/owner/repo).
17
+ required: true
18
+ context:
19
+ description: Build context path.
20
+ required: false
21
+ default: .
22
+ dockerfile:
23
+ description: Path to the Dockerfile.
24
+ required: false
25
+ default: Dockerfile
26
+
27
+ outputs:
28
+ tags:
29
+ description: Newline-separated list of tags that were pushed.
30
+ value: ${{ steps.meta.outputs.tags }}
31
+ digest:
32
+ description: Image digest of the pushed image.
33
+ value: ${{ steps.build.outputs.digest }}
34
+
35
+ runs:
36
+ using: composite
37
+ steps:
38
+ - name: Set up Docker Buildx
39
+ uses: docker/setup-buildx-action@v3
40
+
41
+ - name: Log in to registry
42
+ uses: docker/login-action@v3
43
+ with:
44
+ registry: ${{ inputs.registry }}
45
+ username: ${{ inputs.username }}
46
+ password: ${{ inputs.password }}
47
+
48
+ - name: Extract image metadata
49
+ id: meta
50
+ uses: docker/metadata-action@v5
51
+ with:
52
+ images: ${{ inputs.image }}
53
+ # Release-driven tags: the semver of the published release plus `latest`.
54
+ # No per-commit short-SHA tag — the image only ships on a release, so its
55
+ # version maps 1:1 to the release tag (matching the PyPI/npm packages).
56
+ tags: |
57
+ type=semver,pattern={{version}}
58
+ type=semver,pattern={{major}}.{{minor}}
59
+ type=raw,value=latest
60
+
61
+ - name: Build and push image
62
+ id: build
63
+ uses: docker/build-push-action@v6
64
+ with:
65
+ context: ${{ inputs.context }}
66
+ file: ${{ inputs.dockerfile }}
67
+ push: true
68
+ tags: ${{ steps.meta.outputs.tags }}
69
+ labels: ${{ steps.meta.outputs.labels }}
70
+ cache-from: type=gha
71
+ cache-to: type=gha,mode=max
@@ -0,0 +1,36 @@
1
+ name: Build and publish Docker image
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ # Publish the container image only when a GitHub Release is published, so the
6
+ # image version lines up with the release-gated PyPI + npm packages (both fire
7
+ # on `release: [published]` — see publish-pypi.yml / publish-npm.yml). This
8
+ # replaces the previous per-commit / after-CI trigger, which shipped an image
9
+ # on every green main build.
10
+ release:
11
+ types: [published]
12
+
13
+ permissions:
14
+ contents: read
15
+ packages: write
16
+
17
+ concurrency:
18
+ group: ${{ github.workflow }}-${{ github.ref }}
19
+ cancel-in-progress: true
20
+
21
+ jobs:
22
+ build-and-publish-image:
23
+ runs-on: ubuntu-latest
24
+ timeout-minutes: 30
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+
28
+ - name: Build and push tokenjam image
29
+ uses: ./.github/actions/docker-publish
30
+ with:
31
+ registry: ghcr.io
32
+ username: ${{ github.actor }}
33
+ password: ${{ secrets.GITHUB_TOKEN }}
34
+ image: ghcr.io/${{ github.repository }}
35
+ context: .
36
+ dockerfile: Dockerfile
@@ -0,0 +1,69 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish-npm:
9
+ if: startsWith(github.ref_name, 'v')
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ node-version: "22"
17
+ registry-url: https://registry.npmjs.org
18
+
19
+ - name: Install dependencies
20
+ working-directory: sdk-ts
21
+ run: npm install
22
+
23
+ - name: Run tests
24
+ working-directory: sdk-ts
25
+ run: npm test
26
+
27
+ - name: Build
28
+ working-directory: sdk-ts
29
+ run: npm run build
30
+
31
+ - name: Publish
32
+ working-directory: sdk-ts
33
+ run: npm publish --access public
34
+ env:
35
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
36
+
37
+ # Publishes the zero-install `npx tokenjam` wrapper (npm-wrapper/, issue #6):
38
+ # the unscoped `tokenjam` package that shells out to the Python CLI. Separate
39
+ # job from the SDK publish above so a wrapper failure never blocks @tokenjam/sdk
40
+ # (and vice versa); both fire on the same `v*` release tag.
41
+ publish-npm-wrapper:
42
+ if: startsWith(github.ref_name, 'v')
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+
47
+ - uses: actions/setup-node@v4
48
+ with:
49
+ node-version: "22"
50
+ registry-url: https://registry.npmjs.org
51
+
52
+ # Cheap guard — the wrapper has no test suite (no Python in the JS lane),
53
+ # so a syntax error would otherwise only surface after publish.
54
+ - name: Syntax-check launcher
55
+ working-directory: npm-wrapper
56
+ run: node -c bin/tj.js
57
+
58
+ # Derive the published version from the release tag (v0.5.3 -> 0.5.3) so the
59
+ # wrapper can never drift from the release being cut — no manual bump needed.
60
+ # --allow-same-version tolerates package.json already matching the tag.
61
+ - name: Sync version to release tag
62
+ working-directory: npm-wrapper
63
+ run: npm version "${GITHUB_REF_NAME#v}" --no-git-tag-version --allow-same-version
64
+
65
+ - name: Publish
66
+ working-directory: npm-wrapper
67
+ run: npm publish --access public
68
+ env:
69
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -50,6 +50,8 @@ tj.toml
50
50
 
51
51
  # Superpowers skill output (internal planning artifacts, not for OSS)
52
52
  docs/superpowers/
53
+ .plans/
54
+ .specs/
53
55
  .gstack/
54
56
 
55
57
  # Per-release pre-release test logs are committed (for reference) — see
@@ -146,11 +146,15 @@ Spans enter from two paths, both converging at `IngestPipeline.process()`:
146
146
  1. **In-process**: Python SDK `@watch()` + provider patches -> `TjSpanExporter` -> `IngestPipeline`
147
147
  2. **HTTP**: TypeScript SDK (or any OTLP client) -> `POST /api/v1/spans` (auth required) -> `IngestPipeline`
148
148
 
149
- Post-ingest hooks run synchronously after each span is written to DB:
150
- 1. `CostEngine.process_span()` — calculates USD cost from token counts
149
+ Post-ingest hooks run after each span is written to DB:
150
+ 1. `CostEngine.process_span()` — calculates USD cost from token counts. **Always synchronous** (on the ingest thread) so budget/cost reads immediately after ingest are accurate.
151
151
  2. `AlertEngine.evaluate()` — checks all per-span alert rules
152
152
  3. `SchemaValidator.validate()` — validates tool outputs against JSON Schema
153
153
 
154
+ **Sync vs async hooks (`[alerts] async_hooks`, default `false`).** By default all three hooks run inline on the ingest thread — a config with `async_hooks` unset behaves exactly as the historical synchronous pipeline. When `[alerts] async_hooks = true`, only the *advisory* hooks (`AlertEngine` + `SchemaValidator`) are moved onto a single background worker thread (`TjHookWorker`) fed by a bounded `queue.Queue` (`HOOK_QUEUE_MAXSIZE`, drop-**oldest** overflow policy — dropped spans are logged, never silent); `CostEngine` stays synchronous regardless. This trims span-acknowledgment latency on the hot path at the cost of eventual-consistency for alerts.
155
+ - **Concurrency:** in async mode the worker thread writes alerts/validations while the ingest thread writes spans/cost. DuckDB uses optimistic concurrency (its **only** write-write conflict is same-row UPDATE overlap → `TransactionException: Conflict on update!`; disjoint-row/-table writes never conflict), so `DuckDBBackend` serializes **writes** through a re-entrant `write_lock` (`self._write_lock`) — every mutating method holds it. The per-thread-cursor **read** path (#124) stays lock-free. Do not remove the write lock; the load-bearing guard is `test_async_hooks_same_row_update_contention` in `tests/synthetic/test_async_hooks_concurrency.py` (many threads incrementing one shared session row — verified to raise `Conflict on update!` when the lock is stubbed with `nullcontext()`); the sibling disjoint-INSERT tests in that file don't catch a lock regression on their own. **Caveat:** direct `db.conn` writes issued from routes *outside* `DuckDBBackend`'s mutating methods (e.g. `alerts.py`'s `UPDATE alerts SET acknowledged`, `set_session_label`) do **not** take the write lock — safe today only because they hit disjoint rows/tables from the worker; a future direct write to a worker-touched table on a hot row would need to acquire `db.write_lock` explicitly.
156
+ - **Shutdown is lossless:** `IngestPipeline.flush()` blocks until the queue drains (guarded so it can't hang when the worker isn't alive); `close()` flushes then stops the worker via a sentinel (the worker drains to empty before exiting — it does *not* bail at the top of the loop on the shutdown event). Every shutdown path (`tj serve` lifespan, `TjSpanExporter.shutdown`/`force_flush`) flushes before/while closing so no queued alert is dropped.
157
+
154
158
  ### Package Dependency Rules
155
159
 
156
160
  - `tokenjam/core/` is pure domain logic. **Must never import from `tokenjam.cli` or `tokenjam.api`**. CLI and API import from core, not the reverse.
@@ -159,12 +163,13 @@ Post-ingest hooks run synchronously after each span is written to DB:
159
163
 
160
164
  ### Key Modules
161
165
 
162
- - **`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.
163
- - **`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.
166
+ - **`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. **Schema self-heal (#55):** `run_migrations` keys purely on the version INTEGER, so a version recorded-applied under an older/renumbered definition never re-runs and its `ADD COLUMN` silently never lands — then every ingest that writes the missing column hits a DuckDB Binder Error and is dropped (blank Status page). Guard: `run_migrations` calls `ensure_expected_columns` on every open, re-issuing `ADD COLUMN IF NOT EXISTS` for `EXPECTED_ADDITIVE_COLUMNS` regardless of recorded versions. **When you append a migration that adds a column the code reads/writes, add that column to `EXPECTED_ADDITIVE_COLUMNS` too** (and only additive/backward-compatible defs belong there). `tj doctor` surfaces any residual gap ("Schema integrity"); `tj doctor --repair` reconciles it. **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.
167
+ - **`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. Hooks run synchronously by default; `[alerts] async_hooks = true` moves the alert + schema hooks to a bounded-queue background worker (`TjHookWorker`) with `flush()`/`close()` for lossless shutdown (cost stays sync) — see the Data Flow "Sync vs async hooks" note above.
164
168
  - **`tokenjam/core/pricing.py`**: `ModelRates` (frozen dataclass), `load_pricing_table()` (LRU-cached), `get_rates(provider, model)`. Falls back to default rates for unknown models.
165
169
  - **`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.
166
170
  - **`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.
167
171
  - **`tokenjam/core/drift.py`**: `DriftDetector` — Z-score based behavioral drift detection, fires at session end.
172
+ - **`tokenjam/core/loop.py`**: Close-the-loop primitive (#53) — the loop-closing half that pairs with the capture half (`tj trace`). Pure domain (no cli/api imports), storage helpers accept a backend or raw conn like `db.set_session_label`. Three additive tables (migration 16): `run_annotations` (append-only human note + optional verdict `good`/`bad`/`mixed`/`unknown` on a run/session — NOT the single-row `session_labels` rename), `expectations` (a named case, optionally promoted `origin_session_id` FROM a bad run), `expectation_runs` (fix-history ledger, one row per rerun with `outcome` ∈ `pass`/`regress`/`unknown`). **Deliberately local-first and NOT an eval-runner**: pass/regress is a recorded *human verdict*, never an automated score (Critical Rule 14); tokenjam does not push to Langfuse (that integration is inbound-only). Surfaces that all hit this one storage: `api/routes/loop.py` (`/sessions/{id}/annotations`, `/expectations`, `/expectations/{id}` with history, `/expectations/{id}/runs`), the `tj loop` CLI group (`cmd_loop.py`, dual-path — api_mode when `tj serve` holds the lock, else direct DuckDB), and the Lens Session-Detail **"Loop" tab**. Product decision recorded in `docs/internal/close-the-loop.md`.
168
173
  - **`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**:
169
174
  - `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)
170
175
  - `budget_projection.py` → `@register("budget-projection")` — per-provider cycle spend vs `[budget.<provider>]` ceiling; only fires when budget > 0
@@ -177,6 +182,7 @@ Post-ingest hooks run synchronously after each span is written to DB:
177
182
  **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".
178
183
  - **`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`.
179
184
  - **`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).
185
+ - **`tokenjam/core/output_cap.py`** + **`tokenjam/cli/cmd_hook.py`** (`tj hook cap-output`): a PostToolUse output-trim hook that shrinks bloated tool outputs before they enter context, appending to an out-of-band savings sink (`core/savings_log.py`, folded into `tj savings` / `tj context`). It is **DEFAULT-OFF opt-in** (`[hooks.output_cap].enabled = false`; `cmd_onboard.py` does NOT auto-install it) — the code is safe (fail-open, transparent, 58 tests) but the required A/B gate **failed**. **Durable finding (measured, CC 2.1.198): Claude Code already truncates Bash tool output to exactly 30,000 chars *before* the PostToolUse hook ever sees it.** So the addressable bloat per Bash call is tiny (~1,206 tok reclaimed ≈ 0.06% of a ~2M-token cache-read footprint), agents self-mitigate (`| tail`, `grep`, `Read` — which the hook excludes), and 5-trial/arm sonnet A/B showed treatment cost **+5.6%** (run-to-run noise, no quota win). Users who want aggressive trimming opt in with one line + re-run `tj onboard`; a future rework should target what CC does NOT cap (large `WebFetch` bodies, `Read` behind a higher budget). Spec + numbers: `.specs/2026-07-02-tj-output-trim-hook-{design,AB-result}.md`.
180
186
  - **`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)`. **Plan tier:** `ingest_claude_code(db, …, config=…)` resolves `plan_tier` from `config.budgets["anthropic"].plan` (Claude Code is always Anthropic) and stamps it on each `SessionRecord` — mirroring the live `IngestPipeline._resolve_plan_tier` so backfilled sessions aren't all `"unknown"` (#176). Pass `config` from callers (`cmd_backfill`, `tj onboard`). The Langfuse/Helicone/OTLP adapters create **no** `SessionRecord` (spans only), so there's no plan tier to propagate there.
181
187
  - **`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.
182
188
  - **`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.
@@ -194,28 +200,34 @@ Post-ingest hooks run synchronously after each span is written to DB:
194
200
  - **`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`).
195
201
  - **`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.
196
202
  - **`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`, `reuse.py` (`GET /api/v1/reuse/clusters` — the Reuse finding plus skeleton-rendering extras `planning_texts` + `pricing_mode`; a dedicated endpoint (not bolted onto `/optimize`) so the per-cluster planner text, which can be many KB, isn't paid for on every Overview poll — #154), `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:** the sync (`def`) read routes (`/optimize`, `/cost/compare`) run in Starlette's threadpool, so concurrent requests reach the DB from multiple threads. `DuckDBBackend.conn` is a **per-thread DuckDB cursor** (`threading.local`) over one shared database — cursors are independent connections safe for concurrent use, so fan-out callers (the Overview) can fetch in parallel. Fixed in #124 (was a single shared connection that aborted under concurrent access); do not collapse `conn` back to one shared connection object.
197
- - **`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`.
203
+ - **`tokenjam/core/summarize/`**: Structure-aware prompt summarization (advisory; new feature). `detect.py` classifies prose vs. structure (fenced/inline code, tags, templates, tables); `candidates.py` (+ `catalog.py`/`estimate.py`) powers the `tj summarize list` scan; `wrap.py` is the pure protect→restore algorithm (wrap each structured span behind an id'd `<tj-keep>` marker, restore verbatim by id — structure is a hard guarantee); `session.py` is the no-scratch `prepare`/`check` lifecycle + staging (re-derives the wrap from the live file + a content hash; persists nothing but the staged result); `apply.py` writes a staged rewrite back to the file (default dry-run; `--go` writes) behind an owner + content-hash + symlink guard, with a gzip backup and `undo`; `backup.py` stores the gzipped original + metadata under `~/.tj/summary/backups/`; `delivery.py` is the CLI's automated rewrite step — `claude -p` (subprocess, timeout-guarded) or the Anthropic API (lazy `httpx` + the user's own `TJ_ANTHROPIC_API_KEY`) — plus the "pays for itself" amortization. Pure domain logic — no `tokenjam.cli`/`tokenjam.api` imports (delivery's API path lazily imports `httpx`, its lone outbound dependency).
204
+ - **`tokenjam/mcp/server.py`**: FastMCP stdio server exposing observability data to Claude Code (plus the summarize tools — `list_summarize_candidates`, `summarize_prep`, `summarize_check`, `summarize_apply`, `summarize_undo`; see `core/summarize/`). Uses either a read-only DuckDB connection or HTTP proxy to `tj serve`. Initialized via `init()` from `cmd_mcp.py`.
198
205
  - **`tokenjam/cli/main.py`**: Root Click group with global options (`--config`, `--json`, `--no-color`, `--db`, `--agent`, `-v`). Registers all subcommands.
199
206
 
200
207
  ### CLI Commands
201
208
 
202
209
  `tj --help` lists all commands; most are self-explanatory. Non-obvious ones:
203
210
 
211
+ - **`tj quickstart`** (`cmd_quickstart.py`) — the **zero-install / zero-config first run** (issue #6). Opens a **transient `InMemoryBackend`** (nothing written to `~/.tj`, no config read/written, no daemon started or contacted), backfills `~/.claude/projects/*.jsonl` into it via `ingest_claude_code`, then renders quota composition (reusing `core/context_diagnostic.py` from #4) + a session timeline (`core/session_timeline.py`). Output leads with ccusage-parity framing ("reads the same files ccusage does"). It is in `no_db_commands` so the CLI never opens the on-disk DB / trips the daemon lock for it. The intended zero-install invocations are `npx tokenjam` (via the `npm-wrapper/` package — the bare `tj` name is squatted on npm; the wrapper routes bare `npx tokenjam` to `quickstart`) and `uvx --from tokenjam tj quickstart`. `tj onboard` remains the opt-in "go deeper" (daemon/MCP/live) path. **Bare `tj` (no subcommand) prints the branded home screen** (`cli/home.py` `print_home()`, #240) — banner + next-best-action, reading only config presence (never the DB) — not quickstart; `--help`/`--version` are eager and still short-circuit.
204
212
  - **`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.
205
213
  - **`tj doctor`** (`cmd_doctor.py`) — health checks (config, DB, secrets, webhooks, drift readiness, schema-vs-capture consistency). Exit 0 = ok, 1 = warnings, 2 = errors.
206
214
  - **`tj optimize`** (`cmd_optimize.py`) — seven 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`, `reuse`, `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.
207
- - **`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.
215
+ - **`tj tokenmaxx`** (`cmd_tokenmaxx.py`) — shareable **quota/efficiency card** (#7; reframed from the old spend-tier card for the June-2026 "tokenminimizing" shift — an efficiency brag, never a spend brag). Leads with the context-COMPOSITION headline pulled from #4's `compute_context_diagnostic` (`tokenjam/core/context_diagnostic.py`): what share of quota went to *overhead* (re-reading history / CLAUDE.md / tool output, i.e. `diag.reread_share`) vs *real work* (uncached input + output). Classifies into 5 efficiency tiers keyed on the overhead share (TokenMinimizer ≤30% / LeanOperator ≤50% / SteadyState ≤70% / ContextHeavy ≤85% / QuotaSink) lower overhead = leaner = a better tier. **Quota-native (mirrors #5's polarity):** the headline renders as a token-share / "% of cycle tokens" via `core/framing` (the same `_quota_share` path `cmd_context` uses); dollars are demoted to a secondary "Implied API value" line shown ONLY for `api` plans and suppressed for subscription / local / unknown. Needs a **direct DuckDB connection** (reads context composition; can't run against a live `tj serve` same as `tj context` / `tj quota-audit`). `--weekly` is a 7-day "Quota Wrapped" recap preset. Output is a bordered Panel designed for screenshotting; the action line points at `tj context` (reclaim overhead) or `tj optimize`. Honesty discipline (Rule 14): the efficiency number is a measured token share, never a guaranteed saving.
208
216
  - **`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).
209
217
  - **`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.
210
218
  - **`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.
211
219
  - **`tj report`** (`cmd_report.py`) — generates standalone HTML visualizations of analyzer findings. `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); `tj report --reuse [<agent_id>]` renders the Reuse analyzer's per-cluster planning skeleton (HTML + Markdown sidecars). Writes to `~/.cache/tokenjam/reports/` (override via `TOKENJAM_REPORT_DIR`) and opens in the default browser. `--reuse` works when the daemon holds the DB lock: like `tj optimize`, it dispatches to `ApiBackend.fetch_reuse_clusters` (`GET /api/v1/reuse/clusters`) and renders from the HTTP payload (`write_reuse_report(..., planning_texts=...)`) instead of a direct DB connection (#154). `--trim` remains DB-direct only.
212
220
  - **`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.
213
221
 
222
+ - **`tj summarize`** (`cmd_summarize.py`) — structure-aware prompt summarization (advisory; new feature). `list` scans for prompt files worth summarizing (catalog-default; any scope-widening flag opens it to `*.md`) and estimates the per-call token saving — read-only. `prep`/`check` are the mechanism: `prep` wraps a prompt's structure behind id'd `<tj-keep>` markers and emits it for rewriting (CLI manual/copy, or the MCP `summarize_prep`/`summarize_check` tools for in-session rewrites); `check` re-reads + hash-guards the file, restores every block verbatim by id, and stages the result **only if structure survives** (a hard gate). No scratch state — the file on disk is the source of truth; `summarize` is in `no_db_commands` (config-only). `apply` writes a staged rewrite back to the file — default **dry-run** (it prints the unified diff), `--go` writes — guarded by an owner + content-hash + symlink check and a gzip backup; `undo` restores from that backup (refusing on drift). `prep --via claude-p` (your local `claude -p`, no key) or `prep --via api` (Anthropic with your `TJ_ANTHROPIC_API_KEY` + the required `[summarize] api_model`, reporting a "pays for itself" amortization) runs the rewrite for you in one shot — both still pass the `check` gate before staging.
223
+
224
+ - **`tj pricing list`** (`cmd_pricing.py`) — read-only inspection of the resolved model pricing table. Prints one row per `(provider, model)` with `input` / `output` / `cache_read` / `cache_write` rates and a `source` column (`override` / `packaged`) showing which layer the rate resolved from. Supports `--json` and `--model <substring>` (case-insensitive filter). `pricing` is in `no_db_commands` (config-only). The source derivation lives in `core/pricing.load_pricing_sources()` (public) — the CLI reads it rather than re-deriving precedence; a listed row is always `packaged` or `override` (the `$0.50`/`$2.00` default applies only to models absent from the table, which never appear here). The `set` half of a future `tj pricing set|list` is not built yet (#282 shipped `list` only).
225
+
214
226
  All commands support `--json` for machine-readable output. Commands that query alerts use exit code 1 if active (unacknowledged, unsuppressed) alerts exist.
215
227
 
216
228
  **CLI testing pattern:** Tests use `click.testing.CliRunner` with `unittest.mock.patch` on `tokenjam.cli.main.load_config` and `tokenjam.cli.main.open_db` to inject an `InMemoryBackend` and test config. See `tests/integration/test_cli.py`. Note: `cmd_doctor` opens its own DuckDB connection via `config.storage.path` to verify writability — in tests you must set this to a real temp path (e.g. `tmp_path / "test.duckdb"`).
217
229
 
218
- **`no_db_commands` in `cli/main.py`:** Commands that don't open the DB at startup — currently `{stop, uninstall, onboard, mcp, demo, policy}`. New commands that read only from config (or do their own DB connection later) should be added to this set so they work when `tj serve` holds the write lock. Tests for these commands can patch `open_db` with `side_effect=AssertionError(...)` to verify they never touch the DB.
230
+ **`no_db_commands` in `cli/main.py`:** Commands that don't open the DB at startup — currently `{stop, uninstall, onboard, mcp, demo, policy, proxy, summarize, pricing}`. New commands that read only from config (or do their own DB connection later) should be added to this set so they work when `tj serve` holds the write lock. Tests for these commands can patch `open_db` with `side_effect=AssertionError(...)` to verify they never touch the DB.
219
231
 
220
232
  **Test factories:** `tests/factories.py` provides `make_llm_span(billing_account="anthropic", ...)` and `make_session(plan_tier="api", ...)` with safe defaults that preserve existing test behavior. Tests exercising subscription / local / unknown plan-tier rendering paths should pass the field explicitly.
221
233
 
@@ -233,6 +245,12 @@ For `GET /api/v1/drift`, if `agent_id` is missing, return `JSONResponse(status_c
233
245
 
234
246
  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 = []`.
235
247
 
248
+ ### StorageBackend parity (serve shim vs DB)
249
+
250
+ `tj` ships three `StorageBackend` implementations: `DuckDBBackend`, `InMemoryBackend` (tests), and `ApiBackend` — the read-only HTTP shim the CLI uses when `tj serve` holds the DB write-lock. The shim reconstructs objects from JSON, so it repeatedly diverged from the DB backend *silently* (e.g. dropped `cache_write_tokens` on daemon-fetched spans, cache columns zeroed, `get_daily_cost` returning a cumulative total). That whole class of bug (#51) is now guarded by `tests/integration/test_storage_backend_parity.py`, which runs each faithfully-mirrored read method against both the DuckDB backend and the `ApiBackend` shim (over a **real in-process uvicorn server**, not the ASGI shortcut a sync `ApiBackend` can't use) and fails on any divergence.
251
+
252
+ **Rule: every `StorageBackend` protocol method must be classified in that file** — as parity-covered (`SHIM_PARITY_METHODS` + a spec in `_parity_specs`), a documented `SHIM_KNOWN_GAPS`, deliberately unimplemented (`SHIM_NOT_IMPLEMENTED`), or lifecycle. Adding a protocol method, or teaching the shim a new method, fails CI (`test_every_protocol_method_is_classified` / `test_unimplemented_methods_have_no_silent_shim`) until you classify it — which for a new read method means adding a parity assertion. Don't skip the classification to make CI green; that reopens the exact hole this file closes.
253
+
236
254
  ### Web UI ("TokenJam Lens")
237
255
 
238
256
  `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.
@@ -295,20 +313,22 @@ Reinstall behavior: `--claude-code` and `--codex` onboard check `_daemon_already
295
313
 
296
314
  ## MCP Server
297
315
 
298
- `tj mcp` starts a FastMCP stdio server for Claude Code integration. The connection mode is chosen at startup by `cmd_mcp.py`:
316
+ **The MCP is an SDK / API surface, not a Claude Code / Codex one (#59).** It puts tj *in the request path* — the right place for SDK / API integrations doing real-time enforcement/policy/budgets. It is deliberately **not** wired for Claude Code / Codex subscription users: an in-loop MCP is a per-turn quota burden on them (a measured A/B showed **+36%** model-weighted quota vs a no-tj control). Those users get tj **out-of-band**: the zero-token statusline (`tj statusline`, wired by `tj onboard --claude-code`) plus OTel telemetry ingest. `tj mcp` still works for anyone who invokes it; onboarding just no longer defaults CC/Codex users into it.
317
+
318
+ `tj mcp` starts a FastMCP stdio server. The connection mode is chosen at startup by `cmd_mcp.py`:
299
319
  1. If `tj serve` is reachable on `config.api.{host,port}`, MCP proxies to it via HTTP (live ingest visible).
300
320
  2. Otherwise it tries to spawn `tj serve` in the background and waits up to 10s for the port.
301
321
  3. If neither works, it falls back to a **read-only DuckDB connection** — read tools still work, but newly ingested spans won't appear until restart.
302
322
  4. If no config file is found, `init()` is skipped and tools return a no-config sentinel.
303
323
 
304
- To wire into Claude Code locally: `claude mcp add tj --scope user -- tj mcp` (the `--claude-code` and `--codex` onboard flows do this automatically when the `claude` CLI is on PATH; `--codex` also writes `[mcp_servers.tj]` to `~/.codex/config.toml`).
324
+ SDK / API users who want the in-loop tools can wire it manually: `claude mcp add tj --scope user -- tj mcp`. The `--claude-code` and `--codex` onboard flows **no longer** register the MCP (they wire the out-of-band statusline / OTel instead), and a re-onboard retires any tj-managed `[mcp_servers.tj]` block a previous version wrote to `~/.codex/config.toml`.
305
325
 
306
326
  ## Codex CLI Integration
307
327
 
308
- `tj onboard --codex` writes `[otel]` and `[mcp_servers.tj]` blocks to `~/.codex/config.toml`. Notes:
328
+ `tj onboard --codex` writes an `[otel]` block to `~/.codex/config.toml` (out-of-band telemetry only). It does **not** register the tj MCP for Codex (#59) — Codex has no statusline surface, so tj stays fully out-of-band via OTel + the `tj` CLI; a re-onboard retires any `[mcp_servers.tj]` block a previous version wrote. Notes:
309
329
  - Codex hardcodes `service.name=codex_exec` in its binary and silently ignores `[otel.resource]`, so onboarding does **not** write that block — all Codex traces land under the `codex_exec` agent ID regardless of project. Onboarding is one-time global, not per-project.
310
330
  - Codex emits OTLP **logs** (not spans) to `/v1/logs`. `tokenjam/api/routes/logs.py` converts Codex events (`sse_event`, `user_prompt`, `tool_decision`, `tool_result`, `api_request`) into normalized spans for cost/drift/alerting. Event name is read from `attrs["event.name"]` when the OTLP body is empty (Codex schema quirk); epoch `timeUnixNano=0` falls back to `attrs["event.timestamp"]` ISO-8601. The `/v1/logs` endpoint also silently accepts `resourceSpans`/`resourceMetrics` because Codex's exporter reuses one endpoint for all signal types.
311
- - Re-running `tj onboard --codex` is a no-op only when both `[otel]` and `[mcp_servers.tj]` are present in `~/.codex/config.toml`. Re-onboarding either Codex or Claude Code cross-syncs the ingest secret into the other's config if it's already configured.
331
+ - Re-running `tj onboard --codex` is a no-op only when `[otel]` is already present in `~/.codex/config.toml` (the check is `[otel]`-only now); a re-onboard also strips any legacy `[mcp_servers.tj]` block, since tj is out-of-band for Codex. Re-onboarding either Codex or Claude Code cross-syncs the ingest secret into the other's config if it's already configured.
312
332
 
313
333
  ## Examples Convention
314
334
 
@@ -331,7 +351,7 @@ The packaged table is community-maintained: submit a PR editing `tokenjam/pricin
331
351
  - **`get_rates(provider, model)` lookup order** (first match wins): model-keyed override → provider-keyed table (user `[provider.model]` over packaged) → `None` (→ `calculate_cost` applies the `$0.50`/`$2.00` default and logs once). Each step tries an exact match, then strips a trailing `-YYYYMMDD` suffix.
332
352
  - Both layers are LRU-cached (`load_pricing_table` + `load_model_pricing_overrides`); call `clear_pricing_cache()` or **restart the daemon** to pick up an edit.
333
353
 
334
- The packaged table stays the zero-config default — the override is a *layer*, never a replacement; no user ever has to declare a rate to get started. (Follow-up not yet built: a `tj pricing set|list` CLI for editing overrides without hand-writing TOML.)
354
+ The packaged table stays the zero-config default — the override is a *layer*, never a replacement; no user ever has to declare a rate to get started. Read-only inspection ships via `tj pricing list` (see CLI Commands; #282). The `set` half — a `tj pricing set` to edit overrides without hand-writing TOML — is not built yet.
335
355
 
336
356
  ## CI
337
357
 
@@ -364,6 +384,8 @@ PyPI and npm publishes are triggered by GitHub Release events (`.github/workflow
364
384
  2. Merge to `main`.
365
385
  3. Create a GitHub Release with tag `vX.Y.Z` (e.g. via `gh release create vX.Y.Z --generate-notes`). Publishing the release fires both workflows.
366
386
 
387
+ `publish-npm.yml` has **two independent jobs**: `publish-npm` (the `@tokenjam/sdk` package from `sdk-ts/`, version read from `sdk-ts/package.json`) and `publish-npm-wrapper` (the unscoped `tokenjam` wrapper from `npm-wrapper/`). The wrapper job **derives its published version from the release tag** (`vX.Y.Z` → `X.Y.Z`, via `npm version --no-git-tag-version --allow-same-version`), so the wrapper can never drift from the release and needs no manual version bump — `npm-wrapper/package.json`'s literal is only a floor for local `npm pack`. Both jobs use `secrets.NPM_TOKEN`, which must be able to publish the unscoped `tokenjam` name in addition to the `@tokenjam` scope.
388
+
367
389
  If a version already exists on PyPI or npm, the publish workflow fails with 403 — bump again rather than retrying.
368
390
 
369
391
  ## Packaging
@@ -372,6 +394,8 @@ Build system is hatchling. `[tool.hatch.build.targets.wheel] packages = ["tokenj
372
394
 
373
395
  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.
374
396
 
397
+ **The `tj` npm wrapper** (`npm-wrapper/`, issue #6) is a separate, dependency-free npm package named `tokenjam` (unscoped, distinct from the `@tokenjam/sdk` SDK package; the bare `tj` name is already taken on npm by an unrelated pub/sub library, so the PACKAGE is `tokenjam` while the installed BIN is still `tj`) whose only job is to make `npx tokenjam` work. `bin/tj.js` shells out to the Python CLI via the first available runner (`uvx --from tokenjam tj` → `pipx run --spec tokenjam tj` → an installed `tj` on PATH) and passes every arg through. **It is published to npm by the `publish-npm-wrapper` job in `publish-npm.yml`** (#65), which derives the published version from the release tag — you do **not** need to bump `npm-wrapper/package.json` on release (the CI overwrites it from the tag; keep the literal roughly current so local `npm pack` is sane). `npm-wrapper/` has no CI test (no Python to drive in the JS lane); the publish job runs a `node -c bin/tj.js` syntax guard, and you can validate the same locally with `node -c npm-wrapper/bin/tj.js`.
398
+
375
399
  **Optional extras** (declared under `[project.optional-dependencies]`):
376
400
  - `tokenjam[bloat]` — `llmlingua>=0.2`, used by the Trim analyzer. Pulls PyTorch + transformers (~2GB). Kept out of base install. The analyzer self-registers without the extra installed; the deferred `import llmlingua` inside the analysis function body raises a typed message pointing the user at the install command.
377
401
  - Framework extras `[langchain]`, `[crewai]`, `[autogen]`, `[litellm]` for SDK patches.
@@ -388,6 +412,7 @@ Key runtime dependency: `pytz` is required by DuckDB for `TIMESTAMPTZ` column ha
388
412
  - [`cache.md`](docs/optimize/cache.md) — `cache` (current caching ratio) + `cache-recommend` (Anthropic-only breakpoint suggestions)
389
413
  - [`script.md`](docs/optimize/script.md) — `script` clustering by `(tool_name, arg_shape)` signature (file: `workflow_restructure.py`)
390
414
  - [`trim.md`](docs/optimize/trim.md) — LLMLingua-2 token-significance classifier (`trim`, file: `prompt_bloat.py`), install + capture requirements, performance numbers
415
+ - [`subagent.md`](docs/optimize/subagent.md) — per-subagent right-sizing (`subagent`, file: `subagent_rightsizing.py`), Claude Code-only (keys off `sub_agent_id`, populated only by CC backfill)
391
416
  - **[AGENTS.md](AGENTS.md)** — codebase conventions for contributors (referenced from the top-level README).
392
417
  - **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.
393
418
  - **[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.
@@ -0,0 +1,44 @@
1
+ # ----- Build stage -----
2
+ FROM python:3.12-slim AS builder
3
+
4
+ WORKDIR /src
5
+ RUN pip install --no-cache-dir build
6
+
7
+ COPY pyproject.toml README.md ./
8
+ COPY tokenjam ./tokenjam
9
+ COPY incidents ./incidents
10
+
11
+ RUN python -m build --wheel --outdir /dist
12
+
13
+ # ----- Runtime stage -----
14
+ FROM python:3.12-slim AS runtime
15
+
16
+ LABEL org.opencontainers.image.title="tokenjam" \
17
+ org.opencontainers.image.description="Local-first OTel-native observability for autonomous AI agents" \
18
+ org.opencontainers.image.source="https://github.com/Metabuilder-Labs/tokenjam" \
19
+ org.opencontainers.image.licenses="MIT"
20
+
21
+ ENV PYTHONDONTWRITEBYTECODE=1 \
22
+ PYTHONUNBUFFERED=1 \
23
+ PIP_NO_CACHE_DIR=1 \
24
+ PIP_DISABLE_PIP_VERSION_CHECK=1
25
+
26
+ # Install the wheel from stage 1
27
+ COPY --from=builder /dist/*.whl /tmp/
28
+ RUN pip install --no-cache-dir "$(ls /tmp/*.whl)[mcp]" && rm -rf /tmp/*.whl
29
+
30
+ # Run as a non-root user
31
+ RUN useradd --create-home --uid 10001 tokenjam \
32
+ && mkdir -p /home/tokenjam/.config/tj /home/tokenjam/.tj \
33
+ && chown -R tokenjam:tokenjam /home/tokenjam
34
+ USER tokenjam
35
+ WORKDIR /home/tokenjam
36
+
37
+ # Mounting a volume to persist data across container restarts.
38
+ VOLUME ["/home/tokenjam/.config/tj", "/home/tokenjam/.tj"]
39
+
40
+ # Web UI + REST API served by `tj serve`.
41
+ EXPOSE 7391
42
+
43
+ ENTRYPOINT ["tj"]
44
+ CMD ["serve", "--host", "0.0.0.0", "--port", "7391"]