atlasagent-cli 0.5.0__py3-none-any.whl

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 (411) hide show
  1. _atlas_version.py +31 -0
  2. agent/__init__.py +6 -0
  3. agent/account_usage.py +326 -0
  4. agent/anthropic_adapter.py +1973 -0
  5. agent/auxiliary_client.py +4169 -0
  6. agent/bedrock_adapter.py +1276 -0
  7. agent/codex_responses_adapter.py +999 -0
  8. agent/context_compressor.py +1479 -0
  9. agent/context_engine.py +206 -0
  10. agent/context_references.py +518 -0
  11. agent/copilot_acp_client.py +646 -0
  12. agent/credential_pool.py +1603 -0
  13. agent/credential_sources.py +418 -0
  14. agent/curator.py +1674 -0
  15. agent/curator_backup.py +693 -0
  16. agent/display.py +1004 -0
  17. agent/error_classifier.py +1036 -0
  18. agent/file_safety.py +111 -0
  19. agent/gemini_cloudcode_adapter.py +903 -0
  20. agent/gemini_native_adapter.py +965 -0
  21. agent/gemini_schema.py +99 -0
  22. agent/google_code_assist.py +452 -0
  23. agent/google_oauth.py +1061 -0
  24. agent/i18n.py +233 -0
  25. agent/image_gen_provider.py +242 -0
  26. agent/image_gen_registry.py +120 -0
  27. agent/image_routing.py +301 -0
  28. agent/insights.py +930 -0
  29. agent/lmstudio_reasoning.py +48 -0
  30. agent/manual_compression_feedback.py +49 -0
  31. agent/memory_manager.py +555 -0
  32. agent/memory_provider.py +279 -0
  33. agent/model_metadata.py +1483 -0
  34. agent/models_dev.py +635 -0
  35. agent/moonshot_schema.py +231 -0
  36. agent/nous_rate_guard.py +325 -0
  37. agent/onboarding.py +193 -0
  38. agent/prompt_builder.py +1186 -0
  39. agent/prompt_caching.py +72 -0
  40. agent/rate_limit_tracker.py +246 -0
  41. agent/redact.py +403 -0
  42. agent/retry_utils.py +57 -0
  43. agent/shell_hooks.py +836 -0
  44. agent/skill_commands.py +501 -0
  45. agent/skill_preprocessing.py +131 -0
  46. agent/skill_utils.py +473 -0
  47. agent/subdirectory_hints.py +224 -0
  48. agent/think_scrubber.py +386 -0
  49. agent/title_generator.py +171 -0
  50. agent/tool_guardrails.py +455 -0
  51. agent/trajectory.py +56 -0
  52. agent/transports/__init__.py +68 -0
  53. agent/transports/anthropic.py +179 -0
  54. agent/transports/base.py +89 -0
  55. agent/transports/bedrock.py +154 -0
  56. agent/transports/chat_completions.py +597 -0
  57. agent/transports/codex.py +246 -0
  58. agent/transports/types.py +162 -0
  59. agent/usage_pricing.py +721 -0
  60. atlas_agent_internal/__init__.py +33 -0
  61. atlas_agent_internal/dispatch.py +122 -0
  62. atlas_agent_internal/init_cmd.py +447 -0
  63. atlas_cli/__init__.py +56 -0
  64. atlas_cli/cli.py +700 -0
  65. atlas_cli/client.py +431 -0
  66. atlas_cli/config.py +132 -0
  67. atlasagent_cli-0.5.0.dist-info/METADATA +334 -0
  68. atlasagent_cli-0.5.0.dist-info/RECORD +411 -0
  69. atlasagent_cli-0.5.0.dist-info/WHEEL +5 -0
  70. atlasagent_cli-0.5.0.dist-info/entry_points.txt +7 -0
  71. atlasagent_cli-0.5.0.dist-info/licenses/LICENSE +21 -0
  72. atlasagent_cli-0.5.0.dist-info/top_level.txt +29 -0
  73. batch_runner.py +1287 -0
  74. billing/__init__.py +48 -0
  75. billing/cost_ceiling.py +231 -0
  76. billing/stripe_client.py +284 -0
  77. billing/tiers.py +168 -0
  78. billing/webhook.py +240 -0
  79. brain/__init__.py +45 -0
  80. brain/claude_mem_adapter.py +217 -0
  81. brain/cross_tenant_drift_scan.py +311 -0
  82. brain/doctrine_library.py +293 -0
  83. brain/mail_routes.py +413 -0
  84. brain/mail_store.py +498 -0
  85. brain/mr64_substrate.py +361 -0
  86. brain/mycelium_adapter.py +309 -0
  87. brain/notification_routes.py +231 -0
  88. brain/notification_store.py +429 -0
  89. brain/resend_client.py +191 -0
  90. brain/sangha_identifier.py +274 -0
  91. brain/server.py +3307 -0
  92. brain/test_tenant_reaper.py +214 -0
  93. cli.py +12574 -0
  94. cortex/__init__.py +68 -0
  95. cortex/anti_catastrophic_reframe.py +246 -0
  96. cortex/apex_design_drift.py +94 -0
  97. cortex/architectural_tranche_check.py +310 -0
  98. cortex/architecture_review_gate.py +250 -0
  99. cortex/atomic_decompose.py +368 -0
  100. cortex/background_subagent_default.py +119 -0
  101. cortex/brief_diagnose_widen.py +216 -0
  102. cortex/bushido_pre_claim.py +396 -0
  103. cortex/canonical_touch_detector.py +188 -0
  104. cortex/credentials_search_gate.py +270 -0
  105. cortex/critical_facts_stale_warn.py +138 -0
  106. cortex/destructive_command_guard.py +319 -0
  107. cortex/diagnostic_before_destructive.py +245 -0
  108. cortex/dispatch_dedup.py +246 -0
  109. cortex/dispatch_multi_component_check.py +283 -0
  110. cortex/dispatch_or_die.py +338 -0
  111. cortex/drift_detector_session.py +391 -0
  112. cortex/e2e_evidence_pre_ship.py +320 -0
  113. cortex/empirical_verify_gate.py +878 -0
  114. cortex/evidence_ratio_gate.py +201 -0
  115. cortex/factual_claim_audit.py +316 -0
  116. cortex/hook_block_retry_gate.py +236 -0
  117. cortex/idle_work_queue_aware.py +382 -0
  118. cortex/memory_citation_empirical_verify.py +154 -0
  119. cortex/onboarding_ritual_gate.py +137 -0
  120. cortex/operator_action_intent_detector.py +343 -0
  121. cortex/operator_dispatch_directive.py +445 -0
  122. cortex/operator_tranche_gate.py +274 -0
  123. cortex/output_style_audit.py +842 -0
  124. cortex/per_tenant_meta_judge.py +343 -0
  125. cortex/positive_negative_check.py +155 -0
  126. cortex/post_session_memory_update.py +97 -0
  127. cortex/post_write_file_verify.py +330 -0
  128. cortex/project_files_freshness.py +428 -0
  129. cortex/rq_reco_not_directive.py +290 -0
  130. cortex/sqlite_state_store.py +251 -0
  131. cortex/state_store.py +214 -0
  132. cortex/tenant_context.py +270 -0
  133. cortex/tenant_identity.py +509 -0
  134. cortex/tenant_memory_loader.py +98 -0
  135. cortex/tranche_allow.py +275 -0
  136. hermes_cli/__init__.py +47 -0
  137. hermes_cli/_parser.py +376 -0
  138. hermes_cli/auth.py +5341 -0
  139. hermes_cli/auth_commands.py +718 -0
  140. hermes_cli/azure_detect.py +300 -0
  141. hermes_cli/backup.py +939 -0
  142. hermes_cli/banner.py +635 -0
  143. hermes_cli/browser_connect.py +138 -0
  144. hermes_cli/callbacks.py +242 -0
  145. hermes_cli/checkpoints.py +244 -0
  146. hermes_cli/claw.py +803 -0
  147. hermes_cli/cli_output.py +78 -0
  148. hermes_cli/clipboard.py +481 -0
  149. hermes_cli/codex_models.py +177 -0
  150. hermes_cli/colors.py +38 -0
  151. hermes_cli/commands.py +1713 -0
  152. hermes_cli/completion.py +315 -0
  153. hermes_cli/config.py +5060 -0
  154. hermes_cli/copilot_auth.py +392 -0
  155. hermes_cli/cron.py +307 -0
  156. hermes_cli/curator.py +589 -0
  157. hermes_cli/curses_ui.py +472 -0
  158. hermes_cli/debug.py +746 -0
  159. hermes_cli/default_soul.py +11 -0
  160. hermes_cli/dingtalk_auth.py +293 -0
  161. hermes_cli/doctor.py +1530 -0
  162. hermes_cli/dump.py +325 -0
  163. hermes_cli/env_loader.py +175 -0
  164. hermes_cli/fallback_cmd.py +361 -0
  165. hermes_cli/goals.py +535 -0
  166. hermes_cli/hooks.py +385 -0
  167. hermes_cli/kanban.py +2073 -0
  168. hermes_cli/kanban_db.py +4386 -0
  169. hermes_cli/kanban_diagnostics.py +649 -0
  170. hermes_cli/logs.py +390 -0
  171. hermes_cli/main.py +10932 -0
  172. hermes_cli/mcp_config.py +781 -0
  173. hermes_cli/memory_setup.py +457 -0
  174. hermes_cli/model_catalog.py +329 -0
  175. hermes_cli/model_normalize.py +473 -0
  176. hermes_cli/model_switch.py +1767 -0
  177. hermes_cli/models.py +3569 -0
  178. hermes_cli/nous_subscription.py +799 -0
  179. hermes_cli/oneshot.py +331 -0
  180. hermes_cli/pairing.py +115 -0
  181. hermes_cli/platforms.py +83 -0
  182. hermes_cli/plugins.py +1366 -0
  183. hermes_cli/plugins_cmd.py +1544 -0
  184. hermes_cli/profiles.py +1293 -0
  185. hermes_cli/providers.py +701 -0
  186. hermes_cli/pty_bridge.py +234 -0
  187. hermes_cli/relaunch.py +149 -0
  188. hermes_cli/runtime_provider.py +1344 -0
  189. hermes_cli/setup.py +3449 -0
  190. hermes_cli/skills_config.py +177 -0
  191. hermes_cli/skills_hub.py +1594 -0
  192. hermes_cli/skin_engine.py +892 -0
  193. hermes_cli/slack_cli.py +153 -0
  194. hermes_cli/status.py +551 -0
  195. hermes_cli/timeouts.py +82 -0
  196. hermes_cli/tips.py +487 -0
  197. hermes_cli/tools_config.py +2556 -0
  198. hermes_cli/uninstall.py +481 -0
  199. hermes_cli/vercel_auth.py +70 -0
  200. hermes_cli/voice.py +846 -0
  201. hermes_cli/web_server.py +4234 -0
  202. hermes_cli/webhook.py +274 -0
  203. hermes_constants.py +345 -0
  204. hermes_logging.py +389 -0
  205. hermes_state.py +2669 -0
  206. hermes_time.py +104 -0
  207. model_tools.py +847 -0
  208. orchestrator/__init__.py +67 -0
  209. orchestrator/bootstrap_wildcard.py +285 -0
  210. orchestrator/cloud_init.py +306 -0
  211. orchestrator/cloudflare.py +619 -0
  212. orchestrator/pool_manager.py +1005 -0
  213. orchestrator/provisioner.py +410 -0
  214. orchestrator/vm_destroyer.py +37 -0
  215. orchestrator/vm_lifecycle.py +671 -0
  216. plugins/__init__.py +1 -0
  217. plugins/atlas-architecture-review/__init__.py +23 -0
  218. plugins/atlas-atomic-decompose/__init__.py +23 -0
  219. plugins/atlas-bushido-pre-claim/__init__.py +27 -0
  220. plugins/atlas-dispatch-or-die/__init__.py +27 -0
  221. plugins/atlas-empirical-verify/__init__.py +36 -0
  222. plugins/atlas-hook-block-retry/__init__.py +34 -0
  223. plugins/atlas-idle-work-queue/__init__.py +22 -0
  224. plugins/context_engine/__init__.py +219 -0
  225. plugins/disk-cleanup/__init__.py +316 -0
  226. plugins/disk-cleanup/disk_cleanup.py +496 -0
  227. plugins/example-dashboard/dashboard/plugin_api.py +14 -0
  228. plugins/google_meet/__init__.py +103 -0
  229. plugins/google_meet/audio_bridge.py +244 -0
  230. plugins/google_meet/cli.py +478 -0
  231. plugins/google_meet/meet_bot.py +852 -0
  232. plugins/google_meet/node/__init__.py +54 -0
  233. plugins/google_meet/node/cli.py +125 -0
  234. plugins/google_meet/node/client.py +107 -0
  235. plugins/google_meet/node/protocol.py +124 -0
  236. plugins/google_meet/node/registry.py +112 -0
  237. plugins/google_meet/node/server.py +200 -0
  238. plugins/google_meet/process_manager.py +326 -0
  239. plugins/google_meet/realtime/__init__.py +10 -0
  240. plugins/google_meet/realtime/openai_client.py +332 -0
  241. plugins/google_meet/tools.py +348 -0
  242. plugins/hermes-achievements/dashboard/plugin_api.py +1061 -0
  243. plugins/hermes-achievements/tests/test_achievement_engine.py +156 -0
  244. plugins/image_gen/openai/__init__.py +303 -0
  245. plugins/image_gen/openai-codex/__init__.py +378 -0
  246. plugins/image_gen/xai/__init__.py +314 -0
  247. plugins/kanban/dashboard/plugin_api.py +1536 -0
  248. plugins/memory/__init__.py +407 -0
  249. plugins/memory/byterover/__init__.py +383 -0
  250. plugins/memory/hindsight/__init__.py +1747 -0
  251. plugins/memory/holographic/__init__.py +408 -0
  252. plugins/memory/holographic/holographic.py +203 -0
  253. plugins/memory/holographic/retrieval.py +593 -0
  254. plugins/memory/holographic/store.py +574 -0
  255. plugins/memory/honcho/__init__.py +1328 -0
  256. plugins/memory/honcho/cli.py +1451 -0
  257. plugins/memory/honcho/client.py +766 -0
  258. plugins/memory/honcho/session.py +1255 -0
  259. plugins/memory/mem0/__init__.py +373 -0
  260. plugins/memory/openviking/__init__.py +937 -0
  261. plugins/memory/retaindb/__init__.py +766 -0
  262. plugins/memory/supermemory/__init__.py +791 -0
  263. plugins/model-providers/ai-gateway/__init__.py +43 -0
  264. plugins/model-providers/alibaba/__init__.py +13 -0
  265. plugins/model-providers/alibaba-coding-plan/__init__.py +21 -0
  266. plugins/model-providers/anthropic/__init__.py +52 -0
  267. plugins/model-providers/arcee/__init__.py +13 -0
  268. plugins/model-providers/azure-foundry/__init__.py +21 -0
  269. plugins/model-providers/bedrock/__init__.py +29 -0
  270. plugins/model-providers/copilot/__init__.py +58 -0
  271. plugins/model-providers/copilot-acp/__init__.py +34 -0
  272. plugins/model-providers/custom/__init__.py +68 -0
  273. plugins/model-providers/deepseek/__init__.py +20 -0
  274. plugins/model-providers/gemini/__init__.py +72 -0
  275. plugins/model-providers/gmi/__init__.py +26 -0
  276. plugins/model-providers/huggingface/__init__.py +20 -0
  277. plugins/model-providers/kilocode/__init__.py +14 -0
  278. plugins/model-providers/kimi-coding/__init__.py +71 -0
  279. plugins/model-providers/minimax/__init__.py +45 -0
  280. plugins/model-providers/nous/__init__.py +53 -0
  281. plugins/model-providers/nvidia/__init__.py +21 -0
  282. plugins/model-providers/ollama-cloud/__init__.py +14 -0
  283. plugins/model-providers/openai-codex/__init__.py +15 -0
  284. plugins/model-providers/opencode-zen/__init__.py +30 -0
  285. plugins/model-providers/openrouter/__init__.py +86 -0
  286. plugins/model-providers/qwen-oauth/__init__.py +82 -0
  287. plugins/model-providers/stepfun/__init__.py +14 -0
  288. plugins/model-providers/xai/__init__.py +15 -0
  289. plugins/model-providers/xiaomi/__init__.py +13 -0
  290. plugins/model-providers/zai/__init__.py +21 -0
  291. plugins/observability/langfuse/__init__.py +874 -0
  292. plugins/platforms/google_chat/__init__.py +3 -0
  293. plugins/platforms/google_chat/adapter.py +3085 -0
  294. plugins/platforms/google_chat/oauth.py +638 -0
  295. plugins/platforms/irc/__init__.py +3 -0
  296. plugins/platforms/irc/adapter.py +745 -0
  297. plugins/platforms/teams/__init__.py +3 -0
  298. plugins/platforms/teams/adapter.py +764 -0
  299. plugins/spotify/__init__.py +66 -0
  300. plugins/spotify/client.py +435 -0
  301. plugins/spotify/tools.py +454 -0
  302. providers/__init__.py +191 -0
  303. providers/base.py +165 -0
  304. rl_cli.py +446 -0
  305. run_agent.py +14648 -0
  306. satellite/__init__.py +40 -0
  307. satellite/client.py +284 -0
  308. server/__init__.py +10 -0
  309. server/app.py +148 -0
  310. tenants/__init__.py +33 -0
  311. tenants/auth.py +394 -0
  312. tenants/store.py +1354 -0
  313. tools/__init__.py +25 -0
  314. tools/ansi_strip.py +44 -0
  315. tools/approval.py +1258 -0
  316. tools/binary_extensions.py +42 -0
  317. tools/browser_camofox.py +603 -0
  318. tools/browser_camofox_state.py +47 -0
  319. tools/browser_cdp_tool.py +563 -0
  320. tools/browser_dialog_tool.py +148 -0
  321. tools/browser_providers/__init__.py +10 -0
  322. tools/browser_providers/base.py +59 -0
  323. tools/browser_providers/browser_use.py +215 -0
  324. tools/browser_providers/browserbase.py +217 -0
  325. tools/browser_providers/firecrawl.py +107 -0
  326. tools/browser_supervisor.py +1366 -0
  327. tools/browser_tool.py +3511 -0
  328. tools/budget_config.py +52 -0
  329. tools/checkpoint_manager.py +1638 -0
  330. tools/clarify_tool.py +141 -0
  331. tools/code_execution_tool.py +1621 -0
  332. tools/credential_files.py +436 -0
  333. tools/cronjob_tools.py +662 -0
  334. tools/debug_helpers.py +105 -0
  335. tools/delegate_tool.py +2597 -0
  336. tools/discord_tool.py +959 -0
  337. tools/env_passthrough.py +145 -0
  338. tools/environments/__init__.py +14 -0
  339. tools/environments/base.py +806 -0
  340. tools/environments/daytona.py +259 -0
  341. tools/environments/docker.py +645 -0
  342. tools/environments/file_sync.py +399 -0
  343. tools/environments/local.py +531 -0
  344. tools/environments/managed_modal.py +282 -0
  345. tools/environments/modal.py +460 -0
  346. tools/environments/modal_utils.py +199 -0
  347. tools/environments/singularity.py +262 -0
  348. tools/environments/ssh.py +295 -0
  349. tools/environments/vercel_sandbox.py +638 -0
  350. tools/feishu_doc_tool.py +131 -0
  351. tools/feishu_drive_tool.py +429 -0
  352. tools/file_operations.py +1561 -0
  353. tools/file_state.py +332 -0
  354. tools/file_tools.py +1143 -0
  355. tools/fuzzy_match.py +704 -0
  356. tools/homeassistant_tool.py +513 -0
  357. tools/image_generation_tool.py +1023 -0
  358. tools/interrupt.py +98 -0
  359. tools/kanban_tools.py +871 -0
  360. tools/managed_tool_gateway.py +167 -0
  361. tools/mcp_oauth.py +632 -0
  362. tools/mcp_oauth_manager.py +607 -0
  363. tools/mcp_tool.py +3403 -0
  364. tools/memory_tool.py +586 -0
  365. tools/mixture_of_agents_tool.py +541 -0
  366. tools/neutts_synth.py +104 -0
  367. tools/openrouter_client.py +33 -0
  368. tools/osv_check.py +155 -0
  369. tools/patch_parser.py +592 -0
  370. tools/path_security.py +43 -0
  371. tools/process_registry.py +1434 -0
  372. tools/registry.py +537 -0
  373. tools/rl_training_tool.py +1396 -0
  374. tools/schema_sanitizer.py +370 -0
  375. tools/send_message_tool.py +1788 -0
  376. tools/session_search_tool.py +605 -0
  377. tools/skill_manager_tool.py +931 -0
  378. tools/skill_provenance.py +78 -0
  379. tools/skill_usage.py +609 -0
  380. tools/skills_guard.py +932 -0
  381. tools/skills_hub.py +3229 -0
  382. tools/skills_sync.py +431 -0
  383. tools/skills_tool.py +1533 -0
  384. tools/slash_confirm.py +162 -0
  385. tools/terminal_tool.py +2342 -0
  386. tools/tirith_security.py +691 -0
  387. tools/todo_tool.py +277 -0
  388. tools/tool_backend_helpers.py +144 -0
  389. tools/tool_output_limits.py +92 -0
  390. tools/tool_result_storage.py +226 -0
  391. tools/transcription_tools.py +911 -0
  392. tools/tts_tool.py +2185 -0
  393. tools/url_safety.py +327 -0
  394. tools/vision_tools.py +1168 -0
  395. tools/voice_mode.py +1017 -0
  396. tools/web_providers/__init__.py +6 -0
  397. tools/web_providers/base.py +89 -0
  398. tools/web_providers/searxng.py +131 -0
  399. tools/web_tools.py +2223 -0
  400. tools/website_policy.py +282 -0
  401. tools/xai_http.py +12 -0
  402. tools/yuanbao_tools.py +736 -0
  403. toolset_distributions.py +364 -0
  404. toolsets.py +839 -0
  405. trajectory_compressor.py +1508 -0
  406. utils.py +297 -0
  407. vm_runtime/__init__.py +24 -0
  408. vm_runtime/agent_client.py +589 -0
  409. vm_runtime/cortex_middleware.py +1400 -0
  410. vm_runtime/memory_manager.py +427 -0
  411. vm_runtime/server.py +867 -0
_atlas_version.py ADDED
@@ -0,0 +1,31 @@
1
+ """Single source of truth for the Atlas product line version.
2
+
3
+ Imported by every sub-package that surfaces a version field (brain,
4
+ vm_runtime, atlas_cli, atlas_agent_internal, orchestrator). Bump
5
+ :data:`ATLAS_VERSION` here
6
+ and every ``/healthz``, FastAPI ``version=...``, and CLI ``--version``
7
+ flows through automatically.
8
+
9
+ The pyproject.toml ``version`` field tracks the major.minor.patch part
10
+ only (PEP 440 strict, no local-pre suffixes). The ``-sprintN`` suffix
11
+ lives in this module because it carries pre-release semantics that
12
+ PyPI does not need to see, but runtime surfaces and operators do.
13
+
14
+ Ghost-strike audit 2026-05-13 crack M1 — replaces the prior fan of
15
+ divergent string literals (``BRAIN_VERSION = "0.4.0-phase4a"``,
16
+ ``pyproject.version = "0.14.0"``) that contradicted the canonical
17
+ ``v0.5.0-sprint2-validated`` project-state.
18
+
19
+ This file lives at ``atlas-core/atlas/_atlas_version.py`` so that the
20
+ existing bare-name import path (``import _atlas_version``) works under
21
+ the current pytest rootdir convention (``atlas-core/atlas/`` on
22
+ ``sys.path``). The leading underscore signals private/internal — the
23
+ public re-export is via each sub-package's own version constant.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ __all__ = ["ATLAS_VERSION", "__version__"]
28
+
29
+ # Bump this string to bump the whole Atlas product line.
30
+ ATLAS_VERSION = "0.5.0-sprint2"
31
+ __version__ = ATLAS_VERSION
agent/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Agent internals -- extracted modules from run_agent.py.
2
+
3
+ These modules contain pure utility functions and self-contained classes
4
+ that were previously embedded in the 3,600-line run_agent.py. Extracting
5
+ them makes run_agent.py focused on the AIAgent orchestrator class.
6
+ """
agent/account_usage.py ADDED
@@ -0,0 +1,326 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Optional
6
+
7
+ import httpx
8
+
9
+ from agent.anthropic_adapter import _is_oauth_token, resolve_anthropic_token
10
+ from hermes_cli.auth import _read_codex_tokens, resolve_codex_runtime_credentials
11
+ from hermes_cli.runtime_provider import resolve_runtime_provider
12
+
13
+
14
+ def _utc_now() -> datetime:
15
+ return datetime.now(timezone.utc)
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class AccountUsageWindow:
20
+ label: str
21
+ used_percent: Optional[float] = None
22
+ reset_at: Optional[datetime] = None
23
+ detail: Optional[str] = None
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class AccountUsageSnapshot:
28
+ provider: str
29
+ source: str
30
+ fetched_at: datetime
31
+ title: str = "Account limits"
32
+ plan: Optional[str] = None
33
+ windows: tuple[AccountUsageWindow, ...] = ()
34
+ details: tuple[str, ...] = ()
35
+ unavailable_reason: Optional[str] = None
36
+
37
+ @property
38
+ def available(self) -> bool:
39
+ return bool(self.windows or self.details) and not self.unavailable_reason
40
+
41
+
42
+ def _title_case_slug(value: Optional[str]) -> Optional[str]:
43
+ cleaned = str(value or "").strip()
44
+ if not cleaned:
45
+ return None
46
+ return cleaned.replace("_", " ").replace("-", " ").title()
47
+
48
+
49
+ def _parse_dt(value: Any) -> Optional[datetime]:
50
+ if value in (None, ""):
51
+ return None
52
+ if isinstance(value, (int, float)):
53
+ return datetime.fromtimestamp(float(value), tz=timezone.utc)
54
+ if isinstance(value, str):
55
+ text = value.strip()
56
+ if not text:
57
+ return None
58
+ if text.endswith("Z"):
59
+ text = text[:-1] + "+00:00"
60
+ try:
61
+ dt = datetime.fromisoformat(text)
62
+ return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
63
+ except ValueError:
64
+ return None
65
+ return None
66
+
67
+
68
+ def _format_reset(dt: Optional[datetime]) -> str:
69
+ if not dt:
70
+ return "unknown"
71
+ local_dt = dt.astimezone()
72
+ delta = dt - _utc_now()
73
+ total_seconds = int(delta.total_seconds())
74
+ if total_seconds <= 0:
75
+ return f"now ({local_dt.strftime('%Y-%m-%d %H:%M %Z')})"
76
+ hours, rem = divmod(total_seconds, 3600)
77
+ minutes = rem // 60
78
+ if hours >= 24:
79
+ days, hours = divmod(hours, 24)
80
+ rel = f"in {days}d {hours}h"
81
+ elif hours > 0:
82
+ rel = f"in {hours}h {minutes}m"
83
+ else:
84
+ rel = f"in {minutes}m"
85
+ return f"{rel} ({local_dt.strftime('%Y-%m-%d %H:%M %Z')})"
86
+
87
+
88
+ def render_account_usage_lines(snapshot: Optional[AccountUsageSnapshot], *, markdown: bool = False) -> list[str]:
89
+ if not snapshot:
90
+ return []
91
+ header = f"📈 {'**' if markdown else ''}{snapshot.title}{'**' if markdown else ''}"
92
+ lines = [header]
93
+ if snapshot.plan:
94
+ lines.append(f"Provider: {snapshot.provider} ({snapshot.plan})")
95
+ else:
96
+ lines.append(f"Provider: {snapshot.provider}")
97
+ for window in snapshot.windows:
98
+ if window.used_percent is None:
99
+ base = f"{window.label}: unavailable"
100
+ else:
101
+ remaining = max(0, round(100 - float(window.used_percent)))
102
+ used = max(0, round(float(window.used_percent)))
103
+ base = f"{window.label}: {remaining}% remaining ({used}% used)"
104
+ if window.reset_at:
105
+ base += f" • resets {_format_reset(window.reset_at)}"
106
+ elif window.detail:
107
+ base += f" • {window.detail}"
108
+ lines.append(base)
109
+ for detail in snapshot.details:
110
+ lines.append(detail)
111
+ if snapshot.unavailable_reason:
112
+ lines.append(f"Unavailable: {snapshot.unavailable_reason}")
113
+ return lines
114
+
115
+
116
+ def _resolve_codex_usage_url(base_url: str) -> str:
117
+ normalized = (base_url or "").strip().rstrip("/")
118
+ if not normalized:
119
+ normalized = "https://chatgpt.com/backend-api/codex"
120
+ if normalized.endswith("/codex"):
121
+ normalized = normalized[: -len("/codex")]
122
+ if "/backend-api" in normalized:
123
+ return normalized + "/wham/usage"
124
+ return normalized + "/api/codex/usage"
125
+
126
+
127
+ def _fetch_codex_account_usage() -> Optional[AccountUsageSnapshot]:
128
+ creds = resolve_codex_runtime_credentials(refresh_if_expiring=True)
129
+ token_data = _read_codex_tokens()
130
+ tokens = token_data.get("tokens") or {}
131
+ account_id = str(tokens.get("account_id", "") or "").strip() or None
132
+ headers = {
133
+ "Authorization": f"Bearer {creds['api_key']}",
134
+ "Accept": "application/json",
135
+ "User-Agent": "codex-cli",
136
+ }
137
+ if account_id:
138
+ headers["ChatGPT-Account-Id"] = account_id
139
+ with httpx.Client(timeout=15.0) as client:
140
+ response = client.get(_resolve_codex_usage_url(creds.get("base_url", "")), headers=headers)
141
+ response.raise_for_status()
142
+ payload = response.json() or {}
143
+ rate_limit = payload.get("rate_limit") or {}
144
+ windows: list[AccountUsageWindow] = []
145
+ for key, label in (("primary_window", "Session"), ("secondary_window", "Weekly")):
146
+ window = rate_limit.get(key) or {}
147
+ used = window.get("used_percent")
148
+ if used is None:
149
+ continue
150
+ windows.append(
151
+ AccountUsageWindow(
152
+ label=label,
153
+ used_percent=float(used),
154
+ reset_at=_parse_dt(window.get("reset_at")),
155
+ )
156
+ )
157
+ details: list[str] = []
158
+ credits = payload.get("credits") or {}
159
+ if credits.get("has_credits"):
160
+ balance = credits.get("balance")
161
+ if isinstance(balance, (int, float)):
162
+ details.append(f"Credits balance: ${float(balance):.2f}")
163
+ elif credits.get("unlimited"):
164
+ details.append("Credits balance: unlimited")
165
+ return AccountUsageSnapshot(
166
+ provider="openai-codex",
167
+ source="usage_api",
168
+ fetched_at=_utc_now(),
169
+ plan=_title_case_slug(payload.get("plan_type")),
170
+ windows=tuple(windows),
171
+ details=tuple(details),
172
+ )
173
+
174
+
175
+ def _fetch_anthropic_account_usage() -> Optional[AccountUsageSnapshot]:
176
+ token = (resolve_anthropic_token() or "").strip()
177
+ if not token:
178
+ return None
179
+ if not _is_oauth_token(token):
180
+ return AccountUsageSnapshot(
181
+ provider="anthropic",
182
+ source="oauth_usage_api",
183
+ fetched_at=_utc_now(),
184
+ unavailable_reason="Anthropic account limits are only available for OAuth-backed Claude accounts.",
185
+ )
186
+ headers = {
187
+ "Authorization": f"Bearer {token}",
188
+ "Accept": "application/json",
189
+ "Content-Type": "application/json",
190
+ "anthropic-beta": "oauth-2025-04-20",
191
+ "User-Agent": "claude-code/2.1.0",
192
+ }
193
+ with httpx.Client(timeout=15.0) as client:
194
+ response = client.get("https://api.anthropic.com/api/oauth/usage", headers=headers)
195
+ response.raise_for_status()
196
+ payload = response.json() or {}
197
+ windows: list[AccountUsageWindow] = []
198
+ mapping = (
199
+ ("five_hour", "Current session"),
200
+ ("seven_day", "Current week"),
201
+ ("seven_day_opus", "Opus week"),
202
+ ("seven_day_sonnet", "Sonnet week"),
203
+ )
204
+ for key, label in mapping:
205
+ window = payload.get(key) or {}
206
+ util = window.get("utilization")
207
+ if util is None:
208
+ continue
209
+ used = float(util) * 100 if float(util) <= 1 else float(util)
210
+ windows.append(
211
+ AccountUsageWindow(
212
+ label=label,
213
+ used_percent=used,
214
+ reset_at=_parse_dt(window.get("resets_at")),
215
+ )
216
+ )
217
+ details: list[str] = []
218
+ extra = payload.get("extra_usage") or {}
219
+ if extra.get("is_enabled"):
220
+ used_credits = extra.get("used_credits")
221
+ monthly_limit = extra.get("monthly_limit")
222
+ currency = extra.get("currency") or "USD"
223
+ if isinstance(used_credits, (int, float)) and isinstance(monthly_limit, (int, float)):
224
+ details.append(
225
+ f"Extra usage: {used_credits:.2f} / {monthly_limit:.2f} {currency}"
226
+ )
227
+ return AccountUsageSnapshot(
228
+ provider="anthropic",
229
+ source="oauth_usage_api",
230
+ fetched_at=_utc_now(),
231
+ windows=tuple(windows),
232
+ details=tuple(details),
233
+ )
234
+
235
+
236
+ def _fetch_openrouter_account_usage(base_url: Optional[str], api_key: Optional[str]) -> Optional[AccountUsageSnapshot]:
237
+ runtime = resolve_runtime_provider(
238
+ requested="openrouter",
239
+ explicit_base_url=base_url,
240
+ explicit_api_key=api_key,
241
+ )
242
+ token = str(runtime.get("api_key", "") or "").strip()
243
+ if not token:
244
+ return None
245
+ normalized = str(runtime.get("base_url", "") or "").rstrip("/")
246
+ credits_url = f"{normalized}/credits"
247
+ key_url = f"{normalized}/key"
248
+ headers = {
249
+ "Authorization": f"Bearer {token}",
250
+ "Accept": "application/json",
251
+ }
252
+ with httpx.Client(timeout=10.0) as client:
253
+ credits_resp = client.get(credits_url, headers=headers)
254
+ credits_resp.raise_for_status()
255
+ credits = (credits_resp.json() or {}).get("data") or {}
256
+ try:
257
+ key_resp = client.get(key_url, headers=headers)
258
+ key_resp.raise_for_status()
259
+ key_data = (key_resp.json() or {}).get("data") or {}
260
+ except Exception:
261
+ key_data = {}
262
+ total_credits = float(credits.get("total_credits") or 0.0)
263
+ total_usage = float(credits.get("total_usage") or 0.0)
264
+ details = [f"Credits balance: ${max(0.0, total_credits - total_usage):.2f}"]
265
+ windows: list[AccountUsageWindow] = []
266
+ limit = key_data.get("limit")
267
+ limit_remaining = key_data.get("limit_remaining")
268
+ limit_reset = str(key_data.get("limit_reset") or "").strip()
269
+ usage = key_data.get("usage")
270
+ if (
271
+ isinstance(limit, (int, float))
272
+ and float(limit) > 0
273
+ and isinstance(limit_remaining, (int, float))
274
+ and 0 <= float(limit_remaining) <= float(limit)
275
+ ):
276
+ limit_value = float(limit)
277
+ remaining_value = float(limit_remaining)
278
+ used_percent = ((limit_value - remaining_value) / limit_value) * 100
279
+ detail_parts = [f"${remaining_value:.2f} of ${limit_value:.2f} remaining"]
280
+ if limit_reset:
281
+ detail_parts.append(f"resets {limit_reset}")
282
+ windows.append(
283
+ AccountUsageWindow(
284
+ label="API key quota",
285
+ used_percent=used_percent,
286
+ detail=" • ".join(detail_parts),
287
+ )
288
+ )
289
+ if isinstance(usage, (int, float)):
290
+ usage_parts = [f"API key usage: ${float(usage):.2f} total"]
291
+ for value, label in (
292
+ (key_data.get("usage_daily"), "today"),
293
+ (key_data.get("usage_weekly"), "this week"),
294
+ (key_data.get("usage_monthly"), "this month"),
295
+ ):
296
+ if isinstance(value, (int, float)) and float(value) > 0:
297
+ usage_parts.append(f"${float(value):.2f} {label}")
298
+ details.append(" • ".join(usage_parts))
299
+ return AccountUsageSnapshot(
300
+ provider="openrouter",
301
+ source="credits_api",
302
+ fetched_at=_utc_now(),
303
+ windows=tuple(windows),
304
+ details=tuple(details),
305
+ )
306
+
307
+
308
+ def fetch_account_usage(
309
+ provider: Optional[str],
310
+ *,
311
+ base_url: Optional[str] = None,
312
+ api_key: Optional[str] = None,
313
+ ) -> Optional[AccountUsageSnapshot]:
314
+ normalized = str(provider or "").strip().lower()
315
+ if normalized in {"", "auto", "custom"}:
316
+ return None
317
+ try:
318
+ if normalized == "openai-codex":
319
+ return _fetch_codex_account_usage()
320
+ if normalized == "anthropic":
321
+ return _fetch_anthropic_account_usage()
322
+ if normalized == "openrouter":
323
+ return _fetch_openrouter_account_usage(base_url, api_key)
324
+ except Exception:
325
+ return None
326
+ return None