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.
- pythinker_code/CHANGELOG.md +60 -0
- pythinker_code/__init__.py +0 -0
- pythinker_code/__main__.py +97 -0
- pythinker_code/acp/AGENTS.md +93 -0
- pythinker_code/acp/__init__.py +13 -0
- pythinker_code/acp/convert.py +128 -0
- pythinker_code/acp/host.py +301 -0
- pythinker_code/acp/mcp.py +46 -0
- pythinker_code/acp/server.py +497 -0
- pythinker_code/acp/session.py +502 -0
- pythinker_code/acp/tools.py +174 -0
- pythinker_code/acp/types.py +13 -0
- pythinker_code/acp/version.py +45 -0
- pythinker_code/agents/default/agent.yaml +55 -0
- pythinker_code/agents/default/code_reviewer.yaml +47 -0
- pythinker_code/agents/default/coder.yaml +42 -0
- pythinker_code/agents/default/debugger.yaml +35 -0
- pythinker_code/agents/default/explore.yaml +59 -0
- pythinker_code/agents/default/implementer.yaml +46 -0
- pythinker_code/agents/default/plan.yaml +42 -0
- pythinker_code/agents/default/review.yaml +47 -0
- pythinker_code/agents/default/security_reviewer.yaml +37 -0
- pythinker_code/agents/default/system.md +192 -0
- pythinker_code/agents/default/verifier.yaml +46 -0
- pythinker_code/agents/okabe/agent.yaml +22 -0
- pythinker_code/agentspec.py +163 -0
- pythinker_code/app.py +847 -0
- pythinker_code/approval_runtime/__init__.py +29 -0
- pythinker_code/approval_runtime/models.py +42 -0
- pythinker_code/approval_runtime/runtime.py +235 -0
- pythinker_code/auth/__init__.py +25 -0
- pythinker_code/auth/anthropic_direct.py +207 -0
- pythinker_code/auth/deepseek.py +192 -0
- pythinker_code/auth/github_feedback.py +228 -0
- pythinker_code/auth/lm_studio.py +418 -0
- pythinker_code/auth/minimax.py +203 -0
- pythinker_code/auth/oauth.py +1145 -0
- pythinker_code/auth/ollama.py +293 -0
- pythinker_code/auth/openai.py +783 -0
- pythinker_code/auth/opencode_go.py +203 -0
- pythinker_code/auth/openrouter.py +225 -0
- pythinker_code/auth/platforms.py +475 -0
- pythinker_code/background/__init__.py +36 -0
- pythinker_code/background/agent_runner.py +231 -0
- pythinker_code/background/ids.py +19 -0
- pythinker_code/background/manager.py +668 -0
- pythinker_code/background/models.py +118 -0
- pythinker_code/background/store.py +243 -0
- pythinker_code/background/summary.py +66 -0
- pythinker_code/background/worker.py +209 -0
- pythinker_code/cli/__init__.py +1326 -0
- pythinker_code/cli/__main__.py +19 -0
- pythinker_code/cli/_lazy_group.py +268 -0
- pythinker_code/cli/debug.py +11 -0
- pythinker_code/cli/export.py +322 -0
- pythinker_code/cli/info.py +62 -0
- pythinker_code/cli/mcp.py +362 -0
- pythinker_code/cli/plugin.py +351 -0
- pythinker_code/cli/review.py +74 -0
- pythinker_code/cli/secscan.py +11 -0
- pythinker_code/cli/security_scan.py +35 -0
- pythinker_code/cli/toad.py +74 -0
- pythinker_code/cli/update.py +26 -0
- pythinker_code/cli/vis.py +38 -0
- pythinker_code/cli/web.py +80 -0
- pythinker_code/config.py +511 -0
- pythinker_code/constant.py +33 -0
- pythinker_code/events.py +106 -0
- pythinker_code/exception.py +43 -0
- pythinker_code/extensions.py +151 -0
- pythinker_code/hooks/__init__.py +4 -0
- pythinker_code/hooks/config.py +34 -0
- pythinker_code/hooks/engine.py +383 -0
- pythinker_code/hooks/events.py +190 -0
- pythinker_code/hooks/runner.py +92 -0
- pythinker_code/llm.py +441 -0
- pythinker_code/metadata.py +79 -0
- pythinker_code/notifications/__init__.py +33 -0
- pythinker_code/notifications/llm.py +77 -0
- pythinker_code/notifications/manager.py +145 -0
- pythinker_code/notifications/models.py +50 -0
- pythinker_code/notifications/notifier.py +41 -0
- pythinker_code/notifications/store.py +118 -0
- pythinker_code/notifications/wire.py +21 -0
- pythinker_code/plugin/__init__.py +124 -0
- pythinker_code/plugin/manager.py +166 -0
- pythinker_code/plugin/tool.py +173 -0
- pythinker_code/prompt_templates.py +181 -0
- pythinker_code/prompts/__init__.py +6 -0
- pythinker_code/prompts/compact.md +73 -0
- pythinker_code/prompts/init.md +21 -0
- pythinker_code/py.typed +0 -0
- pythinker_code/session.py +319 -0
- pythinker_code/session_fork.py +325 -0
- pythinker_code/session_state.py +132 -0
- pythinker_code/share.py +14 -0
- pythinker_code/skill/__init__.py +727 -0
- pythinker_code/skill/flow/__init__.py +99 -0
- pythinker_code/skill/flow/d2.py +482 -0
- pythinker_code/skill/flow/mermaid.py +266 -0
- pythinker_code/skills/pythinker-code-help/SKILL.md +54 -0
- pythinker_code/skills/skill-creator/SKILL.md +367 -0
- pythinker_code/soul/__init__.py +304 -0
- pythinker_code/soul/agent.py +552 -0
- pythinker_code/soul/approval.py +267 -0
- pythinker_code/soul/btw.py +220 -0
- pythinker_code/soul/compaction.py +189 -0
- pythinker_code/soul/context.py +339 -0
- pythinker_code/soul/denwarenji.py +39 -0
- pythinker_code/soul/dynamic_injection.py +84 -0
- pythinker_code/soul/dynamic_injections/__init__.py +0 -0
- pythinker_code/soul/dynamic_injections/auto_mode.py +72 -0
- pythinker_code/soul/dynamic_injections/plan_mode.py +239 -0
- pythinker_code/soul/message.py +92 -0
- pythinker_code/soul/permission.py +368 -0
- pythinker_code/soul/pythinkersoul.py +1763 -0
- pythinker_code/soul/slash.py +340 -0
- pythinker_code/soul/toolset.py +826 -0
- pythinker_code/subagents/__init__.py +21 -0
- pythinker_code/subagents/builder.py +43 -0
- pythinker_code/subagents/core.py +86 -0
- pythinker_code/subagents/discovery.py +234 -0
- pythinker_code/subagents/git_context.py +172 -0
- pythinker_code/subagents/models.py +56 -0
- pythinker_code/subagents/output.py +71 -0
- pythinker_code/subagents/registry.py +28 -0
- pythinker_code/subagents/runner.py +442 -0
- pythinker_code/subagents/store.py +200 -0
- pythinker_code/telemetry/__init__.py +217 -0
- pythinker_code/telemetry/config.py +113 -0
- pythinker_code/telemetry/crash.py +191 -0
- pythinker_code/telemetry/errors.py +113 -0
- pythinker_code/telemetry/metrics.py +208 -0
- pythinker_code/telemetry/otel.py +303 -0
- pythinker_code/telemetry/sentry.py +212 -0
- pythinker_code/telemetry/sink.py +189 -0
- pythinker_code/tools/AGENTS.md +6 -0
- pythinker_code/tools/__init__.py +105 -0
- pythinker_code/tools/agent/__init__.py +326 -0
- pythinker_code/tools/agent/description.md +65 -0
- pythinker_code/tools/ask_user/__init__.py +162 -0
- pythinker_code/tools/ask_user/description.md +19 -0
- pythinker_code/tools/background/__init__.py +318 -0
- pythinker_code/tools/background/list.md +10 -0
- pythinker_code/tools/background/output.md +11 -0
- pythinker_code/tools/background/stop.md +8 -0
- pythinker_code/tools/display.py +46 -0
- pythinker_code/tools/dmail/__init__.py +38 -0
- pythinker_code/tools/dmail/dmail.md +17 -0
- pythinker_code/tools/file/__init__.py +31 -0
- pythinker_code/tools/file/glob.md +17 -0
- pythinker_code/tools/file/glob.py +163 -0
- pythinker_code/tools/file/grep.md +6 -0
- pythinker_code/tools/file/grep_local.py +904 -0
- pythinker_code/tools/file/plan_mode.py +45 -0
- pythinker_code/tools/file/read.md +16 -0
- pythinker_code/tools/file/read.py +303 -0
- pythinker_code/tools/file/read_media.md +24 -0
- pythinker_code/tools/file/read_media.py +220 -0
- pythinker_code/tools/file/replace.md +7 -0
- pythinker_code/tools/file/replace.py +204 -0
- pythinker_code/tools/file/utils.py +257 -0
- pythinker_code/tools/file/write.md +5 -0
- pythinker_code/tools/file/write.py +186 -0
- pythinker_code/tools/plan/__init__.py +362 -0
- pythinker_code/tools/plan/description.md +29 -0
- pythinker_code/tools/plan/enter.py +193 -0
- pythinker_code/tools/plan/enter_description.md +35 -0
- pythinker_code/tools/plan/handoff.py +69 -0
- pythinker_code/tools/plan/heroes.py +277 -0
- pythinker_code/tools/shell/__init__.py +263 -0
- pythinker_code/tools/shell/bash.md +35 -0
- pythinker_code/tools/shell/powershell.md +30 -0
- pythinker_code/tools/test.py +55 -0
- pythinker_code/tools/think/__init__.py +21 -0
- pythinker_code/tools/think/think.md +1 -0
- pythinker_code/tools/todo/__init__.py +168 -0
- pythinker_code/tools/todo/set_todo_list.md +23 -0
- pythinker_code/tools/utils.py +200 -0
- pythinker_code/tools/web/__init__.py +4 -0
- pythinker_code/tools/web/fetch.md +1 -0
- pythinker_code/tools/web/fetch.py +261 -0
- pythinker_code/tools/web/search.md +1 -0
- pythinker_code/tools/web/search.py +163 -0
- pythinker_code/ui/__init__.py +0 -0
- pythinker_code/ui/acp/__init__.py +99 -0
- pythinker_code/ui/print/__init__.py +474 -0
- pythinker_code/ui/print/visualize.py +185 -0
- pythinker_code/ui/shell/__init__.py +1806 -0
- pythinker_code/ui/shell/components/__init__.py +110 -0
- pythinker_code/ui/shell/components/base.py +25 -0
- pythinker_code/ui/shell/components/bash_execution.py +249 -0
- pythinker_code/ui/shell/components/bordered_loader.py +62 -0
- pythinker_code/ui/shell/components/diff.py +308 -0
- pythinker_code/ui/shell/components/footer.py +231 -0
- pythinker_code/ui/shell/components/key_hints.py +27 -0
- pythinker_code/ui/shell/components/messages.py +152 -0
- pythinker_code/ui/shell/components/render_utils.py +198 -0
- pythinker_code/ui/shell/components/settings_list.py +369 -0
- pythinker_code/ui/shell/components/special_messages.py +125 -0
- pythinker_code/ui/shell/components/tool_execution.py +261 -0
- pythinker_code/ui/shell/console.py +109 -0
- pythinker_code/ui/shell/debug.py +190 -0
- pythinker_code/ui/shell/echo.py +30 -0
- pythinker_code/ui/shell/export_import.py +117 -0
- pythinker_code/ui/shell/keyboard.py +300 -0
- pythinker_code/ui/shell/keymap.py +84 -0
- pythinker_code/ui/shell/mcp_status.py +112 -0
- pythinker_code/ui/shell/model_picker.py +318 -0
- pythinker_code/ui/shell/oauth.py +273 -0
- pythinker_code/ui/shell/placeholders.py +578 -0
- pythinker_code/ui/shell/prompt.py +2888 -0
- pythinker_code/ui/shell/replay.py +215 -0
- pythinker_code/ui/shell/selector.py +364 -0
- pythinker_code/ui/shell/selectors/__init__.py +38 -0
- pythinker_code/ui/shell/selectors/extension.py +37 -0
- pythinker_code/ui/shell/selectors/oauth.py +63 -0
- pythinker_code/ui/shell/selectors/settings.py +349 -0
- pythinker_code/ui/shell/selectors/show_images.py +29 -0
- pythinker_code/ui/shell/selectors/theme.py +28 -0
- pythinker_code/ui/shell/selectors/thinking.py +42 -0
- pythinker_code/ui/shell/session_picker.py +227 -0
- pythinker_code/ui/shell/setup.py +212 -0
- pythinker_code/ui/shell/slash.py +1433 -0
- pythinker_code/ui/shell/spinner_words.py +222 -0
- pythinker_code/ui/shell/startup.py +32 -0
- pythinker_code/ui/shell/task_browser.py +486 -0
- pythinker_code/ui/shell/tool_renderers/__init__.py +197 -0
- pythinker_code/ui/shell/tool_renderers/_render_utils.py +168 -0
- pythinker_code/ui/shell/tool_renderers/agent.py +140 -0
- pythinker_code/ui/shell/tool_renderers/ask_user.py +93 -0
- pythinker_code/ui/shell/tool_renderers/background.py +144 -0
- pythinker_code/ui/shell/tool_renderers/bash.py +103 -0
- pythinker_code/ui/shell/tool_renderers/edit.py +163 -0
- pythinker_code/ui/shell/tool_renderers/find.py +81 -0
- pythinker_code/ui/shell/tool_renderers/generic.py +60 -0
- pythinker_code/ui/shell/tool_renderers/grep.py +98 -0
- pythinker_code/ui/shell/tool_renderers/plan.py +98 -0
- pythinker_code/ui/shell/tool_renderers/read.py +103 -0
- pythinker_code/ui/shell/tool_renderers/think.py +66 -0
- pythinker_code/ui/shell/tool_renderers/todo.py +164 -0
- pythinker_code/ui/shell/tool_renderers/web.py +128 -0
- pythinker_code/ui/shell/tool_renderers/write.py +102 -0
- pythinker_code/ui/shell/update.py +352 -0
- pythinker_code/ui/shell/usage.py +291 -0
- pythinker_code/ui/shell/usage_adapters/__init__.py +50 -0
- pythinker_code/ui/shell/usage_adapters/anthropic_admin.py +233 -0
- pythinker_code/ui/shell/usage_adapters/base.py +72 -0
- pythinker_code/ui/shell/usage_adapters/deepseek.py +137 -0
- pythinker_code/ui/shell/usage_adapters/minimax.py +236 -0
- pythinker_code/ui/shell/usage_adapters/openai_admin.py +225 -0
- pythinker_code/ui/shell/usage_adapters/openai_chatgpt.py +241 -0
- pythinker_code/ui/shell/usage_adapters/opencode_go.py +232 -0
- pythinker_code/ui/shell/usage_adapters/openrouter.py +105 -0
- pythinker_code/ui/shell/usage_adapters/pythinker.py +189 -0
- pythinker_code/ui/shell/usage_adapters/pythinker_ai.py +50 -0
- pythinker_code/ui/shell/usage_render.py +150 -0
- pythinker_code/ui/shell/visualize/__init__.py +165 -0
- pythinker_code/ui/shell/visualize/_approval_panel.py +539 -0
- pythinker_code/ui/shell/visualize/_blocks.py +802 -0
- pythinker_code/ui/shell/visualize/_btw_panel.py +227 -0
- pythinker_code/ui/shell/visualize/_input_router.py +48 -0
- pythinker_code/ui/shell/visualize/_interactive.py +531 -0
- pythinker_code/ui/shell/visualize/_live_view.py +891 -0
- pythinker_code/ui/shell/visualize/_question_panel.py +586 -0
- pythinker_code/ui/shell/visualize/_worklog.py +245 -0
- pythinker_code/ui/theme.py +395 -0
- pythinker_code/ui/tui_config.py +82 -0
- pythinker_code/usage_ratelimit_cache.py +175 -0
- pythinker_code/utils/__init__.py +0 -0
- pythinker_code/utils/aiohttp.py +24 -0
- pythinker_code/utils/aioqueue.py +72 -0
- pythinker_code/utils/broadcast.py +38 -0
- pythinker_code/utils/changelog.py +108 -0
- pythinker_code/utils/clipboard.py +246 -0
- pythinker_code/utils/datetime.py +64 -0
- pythinker_code/utils/diff.py +135 -0
- pythinker_code/utils/editor.py +91 -0
- pythinker_code/utils/environment.py +73 -0
- pythinker_code/utils/envvar.py +22 -0
- pythinker_code/utils/export.py +696 -0
- pythinker_code/utils/file_filter.py +375 -0
- pythinker_code/utils/frontmatter.py +70 -0
- pythinker_code/utils/io.py +27 -0
- pythinker_code/utils/logging.py +146 -0
- pythinker_code/utils/media_tags.py +29 -0
- pythinker_code/utils/message.py +24 -0
- pythinker_code/utils/path.py +199 -0
- pythinker_code/utils/proctitle.py +33 -0
- pythinker_code/utils/proxy.py +31 -0
- pythinker_code/utils/pyinstaller.py +45 -0
- pythinker_code/utils/rich/__init__.py +33 -0
- pythinker_code/utils/rich/columns.py +99 -0
- pythinker_code/utils/rich/diff_render.py +481 -0
- pythinker_code/utils/rich/markdown.py +935 -0
- pythinker_code/utils/rich/markdown_sample.md +108 -0
- pythinker_code/utils/rich/markdown_sample_short.md +2 -0
- pythinker_code/utils/rich/syntax.py +114 -0
- pythinker_code/utils/sensitive.py +54 -0
- pythinker_code/utils/server.py +121 -0
- pythinker_code/utils/signals.py +43 -0
- pythinker_code/utils/slashcmd.py +124 -0
- pythinker_code/utils/string.py +41 -0
- pythinker_code/utils/subprocess_env.py +83 -0
- pythinker_code/utils/term.py +168 -0
- pythinker_code/utils/typing.py +20 -0
- pythinker_code/vis/__init__.py +0 -0
- pythinker_code/vis/api/__init__.py +5 -0
- pythinker_code/vis/api/sessions.py +714 -0
- pythinker_code/vis/api/statistics.py +209 -0
- pythinker_code/vis/api/system.py +19 -0
- pythinker_code/vis/app.py +199 -0
- pythinker_code/vis/static/assets/highlighted-body-B3W2YXNL-CY1rtwrX.js +1 -0
- pythinker_code/vis/static/assets/index-DSRInNnm.css +1 -0
- pythinker_code/vis/static/assets/index-DgmTI2M_.js +185 -0
- pythinker_code/vis/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- pythinker_code/vis/static/index.html +17 -0
- pythinker_code/web/__init__.py +5 -0
- pythinker_code/web/api/__init__.py +15 -0
- pythinker_code/web/api/config.py +217 -0
- pythinker_code/web/api/open_in.py +233 -0
- pythinker_code/web/api/sessions.py +1256 -0
- pythinker_code/web/app.py +449 -0
- pythinker_code/web/auth.py +191 -0
- pythinker_code/web/models.py +98 -0
- pythinker_code/web/runner/__init__.py +5 -0
- pythinker_code/web/runner/messages.py +57 -0
- pythinker_code/web/runner/process.py +754 -0
- pythinker_code/web/runner/worker.py +97 -0
- pythinker_code/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- pythinker_code/web/static/assets/_baseUniq-DpSMr1jx.js +1 -0
- pythinker_code/web/static/assets/abap-BdImnpbu.js +1 -0
- pythinker_code/web/static/assets/actionscript-3-CfeIJUat.js +1 -0
- pythinker_code/web/static/assets/ada-bCR0ucgS.js +1 -0
- pythinker_code/web/static/assets/andromeeda-C-Jbm3Hp.js +1 -0
- pythinker_code/web/static/assets/angular-html-CU67Zn6k.js +1 -0
- pythinker_code/web/static/assets/angular-ts-BwZT4LLn.js +1 -0
- pythinker_code/web/static/assets/apache-Pmp26Uib.js +1 -0
- pythinker_code/web/static/assets/apex-D8_7TLub.js +1 -0
- pythinker_code/web/static/assets/apl-dKokRX4l.js +1 -0
- pythinker_code/web/static/assets/applescript-Co6uUVPk.js +1 -0
- pythinker_code/web/static/assets/ara-BRHolxvo.js +1 -0
- pythinker_code/web/static/assets/arc-DpsahJyV.js +1 -0
- pythinker_code/web/static/assets/architectureDiagram-VXUJARFQ-DqiRv9Eg.js +36 -0
- pythinker_code/web/static/assets/asciidoc-Dv7Oe6Be.js +1 -0
- pythinker_code/web/static/assets/asm-D_Q5rh1f.js +1 -0
- pythinker_code/web/static/assets/astro-CbQHKStN.js +1 -0
- pythinker_code/web/static/assets/aurora-x-D-2ljcwZ.js +1 -0
- pythinker_code/web/static/assets/awk-DMzUqQB5.js +1 -0
- pythinker_code/web/static/assets/ayu-dark-CmMr59Fi.js +1 -0
- pythinker_code/web/static/assets/ballerina-BFfxhgS-.js +1 -0
- pythinker_code/web/static/assets/bat-BkioyH1T.js +1 -0
- pythinker_code/web/static/assets/beancount-k_qm7-4y.js +1 -0
- pythinker_code/web/static/assets/berry-uYugtg8r.js +1 -0
- pythinker_code/web/static/assets/bibtex-CHM0blh-.js +1 -0
- pythinker_code/web/static/assets/bicep-Bmn6On1c.js +1 -0
- pythinker_code/web/static/assets/blade-D4QpJJKB.js +1 -0
- pythinker_code/web/static/assets/blockDiagram-VD42YOAC-WgtUvqbp.js +122 -0
- pythinker_code/web/static/assets/bsl-BO_Y6i37.js +1 -0
- pythinker_code/web/static/assets/c-BIGW1oBm.js +1 -0
- pythinker_code/web/static/assets/c3-VCDPK7BO.js +1 -0
- pythinker_code/web/static/assets/c4Diagram-YG6GDRKO-rK0RPuZd.js +10 -0
- pythinker_code/web/static/assets/cadence-Bv_4Rxtq.js +1 -0
- pythinker_code/web/static/assets/cairo-KRGpt6FW.js +1 -0
- pythinker_code/web/static/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- pythinker_code/web/static/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- pythinker_code/web/static/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- pythinker_code/web/static/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- pythinker_code/web/static/assets/channel-B0rlvkH-.js +1 -0
- pythinker_code/web/static/assets/chunk-4BX2VUAB-DIkMuLV-.js +1 -0
- pythinker_code/web/static/assets/chunk-55IACEB6-CORdm4k4.js +1 -0
- pythinker_code/web/static/assets/chunk-B4BG7PRW-D9xDhwHO.js +165 -0
- pythinker_code/web/static/assets/chunk-DI55MBZ5-BDmF9Bh-.js +220 -0
- pythinker_code/web/static/assets/chunk-FMBD7UC4-BCse_HmM.js +15 -0
- pythinker_code/web/static/assets/chunk-QN33PNHL-DCpBmTzA.js +1 -0
- pythinker_code/web/static/assets/chunk-QZHKN3VN-BqLuqobw.js +1 -0
- pythinker_code/web/static/assets/chunk-TZMSLE5B-8K2ogOKS.js +1 -0
- pythinker_code/web/static/assets/clarity-D53aC0YG.js +1 -0
- pythinker_code/web/static/assets/classDiagram-2ON5EDUG-D_ZHSii2.js +1 -0
- pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-D_ZHSii2.js +1 -0
- pythinker_code/web/static/assets/clojure-P80f7IUj.js +1 -0
- pythinker_code/web/static/assets/clone-GSXejyY1.js +1 -0
- pythinker_code/web/static/assets/cmake-D1j8_8rp.js +1 -0
- pythinker_code/web/static/assets/cobol-nwyudZeR.js +1 -0
- pythinker_code/web/static/assets/code-block-IT6T5CEO-DWTFYA28.js +2 -0
- pythinker_code/web/static/assets/codeowners-Bp6g37R7.js +1 -0
- pythinker_code/web/static/assets/codeql-DsOJ9woJ.js +1 -0
- pythinker_code/web/static/assets/coffee-Ch7k5sss.js +1 -0
- pythinker_code/web/static/assets/common-lisp-Cg-RD9OK.js +1 -0
- pythinker_code/web/static/assets/coq-DkFqJrB1.js +1 -0
- pythinker_code/web/static/assets/cose-bilkent-S5V4N54A-BRI7ES-N.js +1 -0
- pythinker_code/web/static/assets/cpp-CofmeUqb.js +1 -0
- pythinker_code/web/static/assets/crystal-tKQVLTB8.js +1 -0
- pythinker_code/web/static/assets/csharp-K5feNrxe.js +1 -0
- pythinker_code/web/static/assets/css-DPfMkruS.js +1 -0
- pythinker_code/web/static/assets/csv-fuZLfV_i.js +1 -0
- pythinker_code/web/static/assets/cue-D82EKSYY.js +1 -0
- pythinker_code/web/static/assets/cypher-COkxafJQ.js +1 -0
- pythinker_code/web/static/assets/cytoscape.esm-B6BxUuKW.js +321 -0
- pythinker_code/web/static/assets/d-85-TOEBH.js +1 -0
- pythinker_code/web/static/assets/dagre-6UL2VRFP-Ci5GdWfi.js +4 -0
- pythinker_code/web/static/assets/dark-plus-C3mMm8J8.js +1 -0
- pythinker_code/web/static/assets/dart-CF10PKvl.js +1 -0
- pythinker_code/web/static/assets/dax-CEL-wOlO.js +1 -0
- pythinker_code/web/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- pythinker_code/web/static/assets/desktop-BmXAJ9_W.js +1 -0
- pythinker_code/web/static/assets/diagram-PSM6KHXK-0hhAylV4.js +24 -0
- pythinker_code/web/static/assets/diagram-QEK2KX5R-8fxgaW6d.js +43 -0
- pythinker_code/web/static/assets/diagram-S2PKOQOG-FRr0_atE.js +24 -0
- pythinker_code/web/static/assets/diff-D97Zzqfu.js +1 -0
- pythinker_code/web/static/assets/docker-BcOcwvcX.js +1 -0
- pythinker_code/web/static/assets/dotenv-Da5cRb03.js +1 -0
- pythinker_code/web/static/assets/dracula-BzJJZx-M.js +1 -0
- pythinker_code/web/static/assets/dracula-soft-BXkSAIEj.js +1 -0
- pythinker_code/web/static/assets/dream-maker-BtqSS_iP.js +1 -0
- pythinker_code/web/static/assets/edge-BkV0erSs.js +1 -0
- pythinker_code/web/static/assets/elixir-CDX3lj18.js +1 -0
- pythinker_code/web/static/assets/elm-DbKCFpqz.js +1 -0
- pythinker_code/web/static/assets/emacs-lisp-C9XAeP06.js +1 -0
- pythinker_code/web/static/assets/erDiagram-Q2GNP2WA-B3T-hJUM.js +60 -0
- pythinker_code/web/static/assets/erb-BOJIQeun.js +1 -0
- pythinker_code/web/static/assets/erlang-DsQrWhSR.js +1 -0
- pythinker_code/web/static/assets/everforest-dark-BgDCqdQA.js +1 -0
- pythinker_code/web/static/assets/everforest-light-C8M2exoo.js +1 -0
- pythinker_code/web/static/assets/fennel-BYunw83y.js +1 -0
- pythinker_code/web/static/assets/fish-BvzEVeQv.js +1 -0
- pythinker_code/web/static/assets/flowDiagram-NV44I4VS-D0S3u7ot.js +162 -0
- pythinker_code/web/static/assets/fluent-C4IJs8-o.js +1 -0
- pythinker_code/web/static/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- pythinker_code/web/static/assets/fortran-free-form-BxgE0vQu.js +1 -0
- pythinker_code/web/static/assets/fsharp-CXgrBDvD.js +1 -0
- pythinker_code/web/static/assets/ganttDiagram-JELNMOA3-CHrN2a23.js +267 -0
- pythinker_code/web/static/assets/gdresource-B7Tvp0Sc.js +1 -0
- pythinker_code/web/static/assets/gdscript-DTMYz4Jt.js +1 -0
- pythinker_code/web/static/assets/gdshader-DkwncUOv.js +1 -0
- pythinker_code/web/static/assets/genie-D0YGMca9.js +1 -0
- pythinker_code/web/static/assets/gherkin-DyxjwDmM.js +1 -0
- pythinker_code/web/static/assets/git-commit-F4YmCXRG.js +1 -0
- pythinker_code/web/static/assets/git-rebase-r7XF79zn.js +1 -0
- pythinker_code/web/static/assets/gitGraphDiagram-NY62KEGX-CfcXZWg0.js +65 -0
- pythinker_code/web/static/assets/github-dark-DHJKELXO.js +1 -0
- pythinker_code/web/static/assets/github-dark-default-Cuk6v7N8.js +1 -0
- pythinker_code/web/static/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- pythinker_code/web/static/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- pythinker_code/web/static/assets/github-light-DAi9KRSo.js +1 -0
- pythinker_code/web/static/assets/github-light-default-D7oLnXFd.js +1 -0
- pythinker_code/web/static/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- pythinker_code/web/static/assets/gleam-BspZqrRM.js +1 -0
- pythinker_code/web/static/assets/glimmer-js-Rg0-pVw9.js +1 -0
- pythinker_code/web/static/assets/glimmer-ts-U6CK756n.js +1 -0
- pythinker_code/web/static/assets/glsl-DplSGwfg.js +1 -0
- pythinker_code/web/static/assets/gn-n2N0HUVH.js +1 -0
- pythinker_code/web/static/assets/gnuplot-DdkO51Og.js +1 -0
- pythinker_code/web/static/assets/go-Dn2_MT6a.js +1 -0
- pythinker_code/web/static/assets/graph-8jMJwCqE.js +1 -0
- pythinker_code/web/static/assets/graphql-ChdNCCLP.js +1 -0
- pythinker_code/web/static/assets/groovy-gcz8RCvz.js +1 -0
- pythinker_code/web/static/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- pythinker_code/web/static/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- pythinker_code/web/static/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- pythinker_code/web/static/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- pythinker_code/web/static/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- pythinker_code/web/static/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- pythinker_code/web/static/assets/hack-CaT9iCJl.js +1 -0
- pythinker_code/web/static/assets/haml-B8DHNrY2.js +1 -0
- pythinker_code/web/static/assets/handlebars-BL8al0AC.js +1 -0
- pythinker_code/web/static/assets/haskell-Df6bDoY_.js +1 -0
- pythinker_code/web/static/assets/haxe-CzTSHFRz.js +1 -0
- pythinker_code/web/static/assets/hcl-BWvSN4gD.js +1 -0
- pythinker_code/web/static/assets/hjson-D5-asLiD.js +1 -0
- pythinker_code/web/static/assets/hlsl-D3lLCCz7.js +1 -0
- pythinker_code/web/static/assets/houston-DnULxvSX.js +1 -0
- pythinker_code/web/static/assets/html-GMplVEZG.js +1 -0
- pythinker_code/web/static/assets/html-derivative-BFtXZ54Q.js +1 -0
- pythinker_code/web/static/assets/http-jrhK8wxY.js +1 -0
- pythinker_code/web/static/assets/hurl-irOxFIW8.js +1 -0
- pythinker_code/web/static/assets/hxml-Bvhsp5Yf.js +1 -0
- pythinker_code/web/static/assets/hy-DFXneXwc.js +1 -0
- pythinker_code/web/static/assets/imba-DGztddWO.js +1 -0
- pythinker_code/web/static/assets/index-BXrFnzMy.js +153 -0
- pythinker_code/web/static/assets/index-BpoLgcEt.js +1 -0
- pythinker_code/web/static/assets/index-BrfQJnRD.js +5 -0
- pythinker_code/web/static/assets/index-C4gFzubz.js +2 -0
- pythinker_code/web/static/assets/index-CzV_vCfu.css +1 -0
- pythinker_code/web/static/assets/index-DI2oedCt.js +19 -0
- pythinker_code/web/static/assets/infoDiagram-WHAUD3N6-DdxonBf3.js +2 -0
- pythinker_code/web/static/assets/ini-BEwlwnbL.js +1 -0
- pythinker_code/web/static/assets/init-Gi6I4Gst.js +1 -0
- pythinker_code/web/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- pythinker_code/web/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- pythinker_code/web/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- pythinker_code/web/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- pythinker_code/web/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- pythinker_code/web/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- pythinker_code/web/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- pythinker_code/web/static/assets/java-CylS5w8V.js +1 -0
- pythinker_code/web/static/assets/javascript-wDzz0qaB.js +1 -0
- pythinker_code/web/static/assets/jinja-4LBKfQ-Z.js +1 -0
- pythinker_code/web/static/assets/jison-wvAkD_A8.js +1 -0
- pythinker_code/web/static/assets/journeyDiagram-XKPGCS4Q-BXf4aQei.js +139 -0
- pythinker_code/web/static/assets/json-Cp-IABpG.js +1 -0
- pythinker_code/web/static/assets/json5-C9tS-k6U.js +1 -0
- pythinker_code/web/static/assets/jsonc-Des-eS-w.js +1 -0
- pythinker_code/web/static/assets/jsonl-DcaNXYhu.js +1 -0
- pythinker_code/web/static/assets/jsonnet-DFQXde-d.js +1 -0
- pythinker_code/web/static/assets/jssm-C2t-YnRu.js +1 -0
- pythinker_code/web/static/assets/jsx-g9-lgVsj.js +1 -0
- pythinker_code/web/static/assets/julia-CxzCAyBv.js +1 -0
- pythinker_code/web/static/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- pythinker_code/web/static/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- pythinker_code/web/static/assets/kanagawa-wave-DWedfzmr.js +1 -0
- pythinker_code/web/static/assets/kanban-definition-3W4ZIXB7-DLpPPOu8.js +89 -0
- pythinker_code/web/static/assets/katex-D2lIc1rk.css +1 -0
- pythinker_code/web/static/assets/kdl-DV7GczEv.js +1 -0
- pythinker_code/web/static/assets/kotlin-BdnUsdx6.js +1 -0
- pythinker_code/web/static/assets/kusto-DZf3V79B.js +1 -0
- pythinker_code/web/static/assets/laserwave-DUszq2jm.js +1 -0
- pythinker_code/web/static/assets/latex-B4uzh10-.js +1 -0
- pythinker_code/web/static/assets/layout-DH73UoAH.js +1 -0
- pythinker_code/web/static/assets/lean-BZvkOJ9d.js +1 -0
- pythinker_code/web/static/assets/less-B1dDrJ26.js +1 -0
- pythinker_code/web/static/assets/light-plus-B7mTdjB0.js +1 -0
- pythinker_code/web/static/assets/linear-bAer2-sK.js +1 -0
- pythinker_code/web/static/assets/liquid-DYVedYrR.js +1 -0
- pythinker_code/web/static/assets/llvm-BtvRca6l.js +1 -0
- pythinker_code/web/static/assets/log-2UxHyX5q.js +1 -0
- pythinker_code/web/static/assets/logo-BtOb2qkB.js +1 -0
- pythinker_code/web/static/assets/lua-BbnMAYS6.js +1 -0
- pythinker_code/web/static/assets/luau-C-HG3fhB.js +1 -0
- pythinker_code/web/static/assets/make-CHLpvVh8.js +1 -0
- pythinker_code/web/static/assets/markdown-Cvjx9yec.js +1 -0
- pythinker_code/web/static/assets/marko-DZsq8hO1.js +1 -0
- pythinker_code/web/static/assets/material-theme-D5KoaKCx.js +1 -0
- pythinker_code/web/static/assets/material-theme-darker-BfHTSMKl.js +1 -0
- pythinker_code/web/static/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- pythinker_code/web/static/assets/material-theme-ocean-CyktbL80.js +1 -0
- pythinker_code/web/static/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- pythinker_code/web/static/assets/matlab-D7o27uSR.js +1 -0
- pythinker_code/web/static/assets/mdc-DUICxH0z.js +1 -0
- pythinker_code/web/static/assets/mdx-Cmh6b_Ma.js +1 -0
- pythinker_code/web/static/assets/mermaid-VLURNSYL-B2P5VJ9v.css +1 -0
- pythinker_code/web/static/assets/mermaid-VLURNSYL-CuqbwKXv.js +465 -0
- pythinker_code/web/static/assets/mermaid-mWjccvbQ.js +1 -0
- pythinker_code/web/static/assets/mermaid.core-Nx-rTKiV.js +191 -0
- pythinker_code/web/static/assets/min-DbfD8Ywu.js +1 -0
- pythinker_code/web/static/assets/min-dark-CafNBF8u.js +1 -0
- pythinker_code/web/static/assets/min-light-CTRr51gU.js +1 -0
- pythinker_code/web/static/assets/mindmap-definition-VGOIOE7T-C6l761Ue.js +68 -0
- pythinker_code/web/static/assets/mipsasm-CKIfxQSi.js +1 -0
- pythinker_code/web/static/assets/mojo-B93PlW-d.js +1 -0
- pythinker_code/web/static/assets/monokai-D4h5O-jR.js +1 -0
- pythinker_code/web/static/assets/moonbit-Ba13S78F.js +1 -0
- pythinker_code/web/static/assets/move-Bu9oaDYs.js +1 -0
- pythinker_code/web/static/assets/narrat-DRg8JJMk.js +1 -0
- pythinker_code/web/static/assets/nextflow-BrzmwbiE.js +1 -0
- pythinker_code/web/static/assets/nginx-DknmC5AR.js +1 -0
- pythinker_code/web/static/assets/night-owl-C39BiMTA.js +1 -0
- pythinker_code/web/static/assets/nim-CVrawwO9.js +1 -0
- pythinker_code/web/static/assets/nix-CwoSXNpI.js +1 -0
- pythinker_code/web/static/assets/nord-Ddv68eIx.js +1 -0
- pythinker_code/web/static/assets/nushell-C-sUppwS.js +1 -0
- pythinker_code/web/static/assets/objective-c-DXmwc3jG.js +1 -0
- pythinker_code/web/static/assets/objective-cpp-CLxacb5B.js +1 -0
- pythinker_code/web/static/assets/ocaml-C0hk2d4L.js +1 -0
- pythinker_code/web/static/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- pythinker_code/web/static/assets/one-light-PoHY5YXO.js +1 -0
- pythinker_code/web/static/assets/openscad-C4EeE6gA.js +1 -0
- pythinker_code/web/static/assets/ordinal-Cboi1Yqb.js +1 -0
- pythinker_code/web/static/assets/pascal-D93ZcfNL.js +1 -0
- pythinker_code/web/static/assets/perl-C0TMdlhV.js +1 -0
- pythinker_code/web/static/assets/php-CDn_0X-4.js +1 -0
- pythinker_code/web/static/assets/pieDiagram-ADFJNKIX-fNg41mT9.js +30 -0
- pythinker_code/web/static/assets/pkl-u5AG7uiY.js +1 -0
- pythinker_code/web/static/assets/plastic-3e1v2bzS.js +1 -0
- pythinker_code/web/static/assets/plsql-ChMvpjG-.js +1 -0
- pythinker_code/web/static/assets/po-BTJTHyun.js +1 -0
- pythinker_code/web/static/assets/poimandres-CS3Unz2-.js +1 -0
- pythinker_code/web/static/assets/polar-C0HS_06l.js +1 -0
- pythinker_code/web/static/assets/postcss-CXtECtnM.js +1 -0
- pythinker_code/web/static/assets/powerquery-CEu0bR-o.js +1 -0
- pythinker_code/web/static/assets/powershell-Dpen1YoG.js +1 -0
- pythinker_code/web/static/assets/prisma-Dd19v3D-.js +1 -0
- pythinker_code/web/static/assets/prolog-CbFg5uaA.js +1 -0
- pythinker_code/web/static/assets/proto-C7zT0LnQ.js +1 -0
- pythinker_code/web/static/assets/pug-CGlum2m_.js +1 -0
- pythinker_code/web/static/assets/puppet-BMWR74SV.js +1 -0
- pythinker_code/web/static/assets/purescript-CklMAg4u.js +1 -0
- pythinker_code/web/static/assets/python-B6aJPvgy.js +1 -0
- pythinker_code/web/static/assets/qml-3beO22l8.js +1 -0
- pythinker_code/web/static/assets/qmldir-C8lEn-DE.js +1 -0
- pythinker_code/web/static/assets/qss-IeuSbFQv.js +1 -0
- pythinker_code/web/static/assets/quadrantDiagram-AYHSOK5B-DJz3Kx87.js +7 -0
- pythinker_code/web/static/assets/r-Dspwwk_N.js +1 -0
- pythinker_code/web/static/assets/racket-BqYA7rlc.js +1 -0
- pythinker_code/web/static/assets/raku-DXvB9xmW.js +1 -0
- pythinker_code/web/static/assets/razor-C1TweQQi.js +1 -0
- pythinker_code/web/static/assets/red-bN70gL4F.js +1 -0
- pythinker_code/web/static/assets/reg-C-SQnVFl.js +1 -0
- pythinker_code/web/static/assets/regexp-CDVJQ6XC.js +1 -0
- pythinker_code/web/static/assets/rel-C3B-1QV4.js +1 -0
- pythinker_code/web/static/assets/requirementDiagram-UZGBJVZJ-B4SbrfE9.js +64 -0
- pythinker_code/web/static/assets/riscv-BM1_JUlF.js +1 -0
- pythinker_code/web/static/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
- pythinker_code/web/static/assets/rose-pine-moon-D4_iv3hh.js +1 -0
- pythinker_code/web/static/assets/rose-pine-qdsjHGoJ.js +1 -0
- pythinker_code/web/static/assets/rosmsg-BJDFO7_C.js +1 -0
- pythinker_code/web/static/assets/rst-B0xPkSld.js +1 -0
- pythinker_code/web/static/assets/ruby-BvKwtOVI.js +1 -0
- pythinker_code/web/static/assets/rust-B1yitclQ.js +1 -0
- pythinker_code/web/static/assets/sankeyDiagram-TZEHDZUN-CoSUjLAG.js +10 -0
- pythinker_code/web/static/assets/sas-cz2c8ADy.js +1 -0
- pythinker_code/web/static/assets/sass-Cj5Yp3dK.js +1 -0
- pythinker_code/web/static/assets/scala-C151Ov-r.js +1 -0
- pythinker_code/web/static/assets/scheme-C98Dy4si.js +1 -0
- pythinker_code/web/static/assets/scss-OYdSNvt2.js +1 -0
- pythinker_code/web/static/assets/sdbl-DVxCFoDh.js +1 -0
- pythinker_code/web/static/assets/sequenceDiagram-WL72ISMW-PjhBNHi3.js +145 -0
- pythinker_code/web/static/assets/shaderlab-Dg9Lc6iA.js +1 -0
- pythinker_code/web/static/assets/shellscript-Yzrsuije.js +1 -0
- pythinker_code/web/static/assets/shellsession-BADoaaVG.js +1 -0
- pythinker_code/web/static/assets/slack-dark-BthQWCQV.js +1 -0
- pythinker_code/web/static/assets/slack-ochin-DqwNpetd.js +1 -0
- pythinker_code/web/static/assets/smalltalk-BERRCDM3.js +1 -0
- pythinker_code/web/static/assets/snazzy-light-Bw305WKR.js +1 -0
- pythinker_code/web/static/assets/solarized-dark-DXbdFlpD.js +1 -0
- pythinker_code/web/static/assets/solarized-light-L9t79GZl.js +1 -0
- pythinker_code/web/static/assets/solidity-rGO070M0.js +1 -0
- pythinker_code/web/static/assets/soy-Brmx7dQM.js +1 -0
- pythinker_code/web/static/assets/sparql-rVzFXLq3.js +1 -0
- pythinker_code/web/static/assets/splunk-BtCnVYZw.js +1 -0
- pythinker_code/web/static/assets/sql-BLtJtn59.js +1 -0
- pythinker_code/web/static/assets/ssh-config-_ykCGR6B.js +1 -0
- pythinker_code/web/static/assets/stata-BH5u7GGu.js +1 -0
- pythinker_code/web/static/assets/stateDiagram-FKZM4ZOC-DOwESt8-.js +1 -0
- pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-yl3OHWiP.js +1 -0
- pythinker_code/web/static/assets/stylus-BEDo0Tqx.js +1 -0
- pythinker_code/web/static/assets/svelte-zxCyuUbr.js +1 -0
- pythinker_code/web/static/assets/swift-Dg5xB15N.js +1 -0
- pythinker_code/web/static/assets/synthwave-84-CbfX1IO0.js +1 -0
- pythinker_code/web/static/assets/system-verilog-CnnmHF94.js +1 -0
- pythinker_code/web/static/assets/systemd-4A_iFExJ.js +1 -0
- pythinker_code/web/static/assets/talonscript-CkByrt1z.js +1 -0
- pythinker_code/web/static/assets/tasl-QIJgUcNo.js +1 -0
- pythinker_code/web/static/assets/tcl-dwOrl1Do.js +1 -0
- pythinker_code/web/static/assets/templ-W15q3VgB.js +1 -0
- pythinker_code/web/static/assets/terraform-BETggiCN.js +1 -0
- pythinker_code/web/static/assets/tex-CvyZ59Mk.js +1 -0
- pythinker_code/web/static/assets/timeline-definition-IT6M3QCI-CkCLnAgi.js +61 -0
- pythinker_code/web/static/assets/tokyo-night-hegEt444.js +1 -0
- pythinker_code/web/static/assets/toml-vGWfd6FD.js +1 -0
- pythinker_code/web/static/assets/treemap-KMMF4GRG-CZS5XwTf.js +128 -0
- pythinker_code/web/static/assets/ts-tags-zn1MmPIZ.js +1 -0
- pythinker_code/web/static/assets/tsv-B_m7g4N7.js +1 -0
- pythinker_code/web/static/assets/tsx-COt5Ahok.js +1 -0
- pythinker_code/web/static/assets/turtle-BsS91CYL.js +1 -0
- pythinker_code/web/static/assets/twig-CO9l9SDP.js +1 -0
- pythinker_code/web/static/assets/typescript-BPQ3VLAy.js +1 -0
- pythinker_code/web/static/assets/typespec-BGHnOYBU.js +1 -0
- pythinker_code/web/static/assets/typst-DHCkPAjA.js +1 -0
- pythinker_code/web/static/assets/v-BcVCzyr7.js +1 -0
- pythinker_code/web/static/assets/vala-CsfeWuGM.js +1 -0
- pythinker_code/web/static/assets/vb-D17OF-Vu.js +1 -0
- pythinker_code/web/static/assets/verilog-BQ8w6xss.js +1 -0
- pythinker_code/web/static/assets/vesper-DU1UobuO.js +1 -0
- pythinker_code/web/static/assets/vhdl-CeAyd5Ju.js +1 -0
- pythinker_code/web/static/assets/viml-CJc9bBzg.js +1 -0
- pythinker_code/web/static/assets/vitesse-black-Bkuqu6BP.js +1 -0
- pythinker_code/web/static/assets/vitesse-dark-D0r3Knsf.js +1 -0
- pythinker_code/web/static/assets/vitesse-light-CVO1_9PV.js +1 -0
- pythinker_code/web/static/assets/vue-DN_0RTcg.js +1 -0
- pythinker_code/web/static/assets/vue-html-AaS7Mt5G.js +1 -0
- pythinker_code/web/static/assets/vue-vine-CQOfvN7w.js +1 -0
- pythinker_code/web/static/assets/vyper-CDx5xZoG.js +1 -0
- pythinker_code/web/static/assets/wasm-CG6Dc4jp.js +1 -0
- pythinker_code/web/static/assets/wasm-MzD3tlZU.js +1 -0
- pythinker_code/web/static/assets/wenyan-BV7otONQ.js +1 -0
- pythinker_code/web/static/assets/wgsl-Dx-B1_4e.js +1 -0
- pythinker_code/web/static/assets/wikitext-BhOHFoWU.js +1 -0
- pythinker_code/web/static/assets/wit-5i3qLPDT.js +1 -0
- pythinker_code/web/static/assets/wolfram-lXgVvXCa.js +1 -0
- pythinker_code/web/static/assets/xml-sdJ4AIDG.js +1 -0
- pythinker_code/web/static/assets/xsl-CtQFsRM5.js +1 -0
- pythinker_code/web/static/assets/xychartDiagram-PRI3JC2R-DkqqHNLh.js +7 -0
- pythinker_code/web/static/assets/yaml-Buea-lGh.js +1 -0
- pythinker_code/web/static/assets/zenscript-DVFEvuxE.js +1 -0
- pythinker_code/web/static/assets/zig-VOosw3JB.js +1 -0
- pythinker_code/web/static/brand/apple-touch-icon.png +0 -0
- pythinker_code/web/static/brand/arctecture.webp +0 -0
- pythinker_code/web/static/brand/bimi-logo.svg +46 -0
- pythinker_code/web/static/brand/favicon.ico +0 -0
- pythinker_code/web/static/brand/fonts/dm-sans-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/dm-sans-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/instrument-sans-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/instrument-sans-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/instrument-serif-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/instrument-serif-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/libre-baskerville-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/libre-baskerville-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/roboto-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/roboto-latin.woff2 +0 -0
- pythinker_code/web/static/brand/icon-192.png +0 -0
- pythinker_code/web/static/brand/icon-512.png +0 -0
- pythinker_code/web/static/brand/icon.svg +1 -0
- pythinker_code/web/static/brand/logo.png +0 -0
- pythinker_code/web/static/brand/pythinker_animated.svg +79 -0
- pythinker_code/web/static/brand/robots.txt +4 -0
- pythinker_code/web/static/index.html +15 -0
- pythinker_code/web/static/logo.png +0 -0
- pythinker_code/web/store/__init__.py +1 -0
- pythinker_code/web/store/sessions.py +433 -0
- pythinker_code/wire/__init__.py +148 -0
- pythinker_code/wire/file.py +151 -0
- pythinker_code/wire/jsonrpc.py +263 -0
- pythinker_code/wire/protocol.py +2 -0
- pythinker_code/wire/root_hub.py +27 -0
- pythinker_code/wire/serde.py +26 -0
- pythinker_code/wire/server.py +1072 -0
- pythinker_code/wire/types.py +698 -0
- pythinker_code-0.8.0.dist-info/METADATA +706 -0
- pythinker_code-0.8.0.dist-info/RECORD +790 -0
- pythinker_code-0.8.0.dist-info/WHEEL +4 -0
- pythinker_code-0.8.0.dist-info/entry_points.txt +4 -0
- pythinker_code-0.8.0.dist-info/licenses/LICENSE +202 -0
- 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
|
+
)
|