linhai 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. linhai-0.1.0/.gitea/scripts/check_any_object_type_annotations.py +194 -0
  2. linhai-0.1.0/.gitea/scripts/check_black_overformat.py +89 -0
  3. linhai-0.1.0/.gitea/scripts/check_branch_behind.sh +15 -0
  4. linhai-0.1.0/.gitea/scripts/check_cast_usage.py +167 -0
  5. linhai-0.1.0/.gitea/scripts/check_code_emoji.py +85 -0
  6. linhai-0.1.0/.gitea/scripts/check_commit_count.sh +34 -0
  7. linhai-0.1.0/.gitea/scripts/check_create_task_usage.py +156 -0
  8. linhai-0.1.0/.gitea/scripts/check_e2e_skip.py +121 -0
  9. linhai-0.1.0/.gitea/scripts/check_emoji.py +64 -0
  10. linhai-0.1.0/.gitea/scripts/check_empty_pr.sh +35 -0
  11. linhai-0.1.0/.gitea/scripts/check_git_diff.sh +66 -0
  12. linhai-0.1.0/.gitea/scripts/check_none_default_args.py +192 -0
  13. linhai-0.1.0/.gitea/scripts/check_pr_body.py +28 -0
  14. linhai-0.1.0/.gitea/scripts/check_pr_issue.py +91 -0
  15. linhai-0.1.0/.gitea/scripts/check_pr_tests.sh +29 -0
  16. linhai-0.1.0/.gitea/scripts/check_python_comments.py +143 -0
  17. linhai-0.1.0/.gitea/scripts/check_snapshot_docs.py +90 -0
  18. linhai-0.1.0/.gitea/scripts/test_check_scripts.py +330 -0
  19. linhai-0.1.0/.gitea/workflows/ci.yaml +274 -0
  20. linhai-0.1.0/.gitignore +12 -0
  21. linhai-0.1.0/.pylintrc +3 -0
  22. linhai-0.1.0/1031218.cast +1244 -0
  23. linhai-0.1.0/AGENTS.md +91 -0
  24. linhai-0.1.0/CODE_REQUIREMENTS.md +39 -0
  25. linhai-0.1.0/LICENSE +674 -0
  26. linhai-0.1.0/MESSAGE_DESIGN.md +143 -0
  27. linhai-0.1.0/PKG-INFO +33 -0
  28. linhai-0.1.0/PROJECT.md +48 -0
  29. linhai-0.1.0/README.md +111 -0
  30. linhai-0.1.0/README_zh-CN.md +104 -0
  31. linhai-0.1.0/app.diff +28 -0
  32. linhai-0.1.0/assets/demo.gif +0 -0
  33. linhai-0.1.0/assets/social-preview.jpg +0 -0
  34. linhai-0.1.0/assets/social-preview.xcf +0 -0
  35. linhai-0.1.0/demo.gif +0 -0
  36. linhai-0.1.0/demo_compressed.gif +0 -0
  37. linhai-0.1.0/docs/LETHAL_TRIFECTA.md +36 -0
  38. linhai-0.1.0/docs/QWEN35_CONTEXT_CACHE.md +263 -0
  39. linhai-0.1.0/docs/claude_constitution/article.md +258 -0
  40. linhai-0.1.0/docs/claude_constitution/constitution.md +3280 -0
  41. linhai-0.1.0/docs/claude_constitution/cybersecurity_summary.md +78 -0
  42. linhai-0.1.0/docs/deepseek_tokenizer.md +36 -0
  43. linhai-0.1.0/docs/openclaw-core-markdown/AGENTS.md +214 -0
  44. linhai-0.1.0/docs/openclaw-core-markdown/BOOTSTRAP.md +55 -0
  45. linhai-0.1.0/docs/openclaw-core-markdown/IDENTITY.md +18 -0
  46. linhai-0.1.0/docs/openclaw-core-markdown/SOUL.md +36 -0
  47. linhai-0.1.0/docs/openclaw-core-markdown/USER.md +17 -0
  48. linhai-0.1.0/docs/telegram/README.md +86 -0
  49. linhai-0.1.0/docs/telegram/TESTING.md +171 -0
  50. linhai-0.1.0/docs/telegram/test_botcommand.py +85 -0
  51. linhai-0.1.0/docs/telegram/test_callbackquery.py +197 -0
  52. linhai-0.1.0/docs/telegram/test_conversationhandler.py +173 -0
  53. linhai-0.1.0/docs/telegram/test_filters.py +221 -0
  54. linhai-0.1.0/docs/telegram/test_messagehandler.py +200 -0
  55. linhai-0.1.0/docs/tiktoken-docs.md +131 -0
  56. linhai-0.1.0/docs/tiktoken.md +37 -0
  57. linhai-0.1.0/e2e/conftest.py +139 -0
  58. linhai-0.1.0/e2e/machine_control/test_bash_host_concurrent_e2e.py +102 -0
  59. linhai-0.1.0/e2e/machine_control/test_bash_host_e2e.py +213 -0
  60. linhai-0.1.0/e2e/machine_control/test_bash_terminal_e2e.py +132 -0
  61. linhai-0.1.0/e2e/machine_control/test_posix_shell_e2e.py +108 -0
  62. linhai-0.1.0/e2e/machine_control/test_pty_connect_e2e.py +102 -0
  63. linhai-0.1.0/e2e/machine_control/test_trojan_concurrent_e2e.py +105 -0
  64. linhai-0.1.0/e2e/machine_control/test_trojan_pty.py +268 -0
  65. linhai-0.1.0/e2e/openai_toolcall/__init__.py +0 -0
  66. linhai-0.1.0/e2e/openai_toolcall/test_openai_toolcall_e2e.py +253 -0
  67. linhai-0.1.0/e2e/openai_toolcall/test_toolcall_tmux_e2e.py +133 -0
  68. linhai-0.1.0/e2e/test_agent_loop.py +153 -0
  69. linhai-0.1.0/e2e/test_conversation_restore.py +223 -0
  70. linhai-0.1.0/e2e/test_git_worktree.py +125 -0
  71. linhai-0.1.0/e2e/test_llm_connection.py +120 -0
  72. linhai-0.1.0/e2e/test_mcp_connection.py +149 -0
  73. linhai-0.1.0/e2e/test_mcp_server.py +39 -0
  74. linhai-0.1.0/e2e/test_openai_native_toolcall.py +138 -0
  75. linhai-0.1.0/e2e/test_process.py +291 -0
  76. linhai-0.1.0/e2e/test_process_toolset.py +221 -0
  77. linhai-0.1.0/e2e/test_smoke.py +15 -0
  78. linhai-0.1.0/e2e/test_terminal.py +60 -0
  79. linhai-0.1.0/e2e/test_tool_calling.py +138 -0
  80. linhai-0.1.0/e2e/test_user_plugins.py +138 -0
  81. linhai-0.1.0/e2e/test_webui.py +81 -0
  82. linhai-0.1.0/e2e/test_webui_api.py +184 -0
  83. linhai-0.1.0/e2e/test_webui_status_bar.py +125 -0
  84. linhai-0.1.0/e2e/test_webui_streaming.py +385 -0
  85. linhai-0.1.0/e2e/test_with_secret_split_e2e.py +234 -0
  86. linhai-0.1.0/flake.lock +276 -0
  87. linhai-0.1.0/flake.nix +132 -0
  88. linhai-0.1.0/linhai/__init__.py +3 -0
  89. linhai-0.1.0/linhai/__main__.py +10 -0
  90. linhai-0.1.0/linhai/agent/__init__.py +23 -0
  91. linhai-0.1.0/linhai/agent/answer.py +120 -0
  92. linhai-0.1.0/linhai/agent/callback_slot.py +74 -0
  93. linhai-0.1.0/linhai/agent/command_callback.py +177 -0
  94. linhai-0.1.0/linhai/agent/conversation.py +152 -0
  95. linhai-0.1.0/linhai/agent/conversation_save.py +58 -0
  96. linhai-0.1.0/linhai/agent/create.py +757 -0
  97. linhai-0.1.0/linhai/agent/lifecycle.py +185 -0
  98. linhai-0.1.0/linhai/agent/main.py +335 -0
  99. linhai-0.1.0/linhai/agent/message.py +467 -0
  100. linhai-0.1.0/linhai/agent/messages/__init__.py +16 -0
  101. linhai-0.1.0/linhai/agent/messages/compression.py +64 -0
  102. linhai-0.1.0/linhai/agent/messages/file_content.py +57 -0
  103. linhai-0.1.0/linhai/agent/messages/prompt.py +67 -0
  104. linhai-0.1.0/linhai/agent/messages/reasoning.py +63 -0
  105. linhai-0.1.0/linhai/agent/messages/runtime.py +31 -0
  106. linhai-0.1.0/linhai/agent/orchestration.py +797 -0
  107. linhai-0.1.0/linhai/agent/planning.py +88 -0
  108. linhai-0.1.0/linhai/agent/savable_state.py +8 -0
  109. linhai-0.1.0/linhai/agent/state_machine.py +108 -0
  110. linhai-0.1.0/linhai/agent/toolcall.py +578 -0
  111. linhai-0.1.0/linhai/agent/user_message_handler.py +44 -0
  112. linhai-0.1.0/linhai/agent/workflow.py +275 -0
  113. linhai-0.1.0/linhai/base.py +541 -0
  114. linhai-0.1.0/linhai/cl100k_base.tiktoken +100256 -0
  115. linhai-0.1.0/linhai/config.py +506 -0
  116. linhai-0.1.0/linhai/context_statistics.py +314 -0
  117. linhai-0.1.0/linhai/cron.py +168 -0
  118. linhai-0.1.0/linhai/exceptions.py +19 -0
  119. linhai-0.1.0/linhai/init/__init__.py +14 -0
  120. linhai-0.1.0/linhai/init/app.py +160 -0
  121. linhai-0.1.0/linhai/init/config_writer.py +178 -0
  122. linhai-0.1.0/linhai/init/widgets.py +118 -0
  123. linhai-0.1.0/linhai/llm.py +642 -0
  124. linhai-0.1.0/linhai/llm_manager.py +351 -0
  125. linhai-0.1.0/linhai/machine_control/__init__.py +15 -0
  126. linhai-0.1.0/linhai/machine_control/bash_host/__init__.py +5 -0
  127. linhai-0.1.0/linhai/machine_control/bash_host/bash_host.py +449 -0
  128. linhai-0.1.0/linhai/machine_control/bash_host/file.py +234 -0
  129. linhai-0.1.0/linhai/machine_control/bash_host/http.py +162 -0
  130. linhai-0.1.0/linhai/machine_control/bash_host/process.py +139 -0
  131. linhai-0.1.0/linhai/machine_control/bash_host/terminal.py +200 -0
  132. linhai-0.1.0/linhai/machine_control/ether_ghost_host/__init__.py +5 -0
  133. linhai-0.1.0/linhai/machine_control/ether_ghost_host/ether_ghost_host.py +317 -0
  134. linhai-0.1.0/linhai/machine_control/http_message.py +127 -0
  135. linhai-0.1.0/linhai/machine_control/main.py +496 -0
  136. linhai-0.1.0/linhai/machine_control/master_host/__init__.py +49 -0
  137. linhai-0.1.0/linhai/machine_control/master_host/file.py +462 -0
  138. linhai-0.1.0/linhai/machine_control/master_host/http.py +53 -0
  139. linhai-0.1.0/linhai/machine_control/master_host/master_host.py +361 -0
  140. linhai-0.1.0/linhai/machine_control/master_host/process.py +363 -0
  141. linhai-0.1.0/linhai/machine_control/master_host/terminal.py +286 -0
  142. linhai-0.1.0/linhai/machine_control/master_host/tmux_terminal.py +145 -0
  143. linhai-0.1.0/linhai/machine_control/plugin.py +88 -0
  144. linhai-0.1.0/linhai/machine_control/posix_shell/__init__.py +5 -0
  145. linhai-0.1.0/linhai/machine_control/posix_shell/posix_shell_control.py +406 -0
  146. linhai-0.1.0/linhai/machine_control/posix_shell/process.py +92 -0
  147. linhai-0.1.0/linhai/machine_control/process.py +87 -0
  148. linhai-0.1.0/linhai/machine_control/protocol.py +106 -0
  149. linhai-0.1.0/linhai/machine_control/tools.py +1040 -0
  150. linhai-0.1.0/linhai/machine_control/trojan/__init__.py +1 -0
  151. linhai-0.1.0/linhai/machine_control/trojan/shell_transport.py +218 -0
  152. linhai-0.1.0/linhai/machine_control/trojan/transport.py +167 -0
  153. linhai-0.1.0/linhai/machine_control/trojan/trojan.py +828 -0
  154. linhai-0.1.0/linhai/main.py +263 -0
  155. linhai-0.1.0/linhai/markdown_parser.py +217 -0
  156. linhai-0.1.0/linhai/multimodal.py +341 -0
  157. linhai-0.1.0/linhai/parsed_message.py +397 -0
  158. linhai-0.1.0/linhai/plugin/__init__.py +123 -0
  159. linhai-0.1.0/linhai/plugin/afk_plugin.py +39 -0
  160. linhai-0.1.0/linhai/plugin/catgirl_tone.py +64 -0
  161. linhai-0.1.0/linhai/plugin/claw.py +144 -0
  162. linhai-0.1.0/linhai/plugin/command_hints.py +319 -0
  163. linhai-0.1.0/linhai/plugin/file_operations.py +535 -0
  164. linhai-0.1.0/linhai/plugin/file_permission_plugin.py +81 -0
  165. linhai-0.1.0/linhai/plugin/helpers.py +85 -0
  166. linhai-0.1.0/linhai/plugin/message_checkers.py +768 -0
  167. linhai-0.1.0/linhai/plugin/planning.py +484 -0
  168. linhai-0.1.0/linhai/plugin/python_chore.py +133 -0
  169. linhai-0.1.0/linhai/plugin/reminder.py +128 -0
  170. linhai-0.1.0/linhai/plugin/security_config.py +268 -0
  171. linhai-0.1.0/linhai/plugin/stdio_command_checker.py +193 -0
  172. linhai-0.1.0/linhai/plugin/sudo_stdio_checker.py +115 -0
  173. linhai-0.1.0/linhai/plugin/system_message_leaning.py +88 -0
  174. linhai-0.1.0/linhai/plugin/telegram.py +285 -0
  175. linhai-0.1.0/linhai/plugin/tool_call_managers.py +369 -0
  176. linhai-0.1.0/linhai/plugin/user_reminder.py +41 -0
  177. linhai-0.1.0/linhai/prompt.py +1954 -0
  178. linhai-0.1.0/linhai/registry.py +123 -0
  179. linhai-0.1.0/linhai/sandbox.py +130 -0
  180. linhai-0.1.0/linhai/secret.py +512 -0
  181. linhai-0.1.0/linhai/task_supervisor.py +106 -0
  182. linhai-0.1.0/linhai/telegram.py +228 -0
  183. linhai-0.1.0/linhai/token_manager.py +208 -0
  184. linhai-0.1.0/linhai/tool/__init__.py +35 -0
  185. linhai-0.1.0/linhai/tool/base.py +474 -0
  186. linhai-0.1.0/linhai/tool/general.py +221 -0
  187. linhai-0.1.0/linhai/tool/main.py +198 -0
  188. linhai-0.1.0/linhai/tool/mcp_connector.py +288 -0
  189. linhai-0.1.0/linhai/tool/mcp_server_example.py +42 -0
  190. linhai-0.1.0/linhai/tool/search.py +175 -0
  191. linhai-0.1.0/linhai/tui/__init__.py +23 -0
  192. linhai-0.1.0/linhai/tui/app.py +319 -0
  193. linhai-0.1.0/linhai/tui/components.py +1304 -0
  194. linhai-0.1.0/linhai/tui/context_tab.py +412 -0
  195. linhai-0.1.0/linhai/tui/messages_list.py +284 -0
  196. linhai-0.1.0/linhai/tui/planning_tab.py +83 -0
  197. linhai-0.1.0/linhai/tui/process_tab.py +208 -0
  198. linhai-0.1.0/linhai/type_hints.py +170 -0
  199. linhai-0.1.0/linhai/utils/__init__.py +0 -0
  200. linhai-0.1.0/linhai/utils/common.py +191 -0
  201. linhai-0.1.0/linhai/utils/i18n.py +11 -0
  202. linhai-0.1.0/linhai/utils/input_parser.py +42 -0
  203. linhai-0.1.0/linhai/utils/jsonpubsub.py +216 -0
  204. linhai-0.1.0/linhai/utils/pulse_encoding.py +137 -0
  205. linhai-0.1.0/linhai/utils/streamjson.py +397 -0
  206. linhai-0.1.0/linhai/utils/token_parser.py +156 -0
  207. linhai-0.1.0/linhai/utils/tokenizer.py +120 -0
  208. linhai-0.1.0/linhai/webui/__init__.py +6 -0
  209. linhai-0.1.0/linhai/webui/agent_manager.py +393 -0
  210. linhai-0.1.0/linhai/webui/app.py +66 -0
  211. linhai-0.1.0/linhai/webui/routes.py +295 -0
  212. linhai-0.1.0/linhai/webui/schemas.py +155 -0
  213. linhai-0.1.0/pyproject.toml +61 -0
  214. linhai-0.1.0/requirements.lock +72 -0
  215. linhai-0.1.0/test_webui_messages.py +389 -0
  216. linhai-0.1.0/tests/__init__.py +1 -0
  217. linhai-0.1.0/tests/machine_control/__init__.py +0 -0
  218. linhai-0.1.0/tests/machine_control/ether_ghost_host/test_ether_ghost_host.py +217 -0
  219. linhai-0.1.0/tests/machine_control/test_bash_file.py +378 -0
  220. linhai-0.1.0/tests/machine_control/test_bash_host.py +453 -0
  221. linhai-0.1.0/tests/machine_control/test_bash_http.py +175 -0
  222. linhai-0.1.0/tests/machine_control/test_bash_terminal.py +219 -0
  223. linhai-0.1.0/tests/machine_control/test_env_parameter.py +182 -0
  224. linhai-0.1.0/tests/machine_control/test_heartbeat.py +253 -0
  225. linhai-0.1.0/tests/machine_control/test_http_message.py +251 -0
  226. linhai-0.1.0/tests/machine_control/test_local_process.py +122 -0
  227. linhai-0.1.0/tests/machine_control/test_posix_shell.py +94 -0
  228. linhai-0.1.0/tests/machine_control/test_pty_parameter.py +19 -0
  229. linhai-0.1.0/tests/machine_control/test_trojan_pty_e2e.py +119 -0
  230. linhai-0.1.0/tests/machine_control/trojan/test_shell_transport.py +295 -0
  231. linhai-0.1.0/tests/real_mcp_server.py +44 -0
  232. linhai-0.1.0/tests/test_afk_param.py +211 -0
  233. linhai-0.1.0/tests/test_agent.py +490 -0
  234. linhai-0.1.0/tests/test_agent_answer.py +157 -0
  235. linhai-0.1.0/tests/test_agent_at_system.py +178 -0
  236. linhai-0.1.0/tests/test_agent_build_context.py +387 -0
  237. linhai-0.1.0/tests/test_agent_global_memory.py +75 -0
  238. linhai-0.1.0/tests/test_agent_lifecycle.py +313 -0
  239. linhai-0.1.0/tests/test_agent_main.py +60 -0
  240. linhai-0.1.0/tests/test_agent_marker.py +376 -0
  241. linhai-0.1.0/tests/test_agent_message.py +197 -0
  242. linhai-0.1.0/tests/test_agent_orchestration.py +886 -0
  243. linhai-0.1.0/tests/test_agent_plugin.py +958 -0
  244. linhai-0.1.0/tests/test_agent_workflow.py +604 -0
  245. linhai-0.1.0/tests/test_answer_truncate.py +159 -0
  246. linhai-0.1.0/tests/test_binary.zip +0 -0
  247. linhai-0.1.0/tests/test_catgirl_tone.py +113 -0
  248. linhai-0.1.0/tests/test_claw.py +175 -0
  249. linhai-0.1.0/tests/test_claw_heartbeat.py +133 -0
  250. linhai-0.1.0/tests/test_command_completion.py +148 -0
  251. linhai-0.1.0/tests/test_command_handler.py +226 -0
  252. linhai-0.1.0/tests/test_command_whitelist.py +168 -0
  253. linhai-0.1.0/tests/test_config.py +722 -0
  254. linhai-0.1.0/tests/test_config.toml +18 -0
  255. linhai-0.1.0/tests/test_config_mcp.py +144 -0
  256. linhai-0.1.0/tests/test_connect_posix_shell_as_machine.py +413 -0
  257. linhai-0.1.0/tests/test_context_tab.py +694 -0
  258. linhai-0.1.0/tests/test_conversation_save.py +129 -0
  259. linhai-0.1.0/tests/test_conversation_system.py +160 -0
  260. linhai-0.1.0/tests/test_create.py +694 -0
  261. linhai-0.1.0/tests/test_create_agent.py +142 -0
  262. linhai-0.1.0/tests/test_create_agent_mcp.py +175 -0
  263. linhai-0.1.0/tests/test_create_command_whitelist.py +164 -0
  264. linhai-0.1.0/tests/test_create_integration.py +74 -0
  265. linhai-0.1.0/tests/test_cron.py +256 -0
  266. linhai-0.1.0/tests/test_current_directory.py +47 -0
  267. linhai-0.1.0/tests/test_dummy_tools_migration.py +86 -0
  268. linhai-0.1.0/tests/test_dynamic_connection_restore.py +251 -0
  269. linhai-0.1.0/tests/test_dynamic_file_content_message.py +104 -0
  270. linhai-0.1.0/tests/test_end_think_plugin.py +96 -0
  271. linhai-0.1.0/tests/test_enter_key_clear_input.py +203 -0
  272. linhai-0.1.0/tests/test_exit_tool_functionality.py +85 -0
  273. linhai-0.1.0/tests/test_explicit_cache_config.py +218 -0
  274. linhai-0.1.0/tests/test_extract_usage.py +147 -0
  275. linhai-0.1.0/tests/test_file_permission_plugin.py +144 -0
  276. linhai-0.1.0/tests/test_file_read_write_conflict_plugin.py +330 -0
  277. linhai-0.1.0/tests/test_file_tools.py +285 -0
  278. linhai-0.1.0/tests/test_footer_widget.py +91 -0
  279. linhai-0.1.0/tests/test_glm_insult_mask_plugin.py +210 -0
  280. linhai-0.1.0/tests/test_glm_thinking.py +77 -0
  281. linhai-0.1.0/tests/test_glm_tool_call_plugin.py +114 -0
  282. linhai-0.1.0/tests/test_global_memory_config.py +255 -0
  283. linhai-0.1.0/tests/test_helpers.py +88 -0
  284. linhai-0.1.0/tests/test_http_request.py +421 -0
  285. linhai-0.1.0/tests/test_i18n.py +444 -0
  286. linhai-0.1.0/tests/test_init_app_textual.py +92 -0
  287. linhai-0.1.0/tests/test_init_config_writer.py +230 -0
  288. linhai-0.1.0/tests/test_input_parser.py +75 -0
  289. linhai-0.1.0/tests/test_issue_10_multiple_messages.py +142 -0
  290. linhai-0.1.0/tests/test_json_serialization.py +103 -0
  291. linhai-0.1.0/tests/test_jsonpubsub_generation.py +114 -0
  292. linhai-0.1.0/tests/test_kimi_k25_tool_call_plugin.py +77 -0
  293. linhai-0.1.0/tests/test_large_message_marking.py +138 -0
  294. linhai-0.1.0/tests/test_lifecycle_callbacks.py +94 -0
  295. linhai-0.1.0/tests/test_llm_manager.py +510 -0
  296. linhai-0.1.0/tests/test_llm_switching.py +186 -0
  297. linhai-0.1.0/tests/test_llm_token_usage.py +206 -0
  298. linhai-0.1.0/tests/test_machine_control.py +1129 -0
  299. linhai-0.1.0/tests/test_machine_control_task_supervisor.py +224 -0
  300. linhai-0.1.0/tests/test_main.py +353 -0
  301. linhai-0.1.0/tests/test_markdown_parser.py +136 -0
  302. linhai-0.1.0/tests/test_master_host_sandbox.py +116 -0
  303. linhai-0.1.0/tests/test_max_toolcall_token_in_round_float.py +117 -0
  304. linhai-0.1.0/tests/test_mcp_real_server.py +105 -0
  305. linhai-0.1.0/tests/test_message_collapse.py +375 -0
  306. linhai-0.1.0/tests/test_messages_list_serialize.py +274 -0
  307. linhai-0.1.0/tests/test_minimax_tool_call_plugin.py +107 -0
  308. linhai-0.1.0/tests/test_misplaced_tool_call_plugin.py +97 -0
  309. linhai-0.1.0/tests/test_missing_with_secret_warning.py +165 -0
  310. linhai-0.1.0/tests/test_multimodal.py +312 -0
  311. linhai-0.1.0/tests/test_multimodal_manager.py +169 -0
  312. linhai-0.1.0/tests/test_normal_content_widget.py +175 -0
  313. linhai-0.1.0/tests/test_notification_message_plugin.py +60 -0
  314. linhai-0.1.0/tests/test_only_reasoning_plugin.py +254 -0
  315. linhai-0.1.0/tests/test_openai_timeout_config.py +69 -0
  316. linhai-0.1.0/tests/test_openai_toolcall_execution.py +182 -0
  317. linhai-0.1.0/tests/test_openai_toolcall_feeder.py +138 -0
  318. linhai-0.1.0/tests/test_openai_toolcalls.py +237 -0
  319. linhai-0.1.0/tests/test_override_toolsets.py +297 -0
  320. linhai-0.1.0/tests/test_parsed_message.py +510 -0
  321. linhai-0.1.0/tests/test_pinned_messages.py +391 -0
  322. linhai-0.1.0/tests/test_pkill_checker.py +90 -0
  323. linhai-0.1.0/tests/test_planning_integration.py +349 -0
  324. linhai-0.1.0/tests/test_planning_message.py +57 -0
  325. linhai-0.1.0/tests/test_planning_plugins.py +774 -0
  326. linhai-0.1.0/tests/test_planning_tab.py +358 -0
  327. linhai-0.1.0/tests/test_posix_shell_terminal.py +330 -0
  328. linhai-0.1.0/tests/test_process_argv_checker.py +195 -0
  329. linhai-0.1.0/tests/test_process_tab.py +603 -0
  330. linhai-0.1.0/tests/test_prompt_fast_agent_plugin.py +209 -0
  331. linhai-0.1.0/tests/test_python_chore_plugin.py +316 -0
  332. linhai-0.1.0/tests/test_queue_interrupt.py +141 -0
  333. linhai-0.1.0/tests/test_rainbow_ascii_art.py +133 -0
  334. linhai-0.1.0/tests/test_reasoning_content_widget.py +374 -0
  335. linhai-0.1.0/tests/test_registry_cleanup.py +69 -0
  336. linhai-0.1.0/tests/test_reminder.py +252 -0
  337. linhai-0.1.0/tests/test_runtime_imitation_plugin.py +150 -0
  338. linhai-0.1.0/tests/test_sandbox.py +313 -0
  339. linhai-0.1.0/tests/test_secret.py +784 -0
  340. linhai-0.1.0/tests/test_secret_call_with_secret.py +293 -0
  341. linhai-0.1.0/tests/test_secret_exception_leak.py +295 -0
  342. linhai-0.1.0/tests/test_secret_initialize.py +64 -0
  343. linhai-0.1.0/tests/test_secret_intercept_file.py +197 -0
  344. linhai-0.1.0/tests/test_sed_fragmented_read_plugin.py +382 -0
  345. linhai-0.1.0/tests/test_serialize_restore_system_agent.py +165 -0
  346. linhai-0.1.0/tests/test_sleeping_state.py +275 -0
  347. linhai-0.1.0/tests/test_split_and_save_large_output.py +116 -0
  348. linhai-0.1.0/tests/test_state_machine.py +153 -0
  349. linhai-0.1.0/tests/test_stdio_command_checker.py +172 -0
  350. linhai-0.1.0/tests/test_streamjson.py +161 -0
  351. linhai-0.1.0/tests/test_sudo_stdio_checker.py +121 -0
  352. linhai-0.1.0/tests/test_system_message.py +187 -0
  353. linhai-0.1.0/tests/test_system_message_leaning.py +59 -0
  354. linhai-0.1.0/tests/test_task_supervisor.py +166 -0
  355. linhai-0.1.0/tests/test_telegram_plugin.py +400 -0
  356. linhai-0.1.0/tests/test_telegram_sticker_message.py +329 -0
  357. linhai-0.1.0/tests/test_terminal_backend.py +98 -0
  358. linhai-0.1.0/tests/test_terminal_toolset.py +80 -0
  359. linhai-0.1.0/tests/test_tmux_terminal.py +146 -0
  360. linhai-0.1.0/tests/test_todolist_checker_plugin.py +229 -0
  361. linhai-0.1.0/tests/test_token_cache.py +128 -0
  362. linhai-0.1.0/tests/test_token_manager.py +62 -0
  363. linhai-0.1.0/tests/test_token_usage_integration.py +153 -0
  364. linhai-0.1.0/tests/test_tokenizer.py +59 -0
  365. linhai-0.1.0/tests/test_tool_call_in_reasoning_plugin.py +198 -0
  366. linhai-0.1.0/tests/test_tool_call_managers.py +138 -0
  367. linhai-0.1.0/tests/test_tool_call_widget.py +235 -0
  368. linhai-0.1.0/tests/test_toolcall.py +332 -0
  369. linhai-0.1.0/tests/test_toolcall_collapse.py +259 -0
  370. linhai-0.1.0/tests/test_toolcall_error.py +56 -0
  371. linhai-0.1.0/tests/test_toolcall_token_management.py +335 -0
  372. linhai-0.1.0/tests/test_tui_tabs.py +144 -0
  373. linhai-0.1.0/tests/test_tui_theme.py +123 -0
  374. linhai-0.1.0/tests/test_two_step_compression.py +539 -0
  375. linhai-0.1.0/tests/test_unnecessary_run_command_plugin.py +305 -0
  376. linhai-0.1.0/tests/test_unnecessary_sed_read_plugin.py +1128 -0
  377. linhai-0.1.0/tests/test_user_plugins.py +163 -0
  378. linhai-0.1.0/tests/test_user_reminder.py +122 -0
  379. linhai-0.1.0/tests/test_utils.py +150 -0
  380. linhai-0.1.0/tests/test_volcano_deepseek_fix.py +112 -0
  381. linhai-0.1.0/tests/test_volcano_deepseek_fix_plugin.py +165 -0
  382. linhai-0.1.0/tests/test_waiting_user_reminder.py +98 -0
  383. linhai-0.1.0/tests/test_webui.py +722 -0
  384. linhai-0.1.0/tests/test_workflow_message_prepare.py +220 -0
  385. linhai-0.1.0/tests/tool/test_file_validation.py +156 -0
  386. linhai-0.1.0/tests/tool/test_http_request.py +369 -0
  387. linhai-0.1.0/tests/tool/test_http_tools.py +282 -0
  388. linhai-0.1.0/tests/tool/test_json_schema.py +198 -0
  389. linhai-0.1.0/tests/tool/test_mcp_connector.py +327 -0
  390. linhai-0.1.0/tests/tool/test_process_tools.py +186 -0
  391. linhai-0.1.0/tests/tool/test_terminal.py +118 -0
  392. linhai-0.1.0/tests/tool/test_tool_functions.py +62 -0
  393. linhai-0.1.0/tests/tool/test_tool_i18n.py +129 -0
  394. linhai-0.1.0/tests/tool/test_tool_manager.py +251 -0
  395. linhai-0.1.0/tests/tool/test_tool_result_message.py +162 -0
  396. linhai-0.1.0/uv.lock +2235 -0
@@ -0,0 +1,194 @@
1
+ import sys
2
+ import os
3
+ import subprocess
4
+ import ast
5
+ from typing import List, Set, Tuple, Optional, Dict
6
+
7
+
8
+ def get_any_aliases(tree: ast.AST) -> Set[str]:
9
+ """Extract all aliases for typing.Any from import statements."""
10
+ aliases: Set[str] = {"Any"}
11
+ typing_module_names: Set[str] = set()
12
+
13
+ for node in ast.walk(tree):
14
+ if isinstance(node, ast.ImportFrom):
15
+ if node.module == "typing":
16
+ for alias in node.names:
17
+ if alias.name == "Any":
18
+ if alias.asname:
19
+ aliases.add(alias.asname)
20
+ elif node.module and node.module.startswith("typing."):
21
+ for alias in node.names:
22
+ if alias.name == "Any":
23
+ if alias.asname:
24
+ aliases.add(alias.asname)
25
+ elif isinstance(node, ast.Import):
26
+ for alias in node.names:
27
+ if alias.name == "typing" or alias.name.startswith("typing."):
28
+ typing_module_names.add(alias.asname or alias.name)
29
+ print(aliases)
30
+ return aliases
31
+
32
+
33
+ def get_changed_python_files(base_ref: str = "origin/main") -> List[str]:
34
+ try:
35
+ check_cmd = ["git", "rev-parse", "--verify", base_ref]
36
+ if subprocess.run(check_cmd, capture_output=True).returncode != 0:
37
+ base_ref = "HEAD^"
38
+
39
+ cmd = [
40
+ "git",
41
+ "diff",
42
+ "--name-only",
43
+ "--diff-filter=ACMR",
44
+ base_ref,
45
+ "HEAD",
46
+ "--",
47
+ "linhai/",
48
+ ]
49
+ output = subprocess.check_output(
50
+ cmd, universal_newlines=True, stderr=subprocess.DEVNULL
51
+ ).strip()
52
+
53
+ if not output:
54
+ return []
55
+
56
+ files = output.split("\n")
57
+ return [f for f in files if f.endswith(".py") and os.path.exists(f)]
58
+ except subprocess.CalledProcessError:
59
+ return []
60
+
61
+
62
+ def is_any_or_object_annotation(
63
+ node: ast.AST, any_aliases: Set[str]
64
+ ) -> Tuple[bool, str]:
65
+ if isinstance(node, ast.Name):
66
+ if node.id in any_aliases:
67
+ if node.id == "Any":
68
+ return True, "Any"
69
+ else:
70
+ return True, f"Any (imported as '{node.id}')"
71
+ if node.id == "object":
72
+ return True, "object"
73
+ elif isinstance(node, ast.Attribute):
74
+ if (
75
+ node.attr == "Any"
76
+ and isinstance(node.value, ast.Name)
77
+ and node.value.id == "typing"
78
+ ):
79
+ return True, "typing.Any"
80
+ if (
81
+ node.attr == "Object"
82
+ and isinstance(node.value, ast.Name)
83
+ and node.value.id == "typing"
84
+ ):
85
+ return True, "typing.Object"
86
+ return False, ""
87
+
88
+
89
+ def check_function_args(
90
+ node: ast.FunctionDef, violations: List[Tuple[int, str, str]], any_aliases: Set[str]
91
+ ) -> None:
92
+ for arg in node.args.args:
93
+ if arg.annotation:
94
+ is_invalid, type_name = is_any_or_object_annotation(
95
+ arg.annotation, any_aliases
96
+ )
97
+ if is_invalid:
98
+ violations.append((arg.lineno, arg.arg, type_name))
99
+
100
+ for arg in node.args.kwonlyargs:
101
+ if arg.annotation:
102
+ is_invalid, type_name = is_any_or_object_annotation(
103
+ arg.annotation, any_aliases
104
+ )
105
+ if is_invalid:
106
+ violations.append((arg.lineno, arg.arg, type_name))
107
+
108
+ if node.args.vararg and node.args.vararg.annotation:
109
+ is_invalid, type_name = is_any_or_object_annotation(
110
+ node.args.vararg.annotation, any_aliases
111
+ )
112
+ if is_invalid:
113
+ violations.append(
114
+ (node.args.vararg.lineno, node.args.vararg.arg, type_name)
115
+ )
116
+
117
+ if node.args.kwarg and node.args.kwarg.annotation:
118
+ is_invalid, type_name = is_any_or_object_annotation(
119
+ node.args.kwarg.annotation, any_aliases
120
+ )
121
+ if is_invalid:
122
+ violations.append((node.args.kwarg.lineno, node.args.kwarg.arg, type_name))
123
+
124
+
125
+ def check_any_object_in_file(file_path: str) -> List[Tuple[int, str, str]]:
126
+ violations = []
127
+
128
+ try:
129
+ with open(file_path, "r", encoding="utf-8") as f:
130
+ content = f.read()
131
+ tree = ast.parse(content, filename=file_path)
132
+ except (SyntaxError, UnicodeDecodeError) as e:
133
+ print(f"WARNING: Failed to parse {file_path}: {e}")
134
+ return violations
135
+
136
+ any_aliases = get_any_aliases(tree)
137
+
138
+ for node in ast.walk(tree):
139
+ if isinstance(node, ast.FunctionDef):
140
+ check_function_args(node, violations, any_aliases)
141
+ elif isinstance(node, ast.AsyncFunctionDef):
142
+ check_function_args(node, violations, any_aliases)
143
+
144
+ return violations
145
+
146
+
147
+ def main() -> None:
148
+ base_ref = os.environ.get("BASE_REF")
149
+ if base_ref:
150
+ base_ref = f"origin/{base_ref}"
151
+ else:
152
+ base_ref = "origin/main"
153
+
154
+ python_files = get_changed_python_files(base_ref)
155
+
156
+ if not python_files:
157
+ print("No Python files changed in linhai/ directory.")
158
+ sys.exit(0)
159
+
160
+ print(
161
+ f"Checking {len(python_files)} changed Python file(s) for Any/object type annotations..."
162
+ )
163
+
164
+ errors_found = False
165
+ for file_path in python_files:
166
+ violations = check_any_object_in_file(file_path)
167
+ if violations:
168
+ errors_found = True
169
+ print(f"\nERROR: Any/object type annotations found in {file_path}:")
170
+ for line, arg_name, type_name in violations:
171
+ print(
172
+ f" Line {line}: Parameter '{arg_name}' has type annotation '{type_name}'"
173
+ )
174
+
175
+ if errors_found:
176
+ print(
177
+ "\nERROR: Any or object type annotations detected in function parameters."
178
+ )
179
+ print("Using type aliases (e.g., 'from typing import Any as XXX') to bypass")
180
+ print("detection is FORBIDDEN and violates the code standards.")
181
+ print("\nPlease use specific types instead of Any or object.")
182
+ print("Examples:")
183
+ print(" def foo(x: int) -> str: # Good")
184
+ print(" def bar(x: Optional[int]) -> None: # Good")
185
+ print(" def baz(x: Any) -> None: # BAD")
186
+ print(" def qux(x: XXX) -> None: # BAD (where XXX is an alias for Any)")
187
+ sys.exit(1)
188
+ else:
189
+ print("SUCCESS: No Any/object type annotations found in changed Python files.")
190
+ sys.exit(0)
191
+
192
+
193
+ if __name__ == "__main__":
194
+ main()
@@ -0,0 +1,89 @@
1
+ import sys
2
+ import subprocess
3
+ import tempfile
4
+ import os
5
+
6
+
7
+ def get_changed_py_files(base_ref="origin/main"):
8
+ try:
9
+ check_cmd = ["git", "rev-parse", "--verify", base_ref]
10
+ if subprocess.run(check_cmd, capture_output=True).returncode != 0:
11
+ base_ref = "HEAD^"
12
+ cmd = ["git", "diff", "--name-only", base_ref, "HEAD", "--", "linhai/"]
13
+ output = subprocess.check_output(
14
+ cmd, universal_newlines=True, stderr=subprocess.DEVNULL
15
+ )
16
+ files = [f for f in output.strip().split("\n") if f and f.endswith(".py")]
17
+ return files
18
+ except subprocess.CalledProcessError:
19
+ return []
20
+
21
+
22
+ def check_black_overformat(file_path):
23
+ try:
24
+ with open(file_path, "r", encoding="utf-8") as f:
25
+ original = f.read()
26
+
27
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as tmp:
28
+ tmp.write(original)
29
+ tmp_path = tmp.name
30
+
31
+ cmd = ["uv", "run", "black", "--quiet", tmp_path]
32
+ subprocess.run(cmd, capture_output=True, check=True)
33
+
34
+ with open(tmp_path, "r", encoding="utf-8") as f:
35
+ formatted = f.read()
36
+
37
+ os.unlink(tmp_path)
38
+
39
+ if original != formatted:
40
+ original_lines = original.split("\n")
41
+ formatted_lines = formatted.split("\n")
42
+
43
+ diff_count = 0
44
+ for i, (orig, fmt) in enumerate(zip(original_lines, formatted_lines)):
45
+ if orig != fmt:
46
+ stripped_orig = orig.rstrip()
47
+ stripped_fmt = fmt.rstrip()
48
+ if stripped_orig == stripped_fmt:
49
+ diff_count += 1
50
+
51
+ if diff_count > len(original_lines) * 0.3:
52
+ return True
53
+ return False
54
+ except Exception:
55
+ return False
56
+
57
+
58
+ def main():
59
+ base_ref = "origin/main"
60
+ try:
61
+ check_cmd = ["git", "rev-parse", "--verify", base_ref]
62
+ if subprocess.run(check_cmd, capture_output=True).returncode != 0:
63
+ base_ref = "HEAD^"
64
+ except Exception:
65
+ base_ref = "HEAD^"
66
+
67
+ files = get_changed_py_files(base_ref)
68
+ if not files:
69
+ print("No Python files changed in linhai/ directory.")
70
+ sys.exit(0)
71
+
72
+ errors = []
73
+ for file_path in files:
74
+ if check_black_overformat(file_path):
75
+ errors.append(file_path)
76
+
77
+ if errors:
78
+ print("ERROR: Black appears to be overformatting these files:")
79
+ for err in errors:
80
+ print(f" {err}")
81
+ print("\nPlease check if black is making excessive formatting changes.")
82
+ sys.exit(1)
83
+ else:
84
+ print("SUCCESS: No black overformatting detected.")
85
+ sys.exit(0)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,15 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ if [ -z "${CI:-}" ]; then
5
+ echo "Not running in CI, skipping branch behind check."
6
+ exit 0
7
+ fi
8
+
9
+ if git merge-base --is-ancestor origin/main HEAD 2>/dev/null; then
10
+ echo "Branch is up to date with main."
11
+ else
12
+ echo "ERROR: Branch is behind or diverged from main."
13
+ echo "Please rebase your branch onto main: git fetch upstream && git rebase upstream/main"
14
+ exit 1
15
+ fi
@@ -0,0 +1,167 @@
1
+ import sys
2
+ import os
3
+ import subprocess
4
+ import ast
5
+ from typing import List, Set, Tuple, Optional
6
+
7
+
8
+ def get_changed_python_files(base_ref: str = "origin/main") -> List[str]:
9
+ """
10
+ Get list of changed Python files in the diff.
11
+ """
12
+ try:
13
+ # Check if base_ref exists, fallback to HEAD^
14
+ check_cmd = ["git", "rev-parse", "--verify", base_ref]
15
+ if subprocess.run(check_cmd, capture_output=True).returncode != 0:
16
+ base_ref = "HEAD^"
17
+
18
+ cmd = [
19
+ "git",
20
+ "diff",
21
+ "--name-only",
22
+ "--diff-filter=d",
23
+ base_ref,
24
+ "HEAD",
25
+ "--",
26
+ "linhai/",
27
+ ]
28
+ output = subprocess.check_output(
29
+ cmd, universal_newlines=True, stderr=subprocess.DEVNULL
30
+ ).strip()
31
+
32
+ if not output:
33
+ return []
34
+
35
+ files = output.split("\n")
36
+ return [f for f in files if f.endswith(".py") and os.path.exists(f)]
37
+ except subprocess.CalledProcessError:
38
+ return []
39
+
40
+
41
+ def get_import_aliases(tree: ast.AST) -> Set[str]:
42
+ """
43
+ Extract all import aliases for 'cast' from the AST.
44
+ Returns set of names that refer to cast function.
45
+ """
46
+ cast_names = {"cast"} # Always include 'cast' as a base name
47
+
48
+ for node in ast.walk(tree):
49
+ if isinstance(node, ast.ImportFrom):
50
+ if node.module == "typing" or (
51
+ node.module and node.module.endswith(".typing")
52
+ ):
53
+ for alias in node.names:
54
+ if alias.name == "cast":
55
+ if alias.asname:
56
+ cast_names.add(alias.asname)
57
+ else:
58
+ cast_names.add("cast")
59
+ elif isinstance(node, ast.Import):
60
+ for alias in node.names:
61
+ if alias.name == "typing":
62
+ # import typing - we'll need to check typing.cast
63
+ pass
64
+ elif alias.name == "cast":
65
+ if alias.asname:
66
+ cast_names.add(alias.asname)
67
+ else:
68
+ cast_names.add("cast")
69
+
70
+ return cast_names
71
+
72
+
73
+ def check_cast_usage_in_file(file_path: str) -> List[Tuple[int, int, str]]:
74
+ """
75
+ Check a Python file for cast usage.
76
+ Returns list of violations as (line, col, message).
77
+ """
78
+ violations = []
79
+
80
+ try:
81
+ with open(file_path, "r", encoding="utf-8") as f:
82
+ content = f.read()
83
+ tree = ast.parse(content, filename=file_path)
84
+ except (SyntaxError, UnicodeDecodeError) as e:
85
+ print(f"WARNING: Failed to parse {file_path}: {e}")
86
+ return violations
87
+
88
+ # Get all names that refer to cast function
89
+ cast_names = get_import_aliases(tree)
90
+
91
+ # Walk AST to find cast calls
92
+ for node in ast.walk(tree):
93
+ if isinstance(node, ast.Call):
94
+ # Check for direct cast call: cast(Type, value)
95
+ if isinstance(node.func, ast.Name):
96
+ if node.func.id in cast_names:
97
+ violations.append(
98
+ (
99
+ node.lineno,
100
+ node.col_offset,
101
+ f"Direct cast call: {node.func.id}",
102
+ )
103
+ )
104
+
105
+ # Check for typing.cast call
106
+ elif isinstance(node.func, ast.Attribute):
107
+ if node.func.attr == "cast":
108
+ # Check if it's typing.cast or something.typing.cast
109
+ if isinstance(node.func.value, ast.Name):
110
+ if node.func.value.id == "typing":
111
+ violations.append(
112
+ (node.lineno, node.col_offset, "typing.cast call")
113
+ )
114
+ elif isinstance(node.func.value, ast.Attribute):
115
+ # Handle cases like module.typing.cast
116
+ if isinstance(node.func.value.value, ast.Name):
117
+ if (
118
+ node.func.value.value.id + "." + node.func.value.attr
119
+ == "typing"
120
+ ):
121
+ violations.append(
122
+ (node.lineno, node.col_offset, "typing.cast call")
123
+ )
124
+
125
+ return violations
126
+
127
+
128
+ def main() -> None:
129
+ """
130
+ Main function to check all changed Python files for cast usage.
131
+ """
132
+ base_ref = os.environ.get("BASE_REF")
133
+ if base_ref:
134
+ base_ref = f"origin/{base_ref}"
135
+ else:
136
+ base_ref = "origin/main"
137
+
138
+ python_files = get_changed_python_files(base_ref)
139
+
140
+ if not python_files:
141
+ print("No Python files changed in linhai/ directory.")
142
+ sys.exit(0)
143
+
144
+ print(f"Checking {len(python_files)} changed Python file(s) for cast usage...")
145
+
146
+ errors_found = False
147
+ for file_path in python_files:
148
+ violations = check_cast_usage_in_file(file_path)
149
+ if violations:
150
+ errors_found = True
151
+ print(f"\nERROR: Cast usage found in {file_path}:")
152
+ for line, col, msg in violations:
153
+ print(f" Line {line}, Col {col}: {msg}")
154
+
155
+ if errors_found:
156
+ print("\nERROR: cast() or typing.cast() usage detected.")
157
+ print(
158
+ "Please avoid using cast for type assertions. Use proper type hints instead."
159
+ )
160
+ sys.exit(1)
161
+ else:
162
+ print("SUCCESS: No cast usage found in changed Python files.")
163
+ sys.exit(0)
164
+
165
+
166
+ if __name__ == "__main__":
167
+ main()
@@ -0,0 +1,85 @@
1
+ import sys
2
+ import os
3
+ import re
4
+ import subprocess
5
+
6
+
7
+ def contains_emoji(text):
8
+ emoji_pattern = re.compile(
9
+ "["
10
+ "\U0001f600-\U0001f64f"
11
+ "\U0001f300 \U0001f5ff"
12
+ "\U0001f680-\U0001f6ff"
13
+ "\U0001f1e0-\U0001f1ff"
14
+ "]",
15
+ flags=re.UNICODE,
16
+ )
17
+ return bool(emoji_pattern.search(text))
18
+
19
+
20
+ def get_changed_files():
21
+ current_branch = os.environ.get("GITHUB_REF_NAME", "")
22
+ target_branch = os.environ.get("GITHUB_BASE_REF", "main")
23
+ if not current_branch:
24
+ try:
25
+ result = subprocess.run(
26
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
27
+ capture_output=True,
28
+ text=True,
29
+ )
30
+ current_branch = result.stdout.strip()
31
+ except:
32
+ current_branch = "HEAD"
33
+ try:
34
+ result = subprocess.run(
35
+ ["git", "diff", "--name-only", f"{target_branch}...{current_branch}", "--"],
36
+ capture_output=True,
37
+ text=True,
38
+ )
39
+ if result.returncode != 0:
40
+ print(f"Warning: git diff failed: {result.stderr}")
41
+ return []
42
+ files = result.stdout.strip().split("\n")
43
+ python_files = [f for f in files if f and f.endswith(".py")]
44
+ return python_files
45
+ except Exception as e:
46
+ print(f"Warning: Could not get changed files: {e}")
47
+ return []
48
+
49
+
50
+ def check_changed_files():
51
+ changed_files = get_changed_files()
52
+ if not changed_files:
53
+ print("No Python files changed in this PR.")
54
+ return []
55
+ errors = []
56
+ for filepath in changed_files:
57
+ if not os.path.exists(filepath):
58
+ print(f"Warning: File {filepath} does not exist.")
59
+ continue
60
+ try:
61
+ with open(filepath, "r", encoding="utf-8") as f:
62
+ content = f.read()
63
+ if contains_emoji(content):
64
+ errors.append(filepath)
65
+ except Exception as e:
66
+ print(f"Warning: Could not read {filepath}: {e}")
67
+ return errors
68
+
69
+
70
+ def main():
71
+ print("Checking for emoji in changed Python files...")
72
+ errors = check_changed_files()
73
+ if errors:
74
+ print("ERROR: Emoji detected in changed code files:")
75
+ for err in errors:
76
+ print(f" {err}")
77
+ print("\nPlease remove emoji from changed code files.")
78
+ sys.exit(1)
79
+ else:
80
+ print("SUCCESS: No emoji found in changed code files.")
81
+ sys.exit(0)
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
@@ -0,0 +1,34 @@
1
+ #!/bin/bash
2
+
3
+ # This script checks if the number of commits in a PR is reasonable.
4
+ # It fails if:
5
+ # - number of commits > number of files changed
6
+
7
+ set -e
8
+
9
+ echo "Checking if commit count is reasonable..."
10
+
11
+ # Check if we're in a PR context
12
+ if ! git rev-parse --verify origin/main >/dev/null 2>&1; then
13
+ echo "WARNING: Cannot find origin/main reference, skipping commit count check"
14
+ exit 0
15
+ fi
16
+
17
+ # Get the number of commits in the PR (from origin/main to HEAD)
18
+ commit_count=$(git log --oneline origin/main..HEAD | wc -l)
19
+
20
+ # Get the number of files changed (excluding deleted files)
21
+ file_count=$(git diff --name-only --diff-filter=ACMR origin/main..HEAD | wc -l)
22
+
23
+ echo "Number of commits: $commit_count"
24
+ echo "Number of files changed: $file_count"
25
+
26
+ # If commit count exceeds file count, fail
27
+ if [ "$commit_count" -gt "$file_count" ]; then
28
+ echo "ERROR: PR has $commit_count commits but only $file_count files changed."
29
+ echo " Please squash commits to have at most one commit per file changed."
30
+ exit 1
31
+ else
32
+ echo "SUCCESS: Commit count check passed."
33
+ exit 0
34
+ fi