code-muse 0.0.1__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 (394) hide show
  1. code_muse/__init__.py +26 -0
  2. code_muse/__main__.py +10 -0
  3. code_muse/agents/__init__.py +31 -0
  4. code_muse/agents/_builder.py +214 -0
  5. code_muse/agents/_compaction.py +506 -0
  6. code_muse/agents/_diagnostics.py +171 -0
  7. code_muse/agents/_history.py +382 -0
  8. code_muse/agents/_key_listeners.py +148 -0
  9. code_muse/agents/_non_streaming_render.py +148 -0
  10. code_muse/agents/_runtime.py +596 -0
  11. code_muse/agents/agent_creator_agent.py +603 -0
  12. code_muse/agents/agent_helios.py +47 -0
  13. code_muse/agents/agent_manager.py +740 -0
  14. code_muse/agents/agent_muse.py +78 -0
  15. code_muse/agents/agent_planning.py +44 -0
  16. code_muse/agents/agent_qa_melpomene.py +207 -0
  17. code_muse/agents/base_agent.py +194 -0
  18. code_muse/agents/event_stream_handler.py +361 -0
  19. code_muse/agents/json_agent.py +201 -0
  20. code_muse/agents/prompt_v3.py +521 -0
  21. code_muse/agents/subagent_stream_handler.py +273 -0
  22. code_muse/callbacks.py +941 -0
  23. code_muse/chatgpt_codex_client.py +333 -0
  24. code_muse/claude_cache_client.py +853 -0
  25. code_muse/cli_runner/__init__.py +319 -0
  26. code_muse/cli_runner/args.py +63 -0
  27. code_muse/cli_runner/loop.py +510 -0
  28. code_muse/cli_runner/resume.py +72 -0
  29. code_muse/cli_runner/runner.py +161 -0
  30. code_muse/command_line/__init__.py +1 -0
  31. code_muse/command_line/add_model_menu.py +1331 -0
  32. code_muse/command_line/agent_menu.py +674 -0
  33. code_muse/command_line/attachments.py +397 -0
  34. code_muse/command_line/autosave_menu.py +709 -0
  35. code_muse/command_line/clipboard.py +528 -0
  36. code_muse/command_line/colors_menu.py +530 -0
  37. code_muse/command_line/command_handler.py +262 -0
  38. code_muse/command_line/command_registry.py +150 -0
  39. code_muse/command_line/config_commands.py +711 -0
  40. code_muse/command_line/core_commands.py +740 -0
  41. code_muse/command_line/diff_menu.py +865 -0
  42. code_muse/command_line/file_path_completion.py +73 -0
  43. code_muse/command_line/load_context_completion.py +57 -0
  44. code_muse/command_line/model_picker_completion.py +512 -0
  45. code_muse/command_line/model_settings_menu.py +983 -0
  46. code_muse/command_line/onboarding_slides.py +162 -0
  47. code_muse/command_line/onboarding_wizard.py +337 -0
  48. code_muse/command_line/pagination.py +41 -0
  49. code_muse/command_line/pin_command_completion.py +329 -0
  50. code_muse/command_line/prompt_toolkit_completion.py +886 -0
  51. code_muse/command_line/session_commands.py +304 -0
  52. code_muse/command_line/shell_passthrough.py +145 -0
  53. code_muse/command_line/skills_completion.py +158 -0
  54. code_muse/command_line/types.py +18 -0
  55. code_muse/command_line/uc_menu.py +908 -0
  56. code_muse/command_line/utils.py +105 -0
  57. code_muse/command_line/wiggum_state.py +77 -0
  58. code_muse/config.py +1138 -0
  59. code_muse/config_agent.py +168 -0
  60. code_muse/config_appearance.py +241 -0
  61. code_muse/config_model.py +357 -0
  62. code_muse/config_security.py +73 -0
  63. code_muse/error_logging.py +132 -0
  64. code_muse/evals/__init__.py +35 -0
  65. code_muse/evals/eval_helpers.py +81 -0
  66. code_muse/evals/eval_runner.py +299 -0
  67. code_muse/evals/sample_evals/__init__.py +1 -0
  68. code_muse/evals/sample_evals/eval_frugal_reads.py +59 -0
  69. code_muse/evals/sample_evals/eval_memory_planning.py +31 -0
  70. code_muse/evals/sample_evals/eval_shell_efficiency.py +39 -0
  71. code_muse/evals/sample_evals/eval_tool_masking.py +33 -0
  72. code_muse/fs_scan_cache/__init__.py +31 -0
  73. code_muse/fs_scan_cache/invalidation_hooks.py +89 -0
  74. code_muse/fs_scan_cache/scan_cache_core.cpython-314-darwin.so +0 -0
  75. code_muse/fs_scan_cache/scan_cache_core.pyx +203 -0
  76. code_muse/fs_scan_cache/tool_integration.py +309 -0
  77. code_muse/fs_scan_cache/ttl_policy.py +44 -0
  78. code_muse/gemini_code_assist.py +383 -0
  79. code_muse/gemini_model.py +838 -0
  80. code_muse/hook_engine/README.md +105 -0
  81. code_muse/hook_engine/__init__.py +21 -0
  82. code_muse/hook_engine/aliases.py +153 -0
  83. code_muse/hook_engine/engine.py +221 -0
  84. code_muse/hook_engine/executor.py +347 -0
  85. code_muse/hook_engine/matcher.py +154 -0
  86. code_muse/hook_engine/models.py +245 -0
  87. code_muse/hook_engine/registry.py +114 -0
  88. code_muse/hook_engine/trust.py +268 -0
  89. code_muse/hook_engine/validator.py +144 -0
  90. code_muse/http_utils.py +360 -0
  91. code_muse/keymap.py +128 -0
  92. code_muse/list_filtering.py +26 -0
  93. code_muse/main.py +10 -0
  94. code_muse/messaging/__init__.py +259 -0
  95. code_muse/messaging/bus.py +621 -0
  96. code_muse/messaging/commands.py +166 -0
  97. code_muse/messaging/markdown_patches.py +57 -0
  98. code_muse/messaging/message_queue.py +397 -0
  99. code_muse/messaging/messages.py +591 -0
  100. code_muse/messaging/queue_console.py +269 -0
  101. code_muse/messaging/renderers.py +308 -0
  102. code_muse/messaging/rich_renderer.py +1158 -0
  103. code_muse/messaging/shimmer.py +154 -0
  104. code_muse/messaging/spinner/__init__.py +87 -0
  105. code_muse/messaging/spinner/console_spinner.py +250 -0
  106. code_muse/messaging/spinner/spinner_base.py +82 -0
  107. code_muse/messaging/subagent_console.py +458 -0
  108. code_muse/model_factory.py +1203 -0
  109. code_muse/model_switching.py +59 -0
  110. code_muse/model_utils.py +156 -0
  111. code_muse/models.json +66 -0
  112. code_muse/models_cache/__init__.py +26 -0
  113. code_muse/models_cache/blocking_lru_cache.py +98 -0
  114. code_muse/models_cache/cache_writer.py +86 -0
  115. code_muse/models_cache/sha256_hash.cpython-314-darwin.so +0 -0
  116. code_muse/models_cache/sha256_hash.pyx +34 -0
  117. code_muse/models_cache/startup_integration.py +75 -0
  118. code_muse/models_dev_api.json +1 -0
  119. code_muse/models_dev_parser.py +590 -0
  120. code_muse/motion.py +126 -0
  121. code_muse/plugins/__init__.py +471 -0
  122. code_muse/plugins/agent_skills/__init__.py +32 -0
  123. code_muse/plugins/agent_skills/config.py +176 -0
  124. code_muse/plugins/agent_skills/discovery.py +309 -0
  125. code_muse/plugins/agent_skills/downloader.py +389 -0
  126. code_muse/plugins/agent_skills/installer.py +19 -0
  127. code_muse/plugins/agent_skills/metadata.py +293 -0
  128. code_muse/plugins/agent_skills/prompt_builder.py +66 -0
  129. code_muse/plugins/agent_skills/register_callbacks.py +298 -0
  130. code_muse/plugins/agent_skills/remote_catalog.py +320 -0
  131. code_muse/plugins/agent_skills/skill_catalog.py +254 -0
  132. code_muse/plugins/agent_skills/skills_install_menu.py +690 -0
  133. code_muse/plugins/agent_skills/skills_menu.py +791 -0
  134. code_muse/plugins/autonomous_memory/__init__.py +39 -0
  135. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-darwin.so +0 -0
  136. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-x86_64-linux-gnu.so +0 -0
  137. code_muse/plugins/autonomous_memory/bm25_scorer.pyx +291 -0
  138. code_muse/plugins/autonomous_memory/consolidation.py +82 -0
  139. code_muse/plugins/autonomous_memory/extraction.py +382 -0
  140. code_muse/plugins/autonomous_memory/lease_lock.py +105 -0
  141. code_muse/plugins/autonomous_memory/memory_injection.py +59 -0
  142. code_muse/plugins/autonomous_memory/register_callbacks.py +268 -0
  143. code_muse/plugins/autonomous_memory/secret_scanner.py +62 -0
  144. code_muse/plugins/autonomous_memory/session_scanner.py +163 -0
  145. code_muse/plugins/aws_bedrock/__init__.py +14 -0
  146. code_muse/plugins/aws_bedrock/config.py +99 -0
  147. code_muse/plugins/aws_bedrock/register_callbacks.py +241 -0
  148. code_muse/plugins/aws_bedrock/utils.py +153 -0
  149. code_muse/plugins/azure_foundry/README.md +238 -0
  150. code_muse/plugins/azure_foundry/__init__.py +15 -0
  151. code_muse/plugins/azure_foundry/config.py +125 -0
  152. code_muse/plugins/azure_foundry/discovery.py +187 -0
  153. code_muse/plugins/azure_foundry/register_callbacks.py +495 -0
  154. code_muse/plugins/azure_foundry/token.py +180 -0
  155. code_muse/plugins/azure_foundry/utils.py +345 -0
  156. code_muse/plugins/build_filter/__init__.py +1 -0
  157. code_muse/plugins/build_filter/register_callbacks.py +201 -0
  158. code_muse/plugins/build_filter/strategies/__init__.py +1 -0
  159. code_muse/plugins/build_filter/strategies/build.py +397 -0
  160. code_muse/plugins/chatgpt_oauth/__init__.py +6 -0
  161. code_muse/plugins/chatgpt_oauth/config.py +52 -0
  162. code_muse/plugins/chatgpt_oauth/oauth_flow.py +338 -0
  163. code_muse/plugins/chatgpt_oauth/register_callbacks.py +172 -0
  164. code_muse/plugins/chatgpt_oauth/test_plugin.py +301 -0
  165. code_muse/plugins/chatgpt_oauth/utils.py +538 -0
  166. code_muse/plugins/checkpointing/__init__.py +29 -0
  167. code_muse/plugins/checkpointing/checkpoint_hook.py +51 -0
  168. code_muse/plugins/checkpointing/conversation_snapshots.py +117 -0
  169. code_muse/plugins/checkpointing/register_callbacks.py +51 -0
  170. code_muse/plugins/checkpointing/restore_command.py +263 -0
  171. code_muse/plugins/checkpointing/rewind_shortcut.py +88 -0
  172. code_muse/plugins/checkpointing/shadow_git.py +90 -0
  173. code_muse/plugins/claude_code_hooks/__init__.py +1 -0
  174. code_muse/plugins/claude_code_hooks/config.py +188 -0
  175. code_muse/plugins/claude_code_hooks/register_callbacks.py +208 -0
  176. code_muse/plugins/claude_code_oauth/README.md +167 -0
  177. code_muse/plugins/claude_code_oauth/SETUP.md +93 -0
  178. code_muse/plugins/claude_code_oauth/__init__.py +25 -0
  179. code_muse/plugins/claude_code_oauth/config.py +52 -0
  180. code_muse/plugins/claude_code_oauth/fast_mode.py +124 -0
  181. code_muse/plugins/claude_code_oauth/prompt_handler.py +63 -0
  182. code_muse/plugins/claude_code_oauth/register_callbacks.py +547 -0
  183. code_muse/plugins/claude_code_oauth/test_fast_mode.py +165 -0
  184. code_muse/plugins/claude_code_oauth/test_plugin.py +283 -0
  185. code_muse/plugins/claude_code_oauth/token_refresh_heartbeat.py +237 -0
  186. code_muse/plugins/claude_code_oauth/utils.py +664 -0
  187. code_muse/plugins/copilot_auth/__init__.py +11 -0
  188. code_muse/plugins/copilot_auth/config.py +91 -0
  189. code_muse/plugins/copilot_auth/reasoning_client.py +409 -0
  190. code_muse/plugins/copilot_auth/register_callbacks.py +461 -0
  191. code_muse/plugins/copilot_auth/utils.py +584 -0
  192. code_muse/plugins/custom_commands/__init__.py +14 -0
  193. code_muse/plugins/custom_commands/args_injection.py +82 -0
  194. code_muse/plugins/custom_commands/command_discovery.py +89 -0
  195. code_muse/plugins/custom_commands/command_toml_schema.py +71 -0
  196. code_muse/plugins/custom_commands/register_callbacks.py +176 -0
  197. code_muse/plugins/customizable_commands/__init__.py +0 -0
  198. code_muse/plugins/customizable_commands/register_callbacks.py +136 -0
  199. code_muse/plugins/destructive_command_guard/__init__.py +14 -0
  200. code_muse/plugins/destructive_command_guard/detector.py +375 -0
  201. code_muse/plugins/destructive_command_guard/register_callbacks.py +148 -0
  202. code_muse/plugins/example_custom_command/README.md +280 -0
  203. code_muse/plugins/example_custom_command/register_callbacks.py +51 -0
  204. code_muse/plugins/file_permission_handler/__init__.py +4 -0
  205. code_muse/plugins/file_permission_handler/register_callbacks.py +441 -0
  206. code_muse/plugins/filter_engine/__init__.py +30 -0
  207. code_muse/plugins/filter_engine/classifier.py +153 -0
  208. code_muse/plugins/filter_engine/content_detector.py +184 -0
  209. code_muse/plugins/filter_engine/dispatcher.py +244 -0
  210. code_muse/plugins/filter_engine/register_callbacks.py +188 -0
  211. code_muse/plugins/filter_engine/registry.py +279 -0
  212. code_muse/plugins/filter_engine/strategies/__init__.py +8 -0
  213. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-darwin.so +0 -0
  214. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-x86_64-linux-gnu.so +0 -0
  215. code_muse/plugins/filter_engine/strategies/ast_compressor.pyx +348 -0
  216. code_muse/plugins/filter_engine/strategies/ast_parser.py +167 -0
  217. code_muse/plugins/filter_engine/strategies/code.cpython-314-darwin.so +0 -0
  218. code_muse/plugins/filter_engine/strategies/code.cpython-314-x86_64-linux-gnu.so +0 -0
  219. code_muse/plugins/filter_engine/strategies/code.pyx +584 -0
  220. code_muse/plugins/filter_engine/strategies/git.cpython-314-darwin.so +0 -0
  221. code_muse/plugins/filter_engine/strategies/git.cpython-314-x86_64-linux-gnu.so +0 -0
  222. code_muse/plugins/filter_engine/strategies/git.pyx +438 -0
  223. code_muse/plugins/filter_engine/strategies/json_compressor.cpython-314-darwin.so +0 -0
  224. code_muse/plugins/filter_engine/strategies/json_compressor.pyx +253 -0
  225. code_muse/plugins/filter_engine/strategies/json_patterns.cpython-314-darwin.so +0 -0
  226. code_muse/plugins/filter_engine/strategies/json_patterns.pyx +178 -0
  227. code_muse/plugins/filter_engine/strategies/lint.cpython-314-darwin.so +0 -0
  228. code_muse/plugins/filter_engine/strategies/lint.cpython-314-x86_64-linux-gnu.so +0 -0
  229. code_muse/plugins/filter_engine/strategies/lint.pyx +626 -0
  230. code_muse/plugins/filter_engine/strategies/test.cpython-314-darwin.so +0 -0
  231. code_muse/plugins/filter_engine/strategies/test.cpython-314-x86_64-linux-gnu.so +0 -0
  232. code_muse/plugins/filter_engine/strategies/test.pyx +431 -0
  233. code_muse/plugins/filter_engine/verbosity.py +63 -0
  234. code_muse/plugins/force_push_guard/__init__.py +5 -0
  235. code_muse/plugins/force_push_guard/detector.py +96 -0
  236. code_muse/plugins/force_push_guard/register_callbacks.py +144 -0
  237. code_muse/plugins/force_push_guard/test_detector.py +143 -0
  238. code_muse/plugins/frontend_emitter/__init__.py +25 -0
  239. code_muse/plugins/frontend_emitter/emitter.py +121 -0
  240. code_muse/plugins/frontend_emitter/register_callbacks.py +259 -0
  241. code_muse/plugins/gac/__init__.py +4 -0
  242. code_muse/plugins/gac/git_ops.py +136 -0
  243. code_muse/plugins/gac/prompt.py +191 -0
  244. code_muse/plugins/gac/register_callbacks.py +82 -0
  245. code_muse/plugins/hook_creator/__init__.py +1 -0
  246. code_muse/plugins/hook_creator/register_callbacks.py +34 -0
  247. code_muse/plugins/hook_manager/__init__.py +1 -0
  248. code_muse/plugins/hook_manager/config.py +289 -0
  249. code_muse/plugins/hook_manager/hooks_menu.py +563 -0
  250. code_muse/plugins/hook_manager/register_callbacks.py +227 -0
  251. code_muse/plugins/hook_monitor/register_callbacks.py +36 -0
  252. code_muse/plugins/mindpack/__init__.py +0 -0
  253. code_muse/plugins/mindpack/factory.py +930 -0
  254. code_muse/plugins/mindpack/judge.py +573 -0
  255. code_muse/plugins/mindpack/memory.py +100 -0
  256. code_muse/plugins/mindpack/mindpack_menu.py +1552 -0
  257. code_muse/plugins/mindpack/orchestration.py +605 -0
  258. code_muse/plugins/mindpack/register_callbacks.py +175 -0
  259. code_muse/plugins/mindpack/schemas.py +358 -0
  260. code_muse/plugins/mindpack/tools.py +387 -0
  261. code_muse/plugins/oauth_muse_html.py +226 -0
  262. code_muse/plugins/ollama_setup/__init__.py +5 -0
  263. code_muse/plugins/ollama_setup/completer.py +36 -0
  264. code_muse/plugins/ollama_setup/register_callbacks.py +410 -0
  265. code_muse/plugins/plan_command/__init__.py +0 -0
  266. code_muse/plugins/plan_command/register_callbacks.py +206 -0
  267. code_muse/plugins/plan_mode/__init__.py +37 -0
  268. code_muse/plugins/plan_mode/mode_cycling.py +40 -0
  269. code_muse/plugins/plan_mode/plan_generation.py +68 -0
  270. code_muse/plugins/plan_mode/plan_hooks.py +74 -0
  271. code_muse/plugins/plan_mode/plan_mode_tools.py +138 -0
  272. code_muse/plugins/plan_mode/register_callbacks.py +121 -0
  273. code_muse/plugins/plugin_trust/register_callbacks.py +140 -0
  274. code_muse/plugins/policy_engine/__init__.py +46 -0
  275. code_muse/plugins/policy_engine/approval_flow_integration.py +59 -0
  276. code_muse/plugins/policy_engine/policy_evaluator.py +75 -0
  277. code_muse/plugins/policy_engine/policy_file_discovery.py +90 -0
  278. code_muse/plugins/policy_engine/policy_toml_schema.py +115 -0
  279. code_muse/plugins/policy_engine/register_callbacks.py +112 -0
  280. code_muse/plugins/pop_command/__init__.py +1 -0
  281. code_muse/plugins/pop_command/register_callbacks.py +189 -0
  282. code_muse/plugins/prompt_newline/__init__.py +13 -0
  283. code_muse/plugins/prompt_newline/config.py +19 -0
  284. code_muse/plugins/prompt_newline/register_callbacks.py +159 -0
  285. code_muse/plugins/safety_status/__init__.py +0 -0
  286. code_muse/plugins/safety_status/register_callbacks.py +113 -0
  287. code_muse/plugins/semantic_compression/__init__.py +6 -0
  288. code_muse/plugins/semantic_compression/compressor.py +295 -0
  289. code_muse/plugins/semantic_compression/config.py +123 -0
  290. code_muse/plugins/semantic_compression/register_callbacks.py +320 -0
  291. code_muse/plugins/shell_minimizer/__init__.py +50 -0
  292. code_muse/plugins/shell_minimizer/builtin_filters.toml +393 -0
  293. code_muse/plugins/shell_minimizer/pipeline.py +556 -0
  294. code_muse/plugins/shell_minimizer/primitives.py +482 -0
  295. code_muse/plugins/shell_minimizer/register_callbacks.py +276 -0
  296. code_muse/plugins/shell_safety/__init__.py +6 -0
  297. code_muse/plugins/shell_safety/agent_shell_safety.py +69 -0
  298. code_muse/plugins/shell_safety/command_cache.py +149 -0
  299. code_muse/plugins/shell_safety/register_callbacks.py +202 -0
  300. code_muse/plugins/synthetic_status/__init__.py +1 -0
  301. code_muse/plugins/synthetic_status/register_callbacks.py +128 -0
  302. code_muse/plugins/synthetic_status/status_api.py +145 -0
  303. code_muse/plugins/token_caching/__init__.py +21 -0
  304. code_muse/plugins/token_caching/cache_hit_tracking.py +128 -0
  305. code_muse/plugins/token_caching/cacheable_prefix_detection.py +28 -0
  306. code_muse/plugins/token_caching/register_callbacks.py +54 -0
  307. code_muse/plugins/token_caching/stats_display.py +35 -0
  308. code_muse/plugins/token_tracking/__init__.py +26 -0
  309. code_muse/plugins/token_tracking/database.py +381 -0
  310. code_muse/plugins/token_tracking/edit_analyzer.py +97 -0
  311. code_muse/plugins/token_tracking/record.py +55 -0
  312. code_muse/plugins/token_tracking/register_callbacks.py +277 -0
  313. code_muse/plugins/token_tracking/reports.py +329 -0
  314. code_muse/plugins/universal_constructor/__init__.py +13 -0
  315. code_muse/plugins/universal_constructor/models.py +136 -0
  316. code_muse/plugins/universal_constructor/register_callbacks.py +47 -0
  317. code_muse/plugins/universal_constructor/registry.py +390 -0
  318. code_muse/plugins/universal_constructor/runner.py +474 -0
  319. code_muse/plugins/universal_constructor/safety.py +440 -0
  320. code_muse/plugins/universal_constructor/sandbox.py +584 -0
  321. code_muse/provider_identity.py +105 -0
  322. code_muse/pydantic_patches.py +410 -0
  323. code_muse/reopenable_async_client.py +233 -0
  324. code_muse/round_robin_model.py +151 -0
  325. code_muse/secret_storage.py +74 -0
  326. code_muse/security/__init__.py +1 -0
  327. code_muse/security/redaction.cpython-314-darwin.so +0 -0
  328. code_muse/security/redaction.cpython-314-x86_64-linux-gnu.so +0 -0
  329. code_muse/security/redaction.pyx +135 -0
  330. code_muse/session_storage.py +565 -0
  331. code_muse/status_display.py +261 -0
  332. code_muse/stream_parser/__init__.py +76 -0
  333. code_muse/stream_parser/assistant_text_parser.py +90 -0
  334. code_muse/stream_parser/citation_parser.py +76 -0
  335. code_muse/stream_parser/inline_hidden_tag_parser.py +236 -0
  336. code_muse/stream_parser/proposed_plan_parser.py +158 -0
  337. code_muse/stream_parser/stream_text_chunk.py +23 -0
  338. code_muse/stream_parser/stream_text_parser.py +27 -0
  339. code_muse/stream_parser/tagged_line_parser.cpython-314-darwin.so +0 -0
  340. code_muse/stream_parser/tagged_line_parser.pyx +251 -0
  341. code_muse/stream_parser/utf8_stream_parser.cpython-314-darwin.so +0 -0
  342. code_muse/stream_parser/utf8_stream_parser.pyx +206 -0
  343. code_muse/summarization_agent.py +308 -0
  344. code_muse/terminal_utils.cpython-314-darwin.so +0 -0
  345. code_muse/terminal_utils.cpython-314-x86_64-linux-gnu.so +0 -0
  346. code_muse/terminal_utils.pyx +483 -0
  347. code_muse/tools/__init__.py +459 -0
  348. code_muse/tools/agent_tools.py +613 -0
  349. code_muse/tools/ask_user_question/__init__.py +26 -0
  350. code_muse/tools/ask_user_question/constants.py +73 -0
  351. code_muse/tools/ask_user_question/demo_tui.py +55 -0
  352. code_muse/tools/ask_user_question/handler.py +232 -0
  353. code_muse/tools/ask_user_question/models.py +302 -0
  354. code_muse/tools/ask_user_question/registration.py +37 -0
  355. code_muse/tools/ask_user_question/renderers.py +336 -0
  356. code_muse/tools/ask_user_question/terminal_ui.py +327 -0
  357. code_muse/tools/ask_user_question/theme.py +156 -0
  358. code_muse/tools/ask_user_question/tui_loop.py +422 -0
  359. code_muse/tools/background_jobs.py +99 -0
  360. code_muse/tools/browser/__init__.py +37 -0
  361. code_muse/tools/browser/browser_control.py +289 -0
  362. code_muse/tools/browser/browser_interactions.py +545 -0
  363. code_muse/tools/browser/browser_locators.py +640 -0
  364. code_muse/tools/browser/browser_manager.py +376 -0
  365. code_muse/tools/browser/browser_navigation.py +251 -0
  366. code_muse/tools/browser/browser_screenshot.py +180 -0
  367. code_muse/tools/browser/browser_scripts.py +462 -0
  368. code_muse/tools/browser/browser_workflows.py +222 -0
  369. code_muse/tools/chrome_cdp/__init__.py +1070 -0
  370. code_muse/tools/chrome_cdp/register_callbacks.py +61 -0
  371. code_muse/tools/command_runner.py +1401 -0
  372. code_muse/tools/common.py +1407 -0
  373. code_muse/tools/display.py +87 -0
  374. code_muse/tools/file_modifications.py +1099 -0
  375. code_muse/tools/file_operations.py +860 -0
  376. code_muse/tools/image_tools.py +185 -0
  377. code_muse/tools/meetin_proxy/__init__.py +243 -0
  378. code_muse/tools/meetin_proxy/capture_addon.py +82 -0
  379. code_muse/tools/meetin_proxy/proxy_manager.py +326 -0
  380. code_muse/tools/meetin_proxy/register_callbacks.py +45 -0
  381. code_muse/tools/path_policy.py +219 -0
  382. code_muse/tools/skills_tools.py +586 -0
  383. code_muse/tools/subagent_context.py +158 -0
  384. code_muse/tools/tools_content.py +50 -0
  385. code_muse/tools/universal_constructor.py +965 -0
  386. code_muse/uvx_detection.py +241 -0
  387. code_muse/version_checker.py +86 -0
  388. code_muse-0.0.1.data/data/code_muse/models.json +66 -0
  389. code_muse-0.0.1.data/data/code_muse/models_dev_api.json +1 -0
  390. code_muse-0.0.1.dist-info/METADATA +845 -0
  391. code_muse-0.0.1.dist-info/RECORD +394 -0
  392. code_muse-0.0.1.dist-info/WHEEL +4 -0
  393. code_muse-0.0.1.dist-info/entry_points.txt +2 -0
  394. code_muse-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,165 @@
1
+ """Tests for the fast_mode submodule.
2
+
3
+ These tests don't touch the network or config file - they exercise pure
4
+ helpers for beta-header merging and payload injection.
5
+ """
6
+
7
+ from unittest.mock import patch
8
+
9
+ import pytest
10
+
11
+ from code_muse.plugins.claude_code_oauth.fast_mode import (
12
+ FAST_MODE_BETA,
13
+ FAST_SETTING_KEY,
14
+ ensure_fast_beta_header,
15
+ is_fast_mode_enabled,
16
+ make_fast_mode_wrapper,
17
+ )
18
+
19
+
20
+ class TestEnsureFastBetaHeader:
21
+ def test_adds_marker_when_enabled_and_absent(self):
22
+ headers = {}
23
+ ensure_fast_beta_header(headers, enabled=True)
24
+ assert headers["anthropic-beta"] == FAST_MODE_BETA
25
+
26
+ def test_appends_marker_to_existing(self):
27
+ headers = {"anthropic-beta": "oauth-2025-04-20,interleaved-thinking-2025-05-14"}
28
+ ensure_fast_beta_header(headers, enabled=True)
29
+ parts = headers["anthropic-beta"].split(",")
30
+ assert FAST_MODE_BETA in parts
31
+ assert "oauth-2025-04-20" in parts
32
+ assert "interleaved-thinking-2025-05-14" in parts
33
+
34
+ def test_no_duplicate_when_already_present(self):
35
+ headers = {"anthropic-beta": f"oauth-2025-04-20,{FAST_MODE_BETA}"}
36
+ ensure_fast_beta_header(headers, enabled=True)
37
+ assert headers["anthropic-beta"].count(FAST_MODE_BETA) == 1
38
+
39
+ def test_removes_marker_when_disabled(self):
40
+ headers = {"anthropic-beta": f"oauth-2025-04-20,{FAST_MODE_BETA}"}
41
+ ensure_fast_beta_header(headers, enabled=False)
42
+ assert FAST_MODE_BETA not in headers["anthropic-beta"]
43
+ assert "oauth-2025-04-20" in headers["anthropic-beta"]
44
+
45
+ def test_drops_header_when_only_marker_removed(self):
46
+ headers = {"anthropic-beta": FAST_MODE_BETA}
47
+ ensure_fast_beta_header(headers, enabled=False)
48
+ assert "anthropic-beta" not in headers
49
+
50
+ def test_noop_when_disabled_and_absent(self):
51
+ headers = {"anthropic-beta": "oauth-2025-04-20"}
52
+ ensure_fast_beta_header(headers, enabled=False)
53
+ assert headers == {"anthropic-beta": "oauth-2025-04-20"}
54
+
55
+
56
+ class TestIsFastModeEnabled:
57
+ def test_returns_false_when_setting_absent(self):
58
+ with patch(
59
+ "code_muse.config.get_all_model_settings",
60
+ return_value={},
61
+ ):
62
+ assert is_fast_mode_enabled("claude-code-foo") is False
63
+
64
+ def test_returns_true_when_setting_true(self):
65
+ with patch(
66
+ "code_muse.config.get_all_model_settings",
67
+ return_value={FAST_SETTING_KEY: True},
68
+ ):
69
+ assert is_fast_mode_enabled("claude-code-foo") is True
70
+
71
+ def test_returns_false_when_setting_false(self):
72
+ with patch(
73
+ "code_muse.config.get_all_model_settings",
74
+ return_value={FAST_SETTING_KEY: False},
75
+ ):
76
+ assert is_fast_mode_enabled("claude-code-foo") is False
77
+
78
+
79
+ class TestMakeFastModeWrapper:
80
+ @pytest.mark.asyncio
81
+ async def test_injects_speed_when_enabled(self):
82
+ captured = {}
83
+
84
+ async def fake_create(**kwargs):
85
+ captured.update(kwargs)
86
+ return "ok"
87
+
88
+ wrapped = make_fast_mode_wrapper(fake_create, "claude-code-foo")
89
+
90
+ with patch(
91
+ "code_muse.config.get_all_model_settings",
92
+ return_value={FAST_SETTING_KEY: True},
93
+ ):
94
+ await wrapped(model="foo", messages=[])
95
+
96
+ assert captured.get("speed") == "fast"
97
+
98
+ @pytest.mark.asyncio
99
+ async def test_does_not_inject_when_disabled(self):
100
+ captured = {}
101
+
102
+ async def fake_create(**kwargs):
103
+ captured.update(kwargs)
104
+ return "ok"
105
+
106
+ wrapped = make_fast_mode_wrapper(fake_create, "claude-code-foo")
107
+
108
+ with patch(
109
+ "code_muse.config.get_all_model_settings",
110
+ return_value={FAST_SETTING_KEY: False},
111
+ ):
112
+ await wrapped(model="foo", messages=[])
113
+
114
+ assert "speed" not in captured
115
+
116
+ @pytest.mark.asyncio
117
+ async def test_does_not_clobber_explicit_speed(self):
118
+ captured = {}
119
+
120
+ async def fake_create(**kwargs):
121
+ captured.update(kwargs)
122
+ return "ok"
123
+
124
+ wrapped = make_fast_mode_wrapper(fake_create, "claude-code-foo")
125
+
126
+ with patch(
127
+ "code_muse.config.get_all_model_settings",
128
+ return_value={FAST_SETTING_KEY: True},
129
+ ):
130
+ await wrapped(model="foo", messages=[], speed="slow")
131
+
132
+ # Caller-provided speed wins
133
+ assert captured.get("speed") == "slow"
134
+
135
+
136
+ class TestIsFastModeEnabledRealConfigRoundTrip:
137
+ """End-to-end regression test: set_model_setting -> is_fast_mode_enabled.
138
+
139
+ This would have caught the original bug where get_effective_model_settings
140
+ silently filtered the 'fast' key out because it wasn't in the core
141
+ supported_settings allowlist for claude-* models.
142
+ """
143
+
144
+ def test_real_roundtrip_with_claude_code_model(self, tmp_path, monkeypatch):
145
+ # Point config at a throwaway file so we don't clobber user config.
146
+ cfg = tmp_path / "muse.cfg"
147
+ monkeypatch.setattr("code_muse.config.CONFIG_FILE", str(cfg))
148
+
149
+ from code_muse.config import set_model_setting
150
+
151
+ model = "claude-code-claude-opus-4-7"
152
+
153
+ # Initially off.
154
+ assert is_fast_mode_enabled(model) is False
155
+
156
+ # Flip it on via the same path /claude-code-fast uses.
157
+ set_model_setting(model, FAST_SETTING_KEY, "true")
158
+ assert is_fast_mode_enabled(model) is True, (
159
+ "Regression: fast setting written to config but read as False. "
160
+ "Likely the reader is filtering through the supported_settings allowlist."
161
+ )
162
+
163
+ # Flip it back off.
164
+ set_model_setting(model, FAST_SETTING_KEY, "false")
165
+ assert is_fast_mode_enabled(model) is False
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env python3
2
+ """Manual sanity checks for the Claude Code OAuth plugin."""
3
+
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ # Ensure project root on path
9
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
10
+ sys.path.insert(0, str(PROJECT_ROOT))
11
+
12
+ # Switch to project root for predictable relative paths
13
+ os.chdir(PROJECT_ROOT)
14
+
15
+
16
+ def test_plugin_imports() -> bool:
17
+ """Verify the plugin modules import correctly."""
18
+ print("\n=== Testing Plugin Imports ===")
19
+
20
+ try:
21
+ from code_muse.plugins.claude_code_oauth.config import (
22
+ CLAUDE_CODE_OAUTH_CONFIG,
23
+ get_token_storage_path,
24
+ )
25
+
26
+ print("✅ Config import successful")
27
+ print(f"✅ Token storage path: {get_token_storage_path()}")
28
+ print(f"✅ Known auth URL: {CLAUDE_CODE_OAUTH_CONFIG['auth_url']}")
29
+ except Exception as exc: # pragma: no cover - manual harness
30
+ print(f"❌ Config import failed: {exc}")
31
+ return False
32
+
33
+ try:
34
+ from code_muse.plugins.claude_code_oauth.utils import (
35
+ add_models_to_extra_config,
36
+ build_authorization_url,
37
+ exchange_code_for_tokens,
38
+ fetch_claude_code_models,
39
+ load_claude_models,
40
+ load_stored_tokens,
41
+ parse_authorization_code,
42
+ prepare_oauth_context,
43
+ remove_claude_code_models,
44
+ save_claude_models,
45
+ save_tokens,
46
+ )
47
+
48
+ _ = (
49
+ add_models_to_extra_config,
50
+ build_authorization_url,
51
+ exchange_code_for_tokens,
52
+ fetch_claude_code_models,
53
+ load_claude_models,
54
+ load_stored_tokens,
55
+ parse_authorization_code,
56
+ prepare_oauth_context,
57
+ remove_claude_code_models,
58
+ save_claude_models,
59
+ save_tokens,
60
+ )
61
+ print("✅ Utils import successful")
62
+ except Exception as exc: # pragma: no cover - manual harness
63
+ print(f"❌ Utils import failed: {exc}")
64
+ return False
65
+
66
+ try:
67
+ from code_muse.plugins.claude_code_oauth.register_callbacks import (
68
+ _custom_help,
69
+ _handle_custom_command,
70
+ )
71
+
72
+ commands = _custom_help()
73
+ print("✅ Callback registration import successful")
74
+ for name, description in commands:
75
+ print(f" /{name} - {description}")
76
+ # Ensure handler callable exists
77
+ _ = _handle_custom_command
78
+ except Exception as exc: # pragma: no cover - manual harness
79
+ print(f"❌ Callback import failed: {exc}")
80
+ return False
81
+
82
+ return True
83
+
84
+
85
+ def test_oauth_helpers() -> bool:
86
+ """Exercise helper functions without performing network requests."""
87
+ print("\n=== Testing OAuth Helper Functions ===")
88
+
89
+ try:
90
+ from urllib.parse import parse_qs, urlparse
91
+
92
+ from code_muse.plugins.claude_code_oauth.utils import (
93
+ assign_redirect_uri,
94
+ build_authorization_url,
95
+ parse_authorization_code,
96
+ prepare_oauth_context,
97
+ )
98
+
99
+ context = prepare_oauth_context()
100
+ assert context.state, "Expected non-empty OAuth state"
101
+ assert context.code_verifier, "Expected PKCE code verifier"
102
+ assert context.code_challenge, "Expected PKCE code challenge"
103
+
104
+ assign_redirect_uri(context, 8765)
105
+ auth_url = build_authorization_url(context)
106
+ parsed = urlparse(auth_url)
107
+ params = parse_qs(parsed.query)
108
+ print(f"✅ Authorization URL: {auth_url}")
109
+ assert parsed.scheme == "https", "Authorization URL must use https"
110
+ assert params.get("client_id", [None])[0], "client_id missing"
111
+ assert params.get("code_challenge_method", [None])[0] == "S256"
112
+ assert params.get("state", [None])[0] == context.state
113
+ assert params.get("code_challenge", [None])[0] == context.code_challenge
114
+
115
+ sample_code = f"MYCODE#{context.state}"
116
+ parsed_code, parsed_state = parse_authorization_code(sample_code)
117
+ assert parsed_code == "MYCODE", "Code parsing failed"
118
+ assert parsed_state == context.state, "State parsing failed"
119
+ print("✅ parse_authorization_code handled state suffix correctly")
120
+
121
+ parsed_code, parsed_state = parse_authorization_code("SINGLECODE")
122
+ assert parsed_code == "SINGLECODE" and parsed_state is None
123
+ print("✅ parse_authorization_code handled bare code correctly")
124
+
125
+ return True
126
+
127
+ except AssertionError as exc:
128
+ print(f"❌ Assertion failed: {exc}")
129
+ return False
130
+ except Exception as exc: # pragma: no cover - manual harness
131
+ print(f"❌ OAuth helper test crashed: {exc}")
132
+ import traceback
133
+
134
+ traceback.print_exc()
135
+ return False
136
+
137
+
138
+ def test_file_operations() -> bool:
139
+ """Ensure token/model storage helpers behave sanely."""
140
+ print("\n=== Testing File Operations ===")
141
+
142
+ try:
143
+ from code_muse.plugins.claude_code_oauth.config import (
144
+ get_claude_models_path,
145
+ get_token_storage_path,
146
+ )
147
+ from code_muse.plugins.claude_code_oauth.utils import (
148
+ load_claude_models,
149
+ load_stored_tokens,
150
+ )
151
+
152
+ tokens = load_stored_tokens()
153
+ print(f"✅ Token load result: {'present' if tokens else 'none'}")
154
+
155
+ models = load_claude_models()
156
+ print(f"✅ Loaded {len(models)} Claude models")
157
+ for name, config in models.items():
158
+ print(f" - {name}: {config.get('type', 'unknown type')}")
159
+
160
+ token_path = get_token_storage_path()
161
+ models_path = get_claude_models_path()
162
+ token_path.parent.mkdir(parents=True, exist_ok=True)
163
+ models_path.parent.mkdir(parents=True, exist_ok=True)
164
+ print(f"✅ Token path: {token_path}")
165
+ print(f"✅ Models path: {models_path}")
166
+
167
+ return True
168
+
169
+ except Exception as exc: # pragma: no cover - manual harness
170
+ print(f"❌ File operations test failed: {exc}")
171
+ import traceback
172
+
173
+ traceback.print_exc()
174
+ return False
175
+
176
+
177
+ def test_command_handlers() -> bool:
178
+ """Smoke-test command handler routing without simulating authentication."""
179
+ print("\n=== Testing Command Handlers ===")
180
+
181
+ from code_muse.plugins.claude_code_oauth.register_callbacks import (
182
+ _handle_custom_command,
183
+ )
184
+
185
+ unknown = _handle_custom_command("/bogus", "bogus")
186
+ print(f"✅ Unknown command returned: {unknown}")
187
+
188
+ partial = _handle_custom_command("/claude-code", "claude-code")
189
+ print(f"✅ Partial command returned: {partial}")
190
+
191
+ # Do not invoke the real auth command here because it prompts for input.
192
+ return True
193
+
194
+
195
+ def test_configuration() -> bool:
196
+ """Validate configuration keys and basic formats."""
197
+ print("\n=== Testing Configuration ===")
198
+
199
+ try:
200
+ from code_muse.plugins.claude_code_oauth.config import CLAUDE_CODE_OAUTH_CONFIG
201
+
202
+ required_keys = [
203
+ "auth_url",
204
+ "token_url",
205
+ "api_base_url",
206
+ "client_id",
207
+ "scope",
208
+ "redirect_host",
209
+ "redirect_path",
210
+ "callback_port_range",
211
+ "callback_timeout",
212
+ "token_storage",
213
+ "prefix",
214
+ "default_context_length",
215
+ "api_key_env_var",
216
+ ]
217
+
218
+ missing = [key for key in required_keys if key not in CLAUDE_CODE_OAUTH_CONFIG]
219
+ if missing:
220
+ print(f"❌ Missing configuration keys: {missing}")
221
+ return False
222
+
223
+ for key in required_keys:
224
+ value = CLAUDE_CODE_OAUTH_CONFIG[key]
225
+ print(f"✅ {key}: {value}")
226
+
227
+ for url_key in ["auth_url", "token_url", "api_base_url"]:
228
+ url = CLAUDE_CODE_OAUTH_CONFIG[url_key]
229
+ if not str(url).startswith("https://"):
230
+ print(f"❌ URL must use HTTPS: {url_key} -> {url}")
231
+ return False
232
+ print(f"✅ {url_key} uses HTTPS")
233
+
234
+ return True
235
+
236
+ except Exception as exc: # pragma: no cover - manual harness
237
+ print(f"❌ Configuration test crashed: {exc}")
238
+ import traceback
239
+
240
+ traceback.print_exc()
241
+ return False
242
+
243
+
244
+ def main() -> bool:
245
+ """Run all manual checks."""
246
+ print("Claude Code OAuth Plugin Test Suite")
247
+ print("=" * 40)
248
+
249
+ tests = [
250
+ test_plugin_imports,
251
+ test_oauth_helpers,
252
+ test_file_operations,
253
+ test_command_handlers,
254
+ test_configuration,
255
+ ]
256
+
257
+ passed = 0
258
+ for test in tests:
259
+ try:
260
+ if test():
261
+ passed += 1
262
+ else:
263
+ print("\n❌ Test failed")
264
+ except Exception as exc: # pragma: no cover - manual harness
265
+ print(f"\n❌ Test crashed: {exc}")
266
+
267
+ print("\n=== Test Results ===")
268
+ print(f"Passed: {passed}/{len(tests)}")
269
+
270
+ if passed == len(tests):
271
+ print("✅ All sanity checks passed!")
272
+ print("Next steps:")
273
+ print("1. Restart Muse if it was running")
274
+ print("2. Run /claude-code-auth")
275
+ print("3. Paste the Claude Console authorization code when prompted")
276
+ return True
277
+
278
+ print("❌ Some checks failed. Investigate before using the plugin.")
279
+ return False
280
+
281
+
282
+ if __name__ == "__main__":
283
+ sys.exit(0 if main() else 1)
@@ -0,0 +1,237 @@
1
+ """Token refresh heartbeat for long-running Claude Code OAuth sessions.
2
+
3
+ This module provides a background task that periodically checks and refreshes
4
+ Claude Code OAuth tokens during long-running agentic operations. This ensures
5
+ that tokens don't expire during extended streaming responses or tool processing.
6
+
7
+ Usage:
8
+ async with token_refresh_heartbeat_context():
9
+ # Long running agent operation
10
+ await agent.run(...)
11
+ """
12
+
13
+ import asyncio
14
+ import logging
15
+ import time
16
+ from contextlib import asynccontextmanager, suppress
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Heartbeat interval in seconds - check token every 2 minutes
21
+ # This is frequent enough to catch expiring tokens before they cause issues
22
+ # but not so frequent as to spam the token endpoint
23
+ HEARTBEAT_INTERVAL_SECONDS = 120
24
+
25
+ # Minimum time between refresh attempts to avoid hammering the endpoint
26
+ MIN_REFRESH_INTERVAL_SECONDS = 60
27
+
28
+ # Global tracking of last refresh time to coordinate across heartbeats
29
+ _last_refresh_time: float = 0.0
30
+
31
+
32
+ class TokenRefreshHeartbeat:
33
+ """Background task that periodically refreshes Claude Code OAuth tokens.
34
+
35
+ This runs as an asyncio task during agent operations and checks if the
36
+ token needs refreshing at regular intervals.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ interval: float = HEARTBEAT_INTERVAL_SECONDS,
42
+ min_refresh_interval: float = MIN_REFRESH_INTERVAL_SECONDS,
43
+ ):
44
+ self._interval = interval
45
+ self._min_refresh_interval = min_refresh_interval
46
+ self._task: asyncio.Task | None = None
47
+ self._stop_event = asyncio.Event()
48
+ self._lock = asyncio.Lock()
49
+ self._refresh_count = 0
50
+
51
+ async def start(self) -> None:
52
+ """Start the heartbeat background task."""
53
+ if self._task is not None:
54
+ logger.debug("Heartbeat already running")
55
+ return
56
+
57
+ self._stop_event.clear()
58
+ self._task = asyncio.create_task(self._heartbeat_loop())
59
+ logger.debug("Token refresh heartbeat started")
60
+
61
+ async def stop(self) -> None:
62
+ """Stop the heartbeat background task."""
63
+ if self._task is None:
64
+ return
65
+
66
+ self._stop_event.set()
67
+ self._task.cancel()
68
+
69
+ with suppress(asyncio.CancelledError):
70
+ await self._task
71
+
72
+ self._task = None
73
+ logger.debug(
74
+ "Token refresh heartbeat stopped (refreshed %d times)",
75
+ self._refresh_count,
76
+ )
77
+
78
+ async def _heartbeat_loop(self) -> None:
79
+ """Main heartbeat loop that periodically checks token status."""
80
+ global _last_refresh_time
81
+
82
+ while not self._stop_event.is_set():
83
+ try:
84
+ # Wait for the interval or until stopped
85
+ try:
86
+ await asyncio.wait_for(
87
+ self._stop_event.wait(), timeout=self._interval
88
+ )
89
+ # If we got here, stop event was set
90
+ break
91
+ except TimeoutError:
92
+ # Normal timeout - time to check token
93
+ pass
94
+
95
+ # Check if we should attempt refresh
96
+ async with self._lock:
97
+ now = time.time()
98
+ if now - _last_refresh_time < self._min_refresh_interval:
99
+ logger.debug(
100
+ "Skipping refresh - last refresh was %.1f seconds ago",
101
+ now - _last_refresh_time,
102
+ )
103
+ continue
104
+
105
+ # Attempt the refresh
106
+ refreshed = await self._attempt_refresh()
107
+ if refreshed:
108
+ _last_refresh_time = now
109
+ self._refresh_count += 1
110
+
111
+ except asyncio.CancelledError:
112
+ break
113
+ except Exception as exc:
114
+ logger.debug("Error in heartbeat loop: %s", exc)
115
+ # Continue running - don't let errors kill the heartbeat
116
+ await asyncio.sleep(5) # Brief pause before retrying
117
+
118
+ async def _attempt_refresh(self) -> bool:
119
+ """Attempt to refresh the token if needed.
120
+
121
+ Returns:
122
+ True if a refresh was performed, False otherwise.
123
+ """
124
+ try:
125
+ # Import here to avoid circular imports
126
+ from .utils import (
127
+ is_token_expired,
128
+ load_stored_tokens,
129
+ refresh_access_token,
130
+ )
131
+
132
+ tokens = await asyncio.to_thread(load_stored_tokens)
133
+ if not tokens:
134
+ logger.debug("No stored tokens found")
135
+ return False
136
+
137
+ if not is_token_expired(tokens):
138
+ logger.debug("Token not yet expired, skipping refresh")
139
+ return False
140
+
141
+ # Token is expiring soon, refresh it
142
+ logger.info("Heartbeat: Token expiring soon, refreshing proactively")
143
+ refreshed_token = await asyncio.to_thread(refresh_access_token, force=False)
144
+
145
+ if refreshed_token:
146
+ logger.info("Heartbeat: Successfully refreshed token")
147
+ return True
148
+ else:
149
+ logger.warning("Heartbeat: Token refresh returned None")
150
+ return False
151
+
152
+ except Exception as exc:
153
+ logger.error("Heartbeat: Error during token refresh: %s", exc)
154
+ return False
155
+
156
+ @property
157
+ def refresh_count(self) -> int:
158
+ """Get the number of successful refreshes performed by this heartbeat."""
159
+ return self._refresh_count
160
+
161
+ @property
162
+ def is_running(self) -> bool:
163
+ """Check if the heartbeat is currently running."""
164
+ return self._task is not None and not self._task.done()
165
+
166
+
167
+ # Global heartbeat instance for the current session
168
+ _current_heartbeat: TokenRefreshHeartbeat | None = None
169
+
170
+
171
+ @asynccontextmanager
172
+ async def token_refresh_heartbeat_context(
173
+ interval: float = HEARTBEAT_INTERVAL_SECONDS,
174
+ ):
175
+ """Context manager that runs token refresh heartbeat during its scope.
176
+
177
+ Use this around long-running agent operations to ensure tokens stay fresh.
178
+
179
+ Args:
180
+ interval: Seconds between heartbeat checks. Default is 2 minutes.
181
+
182
+ Example:
183
+ async with token_refresh_heartbeat_context():
184
+ result = await agent.run(prompt)
185
+ """
186
+ global _current_heartbeat
187
+
188
+ heartbeat = TokenRefreshHeartbeat(interval=interval)
189
+
190
+ try:
191
+ await heartbeat.start()
192
+ _current_heartbeat = heartbeat
193
+ yield heartbeat
194
+ finally:
195
+ await heartbeat.stop()
196
+ _current_heartbeat = None
197
+
198
+
199
+ def is_heartbeat_running() -> bool:
200
+ """Check if a token refresh heartbeat is currently active."""
201
+ return _current_heartbeat is not None and _current_heartbeat.is_running
202
+
203
+
204
+ def get_current_heartbeat() -> TokenRefreshHeartbeat | None:
205
+ """Get the currently running heartbeat instance, if any."""
206
+ return _current_heartbeat
207
+
208
+
209
+ async def force_token_refresh() -> bool:
210
+ """Force an immediate token refresh.
211
+
212
+ This can be called from anywhere to trigger a token refresh,
213
+ regardless of whether a heartbeat is running.
214
+
215
+ Returns:
216
+ True if refresh was successful, False otherwise.
217
+ """
218
+ global _last_refresh_time
219
+
220
+ try:
221
+ from .utils import refresh_access_token
222
+
223
+ logger.info("Forcing token refresh")
224
+ # TODO: PEP 734 async bridge — convert refresh_access_token to async
225
+ refreshed_token = await asyncio.to_thread(refresh_access_token, force=True)
226
+
227
+ if refreshed_token:
228
+ _last_refresh_time = time.time()
229
+ logger.info("Force refresh successful")
230
+ return True
231
+ else:
232
+ logger.warning("Force refresh returned None")
233
+ return False
234
+
235
+ except Exception as exc:
236
+ logger.error("Force refresh error: %s", exc)
237
+ return False