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,236 @@
1
+ """Streaming parser for inline hidden tags.
2
+
3
+ Scans text for user-defined open/close delimiter pairs, extracts the content
4
+ between them as hidden payloads, and strips the delimiters from visible text.
5
+ Tags are matched literally and are not nested.
6
+
7
+ Partial delimiters that span chunk boundaries are buffered correctly using
8
+ suffix/prefix overlap checks so that a split ``<oai-mem-citation>`` is still
9
+ recognised when the chunks are ``'<oai-'`` and ``'mem-citation>'``.
10
+ """
11
+
12
+ from dataclasses import dataclass
13
+ from typing import TypeVar
14
+
15
+ from code_muse.stream_parser.stream_text_chunk import StreamTextChunk
16
+ from code_muse.stream_parser.stream_text_parser import StreamTextParser
17
+
18
+ T = TypeVar("T")
19
+
20
+
21
+ @dataclass
22
+ class InlineTagSpec[T]:
23
+ """Specification for a single hidden inline tag type.
24
+
25
+ Attributes:
26
+ tag: Payload type / identifier emitted in extracted results.
27
+ open: Literal opening delimiter (e.g. ``"<oai-mem-citation>"``).
28
+ close: Literal closing delimiter (e.g. ``"</oai-mem-citation>"``).
29
+ """
30
+
31
+ tag: T
32
+ open: str
33
+ close: str
34
+
35
+
36
+ @dataclass
37
+ class ExtractedInlineTag[T]:
38
+ """Payload produced when an inline tag is fully closed.
39
+
40
+ Attributes:
41
+ tag: The tag identifier from the matching :class:`InlineTagSpec`.
42
+ content: Text that appeared between the open and close delimiters.
43
+ """
44
+
45
+ tag: T
46
+ content: str
47
+
48
+
49
+ def _longest_suffix_prefix_len(s: str, needle: str) -> int:
50
+ """Return the largest ``k > 0`` such that ``s`` ends with ``needle[:k]``.
51
+
52
+ This tells us how many characters at the end of ``s`` might be a partial
53
+ occurrence of ``needle`` when more input arrives in the next chunk.
54
+
55
+ Args:
56
+ s: The string to inspect (usually the pending buffer).
57
+ needle: The delimiter we are searching for.
58
+
59
+ Returns:
60
+ Length of the longest overlap, or ``0`` when there is none.
61
+ """
62
+ if not needle:
63
+ return 0
64
+ max_k = min(len(s), len(needle))
65
+ for k in range(max_k, 0, -1):
66
+ if s.endswith(needle[:k]):
67
+ return k
68
+ return 0
69
+
70
+
71
+ class InlineHiddenTagParser(StreamTextParser[T]):
72
+ """Streaming parser that extracts hidden inline tags from text.
73
+
74
+ * Searches for the earliest open delimiter; when two open delimiters start
75
+ at the same position, the longer one wins (longest-match tiebreaker).
76
+ * Once inside a tag, everything up to the matching close delimiter is
77
+ treated as literal content—**nested tags are not parsed**.
78
+ * On chunk boundaries, characters that could be a partial delimiter are
79
+ kept in the pending buffer rather than being emitted as visible text.
80
+ * Unterminated tags are auto-closed at :meth:`finish`; their buffered
81
+ content becomes the extracted payload.
82
+ """
83
+
84
+ def __init__(self, specs: list[InlineTagSpec[T]]) -> None:
85
+ if not specs:
86
+ raise ValueError("InlineHiddenTagParser requires at least one tag spec")
87
+ for spec in specs:
88
+ if not spec.open:
89
+ raise ValueError(
90
+ "InlineHiddenTagParser requires non-empty open delimiters"
91
+ )
92
+ if not spec.close:
93
+ raise ValueError(
94
+ "InlineHiddenTagParser requires non-empty close delimiters"
95
+ )
96
+ self.specs = specs
97
+ self._pending: str = ""
98
+ self._active_spec: InlineTagSpec[T] | None = None
99
+ self._active_content: str = ""
100
+
101
+ def push_str(self, chunk: str) -> StreamTextChunk[T]:
102
+ """Feed a new text chunk and scan for tag delimiters.
103
+
104
+ The chunk is appended to any buffered pending text, then the parser
105
+ loops until it can no longer make forward progress:
106
+
107
+ 1. If a tag is open, search for its close delimiter.
108
+ * Found → emit the extracted tag and drain through the close.
109
+ * Not found → keep a suffix that might be a partial close,
110
+ drain the rest into the tag's content buffer.
111
+ 2. Otherwise, search for the next open delimiter.
112
+ * Found → emit visible text before it, drain through the open,
113
+ start tracking tag content.
114
+ * Not found → keep a suffix that might be a partial open,
115
+ drain the rest as visible text.
116
+
117
+ Args:
118
+ chunk: Incoming text delta.
119
+
120
+ Returns:
121
+ Visible text and any fully-closed tags found in this delta.
122
+ """
123
+ self._pending += chunk
124
+ visible_parts: list[str] = []
125
+ extracted_parts: list[ExtractedInlineTag[T]] = []
126
+
127
+ while True:
128
+ if self._active_spec is not None:
129
+ close_pos = self._pending.find(self._active_spec.close)
130
+ if close_pos != -1:
131
+ # Close delimiter found.
132
+ content = self._active_content + self._pending[:close_pos]
133
+ extracted_parts.append(
134
+ ExtractedInlineTag(self._active_spec.tag, content)
135
+ )
136
+ # Drain through the close delimiter.
137
+ close_len = len(self._active_spec.close)
138
+ self._pending = self._pending[close_pos + close_len :]
139
+ self._active_spec = None
140
+ self._active_content = ""
141
+ continue
142
+
143
+ # No close yet — keep characters that could be a partial
144
+ # close delimiter at the end of the buffer.
145
+ keep = _longest_suffix_prefix_len(
146
+ self._pending, self._active_spec.close
147
+ )
148
+ drain_len = len(self._pending) - keep
149
+ self._active_content += self._pending[:drain_len]
150
+ self._pending = self._pending[drain_len:]
151
+ break
152
+
153
+ # No active tag — look for the next open delimiter.
154
+ next_open = self._find_next_open()
155
+ if next_open is not None:
156
+ pos, spec_idx = next_open
157
+ spec = self.specs[spec_idx]
158
+ # Emit visible text before the open delimiter.
159
+ visible_parts.append(self._pending[:pos])
160
+ # Drain through the open delimiter.
161
+ open_len = len(spec.open)
162
+ self._pending = self._pending[pos + open_len :]
163
+ self._active_spec = spec
164
+ self._active_content = ""
165
+ continue
166
+
167
+ # No open delimiter found — keep characters that could be a
168
+ # partial open delimiter at the end of the buffer.
169
+ keep = self._max_open_prefix_suffix_len()
170
+ drain_len = len(self._pending) - keep
171
+ visible_parts.append(self._pending[:drain_len])
172
+ self._pending = self._pending[drain_len:]
173
+ break
174
+
175
+ return StreamTextChunk(
176
+ visible_text="".join(visible_parts),
177
+ extracted=extracted_parts,
178
+ )
179
+
180
+ def finish(self) -> StreamTextChunk[T]:
181
+ """Flush any remaining state.
182
+
183
+ If a tag is still open, its accumulated content is emitted as an
184
+ :class:`ExtractedInlineTag` with the tag's identifier. Any leftover
185
+ pending text (without an active tag) is emitted as visible text.
186
+
187
+ Returns:
188
+ Final visible text and any auto-closed extracted tags.
189
+ """
190
+ visible = ""
191
+ extracted: list[ExtractedInlineTag[T]] = []
192
+
193
+ if self._active_spec is not None:
194
+ content = self._active_content + self._pending
195
+ extracted.append(ExtractedInlineTag(self._active_spec.tag, content))
196
+ self._active_spec = None
197
+ self._active_content = ""
198
+ self._pending = ""
199
+ else:
200
+ visible = self._pending
201
+ self._pending = ""
202
+
203
+ return StreamTextChunk(visible_text=visible, extracted=extracted)
204
+
205
+ def _find_next_open(self) -> tuple[int, int] | None:
206
+ """Find the earliest open delimiter in the pending buffer.
207
+
208
+ Returns:
209
+ ``(position, spec_index)`` of the earliest open delimiter, or
210
+ ``None`` when no open delimiter is present. Tie-breaking rules:
211
+
212
+ 1. Smallest position (earliest in the buffer).
213
+ 2. Longest open delimiter at that position.
214
+ 3. Lowest spec index (stable ordering).
215
+ """
216
+ candidates: list[tuple[int, int, int]] = []
217
+ for i, spec in enumerate(self.specs):
218
+ pos = self._pending.find(spec.open)
219
+ if pos != -1:
220
+ candidates.append((pos, len(spec.open), i))
221
+ if not candidates:
222
+ return None
223
+ best = min(candidates, key=lambda c: (c[0], -c[1], c[2]))
224
+ return best[0], best[2]
225
+
226
+ def _max_open_prefix_suffix_len(self) -> int:
227
+ """Maximum overlap between the end of ``pending`` and any open delimiter.
228
+
229
+ Returns:
230
+ Largest ``k > 0`` such that ``pending`` ends with the first ``k``
231
+ characters of at least one open delimiter.
232
+ """
233
+ max_len = 0
234
+ for spec in self.specs:
235
+ max_len = max(max_len, _longest_suffix_prefix_len(self._pending, spec.open))
236
+ return max_len
@@ -0,0 +1,158 @@
1
+ """Parser for ``<proposed_plan>…</proposed_plan>`` line-based blocks.
2
+
3
+ Wraps :class:`TaggedLineParser` with a single tag spec and exposes a
4
+ :class:`StreamTextParser` interface that emits
5
+ :class:`ProposedPlanSegment` values.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import TypeVar
11
+
12
+ from code_muse.stream_parser.stream_text_chunk import StreamTextChunk
13
+ from code_muse.stream_parser.stream_text_parser import StreamTextParser
14
+ from code_muse.stream_parser.tagged_line_parser import (
15
+ TaggedLineParser,
16
+ TaggedLineSegment,
17
+ TaggedLineSegmentNormal,
18
+ TaggedLineSegmentTagDelta,
19
+ TaggedLineSegmentTagEnd,
20
+ TaggedLineSegmentTagStart,
21
+ TagSpec,
22
+ )
23
+
24
+ T = TypeVar("T")
25
+
26
+
27
+ class ProposedPlanSegmentType(Enum):
28
+ """Kind of segment produced by :class:`ProposedPlanParser`."""
29
+
30
+ NORMAL = "normal"
31
+ PLAN_START = "plan_start"
32
+ PLAN_DELTA = "plan_delta"
33
+ PLAN_END = "plan_end"
34
+
35
+
36
+ @dataclass
37
+ class ProposedPlanSegment:
38
+ """Single semantic piece of a parsed assistant response.
39
+
40
+ Attributes:
41
+ type: Which kind of segment this is.
42
+ text: Literal text content. Only populated for ``NORMAL`` and
43
+ ``PLAN_DELTA`` segments.
44
+ """
45
+
46
+ type: ProposedPlanSegmentType
47
+ text: str = ""
48
+
49
+
50
+ class ProposedPlanParser(StreamTextParser[ProposedPlanSegment]):
51
+ """Streaming parser that identifies ``<proposed_plan>`` blocks.
52
+
53
+ Lines that exactly equal ``"<proposed_plan>"`` or ``"</proposed_plan>"``
54
+ (after trimming) are removed from visible text and replaced by
55
+ :class:`ProposedPlanSegment` boundary markers. Content lines between
56
+ those boundaries become ``PLAN_DELTA`` segments. Everything else is
57
+ ``NORMAL``.
58
+ """
59
+
60
+ def __init__(self) -> None:
61
+ self._parser = TaggedLineParser(
62
+ [TagSpec(open="<proposed_plan>", close="</proposed_plan>", tag="plan")]
63
+ )
64
+
65
+ def push_str(self, chunk: str) -> StreamTextChunk[ProposedPlanSegment]:
66
+ """Feed a new text chunk.
67
+
68
+ Args:
69
+ chunk: Incoming text delta (may contain partial lines).
70
+
71
+ Returns:
72
+ Visible text outside plan blocks, plus any plan segments
73
+ (``PLAN_START``, ``PLAN_DELTA``, ``PLAN_END``) extracted from
74
+ the chunk.
75
+ """
76
+ segments = self._parser.parse(chunk)
77
+ return self._build_chunk(segments)
78
+
79
+ def finish(self) -> StreamTextChunk[ProposedPlanSegment]:
80
+ """Flush any remaining buffered state.
81
+
82
+ Unterminated plan blocks are auto-closed with a ``PLAN_END`` segment.
83
+
84
+ Returns:
85
+ Final visible text and any trailing plan segments.
86
+ """
87
+ segments = self._parser.finish()
88
+ return self._build_chunk(segments)
89
+
90
+ @staticmethod
91
+ def _map_segment(seg: TaggedLineSegment) -> ProposedPlanSegment:
92
+ """Convert a raw :class:`TaggedLineSegment` to a plan segment."""
93
+ if isinstance(seg, TaggedLineSegmentNormal):
94
+ return ProposedPlanSegment(ProposedPlanSegmentType.NORMAL, seg.text)
95
+ if isinstance(seg, TaggedLineSegmentTagStart):
96
+ return ProposedPlanSegment(ProposedPlanSegmentType.PLAN_START)
97
+ if isinstance(seg, TaggedLineSegmentTagDelta):
98
+ return ProposedPlanSegment(ProposedPlanSegmentType.PLAN_DELTA, seg.text)
99
+ if isinstance(seg, TaggedLineSegmentTagEnd):
100
+ return ProposedPlanSegment(ProposedPlanSegmentType.PLAN_END)
101
+ # Exhaustive because TaggedLineSegment is a union of the four classes.
102
+ raise TypeError(f"unexpected segment type: {type(seg)}")
103
+
104
+ def _build_chunk(
105
+ self, segments: list[TaggedLineSegment]
106
+ ) -> StreamTextChunk[ProposedPlanSegment]:
107
+ """Turn raw line segments into a :class:`StreamTextChunk`."""
108
+ mapped = [self._map_segment(s) for s in segments]
109
+ visible = "".join(
110
+ s.text for s in mapped if s.type == ProposedPlanSegmentType.NORMAL
111
+ )
112
+ return StreamTextChunk(visible_text=visible, extracted=mapped)
113
+
114
+
115
+ def extract_proposed_plan_text(text: str) -> str | None:
116
+ """Extract the raw plan text from a complete string.
117
+
118
+ Runs the parser over the full text and concatenates all ``PLAN_DELTA``
119
+ segments that appear inside ``PLAN_START`` / ``PLAN_END`` pairs.
120
+
121
+ Args:
122
+ text: Full assistant response text.
123
+
124
+ Returns:
125
+ The concatenated plan text, or ``None`` when no plan block is
126
+ present.
127
+ """
128
+ parser = ProposedPlanParser()
129
+ out = parser.push_str(text)
130
+ tail = parser.finish()
131
+ all_segments = out.extracted + tail.extracted
132
+
133
+ parts: list[str] = []
134
+ in_plan = False
135
+ for seg in all_segments:
136
+ if seg.type == ProposedPlanSegmentType.PLAN_START:
137
+ in_plan = True
138
+ elif seg.type == ProposedPlanSegmentType.PLAN_END:
139
+ in_plan = False
140
+ elif seg.type == ProposedPlanSegmentType.PLAN_DELTA and in_plan:
141
+ parts.append(seg.text)
142
+
143
+ return "".join(parts) if parts else None
144
+
145
+
146
+ def strip_proposed_plan_blocks(text: str) -> str:
147
+ """Remove all ``<proposed_plan>…</proposed_plan>`` blocks from text.
148
+
149
+ Args:
150
+ text: Full assistant response text.
151
+
152
+ Returns:
153
+ Visible text with plan blocks and their content stripped.
154
+ """
155
+ parser = ProposedPlanParser()
156
+ out = parser.push_str(text)
157
+ tail = parser.finish()
158
+ return out.visible_text + tail.visible_text
@@ -0,0 +1,23 @@
1
+ """Incremental parser result for one pushed chunk (or final flush)."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TypeVar
5
+
6
+ T = TypeVar("T")
7
+
8
+
9
+ @dataclass
10
+ class StreamTextChunk[T]:
11
+ """Result from feeding a text chunk to a StreamTextParser.
12
+
13
+ Attributes:
14
+ visible_text: Text safe to render immediately.
15
+ extracted: Hidden payloads extracted from the chunk.
16
+ """
17
+
18
+ visible_text: str = ""
19
+ extracted: list[T] = field(default_factory=list)
20
+
21
+ def is_empty(self) -> bool:
22
+ """Return True when no visible text or extracted payloads were produced."""
23
+ return not self.visible_text and not self.extracted
@@ -0,0 +1,27 @@
1
+ """Abstract base class for composable streaming text parsers."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TypeVar
5
+
6
+ from code_muse.stream_parser.stream_text_chunk import StreamTextChunk
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ class StreamTextParser[T](ABC):
12
+ """Base class for parsers that consume streamed text and emit visible text
13
+ plus extracted payloads.
14
+
15
+ Parsers are composable: one parser can wrap another, delegating and
16
+ merging output.
17
+ """
18
+
19
+ @abstractmethod
20
+ def push_str(self, chunk: str) -> StreamTextChunk[T]:
21
+ """Feed a new text chunk. Returns visible text + extracted payloads."""
22
+ ...
23
+
24
+ @abstractmethod
25
+ def finish(self) -> StreamTextChunk[T]:
26
+ """Flush any buffered state at end-of-stream (or end-of-item)."""
27
+ ...
@@ -0,0 +1,251 @@
1
+ # cython: language_level=3
2
+ """Line-based tag-block parser for streamed text.
3
+
4
+ A tag must appear alone on a line after trimming (e.g.
5
+ ``"<proposed_plan>"`` or ``"</proposed_plan>"``). Lines inside a tag block
6
+ are emitted as :class:`TaggedLineSegmentTagDelta`; lines outside are emitted as
7
+ :class:`TaggedLineSegmentNormal`.
8
+
9
+ The parser buffers text until it can disprove that the current partial line is
10
+ a tag line—once the trimmed prefix is no longer a prefix of any known open or
11
+ close delimiter, the line is emitted immediately so that visible text is not
12
+ held back unnecessarily.
13
+ """
14
+
15
+ from dataclasses import dataclass
16
+ from typing import Any
17
+
18
+
19
+ @dataclass
20
+ class TagSpec:
21
+ """Specification for a line-based tag block.
22
+
23
+ Attributes:
24
+ open: Exact opening line text (e.g. ``"<proposed_plan>"``).
25
+ close: Exact closing line text (e.g. ``"</proposed_plan>"``).
26
+ tag: Tag identifier emitted in segment results.
27
+ """
28
+
29
+ open: str
30
+ close: str
31
+ tag: Any
32
+
33
+
34
+ @dataclass
35
+ class TaggedLineSegmentNormal:
36
+ """Plain text that lives outside any tag block."""
37
+
38
+ text: str
39
+
40
+
41
+ @dataclass
42
+ class TaggedLineSegmentTagStart:
43
+ """Emitted when a line exactly matches a tag's ``open`` delimiter."""
44
+
45
+ tag: Any
46
+
47
+
48
+ @dataclass
49
+ class TaggedLineSegmentTagDelta:
50
+ """Text that belongs inside an open tag block.
51
+
52
+ Consecutive deltas with the same tag are coalesced by the parser.
53
+ """
54
+
55
+ tag: Any
56
+ text: str
57
+
58
+
59
+ @dataclass
60
+ class TaggedLineSegmentTagEnd:
61
+ """Emitted when a line exactly matches a tag's ``close`` delimiter."""
62
+
63
+ tag: Any
64
+
65
+
66
+ TaggedLineSegment = (
67
+ TaggedLineSegmentNormal
68
+ | TaggedLineSegmentTagStart
69
+ | TaggedLineSegmentTagDelta
70
+ | TaggedLineSegmentTagEnd
71
+ )
72
+
73
+
74
+ class TaggedLineParser:
75
+ """Streaming line-based tag-block parser.
76
+
77
+ * Buffers partial lines and emits them only once they can no longer be a
78
+ tag line (the trimmed prefix is not a prefix of any open or close).
79
+ * Complete lines (ending in ``\\n``) are classified immediately.
80
+ * Tag lines are stripped from visible output and replaced by
81
+ :class:`TaggedLineSegmentTagStart` / :class:`TaggedLineSegmentTagEnd`.
82
+ * Unterminated tag blocks are auto-closed at :meth:`finish`.
83
+ """
84
+
85
+ def __init__(self, specs: list[TagSpec]) -> None:
86
+ self.specs = specs
87
+ self._line_buffer: str = ""
88
+ self._active_tag: Any = None
89
+
90
+ def parse(self, delta: str) -> list[TaggedLineSegment]:
91
+ """Process a text delta and emit any newly-resolved segments.
92
+
93
+ The delta is appended to the internal line buffer, then every complete
94
+ line (up to and including ``\\n``) is classified. If the remaining
95
+ partial line can no longer become a tag line, it is emitted as well.
96
+
97
+ Args:
98
+ delta: Incoming text chunk (may contain zero or more newlines).
99
+
100
+ Returns:
101
+ List of segments produced by this delta.
102
+ """
103
+ cdef int newline_idx
104
+ cdef str line
105
+ cdef str buf
106
+ cdef list segments
107
+
108
+ self._line_buffer += delta
109
+ segments = []
110
+ buf = self._line_buffer
111
+
112
+ # Drain complete lines.
113
+ while True:
114
+ newline_idx = buf.find("\n")
115
+ if newline_idx == -1:
116
+ break
117
+ line = buf[: newline_idx + 1]
118
+ buf = buf[newline_idx + 1 :]
119
+ self._line_buffer = buf
120
+ self._finish_line(line, segments)
121
+
122
+ self._line_buffer = buf
123
+
124
+ # If the remaining partial line can never become a tag line,
125
+ # flush it immediately so it does not stall visible output.
126
+ if buf and not self._is_tag_prefix(buf.strip()):
127
+ self._push_text(buf, segments)
128
+ self._line_buffer = ""
129
+
130
+ return segments
131
+
132
+ def finish(self) -> list[TaggedLineSegment]:
133
+ """Flush any remaining buffered state.
134
+
135
+ If the pending partial line exactly matches an ``open`` or ``close``
136
+ delimiter, the appropriate boundary segment is emitted. Otherwise the
137
+ text is emitted as Normal or TagDelta (depending on whether a tag is
138
+ currently open). Finally, any still-open tag block is auto-closed with
139
+ a :class:`TaggedLineSegmentTagEnd`.
140
+
141
+ Returns:
142
+ List of final segments.
143
+ """
144
+ cdef list segments
145
+ cdef str slug
146
+ cdef object open_spec
147
+ cdef object close_spec
148
+
149
+ segments = []
150
+
151
+ if self._line_buffer:
152
+ slug = self._line_buffer.strip()
153
+ open_spec = self._match_open(slug)
154
+ if open_spec is not None:
155
+ segments.append(TaggedLineSegmentTagStart(open_spec.tag))
156
+ self._active_tag = open_spec.tag
157
+ else:
158
+ close_spec = self._match_close(slug)
159
+ if close_spec is not None:
160
+ segments.append(TaggedLineSegmentTagEnd(close_spec.tag))
161
+ self._active_tag = None
162
+ else:
163
+ self._push_text(self._line_buffer, segments)
164
+ self._line_buffer = ""
165
+
166
+ if self._active_tag is not None:
167
+ segments.append(TaggedLineSegmentTagEnd(self._active_tag))
168
+ self._active_tag = None
169
+
170
+ return segments
171
+
172
+ def _push_text(self, text: str, segments: list[TaggedLineSegment]) -> None:
173
+ """Emit ``text`` as the appropriate segment type, coalescing when possible.
174
+
175
+ If a tag is currently active, ``text`` becomes a
176
+ :class:`TaggedLineSegmentTagDelta`; otherwise it becomes a
177
+ :class:`TaggedLineSegmentNormal`. Consecutive segments of the same
178
+ type (and same tag, for deltas) are merged into one segment so the
179
+ output stays compact.
180
+ """
181
+ cdef object last
182
+ cdef object active = self._active_tag
183
+
184
+ if active is not None:
185
+ if segments:
186
+ last = segments[-1]
187
+ if isinstance(last, TaggedLineSegmentTagDelta) and last.tag == active:
188
+ last.text += text
189
+ return
190
+ segments.append(TaggedLineSegmentTagDelta(active, text))
191
+ else:
192
+ if segments:
193
+ last = segments[-1]
194
+ if isinstance(last, TaggedLineSegmentNormal):
195
+ last.text += text
196
+ return
197
+ segments.append(TaggedLineSegmentNormal(text))
198
+
199
+ def _finish_line(self, line: str, segments: list[TaggedLineSegment]) -> None:
200
+ """Classify a complete line (including its trailing ``\\n``).
201
+
202
+ The line is trimmed and checked against ``open`` / ``close``
203
+ delimiters in order. Tag lines are consumed entirely; non-tag lines
204
+ are forwarded to :meth:`_push_text`.
205
+ """
206
+ cdef str slug
207
+ cdef object open_spec
208
+ cdef object close_spec
209
+
210
+ slug = line.strip()
211
+ open_spec = self._match_open(slug)
212
+ if open_spec is not None:
213
+ segments.append(TaggedLineSegmentTagStart(open_spec.tag))
214
+ self._active_tag = open_spec.tag
215
+ return
216
+
217
+ close_spec = self._match_close(slug)
218
+ if close_spec is not None:
219
+ segments.append(TaggedLineSegmentTagEnd(close_spec.tag))
220
+ self._active_tag = None
221
+ return
222
+
223
+ self._push_text(line, segments)
224
+
225
+ def _is_tag_prefix(self, slug: str) -> bool:
226
+ """Return ``True`` if ``slug`` is a prefix of any ``open`` or ``close``."""
227
+ cdef object spec
228
+ cdef str open_str
229
+ cdef str close_str
230
+ for spec in self.specs:
231
+ open_str = spec.open
232
+ close_str = spec.close
233
+ if open_str.startswith(slug) or close_str.startswith(slug):
234
+ return True
235
+ return False
236
+
237
+ def _match_open(self, slug: str) -> TagSpec | None:
238
+ """Return the first spec whose ``open`` exactly equals ``slug``."""
239
+ cdef object spec
240
+ for spec in self.specs:
241
+ if spec.open == slug:
242
+ return spec
243
+ return None
244
+
245
+ def _match_close(self, slug: str) -> TagSpec | None:
246
+ """Return the first spec whose ``close`` exactly equals ``slug``."""
247
+ cdef object spec
248
+ for spec in self.specs:
249
+ if spec.close == slug:
250
+ return spec
251
+ return None