synapse-hub 2.2.4 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/.agents/agents/failure-analyst.md +11 -0
  2. package/.agents/agents/live-explorer.md +28 -0
  3. package/.agents/agents/orchestrator.md +20 -4
  4. package/.agents/agents/pom-crafter.md +12 -0
  5. package/.agents/agents/quality-gatekeeper.md +19 -4
  6. package/.agents/agents/test-runner.md +11 -0
  7. package/.agents/rules/communication-style.md +40 -0
  8. package/.agents/rules/context-engineering.md +23 -0
  9. package/.agents/rules/git-push-protocol.md +10 -16
  10. package/.agents/rules/graphify.md +14 -14
  11. package/.agents/rules/mandatory-playwright-live.md +29 -29
  12. package/.agents/rules/mobile_appium_rules.md +132 -0
  13. package/.agents/rules/performance_testing_rules.md +74 -0
  14. package/.agents/rules/systematic-debugging.md +6 -6
  15. package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +64 -215
  16. package/.agents/skills/agent-orchestration-multi-agent-optimize/examples/swarm_workload_config.json +12 -0
  17. package/.agents/skills/antigravity-test-sentinel/SKILL.md +3 -3
  18. package/.agents/skills/antigravity-test-sentinel/examples/dod_gate_policy.json +11 -0
  19. package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
  20. package/.agents/skills/api-network-mocking-interception/examples/network_route_stub.json +9 -0
  21. package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +9 -9
  22. package/.agents/skills/api-spec-to-e2e-matrix/examples/sample_openapi.json +19 -0
  23. package/.agents/skills/architecture-decision-records/SKILL.md +35 -412
  24. package/.agents/skills/architecture-decision-records/examples/0001-record-architecture-decisions.md +12 -0
  25. package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +1 -1
  26. package/.agents/skills/auth-session-and-cookie-manager/examples/storage_state_sample.json +11 -0
  27. package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +80 -186
  28. package/.agents/skills/backend-load-and-stress-patterns/examples/k6_load_test_sample.js +13 -0
  29. package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
  30. package/.agents/skills/bats-testing-patterns/examples/cli_runner_test.bats +6 -0
  31. package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -614
  32. package/.agents/skills/clean-pom-crafter/SKILL.md +21 -3
  33. package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -28
  34. package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -138
  35. package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +2 -2
  36. package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_page_sample.groovy +9 -0
  37. package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +5 -5
  38. package/.agents/skills/db-state-verifier-and-seeder/examples/db_fixture_seed.json +12 -0
  39. package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
  40. package/.agents/skills/e2e-testing-patterns/examples/playwright_sharded_config.ts +12 -0
  41. package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -522
  42. package/.agents/skills/failure-autopsy-and-healing/SKILL.md +17 -3
  43. package/.agents/skills/failure-autopsy-and-healing/examples/sample_junit_failure.xml +8 -0
  44. package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -64
  45. package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -56
  46. package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -98
  47. package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
  48. package/.agents/skills/file-and-report-validator/examples/sample_export_schema.json +14 -0
  49. package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +3 -3
  50. package/.agents/skills/flaky-test-sentinel-and-quarantine/examples/quarantine_registry.json +11 -0
  51. package/.agents/skills/gherkin-to-suite/SKILL.md +103 -0
  52. package/.agents/skills/gherkin-to-suite/examples/checkout.feature +39 -0
  53. package/.agents/skills/gherkin-to-suite/examples/suite_report_sample.json +334 -0
  54. package/.agents/skills/github-actions-templates/SKILL.md +47 -304
  55. package/.agents/skills/github-actions-templates/examples/matrix_test_pipeline.yml +8 -0
  56. package/.agents/skills/graphify-impact-analyzer/SKILL.md +6 -6
  57. package/.agents/skills/graphify-impact-analyzer/examples/dependency_blast_radius.json +8 -0
  58. package/.agents/skills/katalon-cli-runner/SKILL.md +2 -2
  59. package/.agents/skills/katalon-cli-runner/examples/headless_kre_exec.sh +3 -0
  60. package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -209
  61. package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -155
  62. package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -67
  63. package/.agents/skills/lam-script-generator/SKILL.md +42 -1
  64. package/.agents/skills/lam-script-generator/examples/user_story_sample.md +8 -0
  65. package/.agents/skills/lean-code-ladder/SKILL.md +83 -0
  66. package/.agents/skills/lean-code-ladder/examples/ladder_walkthrough.md +37 -0
  67. package/.agents/skills/meta-prompt-crafter/SKILL.md +107 -0
  68. package/.agents/skills/meta-prompt-crafter/examples/qa_prompt_template.md +35 -0
  69. package/.agents/skills/persistent-planning-files/SKILL.md +108 -0
  70. package/.agents/skills/persistent-planning-files/examples/task_plan_example.md +56 -0
  71. package/.agents/skills/playwright-live-interaction/SKILL.md +49 -1
  72. package/.agents/skills/playwright-live-interaction/examples/live_probe_script.js +5 -0
  73. package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -160
  74. package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -75
  75. package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -388
  76. package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -134
  77. package/.agents/skills/playwright-webapp-testing/SKILL.md +93 -0
  78. package/.agents/skills/playwright-webapp-testing/examples/server_lifecycle_playwright.py +63 -0
  79. package/.agents/skills/rbt-manual-testing/SKILL.md +150 -0
  80. package/.agents/skills/rbt-manual-testing/examples/checkout_rbt_manual_suite.md +25 -0
  81. package/.agents/skills/security-dast-sentinel/SKILL.md +54 -135
  82. package/.agents/skills/security-dast-sentinel/examples/dast_scan_config.json +10 -0
  83. package/.agents/skills/self-healing-locator/SKILL.md +24 -10
  84. package/.agents/skills/self-healing-locator/examples/aom_diff_sample.json +16 -0
  85. package/.agents/skills/skillspector-security-guard/SKILL.md +32 -90
  86. package/.agents/skills/skillspector-security-guard/examples/safe_mcp_config.json +12 -0
  87. package/.agents/skills/surgical-coding-principles/SKILL.md +89 -0
  88. package/.agents/skills/surgical-coding-principles/examples/surgical_vs_scope_creep.md +46 -0
  89. package/.agents/skills/synthetic-test-data-generator/SKILL.md +3 -3
  90. package/.agents/skills/synthetic-test-data-generator/examples/idempotent_data_recipe.json +9 -0
  91. package/.agents/skills/systematic-debugging/SKILL.md +100 -0
  92. package/.agents/skills/systematic-debugging/examples/four_phase_case.md +44 -0
  93. package/.agents/skills/test-driven-development/SKILL.md +114 -0
  94. package/.agents/skills/test-driven-development/examples/red_green_refactor_qa.md +78 -0
  95. package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
  96. package/.agents/skills/token-and-context-optimizer/examples/distilled_log_sample.txt +3 -0
  97. package/.agents/skills/verification-before-completion/SKILL.md +100 -0
  98. package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +46 -0
  99. package/.agents/skills/visual-regression-sentinel/SKILL.md +21 -1
  100. package/.agents/skills/visual-regression-sentinel/examples/visual_mask_rules.json +9 -0
  101. package/.agents/skills/wcag-audit-patterns/SKILL.md +3 -3
  102. package/.agents/skills/wcag-audit-patterns/examples/wcag_compliance_checklist.json +10 -0
  103. package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -541
  104. package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
  105. package/.agents/skills/web-vitals-and-performance-profiler/examples/cwv_budget_policy.json +6 -0
  106. package/.agents/templates/completion_report.md +57 -57
  107. package/.agents/workflows/analyze_requirement_document.md +18 -0
  108. package/.agents/workflows/export_testcases_excel.md +32 -0
  109. package/.agents/workflows/generate_combinatorial_test_data.md +28 -0
  110. package/.agents/workflows/generate_cross_module_test_plan.md +37 -0
  111. package/.agents/workflows/generate_manual_testcases_rbt.md +56 -0
  112. package/.agents/workflows/generate_requirements_from_website.md +25 -0
  113. package/.agents/workflows/generate_testcases_from_requirements.md +21 -0
  114. package/.agents/workflows/graphify.md +10 -10
  115. package/.agents/workflows/verify.md +2 -2
  116. package/.cursor/rules/language.mdc +7 -7
  117. package/.cursor/rules/synapse-qa.mdc +23 -0
  118. package/.cursorrules +7 -0
  119. package/.pre-commit-hooks.yaml +14 -0
  120. package/.windsurfrules +7 -0
  121. package/AGENTS.md +54 -15
  122. package/CLAUDE.md +35 -12
  123. package/LICENSE +30 -0
  124. package/NOTICE +180 -0
  125. package/README.en.md +325 -0
  126. package/README.md +247 -516
  127. package/action.yml +51 -0
  128. package/benchmarks/qa_eval/README.md +71 -0
  129. package/benchmarks/qa_eval/build_dataset.py +677 -0
  130. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +22 -0
  131. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +22 -0
  132. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +22 -0
  133. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +22 -0
  134. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +22 -0
  135. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +21 -0
  136. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +21 -0
  137. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +21 -0
  138. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +21 -0
  139. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +21 -0
  140. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +23 -0
  141. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +23 -0
  142. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +23 -0
  143. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +23 -0
  144. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +23 -0
  145. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +22 -0
  146. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +22 -0
  147. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +22 -0
  148. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +22 -0
  149. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +23 -0
  150. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +22 -0
  151. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +22 -0
  152. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +22 -0
  153. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +22 -0
  154. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +22 -0
  155. package/benchmarks/qa_eval/eval_runner.py +418 -0
  156. package/benchmarks/qa_eval/reports/AB_METHODOLOGY_REPORT.md +83 -0
  157. package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +69 -0
  158. package/benchmarks/qa_eval/reports/eval_results.json +105 -0
  159. package/bin/agent-qa.js +10 -3
  160. package/docs/ARCHITECTURE.md +107 -0
  161. package/docs/CI_CD_INTEGRATION.md +94 -0
  162. package/docs/CLI_REFERENCE.md +157 -0
  163. package/docs/CONTRIBUTING.md +91 -0
  164. package/docs/PLUGIN_DEVELOPMENT_GUIDE.md +126 -0
  165. package/docs/TOKEN_QUOTA_PLAYBOOK.md +41 -0
  166. package/docs/adopted-ecosystem-tools.md +87 -0
  167. package/docs/schemas/synapse.config.schema.json +64 -0
  168. package/examples/cypress-ecommerce-suite/README.md +45 -0
  169. package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +17 -0
  170. package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +29 -0
  171. package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +51 -0
  172. package/examples/cypress-ecommerce-suite/cypress.config.js +13 -0
  173. package/examples/cypress-ecommerce-suite/package.json +13 -0
  174. package/examples/katalon-suite/Keywords/pages/BasePage.groovy +19 -0
  175. package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +29 -0
  176. package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +45 -0
  177. package/examples/katalon-suite/Profiles/default.glbl +12 -0
  178. package/examples/katalon-suite/README.md +18 -0
  179. package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +12 -0
  180. package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +15 -0
  181. package/examples/katalon-suite/katalon_suite.prj +21 -0
  182. package/examples/playwright-crm-suite/README.md +40 -0
  183. package/examples/playwright-crm-suite/package.json +14 -0
  184. package/examples/playwright-crm-suite/pages/BasePage.ts +37 -0
  185. package/examples/playwright-crm-suite/pages/CustomerPage.ts +57 -0
  186. package/examples/playwright-crm-suite/pages/LoginPage.ts +32 -0
  187. package/examples/playwright-crm-suite/playwright.config.ts +27 -0
  188. package/examples/playwright-crm-suite/tests/auth.spec.ts +22 -0
  189. package/examples/playwright-crm-suite/tests/customers.spec.ts +33 -0
  190. package/examples/plugins/sample-custom-plugin/README.md +14 -0
  191. package/examples/plugins/sample-custom-plugin/plugin.json +16 -0
  192. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +29 -0
  193. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +8 -0
  194. package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +125 -0
  195. package/examples/practices/README.md +22 -0
  196. package/examples/practices/requirements/crm/requirements_contracts.md +292 -0
  197. package/examples/practices/requirements/crm/requirements_customers.md +462 -0
  198. package/examples/practices/requirements/crm/requirements_login.md +202 -0
  199. package/examples/practices/requirements/crm/requirements_projects.md +396 -0
  200. package/examples/practices/requirements/crm/requirements_tasks.md +343 -0
  201. package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +402 -0
  202. package/examples/practices/requirements/jira/KAN-5/KAN-1/2026-04-03_04-28-27.png +0 -0
  203. package/examples/practices/requirements/jira/KAN-5/KAN-1/KAN-1_requirement.md +52 -0
  204. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-19.png +0 -0
  205. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-57.png +0 -0
  206. package/examples/practices/requirements/jira/KAN-5/KAN-4/KAN-4_requirement.md +119 -0
  207. package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +9 -0
  208. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-08.png +0 -0
  209. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-23.png +0 -0
  210. package/examples/practices/requirements/jira/KAN-5/KAN-6/KAN-6_requirement.md +32 -0
  211. package/examples/practices/testcases/api/book_management_api_test_cases.md +144 -0
  212. package/examples/practices/testcases/api/user_management_api_test_cases.md +139 -0
  213. package/examples/practices/testcases/crm/test_cases_crm_login.md +148 -0
  214. package/examples/practices/testcases/crm/test_cases_customers.md +156 -0
  215. package/examples/pytest-api-suite/README.md +45 -0
  216. package/examples/pytest-api-suite/conftest.py +31 -0
  217. package/examples/pytest-api-suite/pages/user_api_client.py +36 -0
  218. package/examples/pytest-api-suite/requirements.txt +2 -0
  219. package/examples/pytest-api-suite/tests/test_user_api.py +44 -0
  220. package/examples/selenium-suite/README.md +18 -0
  221. package/examples/selenium-suite/pages/base_page.py +27 -0
  222. package/examples/selenium-suite/pages/login_page.py +29 -0
  223. package/examples/selenium-suite/pages/order_page.py +25 -0
  224. package/examples/selenium-suite/pytest.ini +6 -0
  225. package/examples/selenium-suite/requirements.txt +2 -0
  226. package/examples/selenium-suite/tests/test_login.py +10 -0
  227. package/examples/selenium-suite/tests/test_order.py +10 -0
  228. package/package.json +22 -7
  229. package/scripts/generate_skill_examples.py +244 -0
  230. package/scripts/install.ps1 +112 -0
  231. package/scripts/install_global_plugin.ps1 +94 -0
  232. package/scripts/install_global_plugin.sh +49 -0
  233. package/scripts/lint_skills.ps1 +57 -0
  234. package/scripts/package_dist.py +323 -0
  235. package/scripts/setup.sh +59 -0
  236. package/scripts/update_hub.ps1 +19 -0
  237. package/scripts/update_hub.sh +15 -0
  238. package/skills_manifest.json +209 -6
  239. package/synapse.config.json +30 -0
  240. package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +7 -7
  241. package/templates/PROJECT_STATE_TEMPLATE.md +1 -1
  242. package/templates/ai_bridges/CLAUDE.md +33 -16
  243. package/templates/ai_bridges/copilot-instructions.md +14 -21
  244. package/templates/ai_bridges/cursor_synapse_qa.mdc +17 -20
  245. package/templates/ai_bridges/windsurfrules +21 -21
  246. package/templates/ci/github-actions-synapse.yml +71 -4
  247. package/templates/welcome.html.tmpl +1674 -0
  248. package/tools/.infer_baseline.json +1102 -0
  249. package/tools/TOOLS_RUNBOOK.md +92 -130
  250. package/tools/__init__.py +26 -0
  251. package/tools/agent_canary.py +501 -501
  252. package/tools/analyze_project.py +31 -17
  253. package/tools/api_fuzzer.py +278 -6
  254. package/tools/author_integrity.py +6 -6
  255. package/tools/auto_heal.py +1 -1
  256. package/tools/browser_helpers.js +105 -7
  257. package/tools/bug_reporter.py +31 -1
  258. package/tools/bump_version.py +23 -3
  259. package/tools/cache_resolver.py +248 -14
  260. package/tools/check_env.py +55 -14
  261. package/tools/check_version_sync.py +153 -0
  262. package/tools/ci_healing_hook.py +1 -1
  263. package/tools/cli.py +263 -13
  264. package/tools/count_tokens.py +1257 -0
  265. package/tools/dashboard_generator.py +2638 -0
  266. package/tools/db_sandbox.py +198 -20
  267. package/tools/e2e_flow_prober.ps1 +12 -64
  268. package/tools/e2e_flow_prober.py +99 -0
  269. package/tools/e2e_flow_prober.sh +4 -0
  270. package/tools/flaky_detector.py +4 -3
  271. package/tools/generate_allure_report.ps1 +12 -29
  272. package/tools/generate_allure_report.py +77 -0
  273. package/tools/generate_allure_report.sh +4 -0
  274. package/tools/generate_jmx_plan.py +257 -58
  275. package/tools/generate_test_reports.ps1 +12 -134
  276. package/tools/generate_test_reports.py +133 -0
  277. package/tools/generate_test_reports.sh +4 -0
  278. package/tools/gherkin_suite.py +758 -0
  279. package/tools/git_pre_commit_guard.py +12 -6
  280. package/tools/goldfish_review.py +366 -0
  281. package/tools/heal_pr_automator.py +1 -1
  282. package/tools/heuristic_locator_matcher.py +170 -21
  283. package/tools/infer_linter.py +612 -70
  284. package/tools/lint_ai_docs.py +64 -0
  285. package/tools/matrix_sharder.py +1 -1
  286. package/tools/mcp_excel_server.ps1 +12 -250
  287. package/tools/mcp_excel_server.py +249 -0
  288. package/tools/mcp_excel_server.sh +4 -0
  289. package/tools/mcp_server.py +368 -144
  290. package/tools/md_to_xlsx.py +307 -0
  291. package/tools/micro_mutator.py +300 -69
  292. package/tools/omni_transpiler.py +2 -2
  293. package/tools/output_formatter.py +128 -0
  294. package/tools/pinpoint_failure.py +88 -88
  295. package/tools/project_state.py +2 -2
  296. package/tools/repo_map.py +1 -1
  297. package/tools/safety_firewall.py +196 -45
  298. package/tools/setup_project.py +294 -30
  299. package/tools/spec_drift_sentinel.py +439 -0
  300. package/tools/sync_agent_rules.py +248 -0
  301. package/tools/telemetry_correlator.py +45 -0
  302. package/tools/test_impact.py +51 -12
  303. package/tools/tms_sync_bridge.py +21 -5
  304. package/tools/universal_ir.py +3 -0
  305. package/tools/update_sentinel.py +87 -15
  306. package/tools/verify_all.ps1 +13 -0
  307. package/tools/verify_all.py +463 -79
  308. package/tools/visual_diff_canvas.py +1 -0
  309. package/tools/visual_som_helper.py +1 -1
  310. package/tools/watch_mode.py +305 -0
  311. package/tools/welcome_page.py +291 -0
@@ -0,0 +1,1257 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Cross-Platform Token & Cost Auditor (Synapse-Hub)
4
+ Author: Thế Vị (@TeeVee998) <https://github.com/TeeVee998/Synapse-Hub>
5
+ License: MIT
6
+
7
+ Audits and counts exact token usage (Prompt Input, Cache Read, Output),
8
+ tracks 5-hour and 7-day rolling window quota usage, and benchmarks token & cost
9
+ savings enabled by Synapse-Hub context engineering.
10
+
11
+ Supported Platforms via Multi-Client Adapters:
12
+ 1. Google Antigravity (native exact tokens from transcript.jsonl)
13
+ 2. Cursor Agent (parses ~/.cursor/projects/*/agent-transcripts/*/*.jsonl)
14
+ 3. Claude Code (parses ~/.claude/ and session logs)
15
+ 4. Generic / Raw JSONL files (--file <path> with auto-format detection)
16
+
17
+ Usage:
18
+ python3 tools/count_tokens.py # Audit active Antigravity session
19
+ python3 tools/count_tokens.py --platform cursor # Audit latest Cursor session
20
+ python3 tools/count_tokens.py --platform all --list # List sessions across all platforms
21
+ python3 tools/count_tokens.py --platform cursor --turns # Turn-by-turn breakdown for Cursor
22
+ python3 tools/count_tokens.py --current # Analyze current active session
23
+ python3 tools/count_tokens.py --session <id> # Analyze specific session ID
24
+ python3 tools/count_tokens.py --rolling # Show 5-hour and 7-day rolling quota
25
+ python3 tools/count_tokens.py --savings # Show Hub vs. Non-Hub token & cost savings
26
+ python3 tools/count_tokens.py --header # Output markdown lines for Pre-Flight Block
27
+ python3 tools/count_tokens.py --file <path> # Direct audit of any transcript file
28
+ python3 tools/count_tokens.py --pricing claude-sonnet # Calculate cost with specific pricing model
29
+ python3 tools/count_tokens.py --json # Output machine-readable JSON
30
+ """
31
+
32
+ import os
33
+ import sys
34
+ import glob
35
+ import json
36
+ import re
37
+ import time
38
+ import hashlib
39
+ import sqlite3
40
+ import argparse
41
+ from datetime import datetime, timezone, timedelta
42
+ from typing import Dict, List, Any, Optional, Tuple
43
+
44
+ PRICING_TABLE = {
45
+ "gemini-flash": {
46
+ "name": "Google Gemini 2.5/3.x Flash",
47
+ "input_per_m": 0.075,
48
+ "cache_per_m": 0.01875,
49
+ "output_per_m": 0.30,
50
+ },
51
+ "gemini-pro": {
52
+ "name": "Google Gemini 1.5/2.5 Pro",
53
+ "input_per_m": 1.25,
54
+ "cache_per_m": 0.3125,
55
+ "output_per_m": 5.00,
56
+ },
57
+ "claude-sonnet": {
58
+ "name": "Anthropic Claude 3.5/3.7 Sonnet",
59
+ "input_per_m": 3.00,
60
+ "cache_per_m": 0.30,
61
+ "output_per_m": 15.00,
62
+ },
63
+ "claude-opus": {
64
+ "name": "Anthropic Claude 3/3.5 Opus",
65
+ "input_per_m": 15.00,
66
+ "cache_per_m": 1.50,
67
+ "output_per_m": 75.00,
68
+ },
69
+ "gpt-4o": {
70
+ "name": "OpenAI GPT-4o",
71
+ "input_per_m": 2.50,
72
+ "cache_per_m": 1.25,
73
+ "output_per_m": 10.00,
74
+ },
75
+ "gpt-4o-mini": {
76
+ "name": "OpenAI GPT-4o Mini",
77
+ "input_per_m": 0.15,
78
+ "cache_per_m": 0.075,
79
+ "output_per_m": 0.60,
80
+ },
81
+ }
82
+
83
+ DEFAULT_MODEL = "gemini-flash"
84
+ BUDGET_CONFIG_FILE = os.path.expanduser("~/.agent_memory/token_budget.json")
85
+
86
+ # Default quota limits (billable input tokens: uncached input + output)
87
+ DEFAULT_5H_BUDGET = 10_000_000 # 10M tokens / 5h
88
+ DEFAULT_7D_BUDGET = 100_000_000 # 100M tokens / 7d
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Utility & Token Estimation Functions
93
+ # ---------------------------------------------------------------------------
94
+
95
+ def estimate_tokens(text: str) -> int:
96
+ """Accurate token estimator for polyglot text & code when native token metadata is absent."""
97
+ if not text:
98
+ return 0
99
+ non_ascii = len([c for c in text if ord(c) > 127])
100
+ ascii_len = len(text) - non_ascii
101
+ # Standard LLM BPE heuristic: ~3.8 chars/token for ASCII, ~1.8 chars/token for Unicode/CJK/Vietnamese
102
+ est = int((ascii_len / 3.8) + (non_ascii / 1.8))
103
+ return max(1, est)
104
+
105
+
106
+ def parse_token_shorthand(val: str) -> int:
107
+ """Parses token string with suffixes like 10M, 500K, 1B into an integer."""
108
+ v = val.strip().upper()
109
+ if v.endswith("K"):
110
+ return int(float(v[:-1]) * 1_000)
111
+ if v.endswith("M"):
112
+ return int(float(v[:-1]) * 1_000_000)
113
+ if v.endswith("B"):
114
+ return int(float(v[:-1]) * 1_000_000_000)
115
+ return int(float(v))
116
+
117
+
118
+ def format_token_shorthand(num: int) -> str:
119
+ """Formats large integer into clean readable string like 5.4M, 10M, 750K."""
120
+ if abs(num) >= 1_000_000_000:
121
+ val = num / 1_000_000_000
122
+ return f"{int(val)}B" if val.is_integer() else f"{val:.1f}B"
123
+ if abs(num) >= 1_000_000:
124
+ val = num / 1_000_000
125
+ return f"{int(val)}M" if val.is_integer() else f"{val:.1f}M"
126
+ if abs(num) >= 1_000:
127
+ val = num / 1_000
128
+ return f"{int(val)}K" if val.is_integer() else f"{val:.1f}K"
129
+ return str(num)
130
+
131
+
132
+ def get_budget_config() -> Dict[str, int]:
133
+ """Reads or initializes token budget configuration."""
134
+ if os.path.exists(BUDGET_CONFIG_FILE):
135
+ try:
136
+ with open(BUDGET_CONFIG_FILE, "r", encoding="utf-8") as f:
137
+ data = json.load(f)
138
+ return {
139
+ "budget_5h": data.get("budget_5h", DEFAULT_5H_BUDGET),
140
+ "budget_7d": data.get("budget_7d", DEFAULT_7D_BUDGET),
141
+ }
142
+ except Exception:
143
+ pass
144
+ return {
145
+ "budget_5h": DEFAULT_5H_BUDGET,
146
+ "budget_7d": DEFAULT_7D_BUDGET,
147
+ }
148
+
149
+
150
+ def save_budget_config(budget_5h: Optional[int] = None, budget_7d: Optional[int] = None):
151
+ """Saves updated token budget configuration."""
152
+ cfg = get_budget_config()
153
+ if budget_5h is not None:
154
+ cfg["budget_5h"] = budget_5h
155
+ if budget_7d is not None:
156
+ cfg["budget_7d"] = budget_7d
157
+ os.makedirs(os.path.dirname(BUDGET_CONFIG_FILE), exist_ok=True)
158
+ with open(BUDGET_CONFIG_FILE, "w", encoding="utf-8") as f:
159
+ json.dump(cfg, f, indent=2)
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # Platform Adapters
164
+ # ---------------------------------------------------------------------------
165
+
166
+ class AntigravityAdapter:
167
+ """Adapter for Google Antigravity workspace transcripts."""
168
+ name = "antigravity"
169
+
170
+ @staticmethod
171
+ def get_brain_dir() -> str:
172
+ home = os.path.expanduser("~")
173
+ candidates = [
174
+ os.path.join(home, ".gemini", "antigravity", "brain"),
175
+ os.path.join(home, ".config", "gemini", "antigravity", "brain"),
176
+ ]
177
+ for c in candidates:
178
+ if os.path.isdir(c):
179
+ return c
180
+ return candidates[0]
181
+
182
+ @staticmethod
183
+ def get_conversation_summaries_db() -> Optional[str]:
184
+ home = os.path.expanduser("~")
185
+ db_path = os.path.join(home, ".gemini", "antigravity", "conversation_summaries.db")
186
+ if os.path.exists(db_path):
187
+ return db_path
188
+ return None
189
+
190
+ @classmethod
191
+ def get_sessions(cls) -> List[Dict[str, Any]]:
192
+ brain_dir = cls.get_brain_dir()
193
+ if not os.path.exists(brain_dir):
194
+ return []
195
+
196
+ db_path = cls.get_conversation_summaries_db()
197
+ title_map = {}
198
+ if db_path:
199
+ try:
200
+ conn = sqlite3.connect(db_path)
201
+ cursor = conn.cursor()
202
+ cursor.execute("SELECT conversation_id, title, step_count, last_modified_time FROM conversation_summaries")
203
+ for row in cursor.fetchall():
204
+ title_map[row[0]] = {
205
+ "title": row[1],
206
+ "step_count": row[2],
207
+ "last_modified": row[3],
208
+ }
209
+ conn.close()
210
+ except Exception:
211
+ pass
212
+
213
+ conversations = []
214
+ for entry in os.listdir(brain_dir):
215
+ entry_path = os.path.join(brain_dir, entry)
216
+ if not os.path.isdir(entry_path):
217
+ continue
218
+ transcript_file = os.path.join(entry_path, ".system_generated", "logs", "transcript.jsonl")
219
+ if not os.path.exists(transcript_file):
220
+ continue
221
+
222
+ mtime = os.path.getmtime(transcript_file)
223
+ meta = title_map.get(entry, {})
224
+ conversations.append({
225
+ "id": entry,
226
+ "path": entry_path,
227
+ "transcript_file": transcript_file,
228
+ "mtime": mtime,
229
+ "title": meta.get("title") or entry[:12],
230
+ "step_count": meta.get("step_count", 0),
231
+ "platform": "antigravity",
232
+ })
233
+
234
+ conversations.sort(key=lambda x: x["mtime"], reverse=True)
235
+ return conversations
236
+
237
+ @classmethod
238
+ def parse_transcript(cls, transcript_path: str) -> Dict[str, Any]:
239
+ if not os.path.exists(transcript_path):
240
+ return {"error": f"Transcript not found at {transcript_path}"}
241
+
242
+ turns = []
243
+ current_turn = None
244
+ total_input = 0
245
+ total_cache = 0
246
+ total_output = 0
247
+ total_steps = 0
248
+ detected_model = "gemini-flash"
249
+
250
+ with open(transcript_path, "r", encoding="utf-8", errors="replace") as f:
251
+ for line in f:
252
+ line = line.strip()
253
+ if not line or ("input_tokens" not in line and "USER_INPUT" not in line):
254
+ continue
255
+ try:
256
+ data = json.loads(line)
257
+ except json.JSONDecodeError:
258
+ continue
259
+
260
+ msg_type = data.get("type")
261
+ source = data.get("source")
262
+
263
+ if msg_type == "USER_INPUT":
264
+ content = data.get("content", "")
265
+ if "Model Selection" in content:
266
+ m = re.search(r"Model Selection[`'\"]?\s+from\s+.*?\s+to\s+([^.\n<]+)", content, re.IGNORECASE)
267
+ if m:
268
+ val = m.group(1).lower()
269
+ if "pro" in val:
270
+ detected_model = "gemini-pro"
271
+ elif "claude" in val or "sonnet" in val:
272
+ detected_model = "claude-sonnet"
273
+ elif "opus" in val:
274
+ detected_model = "claude-opus"
275
+ elif "gpt" in val:
276
+ detected_model = "gpt-4o"
277
+ elif "flash" in val:
278
+ detected_model = "gemini-flash"
279
+
280
+ if msg_type == "USER_INPUT" and source in ("USER_EXPLICIT", "USER"):
281
+ if current_turn:
282
+ turns.append(current_turn)
283
+
284
+ content = data.get("content", "")
285
+ user_req_match = re.search(r"<USER_REQUEST>\s*(.*?)\s*</USER_REQUEST>", content, re.DOTALL)
286
+ if user_req_match:
287
+ prompt_text = user_req_match.group(1).strip()
288
+ else:
289
+ prompt_text = content.strip()
290
+
291
+ current_turn = {
292
+ "turn_index": len(turns) + 1,
293
+ "prompt": prompt_text,
294
+ "created_at": data.get("created_at", ""),
295
+ "steps": 0,
296
+ "input_tokens": 0,
297
+ "cache_read_tokens": 0,
298
+ "output_tokens": 0,
299
+ "tools_called": [],
300
+ }
301
+ elif current_turn and "input_tokens" in data:
302
+ current_turn["steps"] += 1
303
+ in_tok = data.get("input_tokens", 0)
304
+ cache_tok = data.get("cache_read_tokens", 0)
305
+ out_tok = data.get("output_tokens", 0)
306
+
307
+ current_turn["input_tokens"] += in_tok
308
+ current_turn["cache_read_tokens"] += cache_tok
309
+ current_turn["output_tokens"] += out_tok
310
+
311
+ total_input += in_tok
312
+ total_cache += cache_tok
313
+ total_output += out_tok
314
+ total_steps += 1
315
+
316
+ for tc in data.get("tool_calls", []):
317
+ tool_name = tc.get("name")
318
+ if tool_name and tool_name not in current_turn["tools_called"]:
319
+ current_turn["tools_called"].append(tool_name)
320
+
321
+ if current_turn:
322
+ turns.append(current_turn)
323
+
324
+ total_all = total_input + total_cache + total_output
325
+ cache_hit_rate = (total_cache / (total_input + total_cache) * 100.0) if (total_input + total_cache) > 0 else 0.0
326
+
327
+ return {
328
+ "platform": "antigravity",
329
+ "transcript_path": transcript_path,
330
+ "turns_count": len(turns),
331
+ "steps_count": total_steps,
332
+ "input_tokens": total_input,
333
+ "cache_read_tokens": total_cache,
334
+ "output_tokens": total_output,
335
+ "billable_tokens": total_input + total_output,
336
+ "total_tokens": total_all,
337
+ "cache_hit_rate_pct": round(cache_hit_rate, 2),
338
+ "detected_model": detected_model,
339
+ "turns": turns,
340
+ }
341
+
342
+ @classmethod
343
+ def get_rolling_usage(cls, hours: int = 5, days: int = 7) -> Dict[str, Any]:
344
+ brain_dir = cls.get_brain_dir()
345
+ now = datetime.now(timezone.utc)
346
+ cutoff_hours = now - timedelta(hours=hours)
347
+ cutoff_days = now - timedelta(days=days)
348
+
349
+ usage_hours = {"steps": 0, "input": 0, "cache": 0, "output": 0}
350
+ usage_days = {"steps": 0, "input": 0, "cache": 0, "output": 0}
351
+
352
+ cutoff_timestamp = time.time() - (days * 86400)
353
+ search_pattern = os.path.join(brain_dir, "*", ".system_generated", "logs", "transcript.jsonl")
354
+ transcript_files = [
355
+ f for f in glob.glob(search_pattern)
356
+ if os.path.getmtime(f) >= cutoff_timestamp
357
+ ]
358
+
359
+ for fpath in transcript_files:
360
+ try:
361
+ with open(fpath, "r", encoding="utf-8", errors="replace") as f:
362
+ for line in f:
363
+ if "input_tokens" not in line:
364
+ continue
365
+ try:
366
+ d = json.loads(line)
367
+ except json.JSONDecodeError:
368
+ continue
369
+
370
+ created_str = d.get("created_at")
371
+ if not created_str:
372
+ continue
373
+ t = datetime.fromisoformat(created_str.replace("Z", "+00:00"))
374
+
375
+ in_tok = d.get("input_tokens", 0)
376
+ cache_tok = d.get("cache_read_tokens", 0)
377
+ out_tok = d.get("output_tokens", 0)
378
+
379
+ if t >= cutoff_days:
380
+ usage_days["steps"] += 1
381
+ usage_days["input"] += in_tok
382
+ usage_days["cache"] += cache_tok
383
+ usage_days["output"] += out_tok
384
+
385
+ if t >= cutoff_hours:
386
+ usage_hours["steps"] += 1
387
+ usage_hours["input"] += in_tok
388
+ usage_hours["cache"] += cache_tok
389
+ usage_hours["output"] += out_tok
390
+ except Exception:
391
+ pass
392
+
393
+ budgets = get_budget_config()
394
+ b_5h = budgets["budget_5h"]
395
+ b_7d = budgets["budget_7d"]
396
+
397
+ billable_5h = usage_hours["input"] + usage_hours["output"]
398
+ billable_7d = usage_days["input"] + usage_days["output"]
399
+
400
+ rem_5h = max(0, b_5h - billable_5h)
401
+ rem_7d = max(0, b_7d - billable_7d)
402
+
403
+ pct_used_5h = (billable_5h / b_5h * 100.0) if b_5h > 0 else 0.0
404
+ pct_used_7d = (billable_7d / b_7d * 100.0) if b_7d > 0 else 0.0
405
+
406
+ hit_rate_5h = (usage_hours["cache"] / (usage_hours["input"] + usage_hours["cache"]) * 100.0) if (usage_hours["input"] + usage_hours["cache"]) > 0 else 0.0
407
+ hit_rate_7d = (usage_days["cache"] / (usage_days["input"] + usage_days["cache"]) * 100.0) if (usage_days["input"] + usage_days["cache"]) > 0 else 0.0
408
+
409
+ return {
410
+ "window_5h": {
411
+ "hours": hours,
412
+ "steps": usage_hours["steps"],
413
+ "input_tokens": usage_hours["input"],
414
+ "cache_read_tokens": usage_hours["cache"],
415
+ "output_tokens": usage_hours["output"],
416
+ "billable_tokens": billable_5h,
417
+ "total_tokens": usage_hours["input"] + usage_hours["cache"] + usage_hours["output"],
418
+ "budget": b_5h,
419
+ "remaining_tokens": rem_5h,
420
+ "pct_used": round(pct_used_5h, 1),
421
+ "cache_hit_rate_pct": round(hit_rate_5h, 1),
422
+ },
423
+ "window_7d": {
424
+ "days": days,
425
+ "steps": usage_days["steps"],
426
+ "input_tokens": usage_days["input"],
427
+ "cache_read_tokens": usage_days["cache"],
428
+ "output_tokens": usage_days["output"],
429
+ "billable_tokens": billable_7d,
430
+ "total_tokens": usage_days["input"] + usage_days["cache"] + usage_days["output"],
431
+ "budget": b_7d,
432
+ "remaining_tokens": rem_7d,
433
+ "pct_used": round(pct_used_7d, 1),
434
+ "cache_hit_rate_pct": round(hit_rate_7d, 1),
435
+ },
436
+ }
437
+
438
+
439
+ class CursorAdapter:
440
+ """Adapter for Cursor Agent sessions (~/.cursor/projects/*/agent-transcripts/*/*.jsonl)."""
441
+ name = "cursor"
442
+
443
+ @staticmethod
444
+ def get_cursor_dir() -> str:
445
+ return os.path.expanduser("~/.cursor")
446
+
447
+ @classmethod
448
+ def get_sessions(cls) -> List[Dict[str, Any]]:
449
+ cursor_dir = cls.get_cursor_dir()
450
+ projects_dir = os.path.join(cursor_dir, "projects")
451
+ if not os.path.isdir(projects_dir):
452
+ return []
453
+
454
+ pattern = os.path.join(projects_dir, "*", "agent-transcripts", "*", "*.jsonl")
455
+ sessions = []
456
+ for fpath in glob.glob(pattern):
457
+ try:
458
+ mtime = os.path.getmtime(fpath)
459
+ session_id = os.path.basename(os.path.dirname(fpath))
460
+ title = session_id[:12]
461
+
462
+ # Extract first user query for title
463
+ with open(fpath, "r", encoding="utf-8", errors="replace") as fp:
464
+ for line in fp:
465
+ if '"role":"user"' in line or '"role": "user"' in line:
466
+ d = json.loads(line)
467
+ for c in d.get("message", {}).get("content", []):
468
+ if c.get("type") == "text":
469
+ t = c.get("text", "")
470
+ m = re.search(r"<user_query>\s*(.*?)\s*</user_query>", t, re.DOTALL)
471
+ title = (m.group(1).strip() if m else t.strip()).replace("\n", " ")
472
+ break
473
+ if title != session_id[:12]:
474
+ break
475
+
476
+ sessions.append({
477
+ "id": session_id,
478
+ "path": os.path.dirname(fpath),
479
+ "transcript_file": fpath,
480
+ "mtime": mtime,
481
+ "title": title,
482
+ "platform": "cursor",
483
+ })
484
+ except Exception:
485
+ continue
486
+
487
+ sessions.sort(key=lambda x: x["mtime"], reverse=True)
488
+ return sessions
489
+
490
+ @classmethod
491
+ def parse_transcript(cls, transcript_path: str) -> Dict[str, Any]:
492
+ if not os.path.exists(transcript_path):
493
+ return {"error": f"Transcript not found at {transcript_path}"}
494
+
495
+ turns = []
496
+ current_turn = None
497
+ total_input = 0
498
+ total_output = 0
499
+ total_cache = 0
500
+ total_steps = 0
501
+ accumulated_context = 4000 # Base system instructions context estimation
502
+
503
+ with open(transcript_path, "r", encoding="utf-8", errors="replace") as f:
504
+ for line in f:
505
+ line = line.strip()
506
+ if not line:
507
+ continue
508
+ try:
509
+ data = json.loads(line)
510
+ except json.JSONDecodeError:
511
+ continue
512
+
513
+ role = data.get("role")
514
+ msg = data.get("message", {})
515
+ content = msg.get("content", [])
516
+
517
+ if role == "user":
518
+ text_blocks = [c.get("text", "") for c in content if c.get("type") == "text"]
519
+ tool_results = [c for c in content if c.get("type") == "tool_result"]
520
+ full_text = " ".join(text_blocks)
521
+
522
+ # New user request turn boundary
523
+ if "<user_query>" in full_text or (not tool_results and full_text.strip()):
524
+ if current_turn:
525
+ turns.append(current_turn)
526
+
527
+ m = re.search(r"<user_query>\s*(.*?)\s*</user_query>", full_text, re.DOTALL)
528
+ prompt = m.group(1).strip() if m else full_text.strip()
529
+ p_tokens = estimate_tokens(full_text)
530
+
531
+ # Prefix cache simulation for multi-turn conversations
532
+ uncached_in = p_tokens
533
+ cached_in = int(accumulated_context * 0.85)
534
+ accumulated_context += p_tokens
535
+
536
+ current_turn = {
537
+ "turn_index": len(turns) + 1,
538
+ "prompt": prompt,
539
+ "created_at": "",
540
+ "steps": 0,
541
+ "input_tokens": uncached_in,
542
+ "cache_read_tokens": cached_in,
543
+ "output_tokens": 0,
544
+ "tools_called": [],
545
+ }
546
+ total_input += uncached_in
547
+ total_cache += cached_in
548
+
549
+ elif current_turn and tool_results:
550
+ res_tokens = sum(estimate_tokens(str(tr.get("content", ""))) for tr in tool_results)
551
+ current_turn["input_tokens"] += res_tokens
552
+ current_turn["steps"] += 1
553
+ total_input += res_tokens
554
+ total_steps += 1
555
+ accumulated_context += res_tokens
556
+
557
+ elif role == "assistant" and current_turn:
558
+ current_turn["steps"] += 1
559
+ total_steps += 1
560
+ out_tok = 0
561
+ for block in content:
562
+ b_type = block.get("type")
563
+ if b_type == "text":
564
+ out_tok += estimate_tokens(block.get("text", ""))
565
+ elif b_type == "tool_use":
566
+ tname = block.get("name", "tool")
567
+ if tname not in current_turn["tools_called"]:
568
+ current_turn["tools_called"].append(tname)
569
+ out_tok += estimate_tokens(json.dumps(block.get("input", {})))
570
+
571
+ current_turn["output_tokens"] += out_tok
572
+ total_output += out_tok
573
+ accumulated_context += out_tok
574
+
575
+ if current_turn:
576
+ turns.append(current_turn)
577
+
578
+ total_all = total_input + total_cache + total_output
579
+ cache_hit_rate = (total_cache / (total_input + total_cache) * 100.0) if (total_input + total_cache) > 0 else 0.0
580
+
581
+ return {
582
+ "platform": "cursor",
583
+ "transcript_path": transcript_path,
584
+ "turns_count": len(turns),
585
+ "steps_count": total_steps,
586
+ "input_tokens": total_input,
587
+ "cache_read_tokens": total_cache,
588
+ "output_tokens": total_output,
589
+ "billable_tokens": total_input + total_output,
590
+ "total_tokens": total_all,
591
+ "cache_hit_rate_pct": round(cache_hit_rate, 2),
592
+ "detected_model": "claude-sonnet",
593
+ "turns": turns,
594
+ }
595
+
596
+
597
+ class ClaudeCodeAdapter:
598
+ """Adapter for Claude Code sessions (~/.claude/ and session records)."""
599
+ name = "claude"
600
+
601
+ @staticmethod
602
+ def get_claude_dirs() -> List[str]:
603
+ home = os.path.expanduser("~")
604
+ return [
605
+ os.path.join(home, ".claude"),
606
+ os.path.join(home, "Library", "Application Support", "Claude"),
607
+ ]
608
+
609
+ @classmethod
610
+ def get_sessions(cls) -> List[Dict[str, Any]]:
611
+ sessions = []
612
+ home = os.path.expanduser("~")
613
+ # Search candidate directories
614
+ patterns = [
615
+ os.path.join(home, ".claude", "projects", "*", "*.jsonl"),
616
+ os.path.join(home, ".claude", "sessions", "*.jsonl"),
617
+ os.path.join(home, "Library", "Application Support", "Claude", "claude-code-sessions", "*", "*", "*.json"),
618
+ ]
619
+ for pat in patterns:
620
+ for fpath in glob.glob(pat):
621
+ try:
622
+ mtime = os.path.getmtime(fpath)
623
+ sid = os.path.basename(fpath).split(".")[0]
624
+ sessions.append({
625
+ "id": sid,
626
+ "path": os.path.dirname(fpath),
627
+ "transcript_file": fpath,
628
+ "mtime": mtime,
629
+ "title": f"Claude Session {sid[:8]}",
630
+ "platform": "claude",
631
+ })
632
+ except Exception:
633
+ continue
634
+
635
+ sessions.sort(key=lambda x: x["mtime"], reverse=True)
636
+ return sessions
637
+
638
+ @classmethod
639
+ def parse_transcript(cls, transcript_path: str) -> Dict[str, Any]:
640
+ # Fallback to Cursor / Anthropic message format parser
641
+ data = CursorAdapter.parse_transcript(transcript_path)
642
+ data["platform"] = "claude"
643
+ data["detected_model"] = "claude-sonnet"
644
+ return data
645
+
646
+
647
+ # ---------------------------------------------------------------------------
648
+ # Sniffer & Factory Dispatcher
649
+ # ---------------------------------------------------------------------------
650
+
651
+ def detect_platform_from_file(file_path: str) -> str:
652
+ """Sniffs the first few lines of a transcript to identify the AI platform format."""
653
+ if not os.path.exists(file_path):
654
+ return "antigravity"
655
+ try:
656
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
657
+ for _ in range(20):
658
+ line = f.readline()
659
+ if not line:
660
+ break
661
+ if "USER_EXPLICIT" in line or "USER_INPUT" in line or "input_tokens" in line:
662
+ return "antigravity"
663
+ if '"role":"user"' in line or '"role": "user"' in line or "<user_query>" in line:
664
+ return "cursor"
665
+ if "claude" in line.lower():
666
+ return "claude"
667
+ except Exception:
668
+ pass
669
+ return "antigravity"
670
+
671
+
672
+ def get_adapter(platform: str):
673
+ """Factory retrieving the designated platform adapter."""
674
+ p = platform.lower()
675
+ if p == "cursor":
676
+ return CursorAdapter
677
+ if p == "claude":
678
+ return ClaudeCodeAdapter
679
+ return AntigravityAdapter
680
+
681
+
682
+ def get_all_sessions() -> List[Dict[str, Any]]:
683
+ """Retrieves all sessions across all installed AI platforms."""
684
+ all_sess = []
685
+ all_sess.extend(AntigravityAdapter.get_sessions())
686
+ all_sess.extend(CursorAdapter.get_sessions())
687
+ all_sess.extend(ClaudeCodeAdapter.get_sessions())
688
+ all_sess.sort(key=lambda x: x["mtime"], reverse=True)
689
+ return all_sess
690
+
691
+
692
+ # ---------------------------------------------------------------------------
693
+ # Cost & Savings Calculations
694
+ # ---------------------------------------------------------------------------
695
+
696
+ def calculate_cost(input_tokens: int, cache_tokens: int, output_tokens: int, model_key: str = DEFAULT_MODEL) -> Dict[str, Any]:
697
+ """Calculates estimated costs in USD based on model pricing."""
698
+ pricing = PRICING_TABLE.get(model_key, PRICING_TABLE[DEFAULT_MODEL])
699
+ input_cost = (input_tokens / 1_000_000) * pricing["input_per_m"]
700
+ cache_cost = (cache_tokens / 1_000_000) * pricing["cache_per_m"]
701
+ output_cost = (output_tokens / 1_000_000) * pricing["output_per_m"]
702
+ total_cost = input_cost + cache_cost + output_cost
703
+
704
+ return {
705
+ "model_name": pricing["name"],
706
+ "input_cost_usd": round(input_cost, 6),
707
+ "cache_cost_usd": round(cache_cost, 6),
708
+ "output_cost_usd": round(output_cost, 6),
709
+ "total_cost_usd": round(total_cost, 4),
710
+ }
711
+
712
+
713
+ def calculate_hub_savings(input_tokens: int, cache_tokens: int, output_tokens: int, model_key: str = DEFAULT_MODEL) -> Dict[str, Any]:
714
+ """
715
+ Empirical Hub Savings Model:
716
+ Compares actual Synapse-Hub execution against vanilla/non-Hub agent execution:
717
+ 1. Log Distillation (Observation Masking): Hub cuts CLI noise by 95% (raw vs distilled).
718
+ 2. Sparse Retrieval (Repo Map): 500-token AST map vs reading full 4,000-line classes.
719
+ 3. Compact AOM: <500-token interactive tree vs 15,000-token raw HTML DOM.
720
+ 4. KV-Cache Alignment: 95%+ Cache Hit vs <15% without prefix stability.
721
+ """
722
+ pricing = PRICING_TABLE.get(model_key, PRICING_TABLE[DEFAULT_MODEL])
723
+
724
+ estimated_non_hub_input = int(input_tokens * 3.5 + cache_tokens * 0.82)
725
+ estimated_non_hub_cache = int(cache_tokens * 0.18)
726
+ estimated_non_hub_output = output_tokens
727
+
728
+ estimated_non_hub_total = estimated_non_hub_input + estimated_non_hub_cache + estimated_non_hub_output
729
+ actual_hub_total = input_tokens + cache_tokens + output_tokens
730
+
731
+ cost_without_hub = (
732
+ (estimated_non_hub_input / 1_000_000) * pricing["input_per_m"]
733
+ + (estimated_non_hub_cache / 1_000_000) * pricing["cache_per_m"]
734
+ + (estimated_non_hub_output / 1_000_000) * pricing["output_per_m"]
735
+ )
736
+ cost_with_hub = (
737
+ (input_tokens / 1_000_000) * pricing["input_per_m"]
738
+ + (cache_tokens / 1_000_000) * pricing["cache_per_m"]
739
+ + (output_tokens / 1_000_000) * pricing["output_per_m"]
740
+ )
741
+
742
+ tokens_saved = max(0, estimated_non_hub_total - actual_hub_total)
743
+ cost_saved_usd = max(0.0, cost_without_hub - cost_with_hub)
744
+ savings_pct = (tokens_saved / estimated_non_hub_total * 100.0) if estimated_non_hub_total > 0 else 0.0
745
+ cost_savings_pct = (cost_saved_usd / cost_without_hub * 100.0) if cost_without_hub > 0 else 0.0
746
+
747
+ return {
748
+ "model_name": pricing["name"],
749
+ "non_hub_input": estimated_non_hub_input,
750
+ "non_hub_cache": estimated_non_hub_cache,
751
+ "non_hub_total": estimated_non_hub_total,
752
+ "non_hub_cost_usd": round(cost_without_hub, 4),
753
+ "hub_input": input_tokens,
754
+ "hub_cache": cache_tokens,
755
+ "hub_total": actual_hub_total,
756
+ "hub_cost_usd": round(cost_with_hub, 4),
757
+ "tokens_saved": tokens_saved,
758
+ "tokens_saved_shorthand": format_token_shorthand(tokens_saved),
759
+ "savings_pct": round(savings_pct, 1),
760
+ "cost_saved_usd": round(cost_saved_usd, 4),
761
+ "cost_savings_pct": round(cost_savings_pct, 1),
762
+ }
763
+
764
+
765
+ def get_preflight_block_lines(platform: str = "antigravity", model_key: str = DEFAULT_MODEL, lang: str = "vi") -> List[str]:
766
+ """Generates the dynamic markdown lines for the Pre-Flight Confirmation Block."""
767
+ rolling = AntigravityAdapter.get_rolling_usage()
768
+ w5 = rolling["window_5h"]
769
+ w7 = rolling["window_7d"]
770
+
771
+ adapter = get_adapter(platform)
772
+ convs = adapter.get_sessions()
773
+ last_task_line = ""
774
+
775
+ if convs:
776
+ parsed_cur = adapter.parse_transcript(convs[0]["transcript_file"])
777
+ if model_key == DEFAULT_MODEL and parsed_cur.get("detected_model"):
778
+ model_key = parsed_cur["detected_model"]
779
+ savings = calculate_hub_savings(w5["input_tokens"], w5["cache_read_tokens"], w5["output_tokens"], model_key)
780
+ if parsed_cur.get("turns"):
781
+ turns = parsed_cur["turns"]
782
+ target_t = turns[-1]
783
+ if len(turns) > 1 and target_t["steps"] <= 2:
784
+ target_t = turns[-2]
785
+
786
+ p_snippet = target_t["prompt"].replace("\n", " ").strip()
787
+ if len(p_snippet) > 35:
788
+ p_snippet = p_snippet[:32] + "..."
789
+
790
+ t_cost = calculate_cost(target_t["input_tokens"], target_t["cache_read_tokens"], target_t["output_tokens"], model_key)
791
+ tot_t = target_t["input_tokens"] + target_t["cache_read_tokens"] + target_t["output_tokens"]
792
+ vnd_val = int(t_cost["total_cost_usd"] * 25400)
793
+
794
+ if lang == "vi":
795
+ last_task_line = (
796
+ f"> 🎯 **Task vừa xong (Turn #{target_t['turn_index']}: \"{p_snippet}\"):** "
797
+ f"{target_t['steps']} steps | In: {target_t['input_tokens']:,} | Cache: {target_t['cache_read_tokens']:,} | "
798
+ f"Out: {target_t['output_tokens']:,} | 🔥 Tổng: {tot_t:,} token | 💰 Chi phí: ${t_cost['total_cost_usd']:.4f} (~{vnd_val:,} VNĐ)"
799
+ )
800
+ else:
801
+ last_task_line = (
802
+ f"> 🎯 **Completed Turn (Turn #{target_t['turn_index']}: \"{p_snippet}\"):** "
803
+ f"{target_t['steps']} steps | In: {target_t['input_tokens']:,} | Cache: {target_t['cache_read_tokens']:,} | "
804
+ f"Out: {target_t['output_tokens']:,} | 🔥 Total: {tot_t:,} tokens | 💰 Cost: ${t_cost['total_cost_usd']:.4f} (~{vnd_val:,} VND)"
805
+ )
806
+ else:
807
+ savings = calculate_hub_savings(w5["input_tokens"], w5["cache_read_tokens"], w5["output_tokens"], model_key)
808
+
809
+ u5_str = format_token_shorthand(w5['billable_tokens'])
810
+ b5_str = format_token_shorthand(w5['budget'])
811
+ rem5_str = format_token_shorthand(w5['remaining_tokens'])
812
+
813
+ u7_str = format_token_shorthand(w7['billable_tokens'])
814
+ b7_str = format_token_shorthand(w7['budget'])
815
+ rem7_str = format_token_shorthand(w7['remaining_tokens'])
816
+
817
+ if lang == "vi":
818
+ quota_line = (
819
+ f"> 📊 **Hạn mức Token:** "
820
+ f"5 Giờ: Đã dùng {u5_str} / {b5_str} (Còn lại {rem5_str}) ┃ "
821
+ f"7 Ngày: Đã dùng {u7_str} / {b7_str} (Còn lại {rem7_str}) ┃ "
822
+ f"Cache Hit: {w5['cache_hit_rate_pct']}%"
823
+ )
824
+ savings_line = (
825
+ f"> 💡 **Tiết kiệm nhờ Hub:** -{savings['cost_savings_pct']}% chi phí "
826
+ f"(~{savings['tokens_saved_shorthand']} token / ${savings['cost_saved_usd']:.2f} đã tiết kiệm vs. Không dùng Hub)"
827
+ )
828
+ else:
829
+ quota_line = (
830
+ f"> 📊 **Token Quotas:** "
831
+ f"5-Hour: Used {u5_str} / {b5_str} (Remaining {rem5_str}) ┃ "
832
+ f"7-Day: Used {u7_str} / {b7_str} (Remaining {rem7_str}) ┃ "
833
+ f"Cache Hit: {w5['cache_hit_rate_pct']}%"
834
+ )
835
+ savings_line = (
836
+ f"> 💡 **Hub Savings:** -{savings['cost_savings_pct']}% cost "
837
+ f"(~{savings['tokens_saved_shorthand']} tokens / ${savings['cost_saved_usd']:.2f} saved vs. without Hub)"
838
+ )
839
+
840
+ res = [quota_line]
841
+ if last_task_line:
842
+ res.append(last_task_line)
843
+ res.append(savings_line)
844
+ return res
845
+
846
+
847
+ # ---------------------------------------------------------------------------
848
+ # Output Formatters
849
+ # ---------------------------------------------------------------------------
850
+
851
+ def print_rolling_quota(rolling: Dict[str, Any], model_key: str = DEFAULT_MODEL):
852
+ """Outputs a rich terminal card for 5h and 7d rolling window quota."""
853
+ CYAN = "\033[96m"
854
+ GREEN = "\033[92m"
855
+ YELLOW = "\033[93m"
856
+ BOLD = "\033[1m"
857
+ DIM = "\033[2m"
858
+ RESET = "\033[0m"
859
+
860
+ w5 = rolling["window_5h"]
861
+ w7 = rolling["window_7d"]
862
+
863
+ print(f"\n{BOLD}{CYAN}{'='*75}{RESET}")
864
+ print(f"{BOLD}{CYAN} ⏳ ROLLING TOKEN QUOTA & USAGE (5H / 7-DAY){RESET}")
865
+ print(f"{BOLD}{CYAN}{'='*75}{RESET}")
866
+
867
+ print(f" {BOLD}{YELLOW}⏱️ 5-Hour Rolling Window:{RESET}")
868
+ print(f" • Steps Processed : {w5['steps']:,} steps")
869
+ print(f" • Billable Tokens Used: {BOLD}{w5['billable_tokens']:,}{RESET} / {w5['budget']:,} ({w5['pct_used']} % quota used)")
870
+ print(f" • Tokens Remaining : {BOLD}{GREEN}{w5['remaining_tokens']:,}{RESET} ({format_token_shorthand(w5['remaining_tokens'])})")
871
+ print(f" • KV Cache Read Tokens: {w5['cache_read_tokens']:,} {GREEN}(Cache Hit: {w5['cache_hit_rate_pct']} %){RESET}")
872
+ print(f" • Total Model Context : {w5['total_tokens']:,} tokens processed")
873
+ print(f"{DIM}{'-'*75}{RESET}")
874
+
875
+ print(f" {BOLD}{YELLOW}📅 7-Day (1-Week) Rolling Window:{RESET}")
876
+ print(f" • Steps Processed : {w7['steps']:,} steps")
877
+ print(f" • Billable Tokens Used: {BOLD}{w7['billable_tokens']:,}{RESET} / {w7['budget']:,} ({w7['pct_used']} % quota used)")
878
+ print(f" • Tokens Remaining : {BOLD}{GREEN}{w7['remaining_tokens']:,}{RESET} ({format_token_shorthand(w7['remaining_tokens'])})")
879
+ print(f" • KV Cache Read Tokens: {w7['cache_read_tokens']:,} {GREEN}(Cache Hit: {w7['cache_hit_rate_pct']} %){RESET}")
880
+ print(f" • Total Model Context : {w7['total_tokens']:,} tokens processed")
881
+ print(f"{BOLD}{CYAN}{'='*75}{RESET}\n")
882
+
883
+
884
+ def print_savings_card(savings: Dict[str, Any]):
885
+ """Outputs a rich comparison card: With Synapse-Hub vs. Without Hub."""
886
+ CYAN = "\033[96m"
887
+ GREEN = "\033[92m"
888
+ RED = "\033[91m"
889
+ MAGENTA = "\033[95m"
890
+ BOLD = "\033[1m"
891
+ DIM = "\033[2m"
892
+ RESET = "\033[0m"
893
+
894
+ print(f"\n{BOLD}{CYAN}{'='*75}{RESET}")
895
+ print(f"{BOLD}{CYAN} 💡 SYNAPSE-HUB CONTEXT OPTIMIZATION & SAVINGS BENCHMARK{RESET}")
896
+ print(f"{BOLD}{CYAN}{'='*75}{RESET}")
897
+ print(f" {BOLD}Pricing Model:{RESET} {savings['model_name']}")
898
+ print(f"{DIM}{'-'*75}{RESET}")
899
+ print(f" {'Metric':<28} | {'Without Hub (Raw)':<20} | {'With Synapse-Hub':<20}")
900
+ print(f" {'-'*75}")
901
+ print(f" {'Input Tokens (Fresh)':<28} | {RED}{savings['non_hub_input']:>18,d}{RESET} | {GREEN}{savings['hub_input']:>18,d}{RESET}")
902
+ print(f" {'KV Cache Read Tokens':<28} | {DIM}{savings['non_hub_cache']:>18,d}{RESET} | {GREEN}{savings['hub_cache']:>18,d}{RESET}")
903
+ print(f" {'Total Tokens Processed':<28} | {savings['non_hub_total']:>18,d} | {savings['hub_total']:>18,d}")
904
+ print(f" {'Estimated Cost (USD)':<28} | {RED}${savings['non_hub_cost_usd']:>17.4f}{RESET} | {GREEN}${savings['hub_cost_usd']:>17.4f}{RESET}")
905
+ print(f"{DIM}{'-'*75}{RESET}")
906
+ print(f" {BOLD}{MAGENTA}🎯 TOTAL TOKENS SAVED :{RESET} {BOLD}{GREEN}~{savings['tokens_saved']:,} tokens (-{savings['savings_pct']} % reduction){RESET}")
907
+ print(f" {BOLD}{MAGENTA}💰 ESTIMATED COST SAVED:{RESET} {BOLD}{GREEN}${savings['cost_saved_usd']:.4f} USD (-{savings['cost_savings_pct']} % saved){RESET}")
908
+ print(f"{BOLD}{CYAN}{'='*75}{RESET}\n")
909
+
910
+
911
+ def format_table(parsed_data: Dict[str, Any], title: str, model_key: str = DEFAULT_MODEL, show_turns: bool = False, last_task_only: bool = False):
912
+ """Outputs a clean, rich terminal box with formatted metrics."""
913
+ cost_info = calculate_cost(
914
+ parsed_data["input_tokens"],
915
+ parsed_data["cache_read_tokens"],
916
+ parsed_data["output_tokens"],
917
+ model_key,
918
+ )
919
+
920
+ CYAN = "\033[96m"
921
+ GREEN = "\033[92m"
922
+ YELLOW = "\033[93m"
923
+ MAGENTA = "\033[95m"
924
+ BOLD = "\033[1m"
925
+ DIM = "\033[2m"
926
+ RESET = "\033[0m"
927
+
928
+ platform_tag = parsed_data.get("platform", "generic").upper()
929
+
930
+ print(f"\n{BOLD}{CYAN}{'='*75}{RESET}")
931
+ print(f"{BOLD}{CYAN} 📊 TOKEN & COST AUDITOR | Synapse-Hub [{platform_tag}]{RESET}")
932
+ print(f"{BOLD}{CYAN}{'='*75}{RESET}")
933
+ print(f" {BOLD}Context / Session:{RESET} {title}")
934
+ print(f" {BOLD}Model Rate Basis :{RESET} {cost_info['model_name']}")
935
+ print(f" {BOLD}Total Turns / Req:{RESET} {parsed_data['turns_count']} turns | {parsed_data['steps_count']} steps")
936
+ print(f"{DIM}{'-'*75}{RESET}")
937
+
938
+ print(f" {BOLD}📥 Uncached Input Tokens :{RESET} {parsed_data['input_tokens']:>14,d} {DIM}(Fresh Context & Tool Results){RESET}")
939
+ print(f" {BOLD}⚡ KV Cache Read Tokens :{RESET} {parsed_data['cache_read_tokens']:>14,d} {GREEN}(Cache Hit: {parsed_data['cache_hit_rate_pct']} %){RESET}")
940
+ print(f" {BOLD}📤 Output / Think Tokens :{RESET} {parsed_data['output_tokens']:>14,d} {DIM}(Generated Reasoning & Actions){RESET}")
941
+ print(f"{DIM}{'-'*75}{RESET}")
942
+ print(f" {BOLD}{MAGENTA}🔥 TOTAL TOKENS PROCESSED:{RESET} {BOLD}{parsed_data['total_tokens']:>14,d}{RESET}")
943
+ print(f" {BOLD}{GREEN}💰 ESTIMATED TOTAL COST :{RESET} {BOLD}{GREEN}${cost_info['total_cost_usd']:>13.4f} USD{RESET}")
944
+ print(f"{DIM} ↳ Breakdown: Input: ${cost_info['input_cost_usd']:.4f} | Cache: ${cost_info['cache_cost_usd']:.4f} | Output: ${cost_info['output_cost_usd']:.4f}{RESET}")
945
+
946
+ if last_task_only and parsed_data["turns"]:
947
+ last_turn = parsed_data["turns"][-1]
948
+ t_cost = calculate_cost(last_turn["input_tokens"], last_turn["cache_read_tokens"], last_turn["output_tokens"], model_key)
949
+ prompt_snippet = last_turn["prompt"].replace("\n", " ")
950
+ if len(prompt_snippet) > 80:
951
+ prompt_snippet = prompt_snippet[:77] + "..."
952
+
953
+ print(f"\n{BOLD}{YELLOW}🎯 TARGET TASK (LAST TURN #{last_turn['turn_index']}):{RESET}")
954
+ print(f" {BOLD}User Request :{RESET} \"{prompt_snippet}\"")
955
+ print(f" {BOLD}Steps Run :{RESET} {last_turn['steps']} steps")
956
+ print(f" {BOLD}Tokens Used :{RESET} In: {last_turn['input_tokens']:,} | Cache: {last_turn['cache_read_tokens']:,} | Out: {last_turn['output_tokens']:,} | Total: {last_turn['input_tokens'] + last_turn['cache_read_tokens'] + last_turn['output_tokens']:,}")
957
+ print(f" {BOLD}Cost (USD) :{RESET} ${t_cost['total_cost_usd']:.4f}")
958
+ if last_turn["tools_called"]:
959
+ print(f" {BOLD}Tools Used :{RESET} {', '.join(last_turn['tools_called'])}")
960
+
961
+ elif show_turns and parsed_data["turns"]:
962
+ print(f"\n{BOLD}{YELLOW}📋 TURN-BY-TURN BREAKDOWN:{RESET}")
963
+ print(f" {'#':<3} | {'Steps':<5} | {'Input':<10} | {'Cache':<12} | {'Output':<8} | {'Total':<12} | {'Cost ($)':<8} | {'User Prompt':<30}")
964
+ print(f" {'-'*105}")
965
+ for t in parsed_data["turns"]:
966
+ t_tot = t["input_tokens"] + t["cache_read_tokens"] + t["output_tokens"]
967
+ t_cost = calculate_cost(t["input_tokens"], t["cache_read_tokens"], t["output_tokens"], model_key)
968
+ prompt_snippet = t["prompt"].replace("\n", " ")
969
+ if len(prompt_snippet) > 30:
970
+ prompt_snippet = prompt_snippet[:27] + "..."
971
+ print(f" {t['turn_index']:<3} | {t['steps']:<5} | {t['input_tokens']:<10,d} | {t['cache_read_tokens']:<12,d} | {t['output_tokens']:<8,d} | {t_tot:<12,d} | ${t_cost['total_cost_usd']:<7.4f} | {prompt_snippet}")
972
+
973
+ print(f"{BOLD}{CYAN}{'='*75}{RESET}\n")
974
+
975
+
976
+ def export_opentelemetry_genai_spans(parsed: Dict[str, Any], session_id: str = "sess_001") -> Dict[str, Any]:
977
+ """
978
+ Transforms session turns and tool execution steps into an OpenTelemetry GenAI-compliant Trace payload.
979
+ Adheres to OpenTelemetry Semantic Conventions for Generative AI:
980
+ - gen_ai.system (e.g. google, anthropic, openai)
981
+ - gen_ai.request.model (e.g. gemini-2.5-flash, claude-3-7-sonnet)
982
+ - gen_ai.usage.input_tokens / gen_ai.usage.prompt_tokens
983
+ - gen_ai.usage.output_tokens / gen_ai.usage.completion_tokens
984
+ - gen_ai.usage.total_tokens
985
+ - gen_ai.usage.cache_read_tokens
986
+ - tool.name / tool.system
987
+ """
988
+ trace_id = hashlib.md5(session_id.encode('utf-8')).hexdigest()
989
+ platform = parsed.get("platform", "antigravity")
990
+ model = parsed.get("detected_model", "gemini-flash")
991
+
992
+ system_map = {
993
+ "antigravity": "google",
994
+ "cursor": "anthropic",
995
+ "claude": "anthropic",
996
+ "generic": "custom",
997
+ }
998
+ gen_ai_system = system_map.get(platform, "google")
999
+
1000
+ spans = []
1001
+ turns = parsed.get("turns", [])
1002
+ for idx, turn in enumerate(turns, 1):
1003
+ turn_span_id = hashlib.md5(f"{session_id}_turn_{idx}".encode('utf-8')).hexdigest()[:16]
1004
+ turn_start_nano = int(time.time() * 1e9) - (len(turns) - idx) * int(1e9)
1005
+
1006
+ turn_span = {
1007
+ "traceId": trace_id,
1008
+ "spanId": turn_span_id,
1009
+ "parentSpanId": None,
1010
+ "name": f"chat {model}",
1011
+ "kind": "SPAN_KIND_CLIENT",
1012
+ "startTimeUnixNano": turn_start_nano,
1013
+ "status": {"code": 1}, # STATUS_CODE_OK
1014
+ "attributes": [
1015
+ {"key": "gen_ai.system", "value": {"stringValue": gen_ai_system}},
1016
+ {"key": "gen_ai.operation.name", "value": {"stringValue": "chat"}},
1017
+ {"key": "gen_ai.request.model", "value": {"stringValue": model}},
1018
+ {"key": "gen_ai.usage.input_tokens", "value": {"intValue": turn.get("input_tokens", 0)}},
1019
+ {"key": "gen_ai.usage.output_tokens", "value": {"intValue": turn.get("output_tokens", 0)}},
1020
+ {"key": "gen_ai.usage.total_tokens", "value": {"intValue": turn.get("input_tokens", 0) + turn.get("output_tokens", 0)}},
1021
+ {"key": "gen_ai.usage.cache_read_tokens", "value": {"intValue": turn.get("cache_read_tokens", 0)}},
1022
+ {"key": "agent.turn_index", "value": {"intValue": idx}},
1023
+ ]
1024
+ }
1025
+ spans.append(turn_span)
1026
+
1027
+ for t_idx, tool_name in enumerate(turn.get("tools_called", []), 1):
1028
+ tool_span_id = hashlib.md5(f"{session_id}_turn_{idx}_tool_{t_idx}".encode('utf-8')).hexdigest()[:16]
1029
+ tool_span = {
1030
+ "traceId": trace_id,
1031
+ "spanId": tool_span_id,
1032
+ "parentSpanId": turn_span_id,
1033
+ "name": f"execute_tool {tool_name}",
1034
+ "kind": "SPAN_KIND_INTERNAL",
1035
+ "startTimeUnixNano": turn_start_nano + t_idx * int(1e8),
1036
+ "status": {"code": 1},
1037
+ "attributes": [
1038
+ {"key": "tool.name", "value": {"stringValue": tool_name}},
1039
+ {"key": "tool.system", "value": {"stringValue": "synapse-hub"}},
1040
+ {"key": "gen_ai.system", "value": {"stringValue": gen_ai_system}},
1041
+ ]
1042
+ }
1043
+ spans.append(tool_span)
1044
+
1045
+ return {
1046
+ "resourceSpans": [
1047
+ {
1048
+ "resource": {
1049
+ "attributes": [
1050
+ {"key": "service.name", "value": {"stringValue": "synapse-hub-agent"}},
1051
+ {"key": "service.version", "value": {"stringValue": "2.4.0"}},
1052
+ {"key": "gen_ai.system", "value": {"stringValue": gen_ai_system}},
1053
+ {"key": "session.id", "value": {"stringValue": session_id}},
1054
+ ]
1055
+ },
1056
+ "scopeSpans": [
1057
+ {
1058
+ "scope": {
1059
+ "name": "synapse.agent.opentelemetry",
1060
+ "version": "1.0.0"
1061
+ },
1062
+ "spans": spans
1063
+ }
1064
+ ]
1065
+ }
1066
+ ]
1067
+ }
1068
+
1069
+
1070
+ def list_sessions(conversations: List[Dict[str, Any]], limit: int = 15):
1071
+ """Lists recent conversations across platforms."""
1072
+ CYAN = "\033[96m"
1073
+ BOLD = "\033[1m"
1074
+ RESET = "\033[0m"
1075
+
1076
+ print(f"\n{BOLD}{CYAN}{'='*80}{RESET}")
1077
+ print(f"{BOLD}{CYAN} 📂 RECENT SESSIONS (CROSS-PLATFORM AUDIT){RESET}")
1078
+ print(f"{BOLD}{CYAN}{'='*80}{RESET}")
1079
+ print(f" {'#':<4} | {'Platform':<12} | {'Session ID':<34} | {'Title / Summary':<24}")
1080
+ print(f" {'-'*80}")
1081
+
1082
+ for idx, c in enumerate(conversations[:limit], 1):
1083
+ title = c['title'].replace('\n', ' ')
1084
+ if len(title) > 23:
1085
+ title = title[:21] + ".."
1086
+ plat = c.get('platform', 'unknown').capitalize()
1087
+ sid = c['id']
1088
+ if len(sid) > 32:
1089
+ sid = sid[:30] + ".."
1090
+ print(f" {idx:<4} | {plat:<12} | {sid:<34} | {title:<24}")
1091
+ print(f"{BOLD}{CYAN}{'='*80}{RESET}\n")
1092
+
1093
+
1094
+ # ---------------------------------------------------------------------------
1095
+ # Main CLI Entrypoint
1096
+ # ---------------------------------------------------------------------------
1097
+
1098
+ def main():
1099
+ parser = argparse.ArgumentParser(description="Cross-Platform Token & Cost Auditor (Antigravity, Cursor, Claude Code)")
1100
+ parser.add_argument("--platform", "-p", choices=["auto", "antigravity", "cursor", "claude", "all"], default="auto", help="AI platform to audit (default: auto)")
1101
+ parser.add_argument("--session", type=str, help="Specific session ID or index to audit")
1102
+ parser.add_argument("--current", action="store_true", help="Audit the current active conversation")
1103
+ parser.add_argument("--last", action="store_true", help="Audit the previous completed session (default)")
1104
+ parser.add_argument("--last-task", action="store_true", help="Audit specifically the last turn of the target session")
1105
+ parser.add_argument("--turns", action="store_true", help="Show full turn-by-turn breakdown table")
1106
+ parser.add_argument("--rolling", "--budget", action="store_true", dest="rolling", help="Show 5-hour and 7-day rolling window quota & remaining tokens")
1107
+ parser.add_argument("--savings", action="store_true", help="Show Hub vs. Non-Hub token and cost savings comparison")
1108
+ parser.add_argument("--header", action="store_true", help="Print Pre-Flight confirmation block lines")
1109
+ parser.add_argument("--lang", choices=["vi", "en"], default="vi", help="Language for headers and summaries (default: vi)")
1110
+ parser.add_argument("--set-budget-5h", type=str, help="Set 5-hour quota budget (e.g. 10M, 500K)")
1111
+ parser.add_argument("--set-budget-7d", type=str, help="Set 7-day quota budget (e.g. 100M, 50M)")
1112
+ parser.add_argument("--list", action="store_true", help="List recent conversations")
1113
+ parser.add_argument("--pricing", choices=list(PRICING_TABLE.keys()), default=None, help="Model pricing basis (default: auto-detected from transcript)")
1114
+ parser.add_argument("--json", action="store_true", help="Output machine-readable JSON format")
1115
+ parser.add_argument("--otel", "--opentelemetry", action="store_true", dest="otel", help="Export session spans formatted according to OpenTelemetry GenAI Semantic Conventions")
1116
+ parser.add_argument("--file", type=str, help="Direct path to any transcript JSONL file")
1117
+
1118
+ args = parser.parse_args()
1119
+
1120
+ # Update budgets if requested
1121
+ if args.set_budget_5h or args.set_budget_7d:
1122
+ b5 = parse_token_shorthand(args.set_budget_5h) if args.set_budget_5h else None
1123
+ b7 = parse_token_shorthand(args.set_budget_7d) if args.set_budget_7d else None
1124
+ save_budget_config(b5, b7)
1125
+ print(f"✅ Token budget updated: 5h = {format_token_shorthand(b5 or get_budget_config()['budget_5h'])}, 7d = {format_token_shorthand(b7 or get_budget_config()['budget_7d'])}")
1126
+
1127
+ # Direct file audit mode
1128
+ if args.file:
1129
+ plat = detect_platform_from_file(args.file)
1130
+ adapter = get_adapter(plat)
1131
+ parsed = adapter.parse_transcript(args.file)
1132
+ active_pricing = args.pricing or parsed.get("detected_model", DEFAULT_MODEL)
1133
+
1134
+ if args.otel:
1135
+ otel_spans = export_opentelemetry_genai_spans(parsed, os.path.basename(args.file))
1136
+ print(json.dumps(otel_spans, indent=2, ensure_ascii=False))
1137
+ return
1138
+
1139
+ if args.json:
1140
+ parsed["costs"] = calculate_cost(parsed["input_tokens"], parsed["cache_read_tokens"], parsed["output_tokens"], active_pricing)
1141
+ parsed["savings"] = calculate_hub_savings(parsed["input_tokens"], parsed["cache_read_tokens"], parsed["output_tokens"], active_pricing)
1142
+ print(json.dumps(parsed, indent=2, ensure_ascii=False))
1143
+ else:
1144
+ format_table(parsed, f"File: {args.file}", active_pricing, args.turns, args.last_task)
1145
+ return
1146
+
1147
+ # Header generation mode
1148
+ if args.header:
1149
+ plat = "antigravity" if args.platform in ("auto", "all") else args.platform
1150
+ active_pricing = args.pricing or DEFAULT_MODEL
1151
+ lines = get_preflight_block_lines(plat, active_pricing, args.lang)
1152
+ for line in lines:
1153
+ print(line)
1154
+ return
1155
+
1156
+ # Rolling quota mode
1157
+ if args.rolling:
1158
+ rolling = AntigravityAdapter.get_rolling_usage()
1159
+ active_pricing = args.pricing or DEFAULT_MODEL
1160
+ if args.json:
1161
+ print(json.dumps(rolling, indent=2))
1162
+ else:
1163
+ print_rolling_quota(rolling, active_pricing)
1164
+ return
1165
+
1166
+ # Hub savings mode
1167
+ if args.savings:
1168
+ rolling = AntigravityAdapter.get_rolling_usage()
1169
+ w5 = rolling["window_5h"]
1170
+ active_pricing = args.pricing or DEFAULT_MODEL
1171
+ savings = calculate_hub_savings(w5["input_tokens"], w5["cache_read_tokens"], w5["output_tokens"], active_pricing)
1172
+ if args.json:
1173
+ print(json.dumps(savings, indent=2))
1174
+ else:
1175
+ print_savings_card(savings)
1176
+ return
1177
+
1178
+ # Platform resolution
1179
+ if args.platform == "all":
1180
+ conversations = get_all_sessions()
1181
+ elif args.platform == "cursor":
1182
+ conversations = CursorAdapter.get_sessions()
1183
+ elif args.platform == "claude":
1184
+ conversations = ClaudeCodeAdapter.get_sessions()
1185
+ else:
1186
+ # Default 'auto': check Antigravity first, fallback to Cursor if empty
1187
+ conversations = AntigravityAdapter.get_sessions()
1188
+ if not conversations:
1189
+ conversations = CursorAdapter.get_sessions()
1190
+
1191
+ if not conversations:
1192
+ print(f"❌ No AI conversation sessions found for platform '{args.platform}'.")
1193
+ sys.exit(1)
1194
+
1195
+ if args.list:
1196
+ list_sessions(conversations)
1197
+ return
1198
+
1199
+ # Target session selection logic
1200
+ target_conv = None
1201
+ if args.session:
1202
+ if args.session.isdigit():
1203
+ idx = int(args.session) - 1
1204
+ if 0 <= idx < len(conversations):
1205
+ target_conv = conversations[idx]
1206
+ else:
1207
+ print(f"❌ Index {args.session} out of range (1..{len(conversations)})")
1208
+ sys.exit(1)
1209
+ else:
1210
+ for c in conversations:
1211
+ if c["id"].startswith(args.session):
1212
+ target_conv = c
1213
+ break
1214
+ if not target_conv:
1215
+ print(f"❌ Conversation '{args.session}' not found.")
1216
+ sys.exit(1)
1217
+ elif args.current:
1218
+ target_conv = conversations[0]
1219
+ else:
1220
+ # Default: pick previous session if available
1221
+ if len(conversations) > 1 and not args.current:
1222
+ target_conv = conversations[1]
1223
+ else:
1224
+ target_conv = conversations[0]
1225
+
1226
+ # Parse selected session via its respective adapter
1227
+ session_plat = target_conv.get("platform", "antigravity")
1228
+ adapter = get_adapter(session_plat)
1229
+ parsed = adapter.parse_transcript(target_conv["transcript_file"])
1230
+
1231
+ active_pricing = args.pricing
1232
+ if not active_pricing:
1233
+ active_pricing = parsed.get("detected_model") or ("claude-sonnet" if session_plat == "cursor" else DEFAULT_MODEL)
1234
+
1235
+ if args.otel:
1236
+ otel_spans = export_opentelemetry_genai_spans(parsed, target_conv.get("id", "session_001"))
1237
+ print(json.dumps(otel_spans, indent=2, ensure_ascii=False))
1238
+ return
1239
+
1240
+ if args.json:
1241
+ parsed["session_id"] = target_conv["id"]
1242
+ parsed["title"] = target_conv["title"]
1243
+ parsed["costs"] = calculate_cost(parsed["input_tokens"], parsed["cache_read_tokens"], parsed["output_tokens"], active_pricing)
1244
+ parsed["savings"] = calculate_hub_savings(parsed["input_tokens"], parsed["cache_read_tokens"], parsed["output_tokens"], active_pricing)
1245
+ print(json.dumps(parsed, indent=2, ensure_ascii=False))
1246
+ else:
1247
+ format_table(
1248
+ parsed,
1249
+ f"{target_conv['id']} ({target_conv['title']})",
1250
+ active_pricing,
1251
+ args.turns,
1252
+ args.last_task,
1253
+ )
1254
+
1255
+
1256
+ if __name__ == "__main__":
1257
+ main()