tokenjam 0.4.0__tar.gz → 0.4.2__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.
- tokenjam-0.4.2/.github/scripts/archive_traffic.py +144 -0
- tokenjam-0.4.2/.github/workflows/traffic-archive.yml +72 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/.gitignore +1 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/CLAUDE.md +112 -7
- {tokenjam-0.4.0 → tokenjam-0.4.2}/PKG-INFO +56 -36
- {tokenjam-0.4.0 → tokenjam-0.4.2}/README.md +55 -35
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/configuration.md +80 -0
- tokenjam-0.4.2/docs/optimize/reuse.md +152 -0
- tokenjam-0.4.2/growth/README.md +98 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/pyproject.toml +1 -1
- {tokenjam-0.4.0 → tokenjam-0.4.2}/sdk-ts/package.json +1 -1
- tokenjam-0.4.2/tests/agent-post-release-runner.md +134 -0
- tokenjam-0.4.2/tests/agent-post-release-v0.4.0.md +315 -0
- tokenjam-0.4.0/tests/manual-pre-release-runner.md → tokenjam-0.4.2/tests/agent-pre-release-runner.md +5 -5
- tokenjam-0.4.0/tests/manual-pre-release-v0.4.0.md → tokenjam-0.4.2/tests/agent-pre-release-v0.4.0.md +2 -2
- tokenjam-0.4.2/tests/agent-pre-release-v0.4.1.md +270 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/integration/test_api.py +202 -1
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/integration/test_cli.py +112 -0
- tokenjam-0.4.2/tests/integration/test_cli_api_framing_parity.py +118 -0
- tokenjam-0.4.2/tests/integration/test_cost_framing_window.py +130 -0
- tokenjam-0.4.2/tests/integration/test_litellm_enrichment.py +126 -0
- tokenjam-0.4.2/tests/integration/test_sdk_config_discovery.py +134 -0
- tokenjam-0.4.2/tests/results/agent-post-release-v0.4.0-20260619T221412Z.md +240 -0
- tokenjam-0.4.0/tests/results/manual-pre-release-v0.4.0-20260619T202233Z.md → tokenjam-0.4.2/tests/results/agent-pre-release-v0.4.0-20260619T202233Z.md +1 -1
- tokenjam-0.4.0/tests/results/manual-pre-release-v0.4.0-20260619T205108Z.md → tokenjam-0.4.2/tests/results/agent-pre-release-v0.4.0-20260619T205108Z.md +1 -1
- tokenjam-0.4.2/tests/results/agent-pre-release-v0.4.1-20260620T002546Z.md +247 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_backfill.py +54 -2
- tokenjam-0.4.2/tests/unit/test_cmd_cost.py +87 -0
- tokenjam-0.4.2/tests/unit/test_cycle.py +83 -0
- tokenjam-0.4.2/tests/unit/test_db_helpers.py +35 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_export_claude_code.py +1 -1
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_framing.py +57 -1
- tokenjam-0.4.2/tests/unit/test_lens_ui_regression.py +291 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_litellm_client.py +2 -1
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_litellm_integration.py +175 -5
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_onboard_plan.py +10 -0
- tokenjam-0.4.2/tests/unit/test_plan_tier_apply.py +193 -0
- tokenjam-0.4.2/tests/unit/test_pricing_override.py +286 -0
- tokenjam-0.4.2/tests/unit/test_provider_attribution_194.py +140 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_reuse_skeleton.py +20 -2
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/app.py +2 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/cost.py +34 -2
- tokenjam-0.4.2/tokenjam/api/routes/reuse.py +76 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/status.py +26 -1
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/traces.py +31 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_backfill.py +5 -1
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_cost.py +105 -11
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_doctor.py +70 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_onboard.py +304 -79
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_optimize.py +6 -3
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_report.py +45 -22
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_serve.py +10 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_status.py +40 -8
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_stop.py +25 -24
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/api_backend.py +37 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/backfill.py +39 -5
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/config.py +12 -0
- tokenjam-0.4.2/tokenjam/core/cycle.py +51 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/db.py +64 -13
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/export/claude_code.py +1 -1
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/export/reuse_report.py +51 -7
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/framing.py +195 -11
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/analyzers/budget_projection.py +2 -19
- tokenjam-0.4.2/tokenjam/core/pricing.py +320 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/client.py +3 -1
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/litellm.py +155 -38
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/ui/index.html +160 -49
- tokenjam-0.4.0/CHANGELOG.md +0 -214
- tokenjam-0.4.0/tests/unit/test_lens_ui_regression.py +0 -123
- tokenjam-0.4.0/tests/unit/test_pricing_override.py +0 -114
- tokenjam-0.4.0/tokenjam/core/pricing.py +0 -135
- {tokenjam-0.4.0 → tokenjam-0.4.2}/.github/CODEOWNERS +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/.github/ISSUE_TEMPLATE/integration_request.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/.github/pull_request_template.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/.github/workflows/ci.yml +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/.github/workflows/publish-npm.yml +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/.github/workflows/publish-pypi.yml +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/AGENTS.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/CONTRIBUTING.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/LICENSE +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/Makefile +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/SECURITY.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/alerts.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/architecture.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/backfill/helicone.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/backfill/langfuse.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/backfill/otlp.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/backfill/overview.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/claude-code-integration.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/cli-reference.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/export.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/framework-support.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/installation.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/internal/lens-vendor-versions.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/internal/specs/.gitkeep +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/nemoclaw-integration.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/openclaw.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/optimize/cache.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/optimize/downsize.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/optimize/script.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/optimize/trim.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/policy/overview.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/python-sdk.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/screenshots/tj-alerts.png +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/screenshots/tj-budget.png +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/screenshots/tj-cost.png +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/screenshots/tj-status.png +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/screenshots/tj-traces.png +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/docs/typescript-sdk.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/README.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/alerts_and_drift/_shared.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/alerts_and_drift/budget_breach_demo.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/alerts_and_drift/drift_demo.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/alerts_and_drift/sensitive_actions_demo.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/multi/rag_pipeline.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/multi/research_team.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/multi/router_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/multi/sample_docs/agent_patterns.txt +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/multi/sample_docs/cost_management.txt +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/multi/sample_docs/observability.txt +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/multi/sample_docs/safety.txt +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/openclaw/README.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/single_framework/autogen_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/single_framework/crewai_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/single_framework/langchain_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/single_framework/langgraph_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/single_framework/llamaindex_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/single_provider/anthropic_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/single_provider/bedrock_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/single_provider/gemini_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/single_provider/litellm_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/single_provider/openai_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/examples/single_provider/openai_agents_sdk_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/incidents/hallucination-drift/BLOG.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/incidents/hallucination-drift/README.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/incidents/hallucination-drift/scenario.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/incidents/retry-loop/BLOG.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/incidents/retry-loop/README.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/incidents/retry-loop/scenario.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/incidents/surprise-cost/BLOG.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/incidents/surprise-cost/README.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/incidents/surprise-cost/scenario.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/sdk-ts/README.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/sdk-ts/package-lock.json +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/sdk-ts/src/client.test.ts +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/sdk-ts/src/client.ts +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/sdk-ts/src/index.ts +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/sdk-ts/src/semconv.test.ts +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/sdk-ts/src/semconv.ts +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/sdk-ts/src/span-builder.test.ts +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/sdk-ts/src/span-builder.ts +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/sdk-ts/src/types.ts +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/sdk-ts/tsconfig.json +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/agents/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/agents/email_agent_budget_breach.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/agents/email_agent_drift.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/agents/email_agent_loop.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/agents/email_agent_normal.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/agents/mock_llm.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/agents/test_mock_scenarios.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/conftest.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/e2e/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/e2e/conftest.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/e2e/test_real_llm.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/factories.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/fixtures/helicone_real_response.json +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/fixtures/langfuse_real_response.json +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/fixtures/otlp_sample.json +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/integration/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/integration/test_db.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/integration/test_demos.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/integration/test_full_pipeline.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/integration/test_logs_api.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/manual-new-release-tests.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/manual-pre-release-testing.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/synthetic/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/synthetic/test_alert_rules.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/synthetic/test_cost_tracking.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/synthetic/test_drift_detection.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/synthetic/test_ingest.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/synthetic/test_schema_validation.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/toy_agent/toy_agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_alerts.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_cache_efficacy.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_cache_recommend.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_cmd_policy.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_cmd_stop.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_cmd_tokenmaxx.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_compare.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_config.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_config_secret_divergence.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_cost.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_cost_compare_framing.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_demo_env.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_demo_scenarios.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_drift.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_formatting.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_ingest_helicone.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_ingest_langfuse.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_ingest_otlp.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_logs_converter.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_mcp_server.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_models.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_no_tracked_dev_secrets.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_onboard_codex.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_onboard_daemon.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_onboard_hygiene.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_openclaw_ingest.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_optimize.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_optimize_recoverable.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_optimize_reuse.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_prompt_bloat.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_spans_stats_repair.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_time_parse.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_transport_401.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_ui_offline.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tests/unit/test_workflow_restructure.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/deps.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/middleware.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/agents.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/alerts.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/budget.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/cost_compare.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/drift.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/logs.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/metrics.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/optimize.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/otlp.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/spans.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/tools.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/api/routes/version.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_alerts.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_budget.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_demo.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_drift.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_export.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_mcp.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_policy.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_tokenmaxx.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_tools.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_traces.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/cmd_uninstall.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/cli/main.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/alerts.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/cost.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/drift.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/export/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/export/reuse_skeleton.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/ingest.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/ingest_adapters/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/ingest_adapters/helicone.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/ingest_adapters/langfuse.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/ingest_adapters/otlp.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/models.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/README.md +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/analyzers/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/analyzers/cache_efficacy.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/analyzers/cache_recommend.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/analyzers/model_downgrade.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/analyzers/plan_reuse.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/analyzers/prompt_bloat.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/analyzers/workflow_restructure.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/registry.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/runner.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/optimize/types.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/retention.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/core/schema_validator.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/demo/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/demo/env.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/mcp/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/mcp/server.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/otel/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/otel/exporters.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/otel/otlp_parsing.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/otel/provider.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/otel/semconv.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/pricing/models.toml +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/py.typed +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/agent.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/bootstrap.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/http_exporter.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/anthropic.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/autogen.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/base.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/bedrock.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/crewai.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/gemini.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/langchain.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/langgraph.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/llamaindex.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/nemoclaw.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/openai.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/integrations/openai_agents_sdk.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/sdk/transport.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/ui/vendor/htm.js +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/ui/vendor/preact-hooks.js +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/ui/vendor/preact.js +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/ui/vendor/uplot.css +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/ui/vendor/uplot.js +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/utils/__init__.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/utils/formatting.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/utils/ids.py +0 -0
- {tokenjam-0.4.0 → tokenjam-0.4.2}/tokenjam/utils/time_parse.py +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Archive this repo's GitHub Traffic API data into traffic/<ISO-year>-W<week>.json.
|
|
4
|
+
|
|
5
|
+
GitHub's Traffic API only retains the last 14 days, so this runs weekly (see
|
|
6
|
+
.github/workflows/traffic-archive.yml) and commits a point-in-time snapshot —
|
|
7
|
+
the longitudinal record the 14-day window can't otherwise provide.
|
|
8
|
+
|
|
9
|
+
The output schema is fixed by the growth-instrumentation brief and consumed by
|
|
10
|
+
the Cowork spreadsheet-fill job; keep it stable across releases.
|
|
11
|
+
|
|
12
|
+
Auth: reads GITHUB_TOKEN + GITHUB_REPOSITORY from the environment (both provided
|
|
13
|
+
automatically by GitHub Actions). The default workflow token works because the
|
|
14
|
+
workflow grants `administration: read`, which the Traffic API requires.
|
|
15
|
+
|
|
16
|
+
Failure mode: any API error raises and the process exits non-zero, so the run
|
|
17
|
+
shows red in the Actions tab (the Cowork health check then alerts on the missing
|
|
18
|
+
file). No-data weeks still write the file — its existence is the success signal.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import urllib.error
|
|
26
|
+
import urllib.request
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
API_ROOT = "https://api.github.com"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get(path: str, token: str):
|
|
34
|
+
"""GET an api.github.com path and return parsed JSON. Raises on any error.
|
|
35
|
+
|
|
36
|
+
On an HTTP error, surface the status, GitHub's response message, and the
|
|
37
|
+
relevant rate-limit / SSO headers to stderr before re-raising — a bare
|
|
38
|
+
`HTTPError: 403 Forbidden` hides whether the cause is a missing token scope,
|
|
39
|
+
an unapproved fine-grained PAT, or SAML SSO authorization.
|
|
40
|
+
"""
|
|
41
|
+
req = urllib.request.Request(
|
|
42
|
+
f"{API_ROOT}{path}",
|
|
43
|
+
headers={
|
|
44
|
+
"Authorization": f"Bearer {token}",
|
|
45
|
+
"Accept": "application/vnd.github+json",
|
|
46
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
47
|
+
"User-Agent": "tokenjam-traffic-archive",
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
try:
|
|
51
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
52
|
+
return json.load(resp)
|
|
53
|
+
except urllib.error.HTTPError as exc:
|
|
54
|
+
body = exc.read().decode("utf-8", "replace")[:600]
|
|
55
|
+
sso = exc.headers.get("X-GitHub-SSO")
|
|
56
|
+
remaining = exc.headers.get("X-RateLimit-Remaining")
|
|
57
|
+
print(
|
|
58
|
+
f"error: GET {path} -> HTTP {exc.code} {exc.reason}\n"
|
|
59
|
+
f" body: {body}\n"
|
|
60
|
+
f" x-github-sso: {sso} x-ratelimit-remaining: {remaining}",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
raise
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _daily(payload: dict, key: str) -> list[dict]:
|
|
67
|
+
"""Normalize the views/clones inner array (keyed 'views'/'clones') to 'daily'."""
|
|
68
|
+
return [
|
|
69
|
+
{"timestamp": d["timestamp"], "count": d["count"], "uniques": d["uniques"]}
|
|
70
|
+
for d in payload.get(key, [])
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_record(views: dict, clones: dict, referrers: list, paths: list,
|
|
75
|
+
repo: str, now: datetime) -> dict:
|
|
76
|
+
"""Assemble the combined archive record matching the fixed brief schema."""
|
|
77
|
+
iso_year, iso_week, _ = now.isocalendar()
|
|
78
|
+
iso_week_str = f"{iso_year}-W{iso_week:02d}"
|
|
79
|
+
return {
|
|
80
|
+
"archived_at": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
81
|
+
"iso_week": iso_week_str,
|
|
82
|
+
"repo": repo,
|
|
83
|
+
"views": {
|
|
84
|
+
"count": views.get("count", 0),
|
|
85
|
+
"uniques": views.get("uniques", 0),
|
|
86
|
+
"daily": _daily(views, "views"),
|
|
87
|
+
},
|
|
88
|
+
"clones": {
|
|
89
|
+
"count": clones.get("count", 0),
|
|
90
|
+
"uniques": clones.get("uniques", 0),
|
|
91
|
+
"daily": _daily(clones, "clones"),
|
|
92
|
+
},
|
|
93
|
+
"referrers": [
|
|
94
|
+
{"referrer": r["referrer"], "count": r["count"], "uniques": r["uniques"]}
|
|
95
|
+
for r in (referrers or [])
|
|
96
|
+
],
|
|
97
|
+
"paths": [
|
|
98
|
+
{"path": p["path"], "title": p.get("title", ""),
|
|
99
|
+
"count": p["count"], "uniques": p["uniques"]}
|
|
100
|
+
for p in (paths or [])
|
|
101
|
+
],
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> int:
|
|
106
|
+
token = os.environ.get("GITHUB_TOKEN")
|
|
107
|
+
repo = os.environ.get("GITHUB_REPOSITORY") # "owner/name"
|
|
108
|
+
if not token or not repo:
|
|
109
|
+
print("error: GITHUB_TOKEN and GITHUB_REPOSITORY must be set", file=sys.stderr)
|
|
110
|
+
return 1
|
|
111
|
+
|
|
112
|
+
views = _get(f"/repos/{repo}/traffic/views", token)
|
|
113
|
+
clones = _get(f"/repos/{repo}/traffic/clones", token)
|
|
114
|
+
referrers = _get(f"/repos/{repo}/traffic/popular/referrers", token)
|
|
115
|
+
paths = _get(f"/repos/{repo}/traffic/popular/paths", token)
|
|
116
|
+
|
|
117
|
+
now = datetime.now(timezone.utc)
|
|
118
|
+
record = build_record(views, clones, referrers, paths, repo, now)
|
|
119
|
+
iso_week_str = record["iso_week"]
|
|
120
|
+
|
|
121
|
+
out_dir = Path("traffic")
|
|
122
|
+
out_dir.mkdir(exist_ok=True)
|
|
123
|
+
out_path = out_dir / f"{iso_week_str}.json"
|
|
124
|
+
# Overwrite is intentional: a week's archive is a point-in-time snapshot, so
|
|
125
|
+
# a manual re-run or late-firing schedule just refreshes it (idempotent).
|
|
126
|
+
out_path.write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
|
|
127
|
+
|
|
128
|
+
print(
|
|
129
|
+
f"wrote {out_path} (views={record['views']['count']}, "
|
|
130
|
+
f"clones={record['clones']['count']}, "
|
|
131
|
+
f"referrers={len(record['referrers'])}, paths={len(record['paths'])})"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Expose the ISO week to the workflow for the commit message.
|
|
135
|
+
gh_output = os.environ.get("GITHUB_OUTPUT")
|
|
136
|
+
if gh_output:
|
|
137
|
+
with open(gh_output, "a", encoding="utf-8") as fh:
|
|
138
|
+
fh.write(f"iso_week={iso_week_str}\n")
|
|
139
|
+
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
sys.exit(main())
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
name: traffic-archive
|
|
2
|
+
|
|
3
|
+
# Weekly snapshot of the GitHub Traffic API (which only retains 14 days),
|
|
4
|
+
# committed as traffic/<year>-W<week>.json onto the data-only `traffic-data`
|
|
5
|
+
# branch (NOT main — main is protected and blocks automated direct pushes).
|
|
6
|
+
# See growth/README.md for the schema and how to read the archive.
|
|
7
|
+
on:
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: '0 12 * * 0' # Sundays at 12:00 UTC
|
|
10
|
+
workflow_dispatch: # manual trigger for backfill / testing
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: write # to commit the archive back to the branch
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
archive:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: '3.12'
|
|
24
|
+
|
|
25
|
+
- name: Archive traffic data
|
|
26
|
+
id: archive
|
|
27
|
+
env:
|
|
28
|
+
# The Traffic API requires Administration:Read, which the default
|
|
29
|
+
# GITHUB_TOKEN cannot be granted (there is no such permissions: key —
|
|
30
|
+
# a literal `administration` value is a workflow parse error, and the
|
|
31
|
+
# default token 403s on these endpoints). So this step authenticates
|
|
32
|
+
# with a PAT stored as the TRAFFIC_PAT repo secret.
|
|
33
|
+
#
|
|
34
|
+
# TRAFFIC_PAT must be a fine-grained PAT with:
|
|
35
|
+
# - Resource owner = Metabuilder-Labs (the ORG, not a personal
|
|
36
|
+
# account — otherwise it can't see this repo: 403 "Resource not
|
|
37
|
+
# accessible by personal access token")
|
|
38
|
+
# - Repository access = this repo
|
|
39
|
+
# - Repository permissions -> Administration: Read (the one the
|
|
40
|
+
# Traffic API needs; separate from Contents/Metadata)
|
|
41
|
+
# A classic PAT with the `repo` scope also works. The push step below
|
|
42
|
+
# uses the default GITHUB_TOKEN (not this PAT) since traffic-data is
|
|
43
|
+
# unprotected. See growth/README.md.
|
|
44
|
+
GITHUB_TOKEN: ${{ secrets.TRAFFIC_PAT }}
|
|
45
|
+
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
46
|
+
run: python .github/scripts/archive_traffic.py
|
|
47
|
+
|
|
48
|
+
- name: Publish the archive to the traffic-data branch
|
|
49
|
+
env:
|
|
50
|
+
# The archive lands on the unprotected, data-only `traffic-data` branch,
|
|
51
|
+
# so the default GITHUB_TOKEN (contents: write) can push it — no PAT and
|
|
52
|
+
# no branch-protection bypass needed. The job runs from main (where the
|
|
53
|
+
# script lives), then publishes just the new JSON to traffic-data via a
|
|
54
|
+
# shallow clone so main's tree never leaks onto the data branch.
|
|
55
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
56
|
+
GH_REPO: ${{ github.repository }}
|
|
57
|
+
WEEK: ${{ steps.archive.outputs.iso_week }}
|
|
58
|
+
run: |
|
|
59
|
+
tmp="$(mktemp -d)"
|
|
60
|
+
git clone --quiet --depth 1 --branch traffic-data \
|
|
61
|
+
"https://x-access-token:${GH_TOKEN}@github.com/${GH_REPO}.git" "$tmp"
|
|
62
|
+
mkdir -p "$tmp/traffic"
|
|
63
|
+
cp "traffic/${WEEK}.json" "$tmp/traffic/${WEEK}.json"
|
|
64
|
+
git -C "$tmp" config user.name "github-actions[bot]"
|
|
65
|
+
git -C "$tmp" config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
66
|
+
git -C "$tmp" add traffic/
|
|
67
|
+
if git -C "$tmp" diff --cached --quiet; then
|
|
68
|
+
echo "No changes — archive already current for ${WEEK}."
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
git -C "$tmp" commit -m "chore(traffic): archive week ${WEEK}"
|
|
72
|
+
git -C "$tmp" push origin HEAD:traffic-data
|
|
@@ -58,6 +58,86 @@ Symptom of a missed worktree: `git log` shows a commit on a branch you didn't in
|
|
|
58
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
59
|
|
|
60
60
|
|
|
61
|
+
## PR and commit conventions (for any agent producing a PR)
|
|
62
|
+
|
|
63
|
+
These conventions apply to any agent — feature work, bug fixes, docs, content. Briefs may add task-specific structure but should not contradict these.
|
|
64
|
+
|
|
65
|
+
### Branch + PR titles
|
|
66
|
+
|
|
67
|
+
- **Branch names** are slash-separated, kebab-case, prefixed by type:
|
|
68
|
+
- `fix/<issue-or-area>` — bug fixes (e.g. `fix/175-176-cost-framing-backfill-plan`)
|
|
69
|
+
- `feat/<area>` — new features (e.g. `feat/reuse-analyzer-115`)
|
|
70
|
+
- `docs/<area>` — documentation (e.g. `docs/readme-cleanup-v0.4.1`)
|
|
71
|
+
- `chore/<area>` — refactors, renames, infra
|
|
72
|
+
- `release/<X.Y.Z>` — release-cut PRs
|
|
73
|
+
- **PR titles** lead with the verb / type and reference issues by number when applicable:
|
|
74
|
+
- `Fix #175, #176: tj cost framing + backfill plan_tier propagation (v0.4.2)` (bug fixes)
|
|
75
|
+
- `[feature] Add Reuse analyzer (#115)` (features)
|
|
76
|
+
- `docs: drop stale CHANGELOG.md + add maintainer contact` (docs)
|
|
77
|
+
- `Bump version to 0.4.1` (release-cut PRs — keep these terse)
|
|
78
|
+
- Use **`Closes #N`** in the PR body (not just title) when fixing an issue, so GitHub auto-closes the issue on merge. Multiple `Closes` lines if you're closing several. Do not use the comma form `Closes #1, #2` — GitHub only catches the first; use separate lines.
|
|
79
|
+
|
|
80
|
+
### Commit messages
|
|
81
|
+
|
|
82
|
+
- **Subject line** (first line, ≤72 chars): one-line summary in active voice. Reference issues with `#N` when applicable.
|
|
83
|
+
- **Body** (after blank line): explain *why* the change is needed, not *what* it changes (the diff shows that). Use full sentences, paragraphs, bullet lists.
|
|
84
|
+
- **Trailers** (after another blank line, at the very end):
|
|
85
|
+
- Always include: `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>` (or the appropriate model identifier)
|
|
86
|
+
- When fixing an externally-reported bug: also include `Co-Authored-By: <reporter-handle> <noreply@github.com>` (e.g. `ashwmu` for the external contributor's reports)
|
|
87
|
+
- Use **HEREDOC for multi-line messages** to preserve formatting: `git commit -m "$(cat <<'EOF' ... EOF)"`
|
|
88
|
+
|
|
89
|
+
### PR body structure
|
|
90
|
+
|
|
91
|
+
```markdown
|
|
92
|
+
[1-2 sentence framing of why this exists]
|
|
93
|
+
|
|
94
|
+
## Summary
|
|
95
|
+
- [bullet — what changed at a high level]
|
|
96
|
+
- [bullet — another high-level change]
|
|
97
|
+
|
|
98
|
+
## [Per-issue or per-feature section, repeated as needed]
|
|
99
|
+
[Detail per issue, including the symptom, root cause, fix]
|
|
100
|
+
|
|
101
|
+
## Tests / Verification
|
|
102
|
+
- [test files added or modified, what they cover]
|
|
103
|
+
- [any live verification: workflow run URL, screenshot, command output]
|
|
104
|
+
|
|
105
|
+
## What's NOT in this PR (if scope was deliberately limited)
|
|
106
|
+
- [out-of-scope item 1 — explain why deferred]
|
|
107
|
+
- [out-of-scope item 2]
|
|
108
|
+
|
|
109
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
110
|
+
|
|
111
|
+
Co-Authored-By: <reporter-handle> <noreply@github.com> # if applicable
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The "What's NOT in this PR" section is load-bearing — it makes the reviewer's job 10x easier when the agent explicitly named what they decided to defer. Use it whenever scope is non-obvious.
|
|
115
|
+
|
|
116
|
+
### Self-review checklist before requesting review
|
|
117
|
+
|
|
118
|
+
Run through this before pushing the PR:
|
|
119
|
+
|
|
120
|
+
1. **Tests pass locally.** `pytest tests/unit/ tests/integration/` (or `tests/unit/<file>.py` if narrow).
|
|
121
|
+
2. **`ruff check tokenjam/` and `mypy tokenjam/` clean** for any files you touched.
|
|
122
|
+
3. **CI on the branch is green** for at least the test-ts job (Python jobs may still be running when you push).
|
|
123
|
+
4. **Acceptance criteria from the issue are met** — go through them one by one and verify.
|
|
124
|
+
5. **No accidental files in the diff** — `.tj/config.toml`, `.tj-test-data/`, screenshots that were just for debugging, etc.
|
|
125
|
+
6. **PR body explains the WHY** — symptom + root cause + fix, not just "fixes the bug."
|
|
126
|
+
7. **Honesty discipline preserved.** If the change touches any user-facing string ("recoverable," "estimated," "savings"), verify it matches existing analyzer caveat language. Never silently strengthen claims.
|
|
127
|
+
|
|
128
|
+
### Scope discipline
|
|
129
|
+
|
|
130
|
+
- **Do what the brief / issue says, no more.** If you notice an adjacent issue, file it as a separate issue rather than expanding the PR. Reviewers should never have to mentally separate "the fix" from "drive-by cleanup."
|
|
131
|
+
- **Exception:** when an adjacent change is functionally required to make the primary fix work (e.g., updating a caller of a function you changed). Note it explicitly in the PR body under "What's also in this PR."
|
|
132
|
+
- **When in doubt about scope, ask the master agent before expanding.** A 30-second clarification beats a 30-minute scope review.
|
|
133
|
+
|
|
134
|
+
### Worker vs master
|
|
135
|
+
|
|
136
|
+
- **Worker agents do not merge their own PRs.** Open the PR, request review, the master + Anil handle merge.
|
|
137
|
+
- **Worker agents do not file follow-up issues unprompted.** If you notice something during your work that's out of scope, mention it in the PR body and let the master decide whether to file.
|
|
138
|
+
- **Worker agents do not bump versions.** Release-cut PRs are a separate concern handled by the master / Anil.
|
|
139
|
+
|
|
140
|
+
|
|
61
141
|
## Architecture
|
|
62
142
|
|
|
63
143
|
### Data Flow
|
|
@@ -91,16 +171,17 @@ Post-ingest hooks run synchronously after each span is written to DB:
|
|
|
91
171
|
- `cache_efficacy.py` → `@register("cache")` — current cache-read efficacy per (provider, model)
|
|
92
172
|
- `cache_recommend.py` → `@register("cache-recommend")` — Anthropic-only structural prefix detection for `cache_control` placement
|
|
93
173
|
- `workflow_restructure.py` → `@register("script")` — `(tool_name, arg_shape)` cluster detection for deterministic-script candidates
|
|
174
|
+
- `plan_reuse.py` → `@register("reuse")` — repeated-planning cluster detection; a savings analyzer (carries `estimated_recoverable_usd`). Has a dedicated endpoint/report path (`GET /api/v1/reuse/clusters`, `tj report --reuse`) because its per-cluster planner text can be many KB — see the routes/report bullets below
|
|
94
175
|
- `prompt_bloat.py` → `@register("trim")` — LLMLingua-2 token-significance classification (requires `tokenjam[bloat]` extra)
|
|
95
176
|
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
177
|
**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".
|
|
97
178
|
- **`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`.
|
|
98
179
|
- **`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).
|
|
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)`.
|
|
180
|
+
- **`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.
|
|
100
181
|
- **`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.
|
|
101
182
|
- **`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.
|
|
102
183
|
- **`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.
|
|
184
|
+
- **`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`, `cmd_cost` — both the `--compare` diff and the bare cost table, #175) and the REST API** (which emits `Framing.to_dict()` as the `framing` block) — neither re-derives the rules. The bare `tj cost` table renders COST cells via `render_dollar()` (subscription → "% of cycle", local → "—", api/unknown → `format_cost`) with the qualifier surfaced above; under the daemon it reuses the `framing` block from `/api/v1/cost` via `ApiBackend.fetch_cost_framing`. 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.
|
|
104
185
|
- **`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.
|
|
105
186
|
- **`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.
|
|
106
187
|
- **`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.
|
|
@@ -112,7 +193,7 @@ Post-ingest hooks run synchronously after each span is written to DB:
|
|
|
112
193
|
- **`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`.
|
|
113
194
|
- **`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`).
|
|
114
195
|
- **`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.
|
|
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
|
|
196
|
+
- **`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.
|
|
116
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`.
|
|
117
198
|
- **`tokenjam/cli/main.py`**: Root Click group with global options (`--config`, `--json`, `--no-color`, `--db`, `--agent`, `-v`). Registers all subcommands.
|
|
118
199
|
|
|
@@ -122,12 +203,12 @@ Post-ingest hooks run synchronously after each span is written to DB:
|
|
|
122
203
|
|
|
123
204
|
- **`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.
|
|
124
205
|
- **`tj doctor`** (`cmd_doctor.py`) — health checks (config, DB, secrets, webhooks, drift readiness, schema-vs-capture consistency). Exit 0 = ok, 1 = warnings, 2 = errors.
|
|
125
|
-
- **`tj optimize`** (`cmd_optimize.py`) —
|
|
206
|
+
- **`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.
|
|
126
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.
|
|
127
208
|
- **`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).
|
|
128
209
|
- **`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.
|
|
129
210
|
- **`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.
|
|
211
|
+
- **`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.
|
|
131
212
|
- **`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.
|
|
132
213
|
|
|
133
214
|
All commands support `--json` for machine-readable output. Commands that query alerts use exit code 1 if active (unacknowledged, unsuppressed) alerts exist.
|
|
@@ -161,7 +242,7 @@ Integration tests use `httpx.AsyncClient` with `httpx.ASGITransport(app=app)` ag
|
|
|
161
242
|
- **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
243
|
- **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
244
|
- **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
|
|
245
|
+
- **Polling:** the Overview auto-refreshes every 30s only while the tab is visible (`document.visibilityState`) and **fetches its endpoints in parallel** via `Promise.all` (the daemon DB layer is concurrency-safe since #124 — per-thread cursors). The error handling is deliberately asymmetric: `/cost` is load-bearing (no `.catch` — its failure surfaces the error state), while the other five panels each carry a `.catch` fallback so one failing panel renders empty rather than blanking the Overview. Don't unify them. Detail screens refresh on user action.
|
|
165
246
|
- **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
247
|
|
|
167
248
|
### Session Continuity
|
|
@@ -239,7 +320,18 @@ The Agent Incident Library at `incidents/` is separate: each scenario is a `scen
|
|
|
239
320
|
|
|
240
321
|
Model pricing lives in `tokenjam/pricing/models.toml` (USD per million tokens) — the packaged file `core/pricing.py` loads via `PRICING_FILE = Path(__file__).parent.parent / "pricing" / "models.toml"`. There is no repo-root `pricing/` copy (it was moved into the package in v0.1.x so it ships in the wheel; editing a repo-root file would have no runtime effect). Structure: `[provider.model_name]` with `input_per_mtok`, `output_per_mtok`, and optional `cache_read_per_mtok`/`cache_write_per_mtok`. Unknown models fall back to default rates ($0.50/$2.00 per MTok) with a logged warning. The pricing table is LRU-cached at process startup — restart to pick up changes.
|
|
241
322
|
|
|
242
|
-
|
|
323
|
+
The packaged table is community-maintained: submit a PR editing `tokenjam/pricing/models.toml` when provider prices change. No code changes needed — the file is loaded at runtime.
|
|
324
|
+
|
|
325
|
+
**Local user overrides (no PR needed)** — users correct or add rates *for their own install* via override layers that `core/pricing.py` merges over the packaged table. Two sources, two key forms (see `docs/configuration.md` → "Pricing overrides" for the user-facing version):
|
|
326
|
+
|
|
327
|
+
- **Sources** (lowest priority first; later wins): the packaged `models.toml`, then a standalone file (`~/.config/tj/pricing.toml`, or `TJ_PRICING_FILE`), then a `[pricing]` section in the main config (`tj.toml`). The project-local config `[pricing]` wins over the global standalone file.
|
|
328
|
+
- **Key forms** — told apart deterministically by section name in `_split_pricing_raw()` (the reserved `models` section vs everything-else-is-a-provider; no value-shape guessing, no ordering dependency):
|
|
329
|
+
- **Provider-keyed** (`[pricing.anthropic]` / `[anthropic]` whose values are model sub-tables) — merged per `(provider, model)` over the packaged table. This is the long-standing `[provider.model]` format.
|
|
330
|
+
- **Model-keyed** (the reserved `[pricing.models]` section in tj.toml, or `[models]` in the standalone file) — keyed by **bare model name**, applied **regardless of inferred provider**. This is the attribution-proof path: it prices a span even when the provider resolved to `"unknown"` (the #194 open-weight class). `models` is a reserved key (`MODEL_SECTION_KEY`), never a provider, so the forms never collide.
|
|
331
|
+
- **`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
|
+
- 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
|
+
|
|
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.)
|
|
243
335
|
|
|
244
336
|
## CI
|
|
245
337
|
|
|
@@ -251,6 +343,19 @@ All steps are blocking — lint, typecheck, and tests must pass for CI to go gre
|
|
|
251
343
|
|
|
252
344
|
There is no pre-commit configuration in this repo; `ruff` and `mypy` only run in CI. Run them locally before pushing.
|
|
253
345
|
|
|
346
|
+
## Growth instrumentation — weekly traffic archive
|
|
347
|
+
|
|
348
|
+
A separate `.github/workflows/traffic-archive.yml` runs every Sunday at 12:00 UTC (also `workflow_dispatch`) and archives the GitHub Traffic API (views / clones / referrers / paths — the only sources with the 14-day retention problem) into a JSON file at `traffic/<year>-W<week>.json`. **The archive lives on its own orphan branch `traffic-data`, not on `main`** — code and telemetry are deliberately separated, and `main`'s branch protection stays intact.
|
|
349
|
+
|
|
350
|
+
Key facts:
|
|
351
|
+
- The workflow uses `${{ secrets.TRAFFIC_PAT }}` (fine-grained PAT, resource owner = `Metabuilder-Labs` org, `Administration: Read` on this repo) for the Traffic API read — the default `GITHUB_TOKEN` returns 403 there. The push step uses the default `GITHUB_TOKEN` since `traffic-data` is unprotected.
|
|
352
|
+
- The orphan-branch design was the close-out after several other paths (PAT-as-owner push, classic-protection bypass, rulesets bot-bypass) hit GitHub UI / API limitations. See PRs #171 / #172 / #173 for the trail. Don't try to move the archive back onto `main` — that path was explored and dead-ended.
|
|
353
|
+
- The archive is the only growth-instrumentation surface in this repo. Health-check + spreadsheet-fill workflows were considered but descoped (Cowork sandbox couldn't reach external APIs, and GH Actions for the same was deemed overkill for a solo project). Manual review is the current model — fetch the JSON when wanted, hand it to an agent for analysis.
|
|
354
|
+
- `growth/README.md` documents the schema + read-it-manually flow.
|
|
355
|
+
- TRAFFIC_PAT expires Jun 20 2027 — renew before then.
|
|
356
|
+
|
|
357
|
+
When adding any new automation that needs to *write* to the repo on a schedule, follow the same pattern: data on its own unprotected ref, code on `main`. Don't try to make `main` accept bot commits.
|
|
358
|
+
|
|
254
359
|
## Releases
|
|
255
360
|
|
|
256
361
|
PyPI and npm publishes are triggered by GitHub Release events (`.github/workflows/publish-pypi.yml`, `publish-npm.yml`, both `on: release: types: [published]`). Release flow:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tokenjam
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -64,7 +64,7 @@ Description-Content-Type: text/markdown
|
|
|
64
64
|
|
|
65
65
|
### Token Efficiency For AI Agents
|
|
66
66
|
|
|
67
|
-
TokenJam reads your agent's telemetry and tells you when to downsize, when to trim prompts, what to cache, and what to
|
|
67
|
+
TokenJam reads your agent's telemetry and tells you when to downsize, when to trim prompts, what to cache, what to script, and what plans you've already paid to figure out — then shows it all in a local browser dashboard. Runs entirely on your machine.
|
|
68
68
|
|
|
69
69
|
[](https://github.com/Metabuilder-Labs/tokenjam/actions/workflows/ci.yml)
|
|
70
70
|
[](https://pypi.org/project/tokenjam/)
|
|
@@ -85,9 +85,9 @@ pipx install tokenjam
|
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
-
##
|
|
88
|
+
## Five Analyzers + Lens. One Install.
|
|
89
89
|
|
|
90
|
-
TokenJam reads telemetry from every major agent runtime, framework, provider, and observability tool and surfaces savings across
|
|
90
|
+
TokenJam reads telemetry from every major agent runtime, framework, provider, and observability tool and surfaces savings across five areas — then brings them together in a local browser dashboard.
|
|
91
91
|
|
|
92
92
|
<table>
|
|
93
93
|
<tr>
|
|
@@ -136,11 +136,35 @@ Predicts which regions of your prompts the model gives little weight to. Surface
|
|
|
136
136
|
|
|
137
137
|
[Details →](docs/optimize/trim.md)
|
|
138
138
|
|
|
139
|
+
</td>
|
|
140
|
+
</tr>
|
|
141
|
+
<tr>
|
|
142
|
+
<td width="50%" valign="top">
|
|
143
|
+
|
|
144
|
+
### 🔁 Reuse
|
|
145
|
+
|
|
146
|
+
Detects clusters of sessions where your agent re-plans the same work and exports reviewable skeleton templates you can drop into a slash command or script.
|
|
147
|
+
|
|
148
|
+
<pre><code>tj optimize reuse</code></pre>
|
|
149
|
+
|
|
150
|
+
[Details →](docs/optimize/reuse.md)
|
|
151
|
+
|
|
152
|
+
</td>
|
|
153
|
+
<td width="50%" valign="top">
|
|
154
|
+
|
|
155
|
+
### 🔭 Lens
|
|
156
|
+
|
|
157
|
+
A local browser dashboard that brings every analyzer's findings, your real spend, and your alerts together in one place. No cloud, no signup, fully offline.
|
|
158
|
+
|
|
159
|
+
<pre><code>tj serve</code></pre>
|
|
160
|
+
|
|
161
|
+
[Details →](https://tokenjam.dev/products/lens)
|
|
162
|
+
|
|
139
163
|
</td>
|
|
140
164
|
</tr>
|
|
141
165
|
</table>
|
|
142
166
|
|
|
143
|
-
Run all
|
|
167
|
+
Run all five analyzers with `tj optimize`. Run several with `tj optimize downsize cache reuse`.
|
|
144
168
|
|
|
145
169
|
---
|
|
146
170
|
|
|
@@ -149,9 +173,10 @@ Run all four with `tj optimize`. Run several with `tj optimize downsize cache tr
|
|
|
149
173
|
For **Claude Code** users — zero code, auto-backfills your last 30 days:
|
|
150
174
|
|
|
151
175
|
```bash
|
|
152
|
-
pipx install
|
|
176
|
+
pipx install tokenjam
|
|
153
177
|
tj onboard --claude-code
|
|
154
178
|
tj optimize # cost-saving candidates from your actual usage
|
|
179
|
+
tj serve # open the dashboard at http://127.0.0.1:7391/
|
|
155
180
|
```
|
|
156
181
|
|
|
157
182
|
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).
|
|
@@ -173,64 +198,51 @@ def run(task: str) -> str:
|
|
|
173
198
|
|
|
174
199
|
---
|
|
175
200
|
|
|
176
|
-
##
|
|
177
|
-
|
|
178
|
-
Your spans contain prompts, completions, tool inputs, and customer data. Shipping that to a SaaS vendor for "observability" is a data-egress decision most teams aren't ready to make.
|
|
179
|
-
|
|
180
|
-
| | TokenJam | LangSmith | Langfuse | Datadog LLM Obs |
|
|
181
|
-
|---|---|---|---|---|
|
|
182
|
-
| Signup required | ❌ | ✅ | ✅ | ✅ |
|
|
183
|
-
| Data leaves your machine | ❌ | ✅ | cloud only | ✅ |
|
|
184
|
-
| Cost-optimization analyzers (Downsize, Cache, Script, Trim) | ✅ | ❌ | ❌ | ❌ |
|
|
185
|
-
| Real-time sensitive-action alerts | ✅ | ❌ | ❌ | ❌ |
|
|
186
|
-
| Behavioral drift detection | ✅ | ❌ | ❌ | ❌ |
|
|
187
|
-
| OTel GenAI SemConv native | ✅ | partial | partial | partial |
|
|
188
|
-
| Works with any agent / framework | ✅ | LangChain-first | partial | ❌ |
|
|
189
|
-
| Free, MIT licensed | ✅ | freemium | freemium | paid |
|
|
190
|
-
|
|
191
|
-
---
|
|
192
|
-
|
|
193
|
-
## Web UI
|
|
201
|
+
## Lens — the local dashboard
|
|
194
202
|
|
|
195
|
-
`tj serve` runs
|
|
203
|
+
`tj serve` runs Lens at `http://127.0.0.1:7391/`: an Overview triage screen with spend, recoverable waste, and health at a glance; an Optimize tab showing every analyzer's findings side by side; and the standard Status, Traces, Cost, Alerts, Drift, and Budget screens. Plan-tier-aware, fully offline, no signup.
|
|
196
204
|
|
|
197
205
|
<table>
|
|
198
206
|
<tr>
|
|
199
|
-
<td width="50%"><img src="docs/screenshots/tj-status.png" alt="
|
|
200
|
-
<td width="50%"><img src="docs/screenshots/tj-cost.png" alt="
|
|
207
|
+
<td width="50%"><img src="docs/screenshots/tj-status.png" alt="Status screen" /></td>
|
|
208
|
+
<td width="50%"><img src="docs/screenshots/tj-cost.png" alt="Cost screen with spend-over-time chart" /></td>
|
|
201
209
|
</tr>
|
|
202
210
|
<tr>
|
|
203
|
-
<td width="50%"><img src="docs/screenshots/tj-traces.png" alt="
|
|
204
|
-
<td width="50%"><img src="docs/screenshots/tj-alerts.png" alt="
|
|
211
|
+
<td width="50%"><img src="docs/screenshots/tj-traces.png" alt="Traces table" /></td>
|
|
212
|
+
<td width="50%"><img src="docs/screenshots/tj-alerts.png" alt="Alerts table" /></td>
|
|
205
213
|
</tr>
|
|
206
214
|
</table>
|
|
207
215
|
|
|
216
|
+
→ [tokenjam.dev/products/lens](https://tokenjam.dev/products/lens) for the visual walkthrough.
|
|
217
|
+
|
|
208
218
|
---
|
|
209
219
|
|
|
210
220
|
## Beyond optimization
|
|
211
221
|
|
|
212
|
-
TokenJam is also a full observability stack. The
|
|
222
|
+
TokenJam is also a full observability stack. The five analyzers and Lens ride on top.
|
|
213
223
|
|
|
214
224
|
- **Real-time cost tracking** — every LLM call priced as it happens
|
|
215
225
|
- **Safety alerts** — 13 alert types, 6 channels (ntfy, Discord, Telegram, webhook, file, stdout)
|
|
216
226
|
- **Behavioral drift detection** — Z-score baselines, no LLM required
|
|
217
227
|
- **Schema validation** — declare or infer JSON Schema for tool outputs
|
|
218
228
|
- **OTel-native** — point any OTLP exporter at `tj serve` and you're done
|
|
219
|
-
- **MCP server** —
|
|
229
|
+
- **MCP server** — lets Claude Code query its own telemetry mid-session
|
|
220
230
|
|
|
221
231
|
---
|
|
222
232
|
|
|
223
233
|
## CLI
|
|
224
234
|
|
|
225
235
|
```bash
|
|
226
|
-
tj optimize # all
|
|
227
|
-
tj optimize downsize # one analyzer
|
|
236
|
+
tj optimize # all five cost-optimization analyzers
|
|
237
|
+
tj optimize downsize # one analyzer (positional args)
|
|
238
|
+
tj tokenmaxx # shareable spend-tier callout
|
|
228
239
|
tj status # current cost, tokens, active alerts
|
|
229
240
|
tj cost --since 7d # spend by agent / model / day / tool
|
|
230
241
|
tj alerts # everything that fired while you were away
|
|
231
242
|
tj drift # behavioral drift Z-scores
|
|
243
|
+
tj report --reuse # HTML + Markdown skeleton export for the Reuse analyzer
|
|
232
244
|
tj backfill claude-code # ingest historical ~/.claude/projects/ sessions
|
|
233
|
-
tj serve # start
|
|
245
|
+
tj serve # start Lens + REST API
|
|
234
246
|
```
|
|
235
247
|
|
|
236
248
|
[Full CLI reference →](docs/cli-reference.md)
|
|
@@ -242,6 +254,7 @@ tj serve # start the web UI + REST API
|
|
|
242
254
|
| Topic | Where |
|
|
243
255
|
|---|---|
|
|
244
256
|
| 🪶 Downsize / Cache / Script / Trim deep-dives | [docs/optimize/](docs/optimize/) |
|
|
257
|
+
| 🔁 Reuse analyzer deep-dive | [docs/optimize/reuse.md](docs/optimize/reuse.md) |
|
|
245
258
|
| Claude Code & Codex integration | [docs/claude-code-integration.md](docs/claude-code-integration.md) |
|
|
246
259
|
| Python SDK reference | [docs/python-sdk.md](docs/python-sdk.md) |
|
|
247
260
|
| TypeScript SDK reference | [docs/typescript-sdk.md](docs/typescript-sdk.md) |
|
|
@@ -253,6 +266,7 @@ tj serve # start the web UI + REST API
|
|
|
253
266
|
| Installation extras (Trim, framework patches) | [docs/installation.md](docs/installation.md) |
|
|
254
267
|
| Export to Grafana / Datadog / NDJSON | [docs/export.md](docs/export.md) |
|
|
255
268
|
| NemoClaw sandbox observer | [docs/nemoclaw-integration.md](docs/nemoclaw-integration.md) |
|
|
269
|
+
| Release notes | [GitHub Releases](https://github.com/Metabuilder-Labs/tokenjam/releases) |
|
|
256
270
|
|
|
257
271
|
---
|
|
258
272
|
|
|
@@ -260,9 +274,13 @@ tj serve # start the web UI + REST API
|
|
|
260
274
|
|
|
261
275
|
**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
|
|
262
276
|
|
|
277
|
+
**Shipped in 0.4.x:**
|
|
278
|
+
- [x] **[TokenJam Lens](https://github.com/Metabuilder-Labs/tokenjam/milestone/1)** — local dashboard rebrand: Overview triage front-door, Optimize detail tab, real spend-over-time charts, cross-screen drill-through
|
|
279
|
+
- [x] **[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
|
|
280
|
+
- [x] **Daemon DB concurrency** — per-thread DuckDB cursors so the Overview's fan-out doesn't block on a single shared connection (v0.4.1)
|
|
281
|
+
- [x] **Cache cost transparency** — `cache_read` + `cache_write` token columns surfaced in CLI + UI + API (the previously-hidden ~91% cost driver on cache-heavy workloads)
|
|
282
|
+
|
|
263
283
|
**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
|
|
266
284
|
- [ ] `tj policy add | edit | apply` — unified rule surface
|
|
267
285
|
- [ ] `tj replay` — replay captured sessions against new model versions
|
|
268
286
|
- [ ] TypeScript framework patches (LangChain JS, OpenAI Agents SDK)
|
|
@@ -278,4 +296,6 @@ tj serve # start the web UI + REST API
|
|
|
278
296
|
|
|
279
297
|
MIT License · Built by [Metabuilder Labs](https://github.com/Metabuilder-Labs)
|
|
280
298
|
|
|
299
|
+
TokenJam was created by [Anil Murty](https://github.com/anilmurty) — reach him at [anil@metabldr.com](mailto:anil@metabldr.com).
|
|
300
|
+
|
|
281
301
|
</div>
|