pythinker-code 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (790) hide show
  1. pythinker_code/CHANGELOG.md +60 -0
  2. pythinker_code/__init__.py +0 -0
  3. pythinker_code/__main__.py +97 -0
  4. pythinker_code/acp/AGENTS.md +93 -0
  5. pythinker_code/acp/__init__.py +13 -0
  6. pythinker_code/acp/convert.py +128 -0
  7. pythinker_code/acp/host.py +301 -0
  8. pythinker_code/acp/mcp.py +46 -0
  9. pythinker_code/acp/server.py +497 -0
  10. pythinker_code/acp/session.py +502 -0
  11. pythinker_code/acp/tools.py +174 -0
  12. pythinker_code/acp/types.py +13 -0
  13. pythinker_code/acp/version.py +45 -0
  14. pythinker_code/agents/default/agent.yaml +55 -0
  15. pythinker_code/agents/default/code_reviewer.yaml +47 -0
  16. pythinker_code/agents/default/coder.yaml +42 -0
  17. pythinker_code/agents/default/debugger.yaml +35 -0
  18. pythinker_code/agents/default/explore.yaml +59 -0
  19. pythinker_code/agents/default/implementer.yaml +46 -0
  20. pythinker_code/agents/default/plan.yaml +42 -0
  21. pythinker_code/agents/default/review.yaml +47 -0
  22. pythinker_code/agents/default/security_reviewer.yaml +37 -0
  23. pythinker_code/agents/default/system.md +192 -0
  24. pythinker_code/agents/default/verifier.yaml +46 -0
  25. pythinker_code/agents/okabe/agent.yaml +22 -0
  26. pythinker_code/agentspec.py +163 -0
  27. pythinker_code/app.py +847 -0
  28. pythinker_code/approval_runtime/__init__.py +29 -0
  29. pythinker_code/approval_runtime/models.py +42 -0
  30. pythinker_code/approval_runtime/runtime.py +235 -0
  31. pythinker_code/auth/__init__.py +25 -0
  32. pythinker_code/auth/anthropic_direct.py +207 -0
  33. pythinker_code/auth/deepseek.py +192 -0
  34. pythinker_code/auth/github_feedback.py +228 -0
  35. pythinker_code/auth/lm_studio.py +418 -0
  36. pythinker_code/auth/minimax.py +203 -0
  37. pythinker_code/auth/oauth.py +1145 -0
  38. pythinker_code/auth/ollama.py +293 -0
  39. pythinker_code/auth/openai.py +783 -0
  40. pythinker_code/auth/opencode_go.py +203 -0
  41. pythinker_code/auth/openrouter.py +225 -0
  42. pythinker_code/auth/platforms.py +475 -0
  43. pythinker_code/background/__init__.py +36 -0
  44. pythinker_code/background/agent_runner.py +231 -0
  45. pythinker_code/background/ids.py +19 -0
  46. pythinker_code/background/manager.py +668 -0
  47. pythinker_code/background/models.py +118 -0
  48. pythinker_code/background/store.py +243 -0
  49. pythinker_code/background/summary.py +66 -0
  50. pythinker_code/background/worker.py +209 -0
  51. pythinker_code/cli/__init__.py +1326 -0
  52. pythinker_code/cli/__main__.py +19 -0
  53. pythinker_code/cli/_lazy_group.py +268 -0
  54. pythinker_code/cli/debug.py +11 -0
  55. pythinker_code/cli/export.py +322 -0
  56. pythinker_code/cli/info.py +62 -0
  57. pythinker_code/cli/mcp.py +362 -0
  58. pythinker_code/cli/plugin.py +351 -0
  59. pythinker_code/cli/review.py +74 -0
  60. pythinker_code/cli/secscan.py +11 -0
  61. pythinker_code/cli/security_scan.py +35 -0
  62. pythinker_code/cli/toad.py +74 -0
  63. pythinker_code/cli/update.py +26 -0
  64. pythinker_code/cli/vis.py +38 -0
  65. pythinker_code/cli/web.py +80 -0
  66. pythinker_code/config.py +511 -0
  67. pythinker_code/constant.py +33 -0
  68. pythinker_code/events.py +106 -0
  69. pythinker_code/exception.py +43 -0
  70. pythinker_code/extensions.py +151 -0
  71. pythinker_code/hooks/__init__.py +4 -0
  72. pythinker_code/hooks/config.py +34 -0
  73. pythinker_code/hooks/engine.py +383 -0
  74. pythinker_code/hooks/events.py +190 -0
  75. pythinker_code/hooks/runner.py +92 -0
  76. pythinker_code/llm.py +441 -0
  77. pythinker_code/metadata.py +79 -0
  78. pythinker_code/notifications/__init__.py +33 -0
  79. pythinker_code/notifications/llm.py +77 -0
  80. pythinker_code/notifications/manager.py +145 -0
  81. pythinker_code/notifications/models.py +50 -0
  82. pythinker_code/notifications/notifier.py +41 -0
  83. pythinker_code/notifications/store.py +118 -0
  84. pythinker_code/notifications/wire.py +21 -0
  85. pythinker_code/plugin/__init__.py +124 -0
  86. pythinker_code/plugin/manager.py +166 -0
  87. pythinker_code/plugin/tool.py +173 -0
  88. pythinker_code/prompt_templates.py +181 -0
  89. pythinker_code/prompts/__init__.py +6 -0
  90. pythinker_code/prompts/compact.md +73 -0
  91. pythinker_code/prompts/init.md +21 -0
  92. pythinker_code/py.typed +0 -0
  93. pythinker_code/session.py +319 -0
  94. pythinker_code/session_fork.py +325 -0
  95. pythinker_code/session_state.py +132 -0
  96. pythinker_code/share.py +14 -0
  97. pythinker_code/skill/__init__.py +727 -0
  98. pythinker_code/skill/flow/__init__.py +99 -0
  99. pythinker_code/skill/flow/d2.py +482 -0
  100. pythinker_code/skill/flow/mermaid.py +266 -0
  101. pythinker_code/skills/pythinker-code-help/SKILL.md +54 -0
  102. pythinker_code/skills/skill-creator/SKILL.md +367 -0
  103. pythinker_code/soul/__init__.py +304 -0
  104. pythinker_code/soul/agent.py +552 -0
  105. pythinker_code/soul/approval.py +267 -0
  106. pythinker_code/soul/btw.py +220 -0
  107. pythinker_code/soul/compaction.py +189 -0
  108. pythinker_code/soul/context.py +339 -0
  109. pythinker_code/soul/denwarenji.py +39 -0
  110. pythinker_code/soul/dynamic_injection.py +84 -0
  111. pythinker_code/soul/dynamic_injections/__init__.py +0 -0
  112. pythinker_code/soul/dynamic_injections/auto_mode.py +72 -0
  113. pythinker_code/soul/dynamic_injections/plan_mode.py +239 -0
  114. pythinker_code/soul/message.py +92 -0
  115. pythinker_code/soul/permission.py +368 -0
  116. pythinker_code/soul/pythinkersoul.py +1763 -0
  117. pythinker_code/soul/slash.py +340 -0
  118. pythinker_code/soul/toolset.py +826 -0
  119. pythinker_code/subagents/__init__.py +21 -0
  120. pythinker_code/subagents/builder.py +43 -0
  121. pythinker_code/subagents/core.py +86 -0
  122. pythinker_code/subagents/discovery.py +234 -0
  123. pythinker_code/subagents/git_context.py +172 -0
  124. pythinker_code/subagents/models.py +56 -0
  125. pythinker_code/subagents/output.py +71 -0
  126. pythinker_code/subagents/registry.py +28 -0
  127. pythinker_code/subagents/runner.py +442 -0
  128. pythinker_code/subagents/store.py +200 -0
  129. pythinker_code/telemetry/__init__.py +217 -0
  130. pythinker_code/telemetry/config.py +113 -0
  131. pythinker_code/telemetry/crash.py +191 -0
  132. pythinker_code/telemetry/errors.py +113 -0
  133. pythinker_code/telemetry/metrics.py +208 -0
  134. pythinker_code/telemetry/otel.py +303 -0
  135. pythinker_code/telemetry/sentry.py +212 -0
  136. pythinker_code/telemetry/sink.py +189 -0
  137. pythinker_code/tools/AGENTS.md +6 -0
  138. pythinker_code/tools/__init__.py +105 -0
  139. pythinker_code/tools/agent/__init__.py +326 -0
  140. pythinker_code/tools/agent/description.md +65 -0
  141. pythinker_code/tools/ask_user/__init__.py +162 -0
  142. pythinker_code/tools/ask_user/description.md +19 -0
  143. pythinker_code/tools/background/__init__.py +318 -0
  144. pythinker_code/tools/background/list.md +10 -0
  145. pythinker_code/tools/background/output.md +11 -0
  146. pythinker_code/tools/background/stop.md +8 -0
  147. pythinker_code/tools/display.py +46 -0
  148. pythinker_code/tools/dmail/__init__.py +38 -0
  149. pythinker_code/tools/dmail/dmail.md +17 -0
  150. pythinker_code/tools/file/__init__.py +31 -0
  151. pythinker_code/tools/file/glob.md +17 -0
  152. pythinker_code/tools/file/glob.py +163 -0
  153. pythinker_code/tools/file/grep.md +6 -0
  154. pythinker_code/tools/file/grep_local.py +904 -0
  155. pythinker_code/tools/file/plan_mode.py +45 -0
  156. pythinker_code/tools/file/read.md +16 -0
  157. pythinker_code/tools/file/read.py +303 -0
  158. pythinker_code/tools/file/read_media.md +24 -0
  159. pythinker_code/tools/file/read_media.py +220 -0
  160. pythinker_code/tools/file/replace.md +7 -0
  161. pythinker_code/tools/file/replace.py +204 -0
  162. pythinker_code/tools/file/utils.py +257 -0
  163. pythinker_code/tools/file/write.md +5 -0
  164. pythinker_code/tools/file/write.py +186 -0
  165. pythinker_code/tools/plan/__init__.py +362 -0
  166. pythinker_code/tools/plan/description.md +29 -0
  167. pythinker_code/tools/plan/enter.py +193 -0
  168. pythinker_code/tools/plan/enter_description.md +35 -0
  169. pythinker_code/tools/plan/handoff.py +69 -0
  170. pythinker_code/tools/plan/heroes.py +277 -0
  171. pythinker_code/tools/shell/__init__.py +263 -0
  172. pythinker_code/tools/shell/bash.md +35 -0
  173. pythinker_code/tools/shell/powershell.md +30 -0
  174. pythinker_code/tools/test.py +55 -0
  175. pythinker_code/tools/think/__init__.py +21 -0
  176. pythinker_code/tools/think/think.md +1 -0
  177. pythinker_code/tools/todo/__init__.py +168 -0
  178. pythinker_code/tools/todo/set_todo_list.md +23 -0
  179. pythinker_code/tools/utils.py +200 -0
  180. pythinker_code/tools/web/__init__.py +4 -0
  181. pythinker_code/tools/web/fetch.md +1 -0
  182. pythinker_code/tools/web/fetch.py +261 -0
  183. pythinker_code/tools/web/search.md +1 -0
  184. pythinker_code/tools/web/search.py +163 -0
  185. pythinker_code/ui/__init__.py +0 -0
  186. pythinker_code/ui/acp/__init__.py +99 -0
  187. pythinker_code/ui/print/__init__.py +474 -0
  188. pythinker_code/ui/print/visualize.py +185 -0
  189. pythinker_code/ui/shell/__init__.py +1806 -0
  190. pythinker_code/ui/shell/components/__init__.py +110 -0
  191. pythinker_code/ui/shell/components/base.py +25 -0
  192. pythinker_code/ui/shell/components/bash_execution.py +249 -0
  193. pythinker_code/ui/shell/components/bordered_loader.py +62 -0
  194. pythinker_code/ui/shell/components/diff.py +308 -0
  195. pythinker_code/ui/shell/components/footer.py +231 -0
  196. pythinker_code/ui/shell/components/key_hints.py +27 -0
  197. pythinker_code/ui/shell/components/messages.py +152 -0
  198. pythinker_code/ui/shell/components/render_utils.py +198 -0
  199. pythinker_code/ui/shell/components/settings_list.py +369 -0
  200. pythinker_code/ui/shell/components/special_messages.py +125 -0
  201. pythinker_code/ui/shell/components/tool_execution.py +261 -0
  202. pythinker_code/ui/shell/console.py +109 -0
  203. pythinker_code/ui/shell/debug.py +190 -0
  204. pythinker_code/ui/shell/echo.py +30 -0
  205. pythinker_code/ui/shell/export_import.py +117 -0
  206. pythinker_code/ui/shell/keyboard.py +300 -0
  207. pythinker_code/ui/shell/keymap.py +84 -0
  208. pythinker_code/ui/shell/mcp_status.py +112 -0
  209. pythinker_code/ui/shell/model_picker.py +318 -0
  210. pythinker_code/ui/shell/oauth.py +273 -0
  211. pythinker_code/ui/shell/placeholders.py +578 -0
  212. pythinker_code/ui/shell/prompt.py +2888 -0
  213. pythinker_code/ui/shell/replay.py +215 -0
  214. pythinker_code/ui/shell/selector.py +364 -0
  215. pythinker_code/ui/shell/selectors/__init__.py +38 -0
  216. pythinker_code/ui/shell/selectors/extension.py +37 -0
  217. pythinker_code/ui/shell/selectors/oauth.py +63 -0
  218. pythinker_code/ui/shell/selectors/settings.py +349 -0
  219. pythinker_code/ui/shell/selectors/show_images.py +29 -0
  220. pythinker_code/ui/shell/selectors/theme.py +28 -0
  221. pythinker_code/ui/shell/selectors/thinking.py +42 -0
  222. pythinker_code/ui/shell/session_picker.py +227 -0
  223. pythinker_code/ui/shell/setup.py +212 -0
  224. pythinker_code/ui/shell/slash.py +1433 -0
  225. pythinker_code/ui/shell/spinner_words.py +222 -0
  226. pythinker_code/ui/shell/startup.py +32 -0
  227. pythinker_code/ui/shell/task_browser.py +486 -0
  228. pythinker_code/ui/shell/tool_renderers/__init__.py +197 -0
  229. pythinker_code/ui/shell/tool_renderers/_render_utils.py +168 -0
  230. pythinker_code/ui/shell/tool_renderers/agent.py +140 -0
  231. pythinker_code/ui/shell/tool_renderers/ask_user.py +93 -0
  232. pythinker_code/ui/shell/tool_renderers/background.py +144 -0
  233. pythinker_code/ui/shell/tool_renderers/bash.py +103 -0
  234. pythinker_code/ui/shell/tool_renderers/edit.py +163 -0
  235. pythinker_code/ui/shell/tool_renderers/find.py +81 -0
  236. pythinker_code/ui/shell/tool_renderers/generic.py +60 -0
  237. pythinker_code/ui/shell/tool_renderers/grep.py +98 -0
  238. pythinker_code/ui/shell/tool_renderers/plan.py +98 -0
  239. pythinker_code/ui/shell/tool_renderers/read.py +103 -0
  240. pythinker_code/ui/shell/tool_renderers/think.py +66 -0
  241. pythinker_code/ui/shell/tool_renderers/todo.py +164 -0
  242. pythinker_code/ui/shell/tool_renderers/web.py +128 -0
  243. pythinker_code/ui/shell/tool_renderers/write.py +102 -0
  244. pythinker_code/ui/shell/update.py +352 -0
  245. pythinker_code/ui/shell/usage.py +291 -0
  246. pythinker_code/ui/shell/usage_adapters/__init__.py +50 -0
  247. pythinker_code/ui/shell/usage_adapters/anthropic_admin.py +233 -0
  248. pythinker_code/ui/shell/usage_adapters/base.py +72 -0
  249. pythinker_code/ui/shell/usage_adapters/deepseek.py +137 -0
  250. pythinker_code/ui/shell/usage_adapters/minimax.py +236 -0
  251. pythinker_code/ui/shell/usage_adapters/openai_admin.py +225 -0
  252. pythinker_code/ui/shell/usage_adapters/openai_chatgpt.py +241 -0
  253. pythinker_code/ui/shell/usage_adapters/opencode_go.py +232 -0
  254. pythinker_code/ui/shell/usage_adapters/openrouter.py +105 -0
  255. pythinker_code/ui/shell/usage_adapters/pythinker.py +189 -0
  256. pythinker_code/ui/shell/usage_adapters/pythinker_ai.py +50 -0
  257. pythinker_code/ui/shell/usage_render.py +150 -0
  258. pythinker_code/ui/shell/visualize/__init__.py +165 -0
  259. pythinker_code/ui/shell/visualize/_approval_panel.py +539 -0
  260. pythinker_code/ui/shell/visualize/_blocks.py +802 -0
  261. pythinker_code/ui/shell/visualize/_btw_panel.py +227 -0
  262. pythinker_code/ui/shell/visualize/_input_router.py +48 -0
  263. pythinker_code/ui/shell/visualize/_interactive.py +531 -0
  264. pythinker_code/ui/shell/visualize/_live_view.py +891 -0
  265. pythinker_code/ui/shell/visualize/_question_panel.py +586 -0
  266. pythinker_code/ui/shell/visualize/_worklog.py +245 -0
  267. pythinker_code/ui/theme.py +395 -0
  268. pythinker_code/ui/tui_config.py +82 -0
  269. pythinker_code/usage_ratelimit_cache.py +175 -0
  270. pythinker_code/utils/__init__.py +0 -0
  271. pythinker_code/utils/aiohttp.py +24 -0
  272. pythinker_code/utils/aioqueue.py +72 -0
  273. pythinker_code/utils/broadcast.py +38 -0
  274. pythinker_code/utils/changelog.py +108 -0
  275. pythinker_code/utils/clipboard.py +246 -0
  276. pythinker_code/utils/datetime.py +64 -0
  277. pythinker_code/utils/diff.py +135 -0
  278. pythinker_code/utils/editor.py +91 -0
  279. pythinker_code/utils/environment.py +73 -0
  280. pythinker_code/utils/envvar.py +22 -0
  281. pythinker_code/utils/export.py +696 -0
  282. pythinker_code/utils/file_filter.py +375 -0
  283. pythinker_code/utils/frontmatter.py +70 -0
  284. pythinker_code/utils/io.py +27 -0
  285. pythinker_code/utils/logging.py +146 -0
  286. pythinker_code/utils/media_tags.py +29 -0
  287. pythinker_code/utils/message.py +24 -0
  288. pythinker_code/utils/path.py +199 -0
  289. pythinker_code/utils/proctitle.py +33 -0
  290. pythinker_code/utils/proxy.py +31 -0
  291. pythinker_code/utils/pyinstaller.py +45 -0
  292. pythinker_code/utils/rich/__init__.py +33 -0
  293. pythinker_code/utils/rich/columns.py +99 -0
  294. pythinker_code/utils/rich/diff_render.py +481 -0
  295. pythinker_code/utils/rich/markdown.py +935 -0
  296. pythinker_code/utils/rich/markdown_sample.md +108 -0
  297. pythinker_code/utils/rich/markdown_sample_short.md +2 -0
  298. pythinker_code/utils/rich/syntax.py +114 -0
  299. pythinker_code/utils/sensitive.py +54 -0
  300. pythinker_code/utils/server.py +121 -0
  301. pythinker_code/utils/signals.py +43 -0
  302. pythinker_code/utils/slashcmd.py +124 -0
  303. pythinker_code/utils/string.py +41 -0
  304. pythinker_code/utils/subprocess_env.py +83 -0
  305. pythinker_code/utils/term.py +168 -0
  306. pythinker_code/utils/typing.py +20 -0
  307. pythinker_code/vis/__init__.py +0 -0
  308. pythinker_code/vis/api/__init__.py +5 -0
  309. pythinker_code/vis/api/sessions.py +714 -0
  310. pythinker_code/vis/api/statistics.py +209 -0
  311. pythinker_code/vis/api/system.py +19 -0
  312. pythinker_code/vis/app.py +199 -0
  313. pythinker_code/vis/static/assets/highlighted-body-B3W2YXNL-CY1rtwrX.js +1 -0
  314. pythinker_code/vis/static/assets/index-DSRInNnm.css +1 -0
  315. pythinker_code/vis/static/assets/index-DgmTI2M_.js +185 -0
  316. pythinker_code/vis/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  317. pythinker_code/vis/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  318. pythinker_code/vis/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  319. pythinker_code/vis/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  320. pythinker_code/vis/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  321. pythinker_code/vis/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  322. pythinker_code/vis/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  323. pythinker_code/vis/static/index.html +17 -0
  324. pythinker_code/web/__init__.py +5 -0
  325. pythinker_code/web/api/__init__.py +15 -0
  326. pythinker_code/web/api/config.py +217 -0
  327. pythinker_code/web/api/open_in.py +233 -0
  328. pythinker_code/web/api/sessions.py +1256 -0
  329. pythinker_code/web/app.py +449 -0
  330. pythinker_code/web/auth.py +191 -0
  331. pythinker_code/web/models.py +98 -0
  332. pythinker_code/web/runner/__init__.py +5 -0
  333. pythinker_code/web/runner/messages.py +57 -0
  334. pythinker_code/web/runner/process.py +754 -0
  335. pythinker_code/web/runner/worker.py +97 -0
  336. pythinker_code/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  337. pythinker_code/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  338. pythinker_code/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  339. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  340. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  341. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  342. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  343. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  344. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  345. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  346. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  347. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  348. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  349. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  350. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  351. pythinker_code/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  352. pythinker_code/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  353. pythinker_code/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  354. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  355. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  356. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  357. pythinker_code/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  358. pythinker_code/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  359. pythinker_code/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  360. pythinker_code/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  361. pythinker_code/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  362. pythinker_code/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  363. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  364. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  365. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  366. pythinker_code/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  367. pythinker_code/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  368. pythinker_code/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  369. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  370. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  371. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  372. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  373. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  374. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  375. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  376. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  377. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  378. pythinker_code/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  379. pythinker_code/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  380. pythinker_code/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  381. pythinker_code/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  382. pythinker_code/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  383. pythinker_code/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  384. pythinker_code/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  385. pythinker_code/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  386. pythinker_code/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  387. pythinker_code/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  388. pythinker_code/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  389. pythinker_code/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  390. pythinker_code/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  391. pythinker_code/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  392. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  393. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  394. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  395. pythinker_code/web/static/assets/_baseUniq-DpSMr1jx.js +1 -0
  396. pythinker_code/web/static/assets/abap-BdImnpbu.js +1 -0
  397. pythinker_code/web/static/assets/actionscript-3-CfeIJUat.js +1 -0
  398. pythinker_code/web/static/assets/ada-bCR0ucgS.js +1 -0
  399. pythinker_code/web/static/assets/andromeeda-C-Jbm3Hp.js +1 -0
  400. pythinker_code/web/static/assets/angular-html-CU67Zn6k.js +1 -0
  401. pythinker_code/web/static/assets/angular-ts-BwZT4LLn.js +1 -0
  402. pythinker_code/web/static/assets/apache-Pmp26Uib.js +1 -0
  403. pythinker_code/web/static/assets/apex-D8_7TLub.js +1 -0
  404. pythinker_code/web/static/assets/apl-dKokRX4l.js +1 -0
  405. pythinker_code/web/static/assets/applescript-Co6uUVPk.js +1 -0
  406. pythinker_code/web/static/assets/ara-BRHolxvo.js +1 -0
  407. pythinker_code/web/static/assets/arc-DpsahJyV.js +1 -0
  408. pythinker_code/web/static/assets/architectureDiagram-VXUJARFQ-DqiRv9Eg.js +36 -0
  409. pythinker_code/web/static/assets/asciidoc-Dv7Oe6Be.js +1 -0
  410. pythinker_code/web/static/assets/asm-D_Q5rh1f.js +1 -0
  411. pythinker_code/web/static/assets/astro-CbQHKStN.js +1 -0
  412. pythinker_code/web/static/assets/aurora-x-D-2ljcwZ.js +1 -0
  413. pythinker_code/web/static/assets/awk-DMzUqQB5.js +1 -0
  414. pythinker_code/web/static/assets/ayu-dark-CmMr59Fi.js +1 -0
  415. pythinker_code/web/static/assets/ballerina-BFfxhgS-.js +1 -0
  416. pythinker_code/web/static/assets/bat-BkioyH1T.js +1 -0
  417. pythinker_code/web/static/assets/beancount-k_qm7-4y.js +1 -0
  418. pythinker_code/web/static/assets/berry-uYugtg8r.js +1 -0
  419. pythinker_code/web/static/assets/bibtex-CHM0blh-.js +1 -0
  420. pythinker_code/web/static/assets/bicep-Bmn6On1c.js +1 -0
  421. pythinker_code/web/static/assets/blade-D4QpJJKB.js +1 -0
  422. pythinker_code/web/static/assets/blockDiagram-VD42YOAC-WgtUvqbp.js +122 -0
  423. pythinker_code/web/static/assets/bsl-BO_Y6i37.js +1 -0
  424. pythinker_code/web/static/assets/c-BIGW1oBm.js +1 -0
  425. pythinker_code/web/static/assets/c3-VCDPK7BO.js +1 -0
  426. pythinker_code/web/static/assets/c4Diagram-YG6GDRKO-rK0RPuZd.js +10 -0
  427. pythinker_code/web/static/assets/cadence-Bv_4Rxtq.js +1 -0
  428. pythinker_code/web/static/assets/cairo-KRGpt6FW.js +1 -0
  429. pythinker_code/web/static/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  430. pythinker_code/web/static/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  431. pythinker_code/web/static/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  432. pythinker_code/web/static/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  433. pythinker_code/web/static/assets/channel-B0rlvkH-.js +1 -0
  434. pythinker_code/web/static/assets/chunk-4BX2VUAB-DIkMuLV-.js +1 -0
  435. pythinker_code/web/static/assets/chunk-55IACEB6-CORdm4k4.js +1 -0
  436. pythinker_code/web/static/assets/chunk-B4BG7PRW-D9xDhwHO.js +165 -0
  437. pythinker_code/web/static/assets/chunk-DI55MBZ5-BDmF9Bh-.js +220 -0
  438. pythinker_code/web/static/assets/chunk-FMBD7UC4-BCse_HmM.js +15 -0
  439. pythinker_code/web/static/assets/chunk-QN33PNHL-DCpBmTzA.js +1 -0
  440. pythinker_code/web/static/assets/chunk-QZHKN3VN-BqLuqobw.js +1 -0
  441. pythinker_code/web/static/assets/chunk-TZMSLE5B-8K2ogOKS.js +1 -0
  442. pythinker_code/web/static/assets/clarity-D53aC0YG.js +1 -0
  443. pythinker_code/web/static/assets/classDiagram-2ON5EDUG-D_ZHSii2.js +1 -0
  444. pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-D_ZHSii2.js +1 -0
  445. pythinker_code/web/static/assets/clojure-P80f7IUj.js +1 -0
  446. pythinker_code/web/static/assets/clone-GSXejyY1.js +1 -0
  447. pythinker_code/web/static/assets/cmake-D1j8_8rp.js +1 -0
  448. pythinker_code/web/static/assets/cobol-nwyudZeR.js +1 -0
  449. pythinker_code/web/static/assets/code-block-IT6T5CEO-DWTFYA28.js +2 -0
  450. pythinker_code/web/static/assets/codeowners-Bp6g37R7.js +1 -0
  451. pythinker_code/web/static/assets/codeql-DsOJ9woJ.js +1 -0
  452. pythinker_code/web/static/assets/coffee-Ch7k5sss.js +1 -0
  453. pythinker_code/web/static/assets/common-lisp-Cg-RD9OK.js +1 -0
  454. pythinker_code/web/static/assets/coq-DkFqJrB1.js +1 -0
  455. pythinker_code/web/static/assets/cose-bilkent-S5V4N54A-BRI7ES-N.js +1 -0
  456. pythinker_code/web/static/assets/cpp-CofmeUqb.js +1 -0
  457. pythinker_code/web/static/assets/crystal-tKQVLTB8.js +1 -0
  458. pythinker_code/web/static/assets/csharp-K5feNrxe.js +1 -0
  459. pythinker_code/web/static/assets/css-DPfMkruS.js +1 -0
  460. pythinker_code/web/static/assets/csv-fuZLfV_i.js +1 -0
  461. pythinker_code/web/static/assets/cue-D82EKSYY.js +1 -0
  462. pythinker_code/web/static/assets/cypher-COkxafJQ.js +1 -0
  463. pythinker_code/web/static/assets/cytoscape.esm-B6BxUuKW.js +321 -0
  464. pythinker_code/web/static/assets/d-85-TOEBH.js +1 -0
  465. pythinker_code/web/static/assets/dagre-6UL2VRFP-Ci5GdWfi.js +4 -0
  466. pythinker_code/web/static/assets/dark-plus-C3mMm8J8.js +1 -0
  467. pythinker_code/web/static/assets/dart-CF10PKvl.js +1 -0
  468. pythinker_code/web/static/assets/dax-CEL-wOlO.js +1 -0
  469. pythinker_code/web/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  470. pythinker_code/web/static/assets/desktop-BmXAJ9_W.js +1 -0
  471. pythinker_code/web/static/assets/diagram-PSM6KHXK-0hhAylV4.js +24 -0
  472. pythinker_code/web/static/assets/diagram-QEK2KX5R-8fxgaW6d.js +43 -0
  473. pythinker_code/web/static/assets/diagram-S2PKOQOG-FRr0_atE.js +24 -0
  474. pythinker_code/web/static/assets/diff-D97Zzqfu.js +1 -0
  475. pythinker_code/web/static/assets/docker-BcOcwvcX.js +1 -0
  476. pythinker_code/web/static/assets/dotenv-Da5cRb03.js +1 -0
  477. pythinker_code/web/static/assets/dracula-BzJJZx-M.js +1 -0
  478. pythinker_code/web/static/assets/dracula-soft-BXkSAIEj.js +1 -0
  479. pythinker_code/web/static/assets/dream-maker-BtqSS_iP.js +1 -0
  480. pythinker_code/web/static/assets/edge-BkV0erSs.js +1 -0
  481. pythinker_code/web/static/assets/elixir-CDX3lj18.js +1 -0
  482. pythinker_code/web/static/assets/elm-DbKCFpqz.js +1 -0
  483. pythinker_code/web/static/assets/emacs-lisp-C9XAeP06.js +1 -0
  484. pythinker_code/web/static/assets/erDiagram-Q2GNP2WA-B3T-hJUM.js +60 -0
  485. pythinker_code/web/static/assets/erb-BOJIQeun.js +1 -0
  486. pythinker_code/web/static/assets/erlang-DsQrWhSR.js +1 -0
  487. pythinker_code/web/static/assets/everforest-dark-BgDCqdQA.js +1 -0
  488. pythinker_code/web/static/assets/everforest-light-C8M2exoo.js +1 -0
  489. pythinker_code/web/static/assets/fennel-BYunw83y.js +1 -0
  490. pythinker_code/web/static/assets/fish-BvzEVeQv.js +1 -0
  491. pythinker_code/web/static/assets/flowDiagram-NV44I4VS-D0S3u7ot.js +162 -0
  492. pythinker_code/web/static/assets/fluent-C4IJs8-o.js +1 -0
  493. pythinker_code/web/static/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  494. pythinker_code/web/static/assets/fortran-free-form-BxgE0vQu.js +1 -0
  495. pythinker_code/web/static/assets/fsharp-CXgrBDvD.js +1 -0
  496. pythinker_code/web/static/assets/ganttDiagram-JELNMOA3-CHrN2a23.js +267 -0
  497. pythinker_code/web/static/assets/gdresource-B7Tvp0Sc.js +1 -0
  498. pythinker_code/web/static/assets/gdscript-DTMYz4Jt.js +1 -0
  499. pythinker_code/web/static/assets/gdshader-DkwncUOv.js +1 -0
  500. pythinker_code/web/static/assets/genie-D0YGMca9.js +1 -0
  501. pythinker_code/web/static/assets/gherkin-DyxjwDmM.js +1 -0
  502. pythinker_code/web/static/assets/git-commit-F4YmCXRG.js +1 -0
  503. pythinker_code/web/static/assets/git-rebase-r7XF79zn.js +1 -0
  504. pythinker_code/web/static/assets/gitGraphDiagram-NY62KEGX-CfcXZWg0.js +65 -0
  505. pythinker_code/web/static/assets/github-dark-DHJKELXO.js +1 -0
  506. pythinker_code/web/static/assets/github-dark-default-Cuk6v7N8.js +1 -0
  507. pythinker_code/web/static/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  508. pythinker_code/web/static/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  509. pythinker_code/web/static/assets/github-light-DAi9KRSo.js +1 -0
  510. pythinker_code/web/static/assets/github-light-default-D7oLnXFd.js +1 -0
  511. pythinker_code/web/static/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  512. pythinker_code/web/static/assets/gleam-BspZqrRM.js +1 -0
  513. pythinker_code/web/static/assets/glimmer-js-Rg0-pVw9.js +1 -0
  514. pythinker_code/web/static/assets/glimmer-ts-U6CK756n.js +1 -0
  515. pythinker_code/web/static/assets/glsl-DplSGwfg.js +1 -0
  516. pythinker_code/web/static/assets/gn-n2N0HUVH.js +1 -0
  517. pythinker_code/web/static/assets/gnuplot-DdkO51Og.js +1 -0
  518. pythinker_code/web/static/assets/go-Dn2_MT6a.js +1 -0
  519. pythinker_code/web/static/assets/graph-8jMJwCqE.js +1 -0
  520. pythinker_code/web/static/assets/graphql-ChdNCCLP.js +1 -0
  521. pythinker_code/web/static/assets/groovy-gcz8RCvz.js +1 -0
  522. pythinker_code/web/static/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  523. pythinker_code/web/static/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  524. pythinker_code/web/static/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  525. pythinker_code/web/static/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  526. pythinker_code/web/static/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  527. pythinker_code/web/static/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  528. pythinker_code/web/static/assets/hack-CaT9iCJl.js +1 -0
  529. pythinker_code/web/static/assets/haml-B8DHNrY2.js +1 -0
  530. pythinker_code/web/static/assets/handlebars-BL8al0AC.js +1 -0
  531. pythinker_code/web/static/assets/haskell-Df6bDoY_.js +1 -0
  532. pythinker_code/web/static/assets/haxe-CzTSHFRz.js +1 -0
  533. pythinker_code/web/static/assets/hcl-BWvSN4gD.js +1 -0
  534. pythinker_code/web/static/assets/hjson-D5-asLiD.js +1 -0
  535. pythinker_code/web/static/assets/hlsl-D3lLCCz7.js +1 -0
  536. pythinker_code/web/static/assets/houston-DnULxvSX.js +1 -0
  537. pythinker_code/web/static/assets/html-GMplVEZG.js +1 -0
  538. pythinker_code/web/static/assets/html-derivative-BFtXZ54Q.js +1 -0
  539. pythinker_code/web/static/assets/http-jrhK8wxY.js +1 -0
  540. pythinker_code/web/static/assets/hurl-irOxFIW8.js +1 -0
  541. pythinker_code/web/static/assets/hxml-Bvhsp5Yf.js +1 -0
  542. pythinker_code/web/static/assets/hy-DFXneXwc.js +1 -0
  543. pythinker_code/web/static/assets/imba-DGztddWO.js +1 -0
  544. pythinker_code/web/static/assets/index-BXrFnzMy.js +153 -0
  545. pythinker_code/web/static/assets/index-BpoLgcEt.js +1 -0
  546. pythinker_code/web/static/assets/index-BrfQJnRD.js +5 -0
  547. pythinker_code/web/static/assets/index-C4gFzubz.js +2 -0
  548. pythinker_code/web/static/assets/index-CzV_vCfu.css +1 -0
  549. pythinker_code/web/static/assets/index-DI2oedCt.js +19 -0
  550. pythinker_code/web/static/assets/infoDiagram-WHAUD3N6-DdxonBf3.js +2 -0
  551. pythinker_code/web/static/assets/ini-BEwlwnbL.js +1 -0
  552. pythinker_code/web/static/assets/init-Gi6I4Gst.js +1 -0
  553. pythinker_code/web/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  554. pythinker_code/web/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  555. pythinker_code/web/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  556. pythinker_code/web/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  557. pythinker_code/web/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  558. pythinker_code/web/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  559. pythinker_code/web/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  560. pythinker_code/web/static/assets/java-CylS5w8V.js +1 -0
  561. pythinker_code/web/static/assets/javascript-wDzz0qaB.js +1 -0
  562. pythinker_code/web/static/assets/jinja-4LBKfQ-Z.js +1 -0
  563. pythinker_code/web/static/assets/jison-wvAkD_A8.js +1 -0
  564. pythinker_code/web/static/assets/journeyDiagram-XKPGCS4Q-BXf4aQei.js +139 -0
  565. pythinker_code/web/static/assets/json-Cp-IABpG.js +1 -0
  566. pythinker_code/web/static/assets/json5-C9tS-k6U.js +1 -0
  567. pythinker_code/web/static/assets/jsonc-Des-eS-w.js +1 -0
  568. pythinker_code/web/static/assets/jsonl-DcaNXYhu.js +1 -0
  569. pythinker_code/web/static/assets/jsonnet-DFQXde-d.js +1 -0
  570. pythinker_code/web/static/assets/jssm-C2t-YnRu.js +1 -0
  571. pythinker_code/web/static/assets/jsx-g9-lgVsj.js +1 -0
  572. pythinker_code/web/static/assets/julia-CxzCAyBv.js +1 -0
  573. pythinker_code/web/static/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  574. pythinker_code/web/static/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  575. pythinker_code/web/static/assets/kanagawa-wave-DWedfzmr.js +1 -0
  576. pythinker_code/web/static/assets/kanban-definition-3W4ZIXB7-DLpPPOu8.js +89 -0
  577. pythinker_code/web/static/assets/katex-D2lIc1rk.css +1 -0
  578. pythinker_code/web/static/assets/kdl-DV7GczEv.js +1 -0
  579. pythinker_code/web/static/assets/kotlin-BdnUsdx6.js +1 -0
  580. pythinker_code/web/static/assets/kusto-DZf3V79B.js +1 -0
  581. pythinker_code/web/static/assets/laserwave-DUszq2jm.js +1 -0
  582. pythinker_code/web/static/assets/latex-B4uzh10-.js +1 -0
  583. pythinker_code/web/static/assets/layout-DH73UoAH.js +1 -0
  584. pythinker_code/web/static/assets/lean-BZvkOJ9d.js +1 -0
  585. pythinker_code/web/static/assets/less-B1dDrJ26.js +1 -0
  586. pythinker_code/web/static/assets/light-plus-B7mTdjB0.js +1 -0
  587. pythinker_code/web/static/assets/linear-bAer2-sK.js +1 -0
  588. pythinker_code/web/static/assets/liquid-DYVedYrR.js +1 -0
  589. pythinker_code/web/static/assets/llvm-BtvRca6l.js +1 -0
  590. pythinker_code/web/static/assets/log-2UxHyX5q.js +1 -0
  591. pythinker_code/web/static/assets/logo-BtOb2qkB.js +1 -0
  592. pythinker_code/web/static/assets/lua-BbnMAYS6.js +1 -0
  593. pythinker_code/web/static/assets/luau-C-HG3fhB.js +1 -0
  594. pythinker_code/web/static/assets/make-CHLpvVh8.js +1 -0
  595. pythinker_code/web/static/assets/markdown-Cvjx9yec.js +1 -0
  596. pythinker_code/web/static/assets/marko-DZsq8hO1.js +1 -0
  597. pythinker_code/web/static/assets/material-theme-D5KoaKCx.js +1 -0
  598. pythinker_code/web/static/assets/material-theme-darker-BfHTSMKl.js +1 -0
  599. pythinker_code/web/static/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  600. pythinker_code/web/static/assets/material-theme-ocean-CyktbL80.js +1 -0
  601. pythinker_code/web/static/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  602. pythinker_code/web/static/assets/matlab-D7o27uSR.js +1 -0
  603. pythinker_code/web/static/assets/mdc-DUICxH0z.js +1 -0
  604. pythinker_code/web/static/assets/mdx-Cmh6b_Ma.js +1 -0
  605. pythinker_code/web/static/assets/mermaid-VLURNSYL-B2P5VJ9v.css +1 -0
  606. pythinker_code/web/static/assets/mermaid-VLURNSYL-CuqbwKXv.js +465 -0
  607. pythinker_code/web/static/assets/mermaid-mWjccvbQ.js +1 -0
  608. pythinker_code/web/static/assets/mermaid.core-Nx-rTKiV.js +191 -0
  609. pythinker_code/web/static/assets/min-DbfD8Ywu.js +1 -0
  610. pythinker_code/web/static/assets/min-dark-CafNBF8u.js +1 -0
  611. pythinker_code/web/static/assets/min-light-CTRr51gU.js +1 -0
  612. pythinker_code/web/static/assets/mindmap-definition-VGOIOE7T-C6l761Ue.js +68 -0
  613. pythinker_code/web/static/assets/mipsasm-CKIfxQSi.js +1 -0
  614. pythinker_code/web/static/assets/mojo-B93PlW-d.js +1 -0
  615. pythinker_code/web/static/assets/monokai-D4h5O-jR.js +1 -0
  616. pythinker_code/web/static/assets/moonbit-Ba13S78F.js +1 -0
  617. pythinker_code/web/static/assets/move-Bu9oaDYs.js +1 -0
  618. pythinker_code/web/static/assets/narrat-DRg8JJMk.js +1 -0
  619. pythinker_code/web/static/assets/nextflow-BrzmwbiE.js +1 -0
  620. pythinker_code/web/static/assets/nginx-DknmC5AR.js +1 -0
  621. pythinker_code/web/static/assets/night-owl-C39BiMTA.js +1 -0
  622. pythinker_code/web/static/assets/nim-CVrawwO9.js +1 -0
  623. pythinker_code/web/static/assets/nix-CwoSXNpI.js +1 -0
  624. pythinker_code/web/static/assets/nord-Ddv68eIx.js +1 -0
  625. pythinker_code/web/static/assets/nushell-C-sUppwS.js +1 -0
  626. pythinker_code/web/static/assets/objective-c-DXmwc3jG.js +1 -0
  627. pythinker_code/web/static/assets/objective-cpp-CLxacb5B.js +1 -0
  628. pythinker_code/web/static/assets/ocaml-C0hk2d4L.js +1 -0
  629. pythinker_code/web/static/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  630. pythinker_code/web/static/assets/one-light-PoHY5YXO.js +1 -0
  631. pythinker_code/web/static/assets/openscad-C4EeE6gA.js +1 -0
  632. pythinker_code/web/static/assets/ordinal-Cboi1Yqb.js +1 -0
  633. pythinker_code/web/static/assets/pascal-D93ZcfNL.js +1 -0
  634. pythinker_code/web/static/assets/perl-C0TMdlhV.js +1 -0
  635. pythinker_code/web/static/assets/php-CDn_0X-4.js +1 -0
  636. pythinker_code/web/static/assets/pieDiagram-ADFJNKIX-fNg41mT9.js +30 -0
  637. pythinker_code/web/static/assets/pkl-u5AG7uiY.js +1 -0
  638. pythinker_code/web/static/assets/plastic-3e1v2bzS.js +1 -0
  639. pythinker_code/web/static/assets/plsql-ChMvpjG-.js +1 -0
  640. pythinker_code/web/static/assets/po-BTJTHyun.js +1 -0
  641. pythinker_code/web/static/assets/poimandres-CS3Unz2-.js +1 -0
  642. pythinker_code/web/static/assets/polar-C0HS_06l.js +1 -0
  643. pythinker_code/web/static/assets/postcss-CXtECtnM.js +1 -0
  644. pythinker_code/web/static/assets/powerquery-CEu0bR-o.js +1 -0
  645. pythinker_code/web/static/assets/powershell-Dpen1YoG.js +1 -0
  646. pythinker_code/web/static/assets/prisma-Dd19v3D-.js +1 -0
  647. pythinker_code/web/static/assets/prolog-CbFg5uaA.js +1 -0
  648. pythinker_code/web/static/assets/proto-C7zT0LnQ.js +1 -0
  649. pythinker_code/web/static/assets/pug-CGlum2m_.js +1 -0
  650. pythinker_code/web/static/assets/puppet-BMWR74SV.js +1 -0
  651. pythinker_code/web/static/assets/purescript-CklMAg4u.js +1 -0
  652. pythinker_code/web/static/assets/python-B6aJPvgy.js +1 -0
  653. pythinker_code/web/static/assets/qml-3beO22l8.js +1 -0
  654. pythinker_code/web/static/assets/qmldir-C8lEn-DE.js +1 -0
  655. pythinker_code/web/static/assets/qss-IeuSbFQv.js +1 -0
  656. pythinker_code/web/static/assets/quadrantDiagram-AYHSOK5B-DJz3Kx87.js +7 -0
  657. pythinker_code/web/static/assets/r-Dspwwk_N.js +1 -0
  658. pythinker_code/web/static/assets/racket-BqYA7rlc.js +1 -0
  659. pythinker_code/web/static/assets/raku-DXvB9xmW.js +1 -0
  660. pythinker_code/web/static/assets/razor-C1TweQQi.js +1 -0
  661. pythinker_code/web/static/assets/red-bN70gL4F.js +1 -0
  662. pythinker_code/web/static/assets/reg-C-SQnVFl.js +1 -0
  663. pythinker_code/web/static/assets/regexp-CDVJQ6XC.js +1 -0
  664. pythinker_code/web/static/assets/rel-C3B-1QV4.js +1 -0
  665. pythinker_code/web/static/assets/requirementDiagram-UZGBJVZJ-B4SbrfE9.js +64 -0
  666. pythinker_code/web/static/assets/riscv-BM1_JUlF.js +1 -0
  667. pythinker_code/web/static/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  668. pythinker_code/web/static/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  669. pythinker_code/web/static/assets/rose-pine-qdsjHGoJ.js +1 -0
  670. pythinker_code/web/static/assets/rosmsg-BJDFO7_C.js +1 -0
  671. pythinker_code/web/static/assets/rst-B0xPkSld.js +1 -0
  672. pythinker_code/web/static/assets/ruby-BvKwtOVI.js +1 -0
  673. pythinker_code/web/static/assets/rust-B1yitclQ.js +1 -0
  674. pythinker_code/web/static/assets/sankeyDiagram-TZEHDZUN-CoSUjLAG.js +10 -0
  675. pythinker_code/web/static/assets/sas-cz2c8ADy.js +1 -0
  676. pythinker_code/web/static/assets/sass-Cj5Yp3dK.js +1 -0
  677. pythinker_code/web/static/assets/scala-C151Ov-r.js +1 -0
  678. pythinker_code/web/static/assets/scheme-C98Dy4si.js +1 -0
  679. pythinker_code/web/static/assets/scss-OYdSNvt2.js +1 -0
  680. pythinker_code/web/static/assets/sdbl-DVxCFoDh.js +1 -0
  681. pythinker_code/web/static/assets/sequenceDiagram-WL72ISMW-PjhBNHi3.js +145 -0
  682. pythinker_code/web/static/assets/shaderlab-Dg9Lc6iA.js +1 -0
  683. pythinker_code/web/static/assets/shellscript-Yzrsuije.js +1 -0
  684. pythinker_code/web/static/assets/shellsession-BADoaaVG.js +1 -0
  685. pythinker_code/web/static/assets/slack-dark-BthQWCQV.js +1 -0
  686. pythinker_code/web/static/assets/slack-ochin-DqwNpetd.js +1 -0
  687. pythinker_code/web/static/assets/smalltalk-BERRCDM3.js +1 -0
  688. pythinker_code/web/static/assets/snazzy-light-Bw305WKR.js +1 -0
  689. pythinker_code/web/static/assets/solarized-dark-DXbdFlpD.js +1 -0
  690. pythinker_code/web/static/assets/solarized-light-L9t79GZl.js +1 -0
  691. pythinker_code/web/static/assets/solidity-rGO070M0.js +1 -0
  692. pythinker_code/web/static/assets/soy-Brmx7dQM.js +1 -0
  693. pythinker_code/web/static/assets/sparql-rVzFXLq3.js +1 -0
  694. pythinker_code/web/static/assets/splunk-BtCnVYZw.js +1 -0
  695. pythinker_code/web/static/assets/sql-BLtJtn59.js +1 -0
  696. pythinker_code/web/static/assets/ssh-config-_ykCGR6B.js +1 -0
  697. pythinker_code/web/static/assets/stata-BH5u7GGu.js +1 -0
  698. pythinker_code/web/static/assets/stateDiagram-FKZM4ZOC-DOwESt8-.js +1 -0
  699. pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-yl3OHWiP.js +1 -0
  700. pythinker_code/web/static/assets/stylus-BEDo0Tqx.js +1 -0
  701. pythinker_code/web/static/assets/svelte-zxCyuUbr.js +1 -0
  702. pythinker_code/web/static/assets/swift-Dg5xB15N.js +1 -0
  703. pythinker_code/web/static/assets/synthwave-84-CbfX1IO0.js +1 -0
  704. pythinker_code/web/static/assets/system-verilog-CnnmHF94.js +1 -0
  705. pythinker_code/web/static/assets/systemd-4A_iFExJ.js +1 -0
  706. pythinker_code/web/static/assets/talonscript-CkByrt1z.js +1 -0
  707. pythinker_code/web/static/assets/tasl-QIJgUcNo.js +1 -0
  708. pythinker_code/web/static/assets/tcl-dwOrl1Do.js +1 -0
  709. pythinker_code/web/static/assets/templ-W15q3VgB.js +1 -0
  710. pythinker_code/web/static/assets/terraform-BETggiCN.js +1 -0
  711. pythinker_code/web/static/assets/tex-CvyZ59Mk.js +1 -0
  712. pythinker_code/web/static/assets/timeline-definition-IT6M3QCI-CkCLnAgi.js +61 -0
  713. pythinker_code/web/static/assets/tokyo-night-hegEt444.js +1 -0
  714. pythinker_code/web/static/assets/toml-vGWfd6FD.js +1 -0
  715. pythinker_code/web/static/assets/treemap-KMMF4GRG-CZS5XwTf.js +128 -0
  716. pythinker_code/web/static/assets/ts-tags-zn1MmPIZ.js +1 -0
  717. pythinker_code/web/static/assets/tsv-B_m7g4N7.js +1 -0
  718. pythinker_code/web/static/assets/tsx-COt5Ahok.js +1 -0
  719. pythinker_code/web/static/assets/turtle-BsS91CYL.js +1 -0
  720. pythinker_code/web/static/assets/twig-CO9l9SDP.js +1 -0
  721. pythinker_code/web/static/assets/typescript-BPQ3VLAy.js +1 -0
  722. pythinker_code/web/static/assets/typespec-BGHnOYBU.js +1 -0
  723. pythinker_code/web/static/assets/typst-DHCkPAjA.js +1 -0
  724. pythinker_code/web/static/assets/v-BcVCzyr7.js +1 -0
  725. pythinker_code/web/static/assets/vala-CsfeWuGM.js +1 -0
  726. pythinker_code/web/static/assets/vb-D17OF-Vu.js +1 -0
  727. pythinker_code/web/static/assets/verilog-BQ8w6xss.js +1 -0
  728. pythinker_code/web/static/assets/vesper-DU1UobuO.js +1 -0
  729. pythinker_code/web/static/assets/vhdl-CeAyd5Ju.js +1 -0
  730. pythinker_code/web/static/assets/viml-CJc9bBzg.js +1 -0
  731. pythinker_code/web/static/assets/vitesse-black-Bkuqu6BP.js +1 -0
  732. pythinker_code/web/static/assets/vitesse-dark-D0r3Knsf.js +1 -0
  733. pythinker_code/web/static/assets/vitesse-light-CVO1_9PV.js +1 -0
  734. pythinker_code/web/static/assets/vue-DN_0RTcg.js +1 -0
  735. pythinker_code/web/static/assets/vue-html-AaS7Mt5G.js +1 -0
  736. pythinker_code/web/static/assets/vue-vine-CQOfvN7w.js +1 -0
  737. pythinker_code/web/static/assets/vyper-CDx5xZoG.js +1 -0
  738. pythinker_code/web/static/assets/wasm-CG6Dc4jp.js +1 -0
  739. pythinker_code/web/static/assets/wasm-MzD3tlZU.js +1 -0
  740. pythinker_code/web/static/assets/wenyan-BV7otONQ.js +1 -0
  741. pythinker_code/web/static/assets/wgsl-Dx-B1_4e.js +1 -0
  742. pythinker_code/web/static/assets/wikitext-BhOHFoWU.js +1 -0
  743. pythinker_code/web/static/assets/wit-5i3qLPDT.js +1 -0
  744. pythinker_code/web/static/assets/wolfram-lXgVvXCa.js +1 -0
  745. pythinker_code/web/static/assets/xml-sdJ4AIDG.js +1 -0
  746. pythinker_code/web/static/assets/xsl-CtQFsRM5.js +1 -0
  747. pythinker_code/web/static/assets/xychartDiagram-PRI3JC2R-DkqqHNLh.js +7 -0
  748. pythinker_code/web/static/assets/yaml-Buea-lGh.js +1 -0
  749. pythinker_code/web/static/assets/zenscript-DVFEvuxE.js +1 -0
  750. pythinker_code/web/static/assets/zig-VOosw3JB.js +1 -0
  751. pythinker_code/web/static/brand/apple-touch-icon.png +0 -0
  752. pythinker_code/web/static/brand/arctecture.webp +0 -0
  753. pythinker_code/web/static/brand/bimi-logo.svg +46 -0
  754. pythinker_code/web/static/brand/favicon.ico +0 -0
  755. pythinker_code/web/static/brand/fonts/dm-sans-latin-ext.woff2 +0 -0
  756. pythinker_code/web/static/brand/fonts/dm-sans-latin.woff2 +0 -0
  757. pythinker_code/web/static/brand/fonts/instrument-sans-latin-ext.woff2 +0 -0
  758. pythinker_code/web/static/brand/fonts/instrument-sans-latin.woff2 +0 -0
  759. pythinker_code/web/static/brand/fonts/instrument-serif-latin-ext.woff2 +0 -0
  760. pythinker_code/web/static/brand/fonts/instrument-serif-latin.woff2 +0 -0
  761. pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin-ext.woff2 +0 -0
  762. pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin.woff2 +0 -0
  763. pythinker_code/web/static/brand/fonts/libre-baskerville-latin-ext.woff2 +0 -0
  764. pythinker_code/web/static/brand/fonts/libre-baskerville-latin.woff2 +0 -0
  765. pythinker_code/web/static/brand/fonts/roboto-latin-ext.woff2 +0 -0
  766. pythinker_code/web/static/brand/fonts/roboto-latin.woff2 +0 -0
  767. pythinker_code/web/static/brand/icon-192.png +0 -0
  768. pythinker_code/web/static/brand/icon-512.png +0 -0
  769. pythinker_code/web/static/brand/icon.svg +1 -0
  770. pythinker_code/web/static/brand/logo.png +0 -0
  771. pythinker_code/web/static/brand/pythinker_animated.svg +79 -0
  772. pythinker_code/web/static/brand/robots.txt +4 -0
  773. pythinker_code/web/static/index.html +15 -0
  774. pythinker_code/web/static/logo.png +0 -0
  775. pythinker_code/web/store/__init__.py +1 -0
  776. pythinker_code/web/store/sessions.py +433 -0
  777. pythinker_code/wire/__init__.py +148 -0
  778. pythinker_code/wire/file.py +151 -0
  779. pythinker_code/wire/jsonrpc.py +263 -0
  780. pythinker_code/wire/protocol.py +2 -0
  781. pythinker_code/wire/root_hub.py +27 -0
  782. pythinker_code/wire/serde.py +26 -0
  783. pythinker_code/wire/server.py +1072 -0
  784. pythinker_code/wire/types.py +698 -0
  785. pythinker_code-0.8.0.dist-info/METADATA +706 -0
  786. pythinker_code-0.8.0.dist-info/RECORD +790 -0
  787. pythinker_code-0.8.0.dist-info/WHEEL +4 -0
  788. pythinker_code-0.8.0.dist-info/entry_points.txt +4 -0
  789. pythinker_code-0.8.0.dist-info/licenses/LICENSE +202 -0
  790. pythinker_code-0.8.0.dist-info/licenses/NOTICE +14 -0
@@ -0,0 +1,904 @@
1
+ """
2
+ The local version of the Grep tool using ripgrep.
3
+ Be cautious that `HostPath` is not used in this implementation.
4
+ """
5
+
6
+ import asyncio
7
+ import fnmatch
8
+ import os
9
+ import platform
10
+ import re
11
+ import shutil
12
+ import stat
13
+ import tarfile
14
+ import tempfile
15
+ import zipfile
16
+ from pathlib import Path
17
+ from typing import override
18
+
19
+ import aiohttp
20
+ from pydantic import BaseModel, Field, field_validator
21
+ from pythinker_core.tooling import CallableTool2, ToolError, ToolReturnValue
22
+
23
+ import pythinker_code
24
+ from pythinker_code.share import get_share_dir
25
+ from pythinker_code.tools.utils import ToolResultBuilder, load_desc
26
+ from pythinker_code.utils.aiohttp import new_client_session
27
+ from pythinker_code.utils.logging import logger
28
+ from pythinker_code.utils.sensitive import is_sensitive_file, sensitive_file_warning
29
+
30
+
31
+ class Params(BaseModel):
32
+ pattern: str = Field(
33
+ description="The regular expression pattern to search for in file contents"
34
+ )
35
+ path: str = Field(
36
+ description=(
37
+ "File or directory to search in. Defaults to current working directory. "
38
+ "If specified, it must be an absolute path."
39
+ ),
40
+ default=".",
41
+ )
42
+ glob: str | None = Field(
43
+ description=(
44
+ "Glob pattern to filter files (e.g. `*.js`, `*.{ts,tsx}`). No filter by default."
45
+ ),
46
+ default=None,
47
+ )
48
+ output_mode: str = Field(
49
+ description=(
50
+ "`content`: Show matching lines (supports `-B`, `-A`, `-C`, `-n`, `head_limit`); "
51
+ "`files_with_matches`: Show file paths (supports `head_limit`); "
52
+ "`count_matches`: Show total number of matches. "
53
+ "Defaults to `files_with_matches`."
54
+ ),
55
+ default="files_with_matches",
56
+ )
57
+ before_context: int | None = Field(
58
+ alias="-B",
59
+ description=(
60
+ "Number of lines to show before each match (the `-B` option). "
61
+ "Requires `output_mode` to be `content`."
62
+ ),
63
+ default=None,
64
+ )
65
+ after_context: int | None = Field(
66
+ alias="-A",
67
+ description=(
68
+ "Number of lines to show after each match (the `-A` option). "
69
+ "Requires `output_mode` to be `content`."
70
+ ),
71
+ default=None,
72
+ )
73
+ context: int | None = Field(
74
+ alias="-C",
75
+ description=(
76
+ "Number of lines to show before and after each match (the `-C` option). "
77
+ "Requires `output_mode` to be `content`."
78
+ ),
79
+ default=None,
80
+ )
81
+ line_number: bool = Field(
82
+ alias="-n",
83
+ description=(
84
+ "Show line numbers in output (the `-n` option). "
85
+ "Requires `output_mode` to be `content`. Defaults to true."
86
+ ),
87
+ default=True,
88
+ )
89
+ ignore_case: bool = Field(
90
+ alias="-i",
91
+ description="Case insensitive search (the `-i` option).",
92
+ default=False,
93
+ )
94
+ type: str | None = Field(
95
+ description=(
96
+ "File type to search. Examples: py, rust, js, ts, go, java, etc. "
97
+ "More efficient than `glob` for standard file types."
98
+ ),
99
+ default=None,
100
+ )
101
+ head_limit: int | None = Field(
102
+ description=(
103
+ "Limit output to first N lines/entries, equivalent to `| head -N`. "
104
+ "Works across all output modes: content (limits output lines), "
105
+ "files_with_matches (limits file paths), count_matches (limits count entries). "
106
+ "Defaults to 250. "
107
+ "Pass 0 for unlimited (use sparingly — large result sets waste context)."
108
+ ),
109
+ default=250,
110
+ ge=0,
111
+ )
112
+ offset: int = Field(
113
+ description=(
114
+ "Skip first N lines/entries before applying head_limit, "
115
+ "equivalent to `| tail -n +N | head -N`. "
116
+ "Works across all output modes. Defaults to 0."
117
+ ),
118
+ default=0,
119
+ ge=0,
120
+ )
121
+ multiline: bool = Field(
122
+ description=(
123
+ "Enable multiline mode where `.` matches newlines and patterns can span "
124
+ "lines (the `-U` and `--multiline-dotall` options). "
125
+ "By default, multiline mode is disabled."
126
+ ),
127
+ default=False,
128
+ )
129
+ include_ignored: bool = Field(
130
+ description=(
131
+ "Include files that are ignored by `.gitignore`, `.ignore`, and other ignore "
132
+ "rules. Useful for searching gitignored artifacts such as build outputs "
133
+ "(e.g. `dist/`, `build/`) or `node_modules`. Sensitive files (like `.env`) "
134
+ "remain filtered by the sensitive-file protection layer. Defaults to false."
135
+ ),
136
+ default=False,
137
+ )
138
+
139
+
140
+ RG_VERSION = "15.0.0"
141
+ RG_BASE_URL = "http://cdn.pythinker.com/binaries/pythinker-code/rg"
142
+ RG_GITHUB_BASE_URL = "https://github.com/BurntSushi/ripgrep/releases/download"
143
+ RG_TIMEOUT = 20 # seconds
144
+ RG_MAX_BUFFER = 20_000_000 # 20MB stdout/stderr buffer limit
145
+ RG_KILL_GRACE = 5 # seconds: SIGTERM → SIGKILL
146
+ _RG_DOWNLOAD_LOCK = asyncio.Lock()
147
+ _PYTHON_FALLBACK_TYPE_GLOBS = {
148
+ "bash": ("*.bash", "*.sh"),
149
+ "c": ("*.c", "*.h"),
150
+ "cpp": ("*.cc", "*.cpp", "*.cxx", "*.hpp", "*.hxx"),
151
+ "go": ("*.go",),
152
+ "java": ("*.java",),
153
+ "js": ("*.cjs", "*.js", "*.jsx", "*.mjs"),
154
+ "json": ("*.json",),
155
+ "markdown": ("*.markdown", "*.md"),
156
+ "md": ("*.markdown", "*.md"),
157
+ "py": ("*.py", "*.pyw"),
158
+ "rust": ("*.rs",),
159
+ "sh": ("*.bash", "*.sh"),
160
+ "toml": ("*.toml",),
161
+ "ts": ("*.ts", "*.tsx"),
162
+ "txt": ("*.txt",),
163
+ "yaml": ("*.yaml", "*.yml"),
164
+ "zsh": ("*.zsh",),
165
+ }
166
+
167
+
168
+ def _rg_binary_name() -> str:
169
+ return "rg.exe" if platform.system() == "Windows" else "rg"
170
+
171
+
172
+ def _find_existing_rg(bin_name: str) -> Path | None:
173
+ if env_path := os.getenv("PYTHINKER_RG_PATH"):
174
+ configured = Path(env_path).expanduser()
175
+ if configured.is_file():
176
+ return configured
177
+
178
+ share_bin = get_share_dir() / "bin" / bin_name
179
+ if share_bin.is_file():
180
+ return share_bin
181
+
182
+ assert pythinker_code.__file__ is not None
183
+ local_dep = Path(pythinker_code.__file__).parent / "deps" / "bin" / bin_name
184
+ if local_dep.is_file():
185
+ return local_dep
186
+
187
+ system_rg = shutil.which("rg")
188
+ if system_rg:
189
+ return Path(system_rg)
190
+
191
+ for candidate in (
192
+ Path("/usr/bin") / bin_name,
193
+ Path("/usr/local/bin") / bin_name,
194
+ Path.home() / ".cargo" / "bin" / bin_name,
195
+ Path.home() / ".local" / "bin" / bin_name,
196
+ Path.home() / ".pi" / "agent" / "bin" / bin_name,
197
+ ):
198
+ if candidate.is_file():
199
+ return candidate
200
+
201
+ return None
202
+
203
+
204
+ def _detect_target() -> str | None:
205
+ sys_name = platform.system()
206
+ mach = platform.machine().lower()
207
+
208
+ if mach in ("x86_64", "amd64"):
209
+ arch = "x86_64"
210
+ elif mach in ("arm64", "aarch64"):
211
+ arch = "aarch64"
212
+ else:
213
+ logger.error("Unsupported architecture for ripgrep: {mach}", mach=mach)
214
+ return None
215
+
216
+ if sys_name == "Darwin":
217
+ os_name = "apple-darwin"
218
+ elif sys_name == "Linux":
219
+ os_name = "unknown-linux-musl" if arch == "x86_64" else "unknown-linux-gnu"
220
+ elif sys_name == "Windows":
221
+ os_name = "pc-windows-msvc"
222
+ else:
223
+ logger.error("Unsupported operating system for ripgrep: {sys_name}", sys_name=sys_name)
224
+ return None
225
+
226
+ return f"{arch}-{os_name}"
227
+
228
+
229
+ async def _download_and_install_rg(bin_name: str) -> Path:
230
+ target = _detect_target()
231
+ if not target:
232
+ raise RuntimeError("Unsupported platform for ripgrep download")
233
+
234
+ is_windows = "windows" in target
235
+ archive_ext = "zip" if is_windows else "tar.gz"
236
+ filename = f"ripgrep-{RG_VERSION}-{target}.{archive_ext}"
237
+ urls = [
238
+ f"{RG_BASE_URL}/{filename}",
239
+ f"{RG_GITHUB_BASE_URL}/{RG_VERSION}/{filename}",
240
+ ]
241
+
242
+ share_bin_dir = get_share_dir() / "bin"
243
+ share_bin_dir.mkdir(parents=True, exist_ok=True)
244
+ destination = share_bin_dir / bin_name
245
+
246
+ # Downloading the ripgrep binary can be slow on constrained networks.
247
+ download_timeout = aiohttp.ClientTimeout(total=600, sock_read=60, sock_connect=15)
248
+ async with new_client_session(timeout=download_timeout) as session:
249
+ with tempfile.TemporaryDirectory(prefix="pythinker-rg-") as tmpdir:
250
+ tar_path = Path(tmpdir) / filename
251
+
252
+ download_errors: list[str] = []
253
+ for url in urls:
254
+ logger.info("Downloading ripgrep from {url}", url=url)
255
+ try:
256
+ async with session.get(url) as resp:
257
+ resp.raise_for_status()
258
+ with open(tar_path, "wb") as fh:
259
+ async for chunk in resp.content.iter_chunked(1024 * 64):
260
+ if chunk:
261
+ fh.write(chunk)
262
+ break
263
+ except (aiohttp.ClientError, TimeoutError) as exc:
264
+ download_errors.append(f"{url}: {exc}")
265
+ else:
266
+ raise RuntimeError(
267
+ "Failed to download ripgrep binary from configured mirrors: "
268
+ + "; ".join(download_errors)
269
+ )
270
+
271
+ try:
272
+ if is_windows:
273
+ with zipfile.ZipFile(tar_path, "r") as zf:
274
+ member_name = next(
275
+ (name for name in zf.namelist() if Path(name).name == bin_name),
276
+ None,
277
+ )
278
+ if not member_name:
279
+ raise RuntimeError("Ripgrep binary not found in archive")
280
+ with zf.open(member_name) as source, open(destination, "wb") as dest_fh:
281
+ shutil.copyfileobj(source, dest_fh)
282
+ else:
283
+ with tarfile.open(tar_path, "r:gz") as tar:
284
+ member = next(
285
+ (m for m in tar.getmembers() if Path(m.name).name == bin_name),
286
+ None,
287
+ )
288
+ if not member:
289
+ raise RuntimeError("Ripgrep binary not found in archive")
290
+ extracted = tar.extractfile(member)
291
+ if not extracted:
292
+ raise RuntimeError("Failed to extract ripgrep binary")
293
+ with open(destination, "wb") as dest_fh:
294
+ shutil.copyfileobj(extracted, dest_fh)
295
+ except (zipfile.BadZipFile, tarfile.TarError, OSError) as exc:
296
+ raise RuntimeError("Failed to extract ripgrep archive") from exc
297
+
298
+ destination.chmod(destination.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
299
+ logger.info("Installed ripgrep to {destination}", destination=destination)
300
+ return destination
301
+
302
+
303
+ async def _ensure_rg_path() -> str:
304
+ bin_name = _rg_binary_name()
305
+ existing = _find_existing_rg(bin_name)
306
+ if existing:
307
+ return str(existing)
308
+
309
+ async with _RG_DOWNLOAD_LOCK:
310
+ existing = _find_existing_rg(bin_name)
311
+ if existing:
312
+ return str(existing)
313
+
314
+ downloaded = await _download_and_install_rg(bin_name)
315
+ return str(downloaded)
316
+
317
+
318
+ def _build_rg_args(rg_path: str, params: Params, *, single_threaded: bool = False) -> list[str]:
319
+ """Build ripgrep command-line arguments from Params."""
320
+ args: list[str] = [rg_path]
321
+
322
+ # Fixed args
323
+ if params.output_mode != "content":
324
+ args.extend(["--max-columns", "500"])
325
+ args.append("--hidden")
326
+ if params.include_ignored:
327
+ args.append("--no-ignore")
328
+ for vcs_dir in (".git", ".svn", ".hg", ".bzr", ".jj", ".sl"):
329
+ args.extend(["--glob", f"!{vcs_dir}"])
330
+
331
+ if single_threaded:
332
+ args.extend(["-j", "1"])
333
+
334
+ # Search options
335
+ if params.ignore_case:
336
+ args.append("--ignore-case")
337
+ if params.multiline:
338
+ args.extend(["--multiline", "--multiline-dotall"])
339
+
340
+ # Content display options (only for content mode)
341
+ if params.output_mode == "content":
342
+ if params.before_context is not None:
343
+ args.extend(["--before-context", str(params.before_context)])
344
+ if params.after_context is not None:
345
+ args.extend(["--after-context", str(params.after_context)])
346
+ if params.context is not None:
347
+ args.extend(["--context", str(params.context)])
348
+ if params.line_number:
349
+ args.append("--line-number")
350
+
351
+ # File filtering options
352
+ if params.glob:
353
+ args.extend(["--glob", params.glob])
354
+ if params.type:
355
+ args.extend(["--type", params.type])
356
+
357
+ # Output mode
358
+ if params.output_mode == "files_with_matches":
359
+ args.append("--files-with-matches")
360
+ elif params.output_mode == "count_matches":
361
+ args.append("--count-matches")
362
+
363
+ # Separate pattern from flags to avoid ambiguity (e.g. pattern starting with -)
364
+ args.append("--")
365
+ args.append(params.pattern)
366
+ args.append(os.path.expanduser(params.path))
367
+
368
+ return args
369
+
370
+
371
+ async def _read_stream(
372
+ stream: asyncio.StreamReader,
373
+ buffer: bytearray,
374
+ limit: int,
375
+ truncated_flag: list[bool] | None = None,
376
+ ) -> bool:
377
+ """Incrementally read from stream into buffer, up to limit bytes.
378
+
379
+ After hitting the limit, continues draining the pipe (discarding data)
380
+ so the child process doesn't block on a full pipe buffer.
381
+
382
+ Args:
383
+ truncated_flag: If provided, truncated_flag[0] is set to True at the
384
+ moment truncation occurs (synchronously, before the next await).
385
+ This ensures the flag is available even if the coroutine is
386
+ cancelled by asyncio.wait_for timeout.
387
+
388
+ Returns True if output was truncated (exceeded limit).
389
+ """
390
+ truncated = False
391
+ while True:
392
+ chunk = await stream.read(65536)
393
+ if not chunk:
394
+ break
395
+ if len(buffer) < limit:
396
+ needed = limit - len(buffer)
397
+ buffer.extend(chunk[:needed])
398
+ if len(chunk) > needed:
399
+ truncated = True
400
+ if truncated_flag is not None:
401
+ truncated_flag[0] = True
402
+ else:
403
+ truncated = True
404
+ if truncated_flag is not None:
405
+ truncated_flag[0] = True
406
+ return truncated
407
+
408
+
409
+ async def _kill_process(process: asyncio.subprocess.Process) -> None:
410
+ """Two-phase kill: SIGTERM → grace period → SIGKILL."""
411
+ process.terminate()
412
+ try:
413
+ await asyncio.wait_for(process.wait(), timeout=RG_KILL_GRACE)
414
+ except TimeoutError:
415
+ process.kill()
416
+ await process.wait()
417
+
418
+
419
+ def _is_eagain(stderr: str) -> bool:
420
+ return "os error 11" in stderr or "Resource temporarily unavailable" in stderr
421
+
422
+
423
+ def _strip_path_prefix(output: str, search_base: str) -> str:
424
+ """Strip search_base prefix from each line to produce relative paths."""
425
+ prefix = search_base.rstrip("/\\") + os.sep
426
+ return "\n".join(
427
+ line[len(prefix) :] if line.startswith(prefix) else line for line in output.split("\n")
428
+ )
429
+
430
+
431
+ def _relative_output_path(path: Path, search_base: Path) -> str:
432
+ try:
433
+ return str(path.relative_to(search_base))
434
+ except ValueError:
435
+ return os.path.relpath(path, search_base)
436
+
437
+
438
+ def _load_basic_ignore_patterns(search_base: Path) -> list[str]:
439
+ patterns: list[str] = []
440
+ for ignore_file in (search_base / ".gitignore", search_base / ".ignore"):
441
+ try:
442
+ lines = ignore_file.read_text(encoding="utf-8", errors="replace").splitlines()
443
+ except OSError:
444
+ continue
445
+ for line in lines:
446
+ pattern = line.strip()
447
+ if not pattern or pattern.startswith("#") or pattern.startswith("!"):
448
+ continue
449
+ patterns.append(pattern)
450
+ return patterns
451
+
452
+
453
+ def _matches_basic_ignore_pattern(rel_path: str, pattern: str) -> bool:
454
+ normalized = rel_path.replace(os.sep, "/")
455
+ pattern = pattern.lstrip("/").replace(os.sep, "/")
456
+ if pattern.endswith("/"):
457
+ directory = pattern.rstrip("/")
458
+ return normalized == directory or normalized.startswith(f"{directory}/")
459
+ if "/" in pattern:
460
+ return fnmatch.fnmatch(normalized, pattern)
461
+ parts = normalized.split("/")
462
+ return any(fnmatch.fnmatch(part, pattern) for part in parts)
463
+
464
+
465
+ def _is_ignored_by_basic_patterns(rel_path: str, patterns: list[str]) -> bool:
466
+ return any(_matches_basic_ignore_pattern(rel_path, pattern) for pattern in patterns)
467
+
468
+
469
+ def _matches_python_type_filter(rel_path: str, file_type: str | None) -> bool:
470
+ if not file_type:
471
+ return True
472
+ globs = _PYTHON_FALLBACK_TYPE_GLOBS.get(file_type.lower())
473
+ if globs is None:
474
+ return False
475
+ return any(fnmatch.fnmatch(rel_path, glob_pattern) for glob_pattern in globs)
476
+
477
+
478
+ def _iter_python_search_files(params: Params) -> list[Path]:
479
+ search_path = Path(os.path.expanduser(params.path))
480
+ search_base = search_path if search_path.is_dir() else search_path.parent
481
+ candidates = [search_path] if search_path.is_file() else search_path.rglob("*")
482
+ files: list[Path] = []
483
+ excluded_vcs = {".git", ".svn", ".hg", ".bzr", ".jj", ".sl"}
484
+ ignore_patterns = [] if params.include_ignored else _load_basic_ignore_patterns(search_base)
485
+ for candidate in candidates:
486
+ if not candidate.is_file():
487
+ continue
488
+ if any(part in excluded_vcs for part in candidate.parts):
489
+ continue
490
+ rel_path = _relative_output_path(candidate, search_base)
491
+ if ignore_patterns and _is_ignored_by_basic_patterns(rel_path, ignore_patterns):
492
+ continue
493
+ if params.glob and not fnmatch.fnmatch(rel_path, params.glob):
494
+ continue
495
+ if not _matches_python_type_filter(rel_path, params.type):
496
+ continue
497
+ files.append(candidate)
498
+ return files
499
+
500
+
501
+ def _apply_python_pagination(lines: list[str], params: Params) -> tuple[list[str], str]:
502
+ message = ""
503
+ if params.offset > 0:
504
+ lines = lines[params.offset :]
505
+ effective_limit = params.head_limit
506
+ if effective_limit and len(lines) > effective_limit:
507
+ total = len(lines) + params.offset
508
+ lines = lines[:effective_limit]
509
+ message = (
510
+ f"Results truncated to {effective_limit} lines (total: {total}). "
511
+ f"Use offset={params.offset + effective_limit} to see more."
512
+ )
513
+ return lines, message
514
+
515
+
516
+ def _python_grep(params: Params, unavailable_reason: str) -> ToolReturnValue:
517
+ builder = ToolResultBuilder()
518
+ flags = re.IGNORECASE if params.ignore_case else 0
519
+ if params.multiline:
520
+ flags |= re.DOTALL | re.MULTILINE
521
+ try:
522
+ pattern = re.compile(params.pattern, flags)
523
+ except re.error as exc:
524
+ return ToolError(message=f"Failed to grep. Error: {exc}", brief="Failed to grep")
525
+
526
+ search_path = Path(os.path.expanduser(params.path))
527
+ search_base = search_path if search_path.is_dir() else search_path.parent
528
+ matched_lines: list[str] = []
529
+ filtered_paths: list[str] = []
530
+
531
+ for file_path in _iter_python_search_files(params):
532
+ rel_path = _relative_output_path(file_path, search_base)
533
+ if is_sensitive_file(rel_path):
534
+ filtered_paths.append(rel_path)
535
+ continue
536
+ try:
537
+ text = file_path.read_text(encoding="utf-8", errors="replace")
538
+ except OSError:
539
+ continue
540
+
541
+ if params.output_mode == "files_with_matches":
542
+ if pattern.search(text):
543
+ matched_lines.append(rel_path)
544
+ continue
545
+
546
+ if params.output_mode == "count_matches":
547
+ count = len(pattern.findall(text))
548
+ if count:
549
+ matched_lines.append(f"{rel_path}:{count}")
550
+ continue
551
+
552
+ lines = text.splitlines()
553
+ matching_indexes = [idx for idx, line in enumerate(lines) if pattern.search(line)]
554
+ if params.context is not None:
555
+ before = after = params.context
556
+ else:
557
+ before = params.before_context or 0
558
+ after = params.after_context or 0
559
+ emitted: set[int] = set()
560
+ for match_idx in matching_indexes:
561
+ start = max(0, match_idx - before)
562
+ end = min(len(lines), match_idx + after + 1)
563
+ for idx in range(start, end):
564
+ if idx in emitted:
565
+ continue
566
+ emitted.add(idx)
567
+ sep = ":" if idx == match_idx else "-"
568
+ line_no = f"{idx + 1}{sep}" if params.line_number else ""
569
+ matched_lines.append(f"{rel_path}{sep}{line_no}{lines[idx]}")
570
+
571
+ if params.output_mode == "files_with_matches":
572
+ matched_lines.sort(
573
+ key=lambda p: os.path.getmtime(search_base / p) if (search_base / p).exists() else 0,
574
+ reverse=True,
575
+ )
576
+
577
+ matched_lines, pagination_message = _apply_python_pagination(matched_lines, params)
578
+ messages = [f"ripgrep unavailable ({unavailable_reason}); used Python fallback."]
579
+ if filtered_paths:
580
+ messages.append(sensitive_file_warning(filtered_paths))
581
+ if pagination_message:
582
+ messages.append(pagination_message)
583
+
584
+ if not matched_lines:
585
+ return builder.ok(message="No matches found. " + " ".join(messages))
586
+ builder.write("\n".join(matched_lines))
587
+ return builder.ok(message=" ".join(messages))
588
+
589
+
590
+ class SmartSearchParams(BaseModel):
591
+ query: str = Field(description="Natural-language or symbol query to search for")
592
+ path: str = Field(
593
+ default=".",
594
+ description="File or directory to search in. If specified, prefer an absolute path.",
595
+ )
596
+ type: str | None = Field(default=None, description="Optional ripgrep file type filter")
597
+ glob: str | None = Field(default=None, description="Optional glob filter")
598
+ max_results: int = Field(
599
+ default=60,
600
+ ge=1,
601
+ le=200,
602
+ description="Maximum combined result lines across planned search passes.",
603
+ )
604
+
605
+ @field_validator("query")
606
+ @classmethod
607
+ def query_non_empty(cls, value: str) -> str:
608
+ value = value.strip()
609
+ if not value:
610
+ raise ValueError("query must not be empty")
611
+ return value
612
+
613
+
614
+ def _smart_search_patterns(query: str) -> list[tuple[str, str]]:
615
+ tokens = [token for token in re.split(r"\W+", query) if len(token) >= 3]
616
+ patterns: list[tuple[str, str]] = [("exact", re.escape(query))]
617
+ if tokens:
618
+ patterns.append(("all terms nearby", ".*".join(re.escape(token) for token in tokens[:5])))
619
+ patterns.append(("any term", "|".join(re.escape(token) for token in tokens[:8])))
620
+ seen: set[str] = set()
621
+ unique: list[tuple[str, str]] = []
622
+ for label, pattern in patterns:
623
+ if pattern in seen:
624
+ continue
625
+ seen.add(pattern)
626
+ unique.append((label, pattern))
627
+ return unique
628
+
629
+
630
+ class SmartSearch(CallableTool2[SmartSearchParams]):
631
+ name: str = "SmartSearch"
632
+ description: str = (
633
+ "Plan and run a small set of bounded local grep passes for a symbol or concept. "
634
+ "Returns cited file/line spans and truncation guidance; use for exploration before "
635
+ "falling back to broad shell searches."
636
+ )
637
+ params: type[SmartSearchParams] = SmartSearchParams
638
+
639
+ @override
640
+ async def __call__(self, params: SmartSearchParams) -> ToolReturnValue:
641
+ seen_lines: set[str] = set()
642
+ sections: list[str] = []
643
+ per_pass_limit = max(10, min(params.max_results, 80))
644
+ grep = Grep()
645
+ for label, pattern in _smart_search_patterns(params.query):
646
+ grep_params = Params.model_validate(
647
+ {
648
+ "pattern": pattern,
649
+ "path": params.path,
650
+ "glob": params.glob,
651
+ "type": params.type,
652
+ "output_mode": "content",
653
+ "-C": 2,
654
+ "-n": True,
655
+ "-i": True,
656
+ "head_limit": per_pass_limit,
657
+ }
658
+ )
659
+ result = await grep(grep_params)
660
+ if result.is_error:
661
+ sections.append(f"## {label}\nERROR: {result.message}")
662
+ continue
663
+ text = str(result.output or "").strip()
664
+ if not text:
665
+ continue
666
+ kept: list[str] = []
667
+ for line in text.splitlines():
668
+ if line in seen_lines:
669
+ continue
670
+ seen_lines.add(line)
671
+ kept.append(line)
672
+ if len(seen_lines) >= params.max_results:
673
+ break
674
+ if kept:
675
+ sections.append(f"## {label}\n" + "\n".join(kept))
676
+ if len(seen_lines) >= params.max_results:
677
+ break
678
+
679
+ builder = ToolResultBuilder()
680
+ if not sections:
681
+ return builder.ok(message="No matches found across smart search passes.")
682
+ builder.write("\n\n".join(sections))
683
+ message = (
684
+ f"SmartSearch ran {len(_smart_search_patterns(params.query))} planned grep passes; "
685
+ f"returned {len(seen_lines)} unique result lines."
686
+ )
687
+ if len(seen_lines) >= params.max_results:
688
+ message += " Results truncated; narrow query/path or increase max_results."
689
+ return builder.ok(message=message)
690
+
691
+
692
+ class Grep(CallableTool2[Params]):
693
+ name: str = "Grep"
694
+ description: str = load_desc(Path(__file__).parent / "grep.md")
695
+ params: type[Params] = Params
696
+
697
+ @override
698
+ async def __call__(self, params: Params, *, _retry: bool = False) -> ToolReturnValue:
699
+ try:
700
+ builder = ToolResultBuilder()
701
+ message = ""
702
+
703
+ # Build rg command
704
+ try:
705
+ rg_path = await _ensure_rg_path()
706
+ except Exception as exc:
707
+ logger.warning("ripgrep unavailable, using Python fallback: {error}", error=exc)
708
+ return _python_grep(params, str(exc))
709
+ logger.debug("Using ripgrep binary: {rg_bin}", rg_bin=rg_path)
710
+ args = _build_rg_args(rg_path, params, single_threaded=_retry)
711
+
712
+ # Execute search as async subprocess (non-blocking, cancellable)
713
+ process = await asyncio.create_subprocess_exec(
714
+ *args,
715
+ stdout=asyncio.subprocess.PIPE,
716
+ stderr=asyncio.subprocess.PIPE,
717
+ )
718
+
719
+ # Stream stdout/stderr incrementally with buffer limit
720
+ stdout_buf = bytearray()
721
+ stderr_buf = bytearray()
722
+ timed_out = False
723
+ stdout_truncated_flag: list[bool] = [False]
724
+
725
+ try:
726
+ assert process.stdout is not None
727
+ assert process.stderr is not None
728
+ await asyncio.wait_for(
729
+ asyncio.gather(
730
+ _read_stream(
731
+ process.stdout, stdout_buf, RG_MAX_BUFFER, stdout_truncated_flag
732
+ ),
733
+ _read_stream(process.stderr, stderr_buf, RG_MAX_BUFFER),
734
+ ),
735
+ timeout=RG_TIMEOUT,
736
+ )
737
+ await process.wait()
738
+ except asyncio.CancelledError:
739
+ await _kill_process(process)
740
+ raise
741
+ except TimeoutError:
742
+ await _kill_process(process)
743
+ timed_out = True
744
+
745
+ output = stdout_buf.decode("utf-8", errors="replace")
746
+ stderr_str = stderr_buf.decode("utf-8", errors="replace")
747
+
748
+ # truncated_flag is set synchronously inside _read_stream at
749
+ # the moment of truncation, so it's available even after timeout.
750
+ buffer_truncated = stdout_truncated_flag[0]
751
+
752
+ # Drop last incomplete line if buffer was truncated
753
+ if buffer_truncated:
754
+ last_nl = output.rfind("\n")
755
+ output = output[:last_nl] if last_nl >= 0 else ""
756
+ message = "Output exceeded buffer limit. Some results omitted."
757
+
758
+ # Timeout: return partial results if available, otherwise error
759
+ if timed_out:
760
+ if not output.strip():
761
+ return ToolError(
762
+ message=(
763
+ f"Grep timed out after {RG_TIMEOUT}s. "
764
+ "Try a more specific path or pattern."
765
+ ),
766
+ brief="Grep timed out",
767
+ )
768
+ timeout_msg = f"Grep timed out after {RG_TIMEOUT}s. Partial results returned."
769
+ message = f"{message} {timeout_msg}" if message else timeout_msg
770
+
771
+ # rg exit codes: 0=matches found, 1=no matches, 2+=error
772
+ if not timed_out and process.returncode not in (0, 1):
773
+ # EAGAIN: retry once with single-threaded mode
774
+ if not _retry and _is_eagain(stderr_str):
775
+ logger.warning("rg EAGAIN error, retrying with -j 1")
776
+ return await self.__call__(params, _retry=True)
777
+ return ToolError(
778
+ message=f"Failed to grep. Error: {stderr_str}",
779
+ brief="Failed to grep",
780
+ )
781
+
782
+ # --- Post-processing pipeline ---
783
+
784
+ # Step 1: mtime sorting (files_with_matches only, skip on timeout)
785
+ if not timed_out and params.output_mode == "files_with_matches":
786
+ lines = [x for x in output.split("\n") if x.strip()]
787
+ lines.sort(
788
+ key=lambda p: os.path.getmtime(p) if os.path.exists(p) else 0,
789
+ reverse=True,
790
+ )
791
+ output = "\n".join(lines)
792
+
793
+ # Step 2: shorten paths to relative (prefix stripping)
794
+ search_base = os.path.abspath(os.path.expanduser(params.path))
795
+ if os.path.isfile(search_base):
796
+ search_base = os.path.dirname(search_base)
797
+ output = _strip_path_prefix(output, search_base)
798
+
799
+ # Step 3: filter sensitive files from output
800
+ # Regex for ripgrep content lines: path:linenum:text (match) or
801
+ # path-linenum-text (context). The separator is `:` or `-` followed
802
+ # by digits then the same separator again.
803
+ _RG_LINE_RE = re.compile(r"^(.*?)([:\-])(\d+)\2")
804
+
805
+ out_lines = output.split("\n")
806
+ filtered_paths: list[str] = []
807
+ kept_lines: list[str] = []
808
+ sensitive_path_set: set[str] = set()
809
+ for line in out_lines:
810
+ if params.output_mode == "content":
811
+ # Match lines: "file.py:10:matched text"
812
+ # Context lines: "file.py-10-context text"
813
+ # Separator: "--"
814
+ if line == "--":
815
+ kept_lines.append(line)
816
+ continue
817
+ m = _RG_LINE_RE.match(line)
818
+ file_path = m.group(1) if m else line
819
+ elif params.output_mode == "count_matches":
820
+ # Count lines: "file.py:42"
821
+ idx = line.rfind(":")
822
+ file_path = line[:idx] if idx > 0 else line
823
+ else:
824
+ # files_with_matches: pure path per line
825
+ file_path = line
826
+
827
+ if file_path and is_sensitive_file(file_path):
828
+ if file_path not in sensitive_path_set:
829
+ sensitive_path_set.add(file_path)
830
+ filtered_paths.append(file_path)
831
+ else:
832
+ kept_lines.append(line)
833
+
834
+ if filtered_paths:
835
+ # Remove trailing "--" separators left after filtering
836
+ while kept_lines and kept_lines[-1] == "--":
837
+ kept_lines.pop()
838
+ output = "\n".join(kept_lines)
839
+ warning = sensitive_file_warning(filtered_paths)
840
+ message = f"{message} {warning}" if message else warning
841
+
842
+ # Step 4: count_matches summary (before pagination, on full results)
843
+ lines = output.split("\n")
844
+ if lines and lines[-1] == "":
845
+ lines = lines[:-1]
846
+
847
+ if params.output_mode == "count_matches":
848
+ total_matches = 0
849
+ total_files = 0
850
+ for line in lines:
851
+ idx = line.rfind(":")
852
+ if idx > 0:
853
+ try:
854
+ total_matches += int(line[idx + 1 :])
855
+ total_files += 1
856
+ except ValueError:
857
+ pass
858
+ count_summary = (
859
+ f"Found {total_matches} total occurrences across {total_files} files."
860
+ )
861
+ message = f"{message} {count_summary}" if message else count_summary
862
+
863
+ # Step 5: offset + head_limit pagination
864
+ if params.offset > 0:
865
+ lines = lines[params.offset :]
866
+
867
+ effective_limit = params.head_limit
868
+ if effective_limit and len(lines) > effective_limit:
869
+ total = len(lines) + params.offset
870
+ lines = lines[:effective_limit]
871
+ output = "\n".join(lines)
872
+ truncation_msg = (
873
+ f"Results truncated to {effective_limit} lines (total: {total}). "
874
+ f"Use offset={params.offset + effective_limit} to see more."
875
+ )
876
+ message = f"{message} {truncation_msg}" if message else truncation_msg
877
+ else:
878
+ output = "\n".join(lines)
879
+
880
+ if not output and not buffer_truncated:
881
+ no_match_msg = "No matches found"
882
+ if message:
883
+ no_match_msg = f"{no_match_msg}. {message}"
884
+ return builder.ok(message=no_match_msg)
885
+
886
+ builder.write(output)
887
+ return builder.ok(message=message)
888
+
889
+ except asyncio.CancelledError:
890
+ raise
891
+ except Exception as e:
892
+ from pythinker_code.telemetry.errors import report_handled_error
893
+
894
+ report_handled_error(e, site="tool.grep", tool="Grep")
895
+ logger.warning(
896
+ "Grep failed: pattern={pattern}, path={path}: {error}",
897
+ pattern=params.pattern,
898
+ path=params.path,
899
+ error=e,
900
+ )
901
+ return ToolError(
902
+ message=f"Failed to grep. Error: {str(e)}",
903
+ brief="Failed to grep",
904
+ )