iCode-TUI 0.29.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chrys/__init__.py +16 -0
- chrys/app/__init__.py +3 -0
- chrys/app/acp/__init__.py +3 -0
- chrys/app/acp/bridge.py +664 -0
- chrys/app/acp/content.py +132 -0
- chrys/app/acp/doc/frontend-api.md +425 -0
- chrys/app/acp/history.py +585 -0
- chrys/app/acp/server.py +1796 -0
- chrys/app/acp/session_manager.py +1815 -0
- chrys/app/acp/tool_status.py +45 -0
- chrys/app/cli/__init__.py +3 -0
- chrys/app/cli/acp.py +178 -0
- chrys/app/cli/app.py +165 -0
- chrys/app/cli/approvals.py +101 -0
- chrys/app/cli/headless.py +223 -0
- chrys/app/cli/launch_cwd.py +42 -0
- chrys/app/cli/profiles.py +298 -0
- chrys/app/cli/progress.py +857 -0
- chrys/app/cli/run.py +425 -0
- chrys/app/cli/serve.py +1593 -0
- chrys/app/cli/tool_summary.py +225 -0
- chrys/app/cli/trajectory.py +201 -0
- chrys/app/cli/workflow.py +601 -0
- chrys/app/features/__init__.py +3 -0
- chrys/app/features/buddy/__init__.py +3 -0
- chrys/app/features/buddy/actions.py +68 -0
- chrys/app/features/buddy/animation.py +76 -0
- chrys/app/features/buddy/commands.py +196 -0
- chrys/app/features/buddy/docs/design.md +187 -0
- chrys/app/features/buddy/hatchery.py +80 -0
- chrys/app/features/buddy/lifecycle.py +41 -0
- chrys/app/features/buddy/model.py +265 -0
- chrys/app/features/buddy/pixel_renderer.py +113 -0
- chrys/app/features/buddy/pixel_sprites.py +279 -0
- chrys/app/features/buddy/portrait.py +76 -0
- chrys/app/features/buddy/progression.py +58 -0
- chrys/app/features/buddy/replies.py +191 -0
- chrys/app/features/buddy/sprites/alien.toml +70 -0
- chrys/app/features/buddy/sprites/axolotl.toml +71 -0
- chrys/app/features/buddy/sprites/bat.toml +69 -0
- chrys/app/features/buddy/sprites/bee.toml +69 -0
- chrys/app/features/buddy/sprites/cactus.toml +73 -0
- chrys/app/features/buddy/sprites/capybara.toml +70 -0
- chrys/app/features/buddy/sprites/cat.toml +69 -0
- chrys/app/features/buddy/sprites/crab.toml +70 -0
- chrys/app/features/buddy/sprites/dragon.toml +71 -0
- chrys/app/features/buddy/sprites/duck.toml +69 -0
- chrys/app/features/buddy/sprites/fox.toml +69 -0
- chrys/app/features/buddy/sprites/frog.toml +70 -0
- chrys/app/features/buddy/sprites/ghost.toml +70 -0
- chrys/app/features/buddy/sprites/goose.toml +69 -0
- chrys/app/features/buddy/sprites/jelly.toml +70 -0
- chrys/app/features/buddy/sprites/llama.toml +70 -0
- chrys/app/features/buddy/sprites/mushroom.toml +69 -0
- chrys/app/features/buddy/sprites/octopus.toml +70 -0
- chrys/app/features/buddy/sprites/owl.toml +70 -0
- chrys/app/features/buddy/sprites/panda.toml +71 -0
- chrys/app/features/buddy/sprites/penguin.toml +69 -0
- chrys/app/features/buddy/sprites/rabbit.toml +70 -0
- chrys/app/features/buddy/sprites/robot.toml +72 -0
- chrys/app/features/buddy/sprites/shark.toml +70 -0
- chrys/app/features/buddy/sprites/snail.toml +69 -0
- chrys/app/features/buddy/sprites/snake.toml +69 -0
- chrys/app/features/buddy/sprites/tabby.toml +69 -0
- chrys/app/features/buddy/sprites/turtle.toml +69 -0
- chrys/app/features/buddy/store.py +143 -0
- chrys/app/features/session_title/__init__.py +7 -0
- chrys/app/features/session_title/updater.py +582 -0
- chrys/app/installer.py +651 -0
- chrys/app/parsing.py +17 -0
- chrys/app/tui/__init__.py +3 -0
- chrys/app/tui/app.py +1287 -0
- chrys/app/tui/behaviors/__init__.py +3 -0
- chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
- chrys/app/tui/behaviors/insert_clipboard.py +73 -0
- chrys/app/tui/behaviors/right_click_copy.py +146 -0
- chrys/app/tui/binding_display.py +139 -0
- chrys/app/tui/buddy_messages.py +17 -0
- chrys/app/tui/buddy_reply.py +86 -0
- chrys/app/tui/chrys.tcss +549 -0
- chrys/app/tui/clipboard.py +109 -0
- chrys/app/tui/copy_messages.py +10 -0
- chrys/app/tui/i18n.py +230 -0
- chrys/app/tui/language.py +58 -0
- chrys/app/tui/notifications/__init__.py +12 -0
- chrys/app/tui/notifications/drivers.py +180 -0
- chrys/app/tui/notifications/service.py +126 -0
- chrys/app/tui/notifications/settings.py +141 -0
- chrys/app/tui/screens/__init__.py +3 -0
- chrys/app/tui/screens/agents/__init__.py +8 -0
- chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
- chrys/app/tui/screens/agents/config.py +2142 -0
- chrys/app/tui/screens/agents/config.tcss +169 -0
- chrys/app/tui/screens/agents/panels/__init__.py +3 -0
- chrys/app/tui/screens/agents/panels/acp.py +1176 -0
- chrys/app/tui/screens/agents/panels/basic.py +497 -0
- chrys/app/tui/screens/agents/panels/compaction.py +231 -0
- chrys/app/tui/screens/agents/panels/config_card.py +218 -0
- chrys/app/tui/screens/agents/panels/instructions.py +91 -0
- chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
- chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
- chrys/app/tui/screens/agents/panels/memory.py +684 -0
- chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
- chrys/app/tui/screens/agents/panels/skills.py +806 -0
- chrys/app/tui/screens/agents/panels/subagents.py +652 -0
- chrys/app/tui/screens/agents/panels/tools.py +205 -0
- chrys/app/tui/screens/agents/picker.py +103 -0
- chrys/app/tui/screens/agents/skill_paths.py +38 -0
- chrys/app/tui/screens/agents/validation_messages.py +452 -0
- chrys/app/tui/screens/dialogs/__init__.py +35 -0
- chrys/app/tui/screens/dialogs/agent_load.py +834 -0
- chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
- chrys/app/tui/screens/dialogs/app_mode.py +55 -0
- chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
- chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
- chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
- chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
- chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
- chrys/app/tui/screens/dialogs/approval/body.py +110 -0
- chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
- chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
- chrys/app/tui/screens/dialogs/ask_user.py +148 -0
- chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
- chrys/app/tui/screens/dialogs/base.py +84 -0
- chrys/app/tui/screens/dialogs/confirm.py +184 -0
- chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
- chrys/app/tui/screens/dialogs/connection_test.py +183 -0
- chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
- chrys/app/tui/screens/dialogs/diagram.py +102 -0
- chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
- chrys/app/tui/screens/dialogs/editor.py +378 -0
- chrys/app/tui/screens/dialogs/editor.tcss +132 -0
- chrys/app/tui/screens/dialogs/file_picker.py +582 -0
- chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
- chrys/app/tui/screens/dialogs/fork_session.py +219 -0
- chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
- chrys/app/tui/screens/dialogs/image_compression.py +102 -0
- chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
- chrys/app/tui/screens/dialogs/man_page.py +172 -0
- chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
- chrys/app/tui/screens/dialogs/new_folder.py +277 -0
- chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
- chrys/app/tui/screens/dialogs/notice.tcss +14 -0
- chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
- chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
- chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
- chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
- chrys/app/tui/screens/dialogs/session_title.py +110 -0
- chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
- chrys/app/tui/screens/dialogs/tool_view.py +207 -0
- chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
- chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
- chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
- chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
- chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
- chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
- chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
- chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
- chrys/app/tui/screens/diff/__init__.py +22 -0
- chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
- chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
- chrys/app/tui/screens/diff/screen.py +1532 -0
- chrys/app/tui/screens/diff/screen.tcss +104 -0
- chrys/app/tui/screens/guides/__init__.py +7 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
- chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/index.py +215 -0
- chrys/app/tui/screens/guides/reader.py +173 -0
- chrys/app/tui/screens/guides/screen.py +442 -0
- chrys/app/tui/screens/guides/screen.tcss +52 -0
- chrys/app/tui/screens/logs/__init__.py +7 -0
- chrys/app/tui/screens/logs/screen.py +504 -0
- chrys/app/tui/screens/logs/screen.tcss +45 -0
- chrys/app/tui/screens/main/__init__.py +7 -0
- chrys/app/tui/screens/main/action_availability.py +30 -0
- chrys/app/tui/screens/main/buddy_command.py +143 -0
- chrys/app/tui/screens/main/commands.py +1073 -0
- chrys/app/tui/screens/main/config_actions.py +464 -0
- chrys/app/tui/screens/main/copy_actions.py +117 -0
- chrys/app/tui/screens/main/dialog_controllers.py +826 -0
- chrys/app/tui/screens/main/dialog_gateway.py +166 -0
- chrys/app/tui/screens/main/diff_controller.py +478 -0
- chrys/app/tui/screens/main/engine_read_model.py +184 -0
- chrys/app/tui/screens/main/event_handlers.py +2014 -0
- chrys/app/tui/screens/main/input_flow.py +241 -0
- chrys/app/tui/screens/main/live_diff.py +244 -0
- chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
- chrys/app/tui/screens/main/model_indicator.py +217 -0
- chrys/app/tui/screens/main/navigation.py +343 -0
- chrys/app/tui/screens/main/ports.py +571 -0
- chrys/app/tui/screens/main/recent_dirs.py +123 -0
- chrys/app/tui/screens/main/rollback_controller.py +586 -0
- chrys/app/tui/screens/main/runtime_info.py +71 -0
- chrys/app/tui/screens/main/screen.py +2265 -0
- chrys/app/tui/screens/main/screen.tcss +21 -0
- chrys/app/tui/screens/main/session_handlers.py +1127 -0
- chrys/app/tui/screens/main/session_title.py +239 -0
- chrys/app/tui/screens/main/settings_coordinator.py +365 -0
- chrys/app/tui/screens/main/settings_persistence.py +184 -0
- chrys/app/tui/screens/main/shell_mode.py +112 -0
- chrys/app/tui/screens/main/state.py +289 -0
- chrys/app/tui/screens/main/submit_state.py +11 -0
- chrys/app/tui/screens/main/subscriptions.py +177 -0
- chrys/app/tui/screens/main/suggestions.py +982 -0
- chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
- chrys/app/tui/screens/main/view_adapter.py +1567 -0
- chrys/app/tui/screens/main/workflow_browser.py +747 -0
- chrys/app/tui/screens/main/workflow_content.py +248 -0
- chrys/app/tui/screens/main/workflow_controller.py +240 -0
- chrys/app/tui/screens/main/workflow_feedback.py +137 -0
- chrys/app/tui/screens/main/workflow_flow.py +49 -0
- chrys/app/tui/screens/main/workflow_run_control.py +456 -0
- chrys/app/tui/screens/main/workflow_session_view.py +316 -0
- chrys/app/tui/screens/main/workspace_actions.py +272 -0
- chrys/app/tui/screens/main/workspace_branch.py +194 -0
- chrys/app/tui/screens/menus/__init__.py +8 -0
- chrys/app/tui/screens/menus/languages.py +114 -0
- chrys/app/tui/screens/models/__init__.py +7 -0
- chrys/app/tui/screens/models/picker.py +147 -0
- chrys/app/tui/screens/models/screen.py +2043 -0
- chrys/app/tui/screens/models/screen.tcss +255 -0
- chrys/app/tui/screens/sessions/__init__.py +8 -0
- chrys/app/tui/screens/sessions/presenter.py +586 -0
- chrys/app/tui/screens/sessions/screen.py +779 -0
- chrys/app/tui/screens/sessions/screen.tcss +127 -0
- chrys/app/tui/screens/settings/__init__.py +29 -0
- chrys/app/tui/screens/settings/danger.py +26 -0
- chrys/app/tui/screens/settings/dialog.py +281 -0
- chrys/app/tui/screens/settings/layout.py +519 -0
- chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
- chrys/app/tui/screens/settings/panes/__init__.py +3 -0
- chrys/app/tui/screens/settings/panes/notifications.py +248 -0
- chrys/app/tui/screens/settings/panes/sessions.py +148 -0
- chrys/app/tui/screens/settings/ports.py +55 -0
- chrys/app/tui/screens/settings/provenance.py +102 -0
- chrys/app/tui/screens/settings/rows.py +546 -0
- chrys/app/tui/screens/settings/settings.tcss +406 -0
- chrys/app/tui/screens/themes/__init__.py +8 -0
- chrys/app/tui/screens/themes/dialogs.py +330 -0
- chrys/app/tui/screens/themes/editor.py +632 -0
- chrys/app/tui/screens/themes/editor.tcss +268 -0
- chrys/app/tui/screens/themes/messages.py +95 -0
- chrys/app/tui/screens/themes/palette.py +694 -0
- chrys/app/tui/screens/themes/panel.py +113 -0
- chrys/app/tui/screens/themes/picker.py +203 -0
- chrys/app/tui/screens/themes/save.py +191 -0
- chrys/app/tui/support/__init__.py +3 -0
- chrys/app/tui/support/file_manager.py +64 -0
- chrys/app/tui/support/gc_freeze.py +1114 -0
- chrys/app/tui/support/path_shortcuts.py +50 -0
- chrys/app/tui/support/prompt_history.py +298 -0
- chrys/app/tui/support/workspace_mru.py +476 -0
- chrys/app/tui/terminal/__init__.py +6 -0
- chrys/app/tui/terminal/_pty_backend.py +114 -0
- chrys/app/tui/terminal/emulator/__init__.py +24 -0
- chrys/app/tui/terminal/emulator/buffer.py +456 -0
- chrys/app/tui/terminal/emulator/charsets.py +113 -0
- chrys/app/tui/terminal/emulator/core.py +963 -0
- chrys/app/tui/terminal/emulator/input.py +342 -0
- chrys/app/tui/terminal/emulator/parser.py +227 -0
- chrys/app/tui/terminal/emulator/pen.py +158 -0
- chrys/app/tui/terminal/launcher.py +455 -0
- chrys/app/tui/terminal/leaked_reports.py +44 -0
- chrys/app/tui/terminal/panel.py +323 -0
- chrys/app/tui/terminal/pty_process.py +405 -0
- chrys/app/tui/terminal/rendering.py +151 -0
- chrys/app/tui/terminal/shell.py +273 -0
- chrys/app/tui/terminal/shell_integration.py +199 -0
- chrys/app/tui/terminal/title.py +224 -0
- chrys/app/tui/terminal/widget.py +654 -0
- chrys/app/tui/theme.py +422 -0
- chrys/app/tui/theme_loader.py +201 -0
- chrys/app/tui/theme_stylesheet.py +49 -0
- chrys/app/tui/themes/__init__.py +3 -0
- chrys/app/tui/themes/document.py +120 -0
- chrys/app/tui/themes/preview.py +170 -0
- chrys/app/tui/themes/store.py +176 -0
- chrys/app/tui/toasts.py +69 -0
- chrys/app/tui/util/__init__.py +3 -0
- chrys/app/tui/util/context_pressure.py +146 -0
- chrys/app/tui/util/debug_log.py +292 -0
- chrys/app/tui/util/diff_entries.py +64 -0
- chrys/app/tui/util/formatting.py +53 -0
- chrys/app/tui/util/git_branch.py +326 -0
- chrys/app/tui/util/invocation_progress.py +57 -0
- chrys/app/tui/util/logo.py +32 -0
- chrys/app/tui/util/message_gate.py +35 -0
- chrys/app/tui/util/mutation_sources.py +14 -0
- chrys/app/tui/util/picker_text.py +27 -0
- chrys/app/tui/util/removal.py +47 -0
- chrys/app/tui/util/rich_style.py +36 -0
- chrys/app/tui/util/source_text.py +12 -0
- chrys/app/tui/util/static_update.py +88 -0
- chrys/app/tui/util/todo_format.py +51 -0
- chrys/app/tui/util/visibility.py +214 -0
- chrys/app/tui/widgets/__init__.py +60 -0
- chrys/app/tui/widgets/ask_user_controls.py +652 -0
- chrys/app/tui/widgets/ask_user_markdown.py +17 -0
- chrys/app/tui/widgets/ask_user_prompt.py +606 -0
- chrys/app/tui/widgets/buttons.py +52 -0
- chrys/app/tui/widgets/chat/__init__.py +3 -0
- chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
- chrys/app/tui/widgets/chat/chrome.py +296 -0
- chrys/app/tui/widgets/chat/compaction_card.py +353 -0
- chrys/app/tui/widgets/chat/context_fold.py +77 -0
- chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
- chrys/app/tui/widgets/chat/image_preview.py +214 -0
- chrys/app/tui/widgets/chat/live_renderer.py +383 -0
- chrys/app/tui/widgets/chat/messages.py +1039 -0
- chrys/app/tui/widgets/chat/panel.py +1769 -0
- chrys/app/tui/widgets/chat/ports.py +245 -0
- chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
- chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
- chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
- chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
- chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
- chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
- chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
- chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
- chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
- chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
- chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
- chrys/app/tui/widgets/chat/renderers/search.py +348 -0
- chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
- chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
- chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
- chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
- chrys/app/tui/widgets/chat/renderers/web.py +69 -0
- chrys/app/tui/widgets/chat/replay.py +1605 -0
- chrys/app/tui/widgets/chat/replay_mount.py +322 -0
- chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
- chrys/app/tui/widgets/chat/selection_controller.py +435 -0
- chrys/app/tui/widgets/chat/session_json.py +508 -0
- chrys/app/tui/widgets/chat/toc_model.py +156 -0
- chrys/app/tui/widgets/chat/tool_call.py +2478 -0
- chrys/app/tui/widgets/chat/tool_registry.py +753 -0
- chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
- chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
- chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
- chrys/app/tui/widgets/checkbox.py +24 -0
- chrys/app/tui/widgets/chrome/__init__.py +3 -0
- chrys/app/tui/widgets/chrome/app_header.py +283 -0
- chrys/app/tui/widgets/chrome/commands.py +95 -0
- chrys/app/tui/widgets/chrome/file_index.py +68 -0
- chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
- chrys/app/tui/widgets/chrome/footer.py +284 -0
- chrys/app/tui/widgets/chrome/image_paste.py +351 -0
- chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
- chrys/app/tui/widgets/chrome/status_bar.py +911 -0
- chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
- chrys/app/tui/widgets/click_affordance.py +24 -0
- chrys/app/tui/widgets/color_picker/__init__.py +254 -0
- chrys/app/tui/widgets/color_picker/controls.py +171 -0
- chrys/app/tui/widgets/color_picker/model.py +89 -0
- chrys/app/tui/widgets/dialog_buttons.py +58 -0
- chrys/app/tui/widgets/dialog_scroll.py +24 -0
- chrys/app/tui/widgets/diff_view/__init__.py +20 -0
- chrys/app/tui/widgets/diff_view/cells.py +59 -0
- chrys/app/tui/widgets/diff_view/code.py +280 -0
- chrys/app/tui/widgets/diff_view/compute.py +286 -0
- chrys/app/tui/widgets/diff_view/gutter.py +161 -0
- chrys/app/tui/widgets/diff_view/inline.py +77 -0
- chrys/app/tui/widgets/diff_view/palette.py +87 -0
- chrys/app/tui/widgets/diff_view/protocols.py +24 -0
- chrys/app/tui/widgets/diff_view/rows.py +138 -0
- chrys/app/tui/widgets/diff_view/unified.py +267 -0
- chrys/app/tui/widgets/diff_view/widget.py +209 -0
- chrys/app/tui/widgets/editor/__init__.py +79 -0
- chrys/app/tui/widgets/editor/coordinates.py +30 -0
- chrys/app/tui/widgets/editor/highlighter.py +139 -0
- chrys/app/tui/widgets/editor/keymaps.py +357 -0
- chrys/app/tui/widgets/editor/types.py +216 -0
- chrys/app/tui/widgets/editor/vim.py +1195 -0
- chrys/app/tui/widgets/editor/widget.py +401 -0
- chrys/app/tui/widgets/file_preview.py +58 -0
- chrys/app/tui/widgets/hatch.py +138 -0
- chrys/app/tui/widgets/input.py +66 -0
- chrys/app/tui/widgets/loading.py +125 -0
- chrys/app/tui/widgets/markdown/__init__.py +25 -0
- chrys/app/tui/widgets/markdown/_utils.py +21 -0
- chrys/app/tui/widgets/markdown/anchors.py +71 -0
- chrys/app/tui/widgets/markdown/blocks.py +115 -0
- chrys/app/tui/widgets/markdown/copyable.py +163 -0
- chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
- chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
- chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
- chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
- chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
- chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
- chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
- chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
- chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
- chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
- chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
- chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
- chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
- chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
- chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
- chrys/app/tui/widgets/markdown/links.py +47 -0
- chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
- chrys/app/tui/widgets/markdown/math/layout.py +739 -0
- chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
- chrys/app/tui/widgets/markdown/math/parser.py +325 -0
- chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
- chrys/app/tui/widgets/markdown/navigator.py +80 -0
- chrys/app/tui/widgets/markdown/parser.py +1057 -0
- chrys/app/tui/widgets/markdown/stream.py +73 -0
- chrys/app/tui/widgets/markdown/toc.py +98 -0
- chrys/app/tui/widgets/markdown/viewer.py +146 -0
- chrys/app/tui/widgets/markdown/widget.py +2046 -0
- chrys/app/tui/widgets/marquee.py +101 -0
- chrys/app/tui/widgets/option_menu.py +82 -0
- chrys/app/tui/widgets/page_navigator.py +118 -0
- chrys/app/tui/widgets/select.py +196 -0
- chrys/app/tui/widgets/selection.py +59 -0
- chrys/app/tui/widgets/sidebar/__init__.py +3 -0
- chrys/app/tui/widgets/sidebar/buddy.py +367 -0
- chrys/app/tui/widgets/sidebar/context.py +482 -0
- chrys/app/tui/widgets/sidebar/debug.py +195 -0
- chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
- chrys/app/tui/widgets/sidebar/panel.py +198 -0
- chrys/app/tui/widgets/sidebar/tasks.py +198 -0
- chrys/app/tui/widgets/sidebar/toc.py +166 -0
- chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
- chrys/app/tui/widgets/syntax_theme.py +85 -0
- chrys/app/tui/widgets/text_area.py +258 -0
- chrys/app/tui/widgets/trajectory/__init__.py +8 -0
- chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
- chrys/app/tui/widgets/trajectory/insights.py +1266 -0
- chrys/app/tui/widgets/trajectory/overview.py +1101 -0
- chrys/app/tui/widgets/trajectory/panel.py +983 -0
- chrys/app/tui/widgets/trajectory/presentation.py +311 -0
- chrys/app/tui/widgets/trajectory/session_info.py +124 -0
- chrys/app/tui/widgets/trajectory/text_view.py +124 -0
- chrys/app/tui/widgets/trajectory/timeline.py +380 -0
- chrys/app/tui/widgets/welcome.py +126 -0
- chrys/app/tui/widgets/workflow/__init__.py +5 -0
- chrys/app/tui/widgets/workflow/edges.py +89 -0
- chrys/app/tui/widgets/workflow/graph.py +869 -0
- chrys/app/tui/widgets/workflow/info.py +310 -0
- chrys/app/tui/widgets/workflow/node_view.py +27 -0
- chrys/app/tui/widgets/workflow/output.py +225 -0
- chrys/app/tui/widgets/workflow/panel.py +658 -0
- chrys/app/tui/widgets/workflow/projector.py +324 -0
- chrys/app/tui/widgets/workflow/records.py +69 -0
- chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
- chrys/app/tui/widgets/workflow/selection.py +151 -0
- chrys/app/tui/widgets/workflow/source.py +65 -0
- chrys/app/tui/widgets/workflow/text.py +257 -0
- chrys/app/tui/widgets/workflow/values.py +317 -0
- chrys/foundation/__init__.py +0 -0
- chrys/foundation/branding.py +15 -0
- chrys/foundation/config/__init__.py +7 -0
- chrys/foundation/config/coercion.py +374 -0
- chrys/foundation/config/context.py +34 -0
- chrys/foundation/config/env_file.py +141 -0
- chrys/foundation/config/env_layers.py +370 -0
- chrys/foundation/config/migrations.py +521 -0
- chrys/foundation/config/notification_events.py +22 -0
- chrys/foundation/config/process_settings.py +332 -0
- chrys/foundation/config/runtime_pointer.py +133 -0
- chrys/foundation/config/settings.py +1952 -0
- chrys/foundation/config/settings_store.py +996 -0
- chrys/foundation/config/spec.py +396 -0
- chrys/foundation/config/user_settings.py +140 -0
- chrys/foundation/config/warnings.py +262 -0
- chrys/foundation/config/web_values.py +135 -0
- chrys/foundation/config/yaml_store.py +487 -0
- chrys/foundation/errors/__init__.py +68 -0
- chrys/foundation/errors/_legacy.py +117 -0
- chrys/foundation/errors/_text.py +13 -0
- chrys/foundation/errors/_walk.py +85 -0
- chrys/foundation/errors/classify.py +278 -0
- chrys/foundation/errors/display.py +274 -0
- chrys/foundation/errors/formatting.py +283 -0
- chrys/foundation/errors/kinds.py +67 -0
- chrys/foundation/errors/network.py +497 -0
- chrys/foundation/errors/provider.py +445 -0
- chrys/foundation/errors/route.py +85 -0
- chrys/foundation/events/__init__.py +8 -0
- chrys/foundation/events/bus.py +143 -0
- chrys/foundation/events/types.py +1629 -0
- chrys/foundation/events/workflow.py +38 -0
- chrys/foundation/hosted_tools.py +142 -0
- chrys/foundation/i18n/__init__.py +32 -0
- chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
- chrys/foundation/i18n/catalogs.py +42 -0
- chrys/foundation/i18n/formatting.py +202 -0
- chrys/foundation/i18n/locale.py +92 -0
- chrys/foundation/i18n/localizer.py +187 -0
- chrys/foundation/i18n/messages.py +268 -0
- chrys/foundation/io/__init__.py +3 -0
- chrys/foundation/io/result_content.py +237 -0
- chrys/foundation/models/__init__.py +3 -0
- chrys/foundation/models/approval_reuse.py +23 -0
- chrys/foundation/models/ask_user.py +468 -0
- chrys/foundation/models/execution.py +18 -0
- chrys/foundation/models/history_markers.py +115 -0
- chrys/foundation/models/invocations.py +63 -0
- chrys/foundation/models/mutation_scope.py +28 -0
- chrys/foundation/models/session_env.py +87 -0
- chrys/foundation/models/session_surface.py +29 -0
- chrys/foundation/models/todos.py +74 -0
- chrys/foundation/models/turns.py +125 -0
- chrys/foundation/models/workflow_session.py +89 -0
- chrys/foundation/models/workspace.py +59 -0
- chrys/foundation/net/__init__.py +3 -0
- chrys/foundation/net/address_class.py +57 -0
- chrys/foundation/net/route_probe.py +94 -0
- chrys/foundation/net/url.py +107 -0
- chrys/foundation/observability/__init__.py +3 -0
- chrys/foundation/observability/exporters.py +174 -0
- chrys/foundation/observability/gate.py +31 -0
- chrys/foundation/observability/setup.py +497 -0
- chrys/foundation/observability/sink.py +228 -0
- chrys/foundation/patches/__init__.py +19 -0
- chrys/foundation/patches/mdurl_cache.py +59 -0
- chrys/foundation/patches/patcher.py +287 -0
- chrys/foundation/patches/staged_members.py +205 -0
- chrys/foundation/patches/textual_block_border.py +105 -0
- chrys/foundation/patches/textual_button_ansi.py +65 -0
- chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
- chrys/foundation/patches/textual_callback_cache.py +248 -0
- chrys/foundation/patches/textual_callback_dispatch.py +109 -0
- chrys/foundation/patches/textual_compositor_cjk.py +566 -0
- chrys/foundation/patches/textual_dispatch_cache.py +150 -0
- chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
- chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
- chrys/foundation/patches/textual_lru_acyclic.py +241 -0
- chrys/foundation/patches/textual_message_pump.py +136 -0
- chrys/foundation/patches/textual_node_diet.py +875 -0
- chrys/foundation/patches/textual_one_shot_timer.py +48 -0
- chrys/foundation/patches/textual_option_list.py +90 -0
- chrys/foundation/patches/textual_precompose.py +89 -0
- chrys/foundation/patches/textual_pruned_tabs.py +69 -0
- chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
- chrys/foundation/patches/textual_removed_node_caches.py +86 -0
- chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
- chrys/foundation/patches/textual_selection_extract.py +48 -0
- chrys/foundation/patches/textual_strip_cycles.py +119 -0
- chrys/foundation/patches/textual_tab_selection.py +1611 -0
- chrys/foundation/patches/textual_timer_skip.py +70 -0
- chrys/foundation/patches/textual_utf8_decoder.py +106 -0
- chrys/foundation/patches/textual_win_sleep.py +167 -0
- chrys/foundation/patches/watchdog_fsevents.py +70 -0
- chrys/foundation/patches/watchdog_windows.py +52 -0
- chrys/foundation/platform/__init__.py +556 -0
- chrys/foundation/platform/c_api.py +32 -0
- chrys/foundation/platform/child_reap.py +107 -0
- chrys/foundation/platform/command_line.py +95 -0
- chrys/foundation/platform/files.py +1470 -0
- chrys/foundation/platform/output_capture.py +113 -0
- chrys/foundation/platform/paths.py +193 -0
- chrys/foundation/platform/process.py +1468 -0
- chrys/foundation/platform/pty_output.py +19 -0
- chrys/foundation/platform/runtime_paths.py +281 -0
- chrys/foundation/platform/win_console.py +129 -0
- chrys/foundation/reasoning_origin.py +67 -0
- chrys/foundation/recovery.py +16 -0
- chrys/foundation/retry.py +278 -0
- chrys/foundation/text/__init__.py +3 -0
- chrys/foundation/text/encoding.py +603 -0
- chrys/foundation/text/images.py +304 -0
- chrys/foundation/text/lines.py +24 -0
- chrys/foundation/text/mentions.py +90 -0
- chrys/foundation/text/model_json.py +25 -0
- chrys/foundation/text/reading_order.py +20 -0
- chrys/foundation/text/tokenizer.py +25 -0
- chrys/foundation/text/tool_output.py +287 -0
- chrys/foundation/text/yaml_io.py +62 -0
- chrys/foundation/tool_call_context.py +202 -0
- chrys/foundation/tool_execution_stamp.py +184 -0
- chrys/foundation/tool_invocation_order.py +48 -0
- chrys/foundation/tool_kinds.py +102 -0
- chrys/foundation/tool_result_metadata.py +287 -0
- chrys/foundation/trajectory/__init__.py +10 -0
- chrys/foundation/trajectory/context.py +481 -0
- chrys/foundation/trajectory/envelope.py +571 -0
- chrys/foundation/trajectory/event_types.py +294 -0
- chrys/foundation/trajectory/finalizers.py +46 -0
- chrys/foundation/trajectory/fingerprint.py +62 -0
- chrys/foundation/trajectory/ids.py +155 -0
- chrys/foundation/trajectory/keys.py +99 -0
- chrys/foundation/trajectory/lease.py +173 -0
- chrys/foundation/trajectory/metadata.py +72 -0
- chrys/foundation/trajectory/reader.py +271 -0
- chrys/foundation/trajectory/recovery.py +225 -0
- chrys/foundation/trajectory/revisions.py +152 -0
- chrys/foundation/trajectory/segments.py +225 -0
- chrys/foundation/trajectory/tools.py +117 -0
- chrys/foundation/trajectory/usage.py +151 -0
- chrys/foundation/trajectory/writer.py +972 -0
- chrys/foundation/trajectory_timing.py +97 -0
- chrys/foundation/util/__init__.py +3 -0
- chrys/foundation/util/async_tasks.py +29 -0
- chrys/foundation/util/chrys_headers.py +23 -0
- chrys/foundation/util/env_templates.py +104 -0
- chrys/foundation/util/filenames.py +96 -0
- chrys/foundation/util/header_charset.py +138 -0
- chrys/foundation/util/httpx_helpers.py +12 -0
- chrys/foundation/util/lock.py +236 -0
- chrys/foundation/util/once_close.py +46 -0
- chrys/foundation/util/session_ids.py +29 -0
- chrys/foundation/util/shell_tokens.py +88 -0
- chrys/foundation/util/sub_agent_context.py +38 -0
- chrys/foundation/util/time.py +35 -0
- chrys/foundation/util/unicode_scalars.py +36 -0
- chrys/foundation/vendor/__init__.py +43 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
- chrys/kernel/__init__.py +256 -0
- chrys/kernel/_content.py +1866 -0
- chrys/kernel/_loop_recorder.py +631 -0
- chrys/kernel/_null_overlay.py +1026 -0
- chrys/kernel/_result_ceiling.py +216 -0
- chrys/kernel/_serialization.py +431 -0
- chrys/kernel/_tool_arg_errors.py +1629 -0
- chrys/kernel/_tool_execution.py +1172 -0
- chrys/kernel/_tool_expansion.py +26 -0
- chrys/kernel/_types.py +2598 -0
- chrys/kernel/agent.py +994 -0
- chrys/kernel/client.py +1084 -0
- chrys/kernel/compaction.py +1315 -0
- chrys/kernel/exceptions.py +211 -0
- chrys/kernel/exchanges.py +683 -0
- chrys/kernel/identity.py +95 -0
- chrys/kernel/images.py +36 -0
- chrys/kernel/instrumentation.py +1618 -0
- chrys/kernel/loop.py +2308 -0
- chrys/kernel/middleware.py +616 -0
- chrys/kernel/sessions.py +604 -0
- chrys/kernel/tools.py +1061 -0
- chrys/kernel/types.py +126 -0
- chrys/orchestration/__init__.py +3 -0
- chrys/orchestration/engine/__init__.py +3 -0
- chrys/orchestration/engine/assembly.py +236 -0
- chrys/orchestration/engine/build/__init__.py +3 -0
- chrys/orchestration/engine/build/builder.py +1061 -0
- chrys/orchestration/engine/build/construction.py +408 -0
- chrys/orchestration/engine/build/loaded.py +139 -0
- chrys/orchestration/engine/engine.py +789 -0
- chrys/orchestration/engine/execution.py +771 -0
- chrys/orchestration/engine/loader.py +865 -0
- chrys/orchestration/engine/rollback.py +918 -0
- chrys/orchestration/engine/run/__init__.py +3 -0
- chrys/orchestration/engine/run/active_injection.py +419 -0
- chrys/orchestration/engine/run/attachments.py +519 -0
- chrys/orchestration/engine/run/bindings.py +1095 -0
- chrys/orchestration/engine/run/coordinator.py +1062 -0
- chrys/orchestration/engine/run/finalizer.py +417 -0
- chrys/orchestration/engine/run/input_refs.py +60 -0
- chrys/orchestration/engine/run/prompt_content.py +457 -0
- chrys/orchestration/engine/run/resume.py +299 -0
- chrys/orchestration/engine/run/retry.py +948 -0
- chrys/orchestration/engine/run/runner.py +625 -0
- chrys/orchestration/engine/run/runtime_skills.py +146 -0
- chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
- chrys/orchestration/engine/run/turn_hooks.py +258 -0
- chrys/orchestration/engine/run/turn_state.py +149 -0
- chrys/orchestration/engine/run/working_dir.py +50 -0
- chrys/orchestration/engine/session_lifecycle.py +2013 -0
- chrys/orchestration/engine/state/__init__.py +3 -0
- chrys/orchestration/engine/state/active_session.py +183 -0
- chrys/orchestration/engine/state/controls.py +639 -0
- chrys/orchestration/engine/state/current_agent.py +26 -0
- chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
- chrys/orchestration/engine/state/machine.py +193 -0
- chrys/orchestration/engine/state/session_writer.py +434 -0
- chrys/orchestration/engine/trajectory.py +714 -0
- chrys/orchestration/engine/usage.py +118 -0
- chrys/orchestration/invoker/__init__.py +3 -0
- chrys/orchestration/invoker/acp.py +727 -0
- chrys/orchestration/invoker/acp_protocol.py +1115 -0
- chrys/orchestration/invoker/acp_spec.py +79 -0
- chrys/orchestration/invoker/attempts.py +994 -0
- chrys/orchestration/invoker/child_compaction.py +131 -0
- chrys/orchestration/invoker/child_history.py +99 -0
- chrys/orchestration/invoker/contracts.py +253 -0
- chrys/orchestration/invoker/evidence.py +94 -0
- chrys/orchestration/invoker/kernel.py +343 -0
- chrys/orchestration/invoker/origin.py +79 -0
- chrys/orchestration/invoker/resources.py +343 -0
- chrys/orchestration/invoker/runtime.py +299 -0
- chrys/orchestration/invoker/usage.py +80 -0
- chrys/orchestration/session_hooks.py +117 -0
- chrys/orchestration/session_host.py +1023 -0
- chrys/orchestration/session_resources.py +95 -0
- chrys/orchestration/session_usage.py +219 -0
- chrys/orchestration/startup.py +255 -0
- chrys/orchestration/sub_agents/__init__.py +3 -0
- chrys/orchestration/sub_agents/acp_policy.py +269 -0
- chrys/orchestration/sub_agents/kernel_policy.py +848 -0
- chrys/orchestration/sub_agents/registration.py +44 -0
- chrys/orchestration/sub_agents/shell.py +337 -0
- chrys/orchestration/sub_agents/tools.py +2228 -0
- chrys/orchestration/workflows/__init__.py +3 -0
- chrys/orchestration/workflows/agent_archive.py +156 -0
- chrys/orchestration/workflows/agent_node.py +894 -0
- chrys/orchestration/workflows/agent_node_build.py +687 -0
- chrys/orchestration/workflows/catalog.py +332 -0
- chrys/orchestration/workflows/coordinator.py +771 -0
- chrys/orchestration/workflows/hooks.py +176 -0
- chrys/orchestration/workflows/preview.py +322 -0
- chrys/orchestration/workflows/rollback.py +151 -0
- chrys/orchestration/workflows/runner.py +986 -0
- chrys/orchestration/workflows/session.py +390 -0
- chrys/orchestration/workflows/settings.py +97 -0
- chrys/orchestration/workflows/validation.py +674 -0
- chrys/orchestration/workflows/worker_client.py +1002 -0
- chrys/service/__init__.py +3 -0
- chrys/service/acp_client/__init__.py +65 -0
- chrys/service/acp_client/_transport.py +922 -0
- chrys/service/acp_client/client.py +1311 -0
- chrys/service/acp_client/errors.py +224 -0
- chrys/service/acp_client/protocol.py +220 -0
- chrys/service/acp_client/spawn.py +239 -0
- chrys/service/acp_client/spec.py +100 -0
- chrys/service/agent_middleware/__init__.py +60 -0
- chrys/service/agent_middleware/_metadata_keys.py +52 -0
- chrys/service/agent_middleware/control/__init__.py +3 -0
- chrys/service/agent_middleware/control/approval.py +769 -0
- chrys/service/agent_middleware/control/approval_hooks.py +62 -0
- chrys/service/agent_middleware/control/ask_user.py +207 -0
- chrys/service/agent_middleware/control/interrupt.py +50 -0
- chrys/service/agent_middleware/control/sleep.py +118 -0
- chrys/service/agent_middleware/control/todo.py +117 -0
- chrys/service/agent_middleware/events/__init__.py +3 -0
- chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
- chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
- chrys/service/agent_middleware/events/intermediate_text.py +128 -0
- chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
- chrys/service/agent_middleware/events/result_persistence.py +98 -0
- chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
- chrys/service/agent_middleware/events/tool_error_text.py +22 -0
- chrys/service/agent_middleware/events/tool_events.py +779 -0
- chrys/service/agent_middleware/injection.py +510 -0
- chrys/service/agent_middleware/reminders/__init__.py +91 -0
- chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
- chrys/service/agent_middleware/reminders/base.py +25 -0
- chrys/service/agent_middleware/reminders/context_usage.py +74 -0
- chrys/service/agent_middleware/reminders/file_change.py +63 -0
- chrys/service/agent_middleware/reminders/mcp.py +39 -0
- chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
- chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
- chrys/service/agent_middleware/reminders/skills.py +44 -0
- chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
- chrys/service/agent_middleware/reminders/todo.py +35 -0
- chrys/service/agent_middleware/reminders/turn_line.py +48 -0
- chrys/service/agent_middleware/response_validation.py +1380 -0
- chrys/service/agent_middleware/system_reminder.py +1107 -0
- chrys/service/agent_middleware/validators.py +396 -0
- chrys/service/analytics/__init__.py +124 -0
- chrys/service/analytics/_actions.py +896 -0
- chrys/service/analytics/_context_evidence.py +357 -0
- chrys/service/analytics/_critical_path.py +179 -0
- chrys/service/analytics/_facts.py +919 -0
- chrys/service/analytics/_insights.py +769 -0
- chrys/service/analytics/_metric_ops.py +83 -0
- chrys/service/analytics/_session_projection.py +460 -0
- chrys/service/analytics/_timeline.py +434 -0
- chrys/service/analytics/_turn_graph.py +1237 -0
- chrys/service/analytics/_turns.py +1377 -0
- chrys/service/analytics/_workflows.py +171 -0
- chrys/service/analytics/aggregation.py +1474 -0
- chrys/service/analytics/classification.py +128 -0
- chrys/service/analytics/export.py +803 -0
- chrys/service/analytics/findings.py +282 -0
- chrys/service/analytics/math.py +79 -0
- chrys/service/analytics/model.py +886 -0
- chrys/service/analytics/reader.py +321 -0
- chrys/service/approval/__init__.py +3 -0
- chrys/service/approval/arbitration.py +102 -0
- chrys/service/approval/command_identity.py +109 -0
- chrys/service/approval/correlation.py +57 -0
- chrys/service/approval/grant_store.py +108 -0
- chrys/service/approval/judge.py +589 -0
- chrys/service/approval/level.py +15 -0
- chrys/service/approval/policy.py +209 -0
- chrys/service/approval/reuse.py +223 -0
- chrys/service/approval/reuse_binding.py +133 -0
- chrys/service/approval/safety_classifier.py +536 -0
- chrys/service/approval/turn_context.py +49 -0
- chrys/service/context/__init__.py +3 -0
- chrys/service/context/compaction/__init__.py +38 -0
- chrys/service/context/compaction/breaker.py +156 -0
- chrys/service/context/compaction/budgets.py +93 -0
- chrys/service/context/compaction/calibration.py +77 -0
- chrys/service/context/compaction/compression.py +690 -0
- chrys/service/context/compaction/current_turn_drop.py +458 -0
- chrys/service/context/compaction/events.py +201 -0
- chrys/service/context/compaction/exclusions.py +261 -0
- chrys/service/context/compaction/groups.py +361 -0
- chrys/service/context/compaction/last_words.py +1996 -0
- chrys/service/context/compaction/last_words_state.py +752 -0
- chrys/service/context/compaction/scoped.py +472 -0
- chrys/service/context/compaction/spill.py +1947 -0
- chrys/service/context/compaction/strategy.py +1116 -0
- chrys/service/context/compaction/summaries.py +319 -0
- chrys/service/context/compaction/turns.py +375 -0
- chrys/service/context/manager.py +174 -0
- chrys/service/context/memory_loader.py +792 -0
- chrys/service/context/middleware/__init__.py +3 -0
- chrys/service/context/middleware/usage.py +332 -0
- chrys/service/context/providers/__init__.py +3 -0
- chrys/service/context/providers/context_management.py +432 -0
- chrys/service/context/providers/history.py +647 -0
- chrys/service/context/providers/memory.py +50 -0
- chrys/service/context/tool_content.py +22 -0
- chrys/service/hooks/__init__.py +37 -0
- chrys/service/hooks/detached_worker.py +118 -0
- chrys/service/hooks/docs/README.md +14 -0
- chrys/service/hooks/docs/authoring.md +684 -0
- chrys/service/hooks/docs/configuration.md +499 -0
- chrys/service/hooks/docs/design.md +359 -0
- chrys/service/hooks/docs/project-hooks.md +348 -0
- chrys/service/hooks/events.py +157 -0
- chrys/service/hooks/loader.py +665 -0
- chrys/service/hooks/manager.py +807 -0
- chrys/service/hooks/matcher.py +111 -0
- chrys/service/hooks/outbox.py +294 -0
- chrys/service/hooks/runner.py +572 -0
- chrys/service/hooks/schema.py +407 -0
- chrys/service/llm/__init__.py +7 -0
- chrys/service/llm/_structured_outputs.py +192 -0
- chrys/service/llm/anthropic_messages/__init__.py +9 -0
- chrys/service/llm/anthropic_messages/client.py +254 -0
- chrys/service/llm/anthropic_messages/decode.py +331 -0
- chrys/service/llm/anthropic_messages/history.py +279 -0
- chrys/service/llm/anthropic_messages/request.py +323 -0
- chrys/service/llm/anthropic_messages/server_tools.py +319 -0
- chrys/service/llm/anthropic_messages/stream.py +297 -0
- chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
- chrys/service/llm/chat_completions/__init__.py +9 -0
- chrys/service/llm/chat_completions/client.py +369 -0
- chrys/service/llm/chat_completions/decode.py +338 -0
- chrys/service/llm/chat_completions/history.py +657 -0
- chrys/service/llm/chat_completions/reasoning.py +244 -0
- chrys/service/llm/chat_completions/request.py +192 -0
- chrys/service/llm/chat_completions/stream.py +546 -0
- chrys/service/llm/chat_completions/validation.py +308 -0
- chrys/service/llm/clients.py +613 -0
- chrys/service/llm/images.py +65 -0
- chrys/service/llm/mock.py +413 -0
- chrys/service/llm/observer.py +446 -0
- chrys/service/llm/one_shot.py +106 -0
- chrys/service/llm/openai_exceptions.py +93 -0
- chrys/service/llm/openai_responses/__init__.py +10 -0
- chrys/service/llm/openai_responses/client.py +323 -0
- chrys/service/llm/openai_responses/decode.py +432 -0
- chrys/service/llm/openai_responses/history.py +519 -0
- chrys/service/llm/openai_responses/hosted.py +625 -0
- chrys/service/llm/openai_responses/replay.py +629 -0
- chrys/service/llm/openai_responses/request.py +342 -0
- chrys/service/llm/openai_responses/stream.py +847 -0
- chrys/service/llm/openai_timestamps.py +57 -0
- chrys/service/llm/providers.py +107 -0
- chrys/service/llm/proxy_route.py +54 -0
- chrys/service/llm/raw_http_log.py +309 -0
- chrys/service/llm/route_facts.py +63 -0
- chrys/service/llm/route_sessions.py +49 -0
- chrys/service/llm/sdk_anthropic.py +18 -0
- chrys/service/llm/sdk_headers.py +47 -0
- chrys/service/llm/sdk_openai.py +18 -0
- chrys/service/llm/sdk_retry.py +59 -0
- chrys/service/llm/wire_client.py +246 -0
- chrys/service/mcp/__init__.py +3 -0
- chrys/service/mcp/_connection.py +235 -0
- chrys/service/mcp/_http_transport.py +350 -0
- chrys/service/mcp/_progressive.py +459 -0
- chrys/service/mcp/_stdio_transport.py +614 -0
- chrys/service/mcp/_tool_mixins.py +96 -0
- chrys/service/mcp/adapter.py +1031 -0
- chrys/service/mcp/cache.py +476 -0
- chrys/service/mcp/content.py +32 -0
- chrys/service/mcp/errors.py +169 -0
- chrys/service/mcp/owned.py +1330 -0
- chrys/service/mcp/result_limits.py +168 -0
- chrys/service/mcp/thinking_warning.py +95 -0
- chrys/service/mcp/validation.py +74 -0
- chrys/service/mutations/__init__.py +84 -0
- chrys/service/mutations/coordination.py +894 -0
- chrys/service/mutations/detector.py +458 -0
- chrys/service/mutations/git_calibrator.py +397 -0
- chrys/service/mutations/git_state.py +647 -0
- chrys/service/mutations/pipeline.py +779 -0
- chrys/service/mutations/scanner.py +466 -0
- chrys/service/mutations/snapshot_files.py +233 -0
- chrys/service/mutations/store.py +625 -0
- chrys/service/mutations/tool_names.py +15 -0
- chrys/service/mutations/trace.py +246 -0
- chrys/service/mutations/tracker.py +1289 -0
- chrys/service/mutations/types.py +559 -0
- chrys/service/mutations/workspace_changes.py +1583 -0
- chrys/service/profiles/__init__.py +2 -0
- chrys/service/profiles/agents/__init__.py +3 -0
- chrys/service/profiles/agents/builtins/Code.yaml +199 -0
- chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
- chrys/service/profiles/agents/builtins/General.yaml +114 -0
- chrys/service/profiles/agents/builtins/QA.yaml +128 -0
- chrys/service/profiles/agents/loader.py +808 -0
- chrys/service/profiles/agents/registry.py +727 -0
- chrys/service/profiles/agents/schema.py +371 -0
- chrys/service/profiles/agents/serializer.py +196 -0
- chrys/service/profiles/coercion.py +22 -0
- chrys/service/profiles/models/__init__.py +3 -0
- chrys/service/profiles/models/env_bridge.py +157 -0
- chrys/service/profiles/models/loader.py +282 -0
- chrys/service/profiles/models/options.py +438 -0
- chrys/service/profiles/models/registry.py +104 -0
- chrys/service/profiles/models/resolver.py +286 -0
- chrys/service/profiles/models/schema.py +91 -0
- chrys/service/profiles/models/serializer.py +76 -0
- chrys/service/session/__init__.py +3 -0
- chrys/service/session/checkpoint.py +156 -0
- chrys/service/session/history.py +1616 -0
- chrys/service/session/message_metadata.py +151 -0
- chrys/service/session/persistence.py +435 -0
- chrys/service/session/runtime_metadata.py +210 -0
- chrys/service/session/sub_agent_logs.py +1069 -0
- chrys/service/session/sub_agent_transcript.py +654 -0
- chrys/service/skills/__init__.py +3 -0
- chrys/service/skills/adapter.py +408 -0
- chrys/service/skills/constants.py +8 -0
- chrys/service/skills/loader.py +575 -0
- chrys/service/skills/model.py +130 -0
- chrys/service/skills/provider.py +841 -0
- chrys/service/skills/runner.py +390 -0
- chrys/service/state/__init__.py +3 -0
- chrys/service/state/_fork.py +710 -0
- chrys/service/state/_session_files.py +175 -0
- chrys/service/state/_session_meta.py +552 -0
- chrys/service/state/locks.py +154 -0
- chrys/service/state/serializers.py +156 -0
- chrys/service/state/session_catalog.py +361 -0
- chrys/service/state/session_listing.py +84 -0
- chrys/service/state/session_migration.py +446 -0
- chrys/service/state/session_mru.py +315 -0
- chrys/service/state/store.py +2132 -0
- chrys/service/state/workflow.py +158 -0
- chrys/service/text_blocks.py +47 -0
- chrys/service/todos/__init__.py +3 -0
- chrys/service/todos/reminder.py +31 -0
- chrys/service/todos/tracker.py +58 -0
- chrys/service/tools/__init__.py +3 -0
- chrys/service/tools/approval_targets.py +92 -0
- chrys/service/tools/builtins/__init__.py +3 -0
- chrys/service/tools/builtins/ask_user.py +326 -0
- chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
- chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
- chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
- chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
- chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
- chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
- chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
- chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
- chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
- chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
- chrys/service/tools/builtins/doc_converter/registry.py +55 -0
- chrys/service/tools/builtins/filesystem.py +1071 -0
- chrys/service/tools/builtins/search.py +1265 -0
- chrys/service/tools/builtins/shell.py +967 -0
- chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
- chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
- chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
- chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
- chrys/service/tools/builtins/shell_filter.py +24 -0
- chrys/service/tools/builtins/sleep.py +91 -0
- chrys/service/tools/builtins/todo.py +64 -0
- chrys/service/tools/builtins/web/__init__.py +6 -0
- chrys/service/tools/builtins/web/build.py +268 -0
- chrys/service/tools/builtins/web/config.py +338 -0
- chrys/service/tools/builtins/web/credentials.py +37 -0
- chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
- chrys/service/tools/builtins/web/fetch/cache.py +68 -0
- chrys/service/tools/builtins/web/fetch/convert.py +56 -0
- chrys/service/tools/builtins/web/fetch/tool.py +223 -0
- chrys/service/tools/builtins/web/hosted.py +61 -0
- chrys/service/tools/builtins/web/http.py +330 -0
- chrys/service/tools/builtins/web/progress.py +18 -0
- chrys/service/tools/builtins/web/search/__init__.py +3 -0
- chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
- chrys/service/tools/builtins/web/search/html.py +152 -0
- chrys/service/tools/builtins/web/search/json_api.py +227 -0
- chrys/service/tools/builtins/web/search/providers.py +77 -0
- chrys/service/tools/builtins/web/search/tool.py +218 -0
- chrys/service/tools/builtins/web/search/types.py +28 -0
- chrys/service/tools/builtins/web/tls_identity.py +66 -0
- chrys/service/tools/kinds.py +115 -0
- chrys/service/tools/names.py +47 -0
- chrys/service/tools/registry.py +242 -0
- chrys/service/tools/result_metadata.py +103 -0
- chrys/service/tools/session_artifacts.py +196 -0
- chrys/service/tools/spill.py +218 -0
- chrys/service/tools/workspace_paths.py +81 -0
- chrys/service/trajectory/__init__.py +11 -0
- chrys/service/trajectory/approvals.py +201 -0
- chrys/service/trajectory/compaction.py +420 -0
- chrys/service/trajectory/fork.py +75 -0
- chrys/service/trajectory/hooks.py +217 -0
- chrys/service/trajectory/items.py +41 -0
- chrys/service/trajectory/preparation.py +374 -0
- chrys/service/trajectory/retries.py +95 -0
- chrys/service/trajectory/revisions.py +129 -0
- chrys/service/trajectory/segmented.py +163 -0
- chrys/service/trajectory/session.py +762 -0
- chrys/service/trajectory/state.py +83 -0
- chrys/service/trajectory/sub_agents.py +169 -0
- chrys/service/trajectory/tombstone.py +262 -0
- chrys/service/trajectory/tools.py +323 -0
- chrys/service/trajectory/validation.py +130 -0
- chrys/service/trajectory/waits.py +161 -0
- chrys/service/trajectory/workflow.py +148 -0
- chrys/service/vision.py +128 -0
- chrys/service/workflows/__init__.py +3 -0
- chrys/service/workflows/admission.py +151 -0
- chrys/service/workflows/artifacts.py +109 -0
- chrys/service/workflows/asks.py +130 -0
- chrys/service/workflows/builtins/__init__.py +12 -0
- chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
- chrys/service/workflows/builtins/demo-workflow.py +756 -0
- chrys/service/workflows/discovery.py +554 -0
- chrys/service/workflows/environment.py +284 -0
- chrys/service/workflows/graph.py +430 -0
- chrys/service/workflows/history.py +82 -0
- chrys/service/workflows/interpreter.py +107 -0
- chrys/service/workflows/journal.py +334 -0
- chrys/service/workflows/layout.py +56 -0
- chrys/service/workflows/ledger.py +149 -0
- chrys/service/workflows/model_selection.py +18 -0
- chrys/service/workflows/orphans.py +209 -0
- chrys/service/workflows/outcomes.py +46 -0
- chrys/service/workflows/protocol.py +210 -0
- chrys/service/workflows/records.py +142 -0
- chrys/service/workflows/scheduler.py +770 -0
- chrys/service/workflows/sdk/__init__.py +51 -0
- chrys/service/workflows/sdk/_ask.py +167 -0
- chrys/service/workflows/sdk/_builder.py +781 -0
- chrys/service/workflows/sdk/_values.py +92 -0
- chrys/service/workflows/sdk_artifact.py +74 -0
- chrys/service/workflows/store.py +494 -0
- chrys/service/workflows/transcript.py +102 -0
- chrys/service/workflows/values.py +73 -0
- chrys/service/workflows/worker_host.py +1820 -0
- chrys/workflows.py +38 -0
- icode_tui-0.29.1.dist-info/METADATA +104 -0
- icode_tui-0.29.1.dist-info/RECORD +1138 -0
- icode_tui-0.29.1.dist-info/WHEEL +4 -0
- icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
- icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
- icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
|
@@ -0,0 +1,1265 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Code search tools — grep and glob, powered by ripgrep.
|
|
4
|
+
|
|
5
|
+
Uses the ``rg`` binary for fast, correct file searching with full glob and
|
|
6
|
+
regex support. The binary is bundled as package data in
|
|
7
|
+
``chrys/foundation/vendor/ripgrep/`` (downloaded at build time via
|
|
8
|
+
``scripts/fetch_rg.sh``). A system-installed ``rg`` on PATH is also accepted
|
|
9
|
+
as a fallback.
|
|
10
|
+
|
|
11
|
+
When the search pattern contains non-ASCII characters, multiple encoding
|
|
12
|
+
passes (UTF-8, GBK, and the system ANSI codepage) are run automatically
|
|
13
|
+
to catch files in legacy encodings.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import base64
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import locale
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import shutil
|
|
26
|
+
import subprocess
|
|
27
|
+
from collections.abc import Callable, Iterator
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from pathlib import Path, PurePath
|
|
30
|
+
from typing import TYPE_CHECKING, Annotated, Any, cast
|
|
31
|
+
|
|
32
|
+
from pathspec import GitIgnoreSpec
|
|
33
|
+
|
|
34
|
+
from chrys.foundation.platform import get_platform
|
|
35
|
+
from chrys.foundation.platform.files import surrogate_safe_text
|
|
36
|
+
from chrys.foundation.platform.output_capture import BoundedCapture, drain_process_pipes
|
|
37
|
+
from chrys.foundation.platform.paths import resolve_workspace_path
|
|
38
|
+
from chrys.foundation.platform.process import (
|
|
39
|
+
MissingWorkingDirectoryError,
|
|
40
|
+
decode_subprocess_output,
|
|
41
|
+
managed_subprocess,
|
|
42
|
+
)
|
|
43
|
+
from chrys.foundation.vendor import find_rg
|
|
44
|
+
from chrys.service.tools.kinds import KIND_SEARCH, tool
|
|
45
|
+
from chrys.service.tools.result_metadata import record_process_result, record_process_timeout, tool_error
|
|
46
|
+
from chrys.service.tools.workspace_paths import missing_base_cwd_error
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from chrys.foundation.models.session_env import SessionEnvironment
|
|
50
|
+
|
|
51
|
+
_MAX_RESULTS = 50
|
|
52
|
+
_MAX_RESULTS_HARD_LIMIT = 100
|
|
53
|
+
_TIMEOUT = 30
|
|
54
|
+
_MAX_LINE_DISPLAY_CHARS = 2048
|
|
55
|
+
_MAX_ERROR_DIAGNOSTIC_CHARS = 512
|
|
56
|
+
_WINDOWS_COMMAND_LIMIT = 32_767 # UTF-16 code units, including the terminating NUL.
|
|
57
|
+
_WINDOWS_COMMAND_HEADROOM = 512 # Allow PATH shims to expand the executable path when forwarding argv.
|
|
58
|
+
_POSIX_COMMAND_LIMIT = 128 * 1024 # Leave room for the inherited environment and OS overhead.
|
|
59
|
+
_MAX_LISTING_BYTES = 64 * 1024 * 1024
|
|
60
|
+
"""File names one ``rg --files`` listing may print before the search is refused as too broad."""
|
|
61
|
+
_MAX_RECORD_BYTES = 1024 * 1024
|
|
62
|
+
"""The longest ``rg --json`` record parsed whole.
|
|
63
|
+
|
|
64
|
+
Each record carries its whole source line, and a match record lists every match
|
|
65
|
+
on it, so one minified line can print megabytes. A longer record keeps only
|
|
66
|
+
``_RECORD_HEAD_BYTES``, enough to name its file.
|
|
67
|
+
"""
|
|
68
|
+
_RECORD_HEAD_BYTES = 64 * 1024
|
|
69
|
+
_MAX_STDERR_BYTES = 64 * 1024
|
|
70
|
+
_MAX_OVERSIZED_LINES_SHOWN = 10
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(slots=True)
|
|
74
|
+
class GrepEntry:
|
|
75
|
+
"""A single line from ripgrep output (match or context)."""
|
|
76
|
+
|
|
77
|
+
rel_path: str
|
|
78
|
+
line_num: int
|
|
79
|
+
marker: str # ">" for match, " " for context
|
|
80
|
+
text: str
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(slots=True)
|
|
84
|
+
class LongLine:
|
|
85
|
+
"""A line that was truncated for exceeding ``_MAX_LINE_DISPLAY_CHARS``."""
|
|
86
|
+
|
|
87
|
+
rel_path: str
|
|
88
|
+
line_num: int
|
|
89
|
+
actual_length: int
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(slots=True)
|
|
93
|
+
class GrepParseResult:
|
|
94
|
+
"""Parsed output from a single ``rg --json`` invocation."""
|
|
95
|
+
|
|
96
|
+
entries: list[GrepEntry] = field(default_factory=list)
|
|
97
|
+
match_count: int = 0
|
|
98
|
+
"""Matches counted toward the limit, those too long to show included."""
|
|
99
|
+
long_lines: list[LongLine] = field(default_factory=list)
|
|
100
|
+
oversized: set[tuple[str, int]] = field(default_factory=set)
|
|
101
|
+
"""Matching lines, by file and line number, whose record was longer than ``_MAX_RECORD_BYTES``."""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(slots=True)
|
|
105
|
+
class _SearchErrors:
|
|
106
|
+
"""Bound diagnostics before they enter tool results or persisted metadata."""
|
|
107
|
+
|
|
108
|
+
first_diagnostic: str = ""
|
|
109
|
+
diagnostic_count: int = 0
|
|
110
|
+
exit_code: int | None = None
|
|
111
|
+
_seen_headers: set[bytes] = field(default_factory=set)
|
|
112
|
+
|
|
113
|
+
def add(self, stderr: str, exit_code: int) -> None:
|
|
114
|
+
if self.exit_code is None:
|
|
115
|
+
self.exit_code = exit_code
|
|
116
|
+
diagnostic = stderr.strip() or f"rg exited with code {exit_code}"
|
|
117
|
+
capture_first = False
|
|
118
|
+
for index, line in enumerate(diagnostic.splitlines()):
|
|
119
|
+
# Continuation lines contain regex causes, source locations and hints.
|
|
120
|
+
# Unprefixed stderr is one diagnostic per invocation.
|
|
121
|
+
if index == 0 or line.startswith("rg:"):
|
|
122
|
+
capture_first = self.diagnostic_count == 0
|
|
123
|
+
digest = hashlib.blake2b(line.encode("utf-8", errors="surrogatepass"), digest_size=16).digest()
|
|
124
|
+
if digest not in self._seen_headers:
|
|
125
|
+
self._seen_headers.add(digest)
|
|
126
|
+
self.diagnostic_count += 1
|
|
127
|
+
if capture_first:
|
|
128
|
+
separator = "\n" if index else ""
|
|
129
|
+
# One extra character records truncation without retaining the tail.
|
|
130
|
+
limit = _MAX_ERROR_DIAGNOSTIC_CHARS + 1
|
|
131
|
+
self.first_diagnostic = (self.first_diagnostic + separator + line[:limit])[:limit]
|
|
132
|
+
|
|
133
|
+
def summary(self) -> str:
|
|
134
|
+
message = self.first_diagnostic.rstrip()
|
|
135
|
+
if len(self.first_diagnostic) > _MAX_ERROR_DIAGNOSTIC_CHARS:
|
|
136
|
+
marker = "... [truncated]"
|
|
137
|
+
message = self.first_diagnostic[: _MAX_ERROR_DIAGNOSTIC_CHARS - len(marker)] + marker
|
|
138
|
+
if self.diagnostic_count > 1:
|
|
139
|
+
return f"{message} [{self.diagnostic_count - 1} additional diagnostics omitted]"
|
|
140
|
+
return message
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_PLATFORM = get_platform()
|
|
144
|
+
|
|
145
|
+
# Map Python codec names → WHATWG encoding labels accepted by ``rg -E``.
|
|
146
|
+
# Only entries where the names differ need to be listed.
|
|
147
|
+
_CODEC_TO_WHATWG: dict[str, str] = {
|
|
148
|
+
"cp874": "windows-874",
|
|
149
|
+
"cp932": "shift_jis",
|
|
150
|
+
"cp949": "euc-kr",
|
|
151
|
+
"cp950": "big5",
|
|
152
|
+
"iso8859-1": "windows-1252",
|
|
153
|
+
"latin-1": "windows-1252",
|
|
154
|
+
"euc_kr": "euc-kr",
|
|
155
|
+
"euc_jp": "euc-jp",
|
|
156
|
+
"shift_jis": "shift_jis",
|
|
157
|
+
"gb2312": "gbk",
|
|
158
|
+
"gb18030": "gb18030",
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _find_rg() -> str:
|
|
163
|
+
"""Locate the ``rg`` binary: bundled vendor copy → PATH → error."""
|
|
164
|
+
path = find_rg()
|
|
165
|
+
if path:
|
|
166
|
+
return path
|
|
167
|
+
raise FileNotFoundError(
|
|
168
|
+
"ripgrep (rg) not found — run scripts/fetch_rg.sh to bundle it, or install rg on your system PATH"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _to_posix(path: str) -> str:
|
|
173
|
+
"""Normalize a path to forward slashes for consistent output across platforms.
|
|
174
|
+
|
|
175
|
+
``PurePath.as_posix()`` is platform-aware: on Windows it converts
|
|
176
|
+
``\\`` separators; on POSIX it preserves any literal backslash a
|
|
177
|
+
filename may legitimately contain.
|
|
178
|
+
"""
|
|
179
|
+
return PurePath(path).as_posix()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _codec_to_whatwg(codec_name: str) -> str:
|
|
183
|
+
"""Convert a Python codec name to a WHATWG encoding label for ``rg -E``."""
|
|
184
|
+
normalized = codec_name.lower().replace("-", "_").replace(" ", "_")
|
|
185
|
+
if normalized in _CODEC_TO_WHATWG:
|
|
186
|
+
return _CODEC_TO_WHATWG[normalized]
|
|
187
|
+
# Many names work directly (utf-8, gbk, big5, etc.)
|
|
188
|
+
return codec_name.lower().replace("_", "-")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _get_system_ansi_codepage() -> str | None:
|
|
192
|
+
"""Detect the system ANSI codepage and return its WHATWG label.
|
|
193
|
+
|
|
194
|
+
Returns ``None`` if detection fails or the codepage is already UTF-8.
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
if _PLATFORM.is_windows:
|
|
198
|
+
import ctypes
|
|
199
|
+
|
|
200
|
+
ctypes_module = cast(Any, ctypes)
|
|
201
|
+
acp = ctypes_module.windll.kernel32.GetACP()
|
|
202
|
+
codec_name = f"cp{acp}"
|
|
203
|
+
else:
|
|
204
|
+
codec_name = locale.getpreferredencoding(False)
|
|
205
|
+
|
|
206
|
+
import codecs
|
|
207
|
+
|
|
208
|
+
info = codecs.lookup(codec_name)
|
|
209
|
+
normalized = info.name.lower().replace("-", "_")
|
|
210
|
+
|
|
211
|
+
# Skip if it's already UTF-8 or GBK (always included)
|
|
212
|
+
if normalized in ("utf_8", "gbk", "cp936", "gb2312"):
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
return _codec_to_whatwg(info.name)
|
|
216
|
+
except Exception:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _get_encodings() -> list[str]:
|
|
221
|
+
"""Return the list of WHATWG encoding labels to search with.
|
|
222
|
+
|
|
223
|
+
Always includes UTF-8 and GBK. Adds the system ANSI codepage if it
|
|
224
|
+
differs from both.
|
|
225
|
+
"""
|
|
226
|
+
encodings = ["utf-8", "gbk"]
|
|
227
|
+
ansi = _get_system_ansi_codepage()
|
|
228
|
+
if ansi and ansi not in encodings:
|
|
229
|
+
encodings.append(ansi)
|
|
230
|
+
return encodings
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _is_ascii_only(text: str) -> bool:
|
|
234
|
+
"""Return ``True`` if *text* contains only ASCII characters."""
|
|
235
|
+
try:
|
|
236
|
+
text.encode("ascii")
|
|
237
|
+
return True
|
|
238
|
+
except UnicodeEncodeError:
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class _ListingTooLarge(Exception):
|
|
243
|
+
"""An ``rg --files`` listing printed more than ``_MAX_LISTING_BYTES``."""
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class _RgStopped(Exception):
|
|
247
|
+
"""Leaves ``managed_subprocess`` once rg's output is no longer needed.
|
|
248
|
+
|
|
249
|
+
Leaving it by an exception kills rg's whole tree; leaving it normally does so
|
|
250
|
+
only while the process it started runs, and a launcher rg runs under may
|
|
251
|
+
already have exited.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class _RgStdout:
|
|
256
|
+
"""rg's stdout: kept whole up to ``_MAX_LISTING_BYTES``, or handed to *consume* as it arrives."""
|
|
257
|
+
|
|
258
|
+
def __init__(self, consume: Callable[[bytes], bool] | None) -> None:
|
|
259
|
+
self._consume = consume
|
|
260
|
+
self.data = bytearray()
|
|
261
|
+
self.stopped = asyncio.Event()
|
|
262
|
+
"""Set once no more output is needed."""
|
|
263
|
+
self.too_large = False
|
|
264
|
+
|
|
265
|
+
def feed(self, chunk: bytes) -> None:
|
|
266
|
+
if self.stopped.is_set():
|
|
267
|
+
return
|
|
268
|
+
if self._consume is not None:
|
|
269
|
+
if not self._consume(chunk):
|
|
270
|
+
self.stopped.set()
|
|
271
|
+
elif len(self.data) + len(chunk) > _MAX_LISTING_BYTES:
|
|
272
|
+
self.too_large = True
|
|
273
|
+
self.stopped.set()
|
|
274
|
+
else:
|
|
275
|
+
self.data += chunk
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
async def _drain_until_stopped(proc: asyncio.subprocess.Process, stdout: _RgStdout, stderr: BoundedCapture) -> None:
|
|
279
|
+
"""Read rg's output until it exits, or until *stdout* needs no more of it.
|
|
280
|
+
|
|
281
|
+
A stopped rg is left running for ``managed_subprocess`` to kill with its whole
|
|
282
|
+
tree, which on Windows must happen while a launcher rg runs under lives.
|
|
283
|
+
"""
|
|
284
|
+
drain = asyncio.ensure_future(drain_process_pipes(proc, stdout, stderr))
|
|
285
|
+
stopped = asyncio.ensure_future(stdout.stopped.wait())
|
|
286
|
+
try:
|
|
287
|
+
await asyncio.wait((drain, stopped), return_when=asyncio.FIRST_COMPLETED)
|
|
288
|
+
finally:
|
|
289
|
+
drain.cancel()
|
|
290
|
+
stopped.cancel()
|
|
291
|
+
await asyncio.gather(drain, stopped, return_exceptions=True)
|
|
292
|
+
error = None if drain.cancelled() else drain.exception()
|
|
293
|
+
if error is not None:
|
|
294
|
+
raise error
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def _run_rg(
|
|
298
|
+
args: list[str],
|
|
299
|
+
*,
|
|
300
|
+
timeout: int = _TIMEOUT,
|
|
301
|
+
cwd: str | None = None,
|
|
302
|
+
consume: Callable[[bytes], bool] | None = None,
|
|
303
|
+
) -> tuple[str, str, int]:
|
|
304
|
+
"""Run ``rg`` asynchronously and return ``(stdout, stderr, returncode)``.
|
|
305
|
+
|
|
306
|
+
With *consume*, stdout goes to it as rg prints it and comes back empty;
|
|
307
|
+
once *consume* returns False, rg is stopped, and its exit code then says
|
|
308
|
+
nothing about the search. Without, stdout comes back whole, and a listing
|
|
309
|
+
longer than ``_MAX_LISTING_BYTES`` stops rg and raises ``_ListingTooLarge``.
|
|
310
|
+
"""
|
|
311
|
+
rg = _find_rg()
|
|
312
|
+
stdout = _RgStdout(consume)
|
|
313
|
+
stderr = BoundedCapture(_MAX_STDERR_BYTES)
|
|
314
|
+
returncode = 0
|
|
315
|
+
try:
|
|
316
|
+
async with managed_subprocess(
|
|
317
|
+
rg,
|
|
318
|
+
*args,
|
|
319
|
+
cwd=cwd,
|
|
320
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
321
|
+
stdout=asyncio.subprocess.PIPE,
|
|
322
|
+
stderr=asyncio.subprocess.PIPE,
|
|
323
|
+
) as proc:
|
|
324
|
+
await asyncio.wait_for(_drain_until_stopped(proc, stdout, stderr), timeout=timeout)
|
|
325
|
+
if stdout.stopped.is_set():
|
|
326
|
+
raise _RgStopped
|
|
327
|
+
returncode = proc.returncode or 0
|
|
328
|
+
except _RgStopped:
|
|
329
|
+
pass
|
|
330
|
+
except (FileNotFoundError, NotADirectoryError, MissingWorkingDirectoryError) as exc:
|
|
331
|
+
# Process creation can fail because cwd disappeared, even while rg exists.
|
|
332
|
+
if cwd is not None and not os.path.isdir(cwd):
|
|
333
|
+
raise NotADirectoryError(f"search directory unavailable — {cwd}") from exc
|
|
334
|
+
raise
|
|
335
|
+
if stdout.too_large:
|
|
336
|
+
raise _ListingTooLarge
|
|
337
|
+
output = bytes(stdout.data)
|
|
338
|
+
return (
|
|
339
|
+
os.fsdecode(output) if "--null" in args else decode_subprocess_output(output),
|
|
340
|
+
stderr.snapshot().text(),
|
|
341
|
+
returncode,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _ignore_args(respect_gitignore: bool) -> list[str]:
|
|
346
|
+
# User ripgrep configuration must not silently override this tool's policy.
|
|
347
|
+
return ["--no-config", "--glob", "!.git/", *([] if respect_gitignore else ["--no-ignore-vcs"])]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _search_cwd(root: str) -> str:
|
|
351
|
+
"""Keep CreateProcess's working directory below its Windows path limit."""
|
|
352
|
+
if _PLATFORM.is_windows:
|
|
353
|
+
while len(root.encode("utf-16-le", errors="surrogatepass")) // 2 >= 258:
|
|
354
|
+
parent = os.path.dirname(root)
|
|
355
|
+
if parent == root:
|
|
356
|
+
break
|
|
357
|
+
root = parent
|
|
358
|
+
return root
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _literal_glob(pattern: str) -> str | None:
|
|
362
|
+
"""Decode a literal glob component without implementing glob matching."""
|
|
363
|
+
literal: list[str] = []
|
|
364
|
+
escaped = False
|
|
365
|
+
for char in pattern:
|
|
366
|
+
if escaped:
|
|
367
|
+
literal.append(char)
|
|
368
|
+
escaped = False
|
|
369
|
+
elif char == "\\":
|
|
370
|
+
escaped = True
|
|
371
|
+
elif char in "*?[]{}":
|
|
372
|
+
return None
|
|
373
|
+
else:
|
|
374
|
+
literal.append(char)
|
|
375
|
+
return None if escaped else "".join(literal)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _glob_scope(root: str, pattern: str | None) -> tuple[str, str | None]:
|
|
379
|
+
"""Pass explicitly named directory prefixes to rg as search operands.
|
|
380
|
+
|
|
381
|
+
Keep the remaining glob anchored so src/*.py does not become a recursive
|
|
382
|
+
*.py search. The caller chooses the ignore policy for the explicit target.
|
|
383
|
+
"""
|
|
384
|
+
if not pattern or pattern.startswith(("!", "#")) or not os.path.isdir(root):
|
|
385
|
+
return root, pattern
|
|
386
|
+
parts = pattern.removeprefix("/").split("/")
|
|
387
|
+
prefix: list[str] = []
|
|
388
|
+
scoped = root
|
|
389
|
+
for part in parts[:-1]:
|
|
390
|
+
literal = _literal_glob(part)
|
|
391
|
+
if not literal or literal in (".", "..") or os.path.splitdrive(literal)[0]:
|
|
392
|
+
break
|
|
393
|
+
# Filesystem lookup may accept SRC for src (or .git. for .git).
|
|
394
|
+
# A glob is case-sensitive: only promote an exact directory entry.
|
|
395
|
+
try:
|
|
396
|
+
with os.scandir(scoped) as entries:
|
|
397
|
+
if not any(entry.name == literal and entry.is_dir() for entry in entries):
|
|
398
|
+
break
|
|
399
|
+
except OSError:
|
|
400
|
+
break # Let rg report an unavailable/unreadable search directory.
|
|
401
|
+
if literal == ".git":
|
|
402
|
+
break
|
|
403
|
+
prefix.append(literal)
|
|
404
|
+
scoped = os.path.join(scoped, literal)
|
|
405
|
+
if not prefix:
|
|
406
|
+
return root, pattern
|
|
407
|
+
return scoped, "/" + ("/".join(parts[len(prefix) :]) or "**")
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _read_ignore_probe_file(path: Path) -> str | None:
|
|
411
|
+
"""Read small rule/config files; defer unreadable or large inputs to Git."""
|
|
412
|
+
try:
|
|
413
|
+
with path.open("rb") as stream:
|
|
414
|
+
data = stream.read(65_537)
|
|
415
|
+
# Rule/config contents can contain legacy-encoded comments on any OS.
|
|
416
|
+
return data.decode("utf-8", errors="surrogateescape") if len(data) <= 65_536 else None
|
|
417
|
+
except FileNotFoundError:
|
|
418
|
+
return ""
|
|
419
|
+
except OSError:
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _ignore_rule_names_directory(rule: str, target: str) -> bool:
|
|
424
|
+
"""Recognize rules such as /log/* and **/dist/** without broadening '*' rules."""
|
|
425
|
+
named = rule.removeprefix("/")
|
|
426
|
+
recursive = named.startswith("**/")
|
|
427
|
+
named = named.removeprefix("**/")
|
|
428
|
+
if not named.endswith(("/*", "/**")):
|
|
429
|
+
return False
|
|
430
|
+
named = _literal_glob(named.removesuffix("/**").removesuffix("/*"))
|
|
431
|
+
return bool(
|
|
432
|
+
named and (target.lower() == named.lower() or (recursive and target.lower().endswith("/" + named.lower())))
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _gitignore_probe_needed(root: str, repo: str, git: str) -> bool:
|
|
437
|
+
"""Skip Git only when local rules cannot affect the explicitly selected directory.
|
|
438
|
+
|
|
439
|
+
This is a conservative prefilter, not the authoritative ignore decision.
|
|
440
|
+
Negations never suppress a probe; Git resolves their precedence. Unusual
|
|
441
|
+
configuration and linked worktrees retain the original Git-based path.
|
|
442
|
+
"""
|
|
443
|
+
repository = Path(repo)
|
|
444
|
+
if not (repository / ".git").is_dir() or any(
|
|
445
|
+
key in os.environ
|
|
446
|
+
for key in ("GIT_DIR", "GIT_COMMON_DIR", "GIT_WORK_TREE", "GIT_CONFIG_PARAMETERS", "GIT_CONFIG_COUNT")
|
|
447
|
+
):
|
|
448
|
+
return True
|
|
449
|
+
git_home = Path(os.environ.get("HOME") or Path.home())
|
|
450
|
+
xdg_git = Path(os.environ.get("XDG_CONFIG_HOME") or git_home / ".config") / "git"
|
|
451
|
+
configs = [repository / ".git/config"]
|
|
452
|
+
configs.extend(
|
|
453
|
+
[Path(os.environ["GIT_CONFIG_GLOBAL"])]
|
|
454
|
+
if "GIT_CONFIG_GLOBAL" in os.environ
|
|
455
|
+
else [git_home / ".gitconfig", xdg_git / "config"]
|
|
456
|
+
)
|
|
457
|
+
configs.extend(
|
|
458
|
+
[Path(os.environ["GIT_CONFIG_SYSTEM"])]
|
|
459
|
+
if "GIT_CONFIG_SYSTEM" in os.environ
|
|
460
|
+
else [
|
|
461
|
+
*([] if _PLATFORM.is_windows else [Path("/etc/gitconfig")]),
|
|
462
|
+
Path(git).parent.parent / "etc/gitconfig",
|
|
463
|
+
Path(git).resolve().parent.parent / "etc/gitconfig",
|
|
464
|
+
*([Path(git).parent.parent.parent / "etc/gitconfig"] if _PLATFORM.is_windows else []),
|
|
465
|
+
]
|
|
466
|
+
)
|
|
467
|
+
for config in configs:
|
|
468
|
+
if not config.is_absolute() and os.fspath(config) != os.devnull:
|
|
469
|
+
return True # Relative config paths may resolve against Git's -C directory.
|
|
470
|
+
contents = _read_ignore_probe_file(config)
|
|
471
|
+
if contents is None or re.search(r"(?i)\bexcludesfile\b|\[\s*include|\bworktreeconfig\b", contents):
|
|
472
|
+
return True
|
|
473
|
+
|
|
474
|
+
relative = _to_posix(os.path.relpath(root, repo))
|
|
475
|
+
sources = [(repository / ".git/info/exclude", relative), (xdg_git / "ignore", relative)]
|
|
476
|
+
directory = Path(root)
|
|
477
|
+
while True:
|
|
478
|
+
sources.append((directory / ".gitignore", _to_posix(os.path.relpath(root, directory))))
|
|
479
|
+
if directory == repository:
|
|
480
|
+
break
|
|
481
|
+
directory = directory.parent
|
|
482
|
+
for source, target in sources:
|
|
483
|
+
contents = _read_ignore_probe_file(source)
|
|
484
|
+
if contents is None:
|
|
485
|
+
return True
|
|
486
|
+
lines = contents.removeprefix("\ufeff").splitlines()
|
|
487
|
+
# Git drops the initial UTF-8 BOM and unescaped trailing spaces. Keep
|
|
488
|
+
# raw variants for escaped whitespace; extra matches only request a Git probe.
|
|
489
|
+
lines += [line.rstrip() for line in lines if line.rstrip() != line]
|
|
490
|
+
if target == "." and any(line in ("*", "**", "/*", "/**") for line in lines):
|
|
491
|
+
return True
|
|
492
|
+
if any(_ignore_rule_names_directory(line, target) for line in lines if not line.startswith("!")):
|
|
493
|
+
return True
|
|
494
|
+
try:
|
|
495
|
+
# Case-folding can overestimate matches on a case-sensitive repo;
|
|
496
|
+
# it must not miss one when Git has core.ignoreCase enabled.
|
|
497
|
+
spec = GitIgnoreSpec.from_lines(line.lower() for line in lines if not line.startswith("!"))
|
|
498
|
+
if spec.match_file(target.lower()) or spec.match_file(target.lower() + "/"):
|
|
499
|
+
return True
|
|
500
|
+
except ValueError, re.error:
|
|
501
|
+
return True
|
|
502
|
+
return False
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
async def _directory_is_gitignored(root: str) -> bool:
|
|
506
|
+
"""Recognize explicit ignored targets, including a directory's own '*' rule."""
|
|
507
|
+
repo = root
|
|
508
|
+
while not os.path.exists(os.path.join(repo, ".git")):
|
|
509
|
+
parent = os.path.dirname(repo)
|
|
510
|
+
if parent == repo:
|
|
511
|
+
return False
|
|
512
|
+
repo = parent
|
|
513
|
+
# A worktree root's rules still govern ordinary project-wide discovery.
|
|
514
|
+
git = shutil.which("git")
|
|
515
|
+
if repo == root or git is None:
|
|
516
|
+
return False
|
|
517
|
+
if not await asyncio.to_thread(_gitignore_probe_needed, root, repo, git):
|
|
518
|
+
return False
|
|
519
|
+
relative = _to_posix(os.path.relpath(root, repo))
|
|
520
|
+
operands = os.fsencode(relative + "\0" + relative + "/\0")
|
|
521
|
+
try:
|
|
522
|
+
async with managed_subprocess(
|
|
523
|
+
git,
|
|
524
|
+
"-C",
|
|
525
|
+
repo,
|
|
526
|
+
"check-ignore",
|
|
527
|
+
"--no-index",
|
|
528
|
+
"--verbose",
|
|
529
|
+
"-z",
|
|
530
|
+
"--stdin",
|
|
531
|
+
cwd=_search_cwd(repo),
|
|
532
|
+
stdin=asyncio.subprocess.PIPE,
|
|
533
|
+
stdout=asyncio.subprocess.PIPE,
|
|
534
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
535
|
+
) as proc:
|
|
536
|
+
stdout, _ = await asyncio.wait_for(proc.communicate(operands), timeout=5)
|
|
537
|
+
if proc.returncode not in (0, 1):
|
|
538
|
+
return False
|
|
539
|
+
except OSError, TimeoutError:
|
|
540
|
+
return False
|
|
541
|
+
try:
|
|
542
|
+
text = os.fsdecode(stdout)
|
|
543
|
+
except UnicodeError:
|
|
544
|
+
# Git can report raw non-UTF-8 source paths or matching patterns.
|
|
545
|
+
text = stdout.decode("utf-8", errors="surrogateescape")
|
|
546
|
+
fields = text.split("\0")
|
|
547
|
+
for index in range(0, len(fields) - 3, 4):
|
|
548
|
+
source, _, rule, operand = fields[index : index + 4]
|
|
549
|
+
if rule.startswith("!"):
|
|
550
|
+
continue
|
|
551
|
+
if operand == relative:
|
|
552
|
+
return True
|
|
553
|
+
# A trailing slash also probes rules for the target's contents. An
|
|
554
|
+
# inherited blanket '*' may coexist with !directory/, so only accept
|
|
555
|
+
# a local blanket rule or an inherited rule naming this directory.
|
|
556
|
+
if source == relative + "/.gitignore" and rule in ("*", "**", "/*", "/**"):
|
|
557
|
+
return True
|
|
558
|
+
origin = (
|
|
559
|
+
os.path.dirname(os.path.join(repo, source))
|
|
560
|
+
if not os.path.isabs(source) and os.path.basename(source) == ".gitignore"
|
|
561
|
+
else repo
|
|
562
|
+
)
|
|
563
|
+
target = _to_posix(os.path.relpath(root, origin))
|
|
564
|
+
if _ignore_rule_names_directory(rule, target):
|
|
565
|
+
return True
|
|
566
|
+
return False
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
async def _search_files(root: str, pattern: str | None, respect_gitignore: bool) -> list[str] | str:
|
|
570
|
+
"""Intersect ignore-aware discovery with rg's native glob matching.
|
|
571
|
+
|
|
572
|
+
Wildcards intersect an ignore-aware listing with rg's native glob matcher.
|
|
573
|
+
Literal names are explicit requests and use rg's override semantics; named
|
|
574
|
+
directory prefixes similarly become explicit operands. NUL delimiters
|
|
575
|
+
preserve whitespace and newlines in filenames.
|
|
576
|
+
Explicit ignored directories bypass Git rules for their contents.
|
|
577
|
+
"""
|
|
578
|
+
files: list[str] = []
|
|
579
|
+
root, pattern = _glob_scope(root, pattern)
|
|
580
|
+
if respect_gitignore and os.path.isdir(root) and await _directory_is_gitignored(root):
|
|
581
|
+
respect_gitignore = False
|
|
582
|
+
explicit_name = bool(pattern and not pattern.startswith(("!", "#")) and _literal_glob(pattern) is not None)
|
|
583
|
+
# Recognize explicit dots at component/alternative starts and in positive
|
|
584
|
+
# character classes. The extension dot in *.py must not enable hidden files.
|
|
585
|
+
hidden = bool(
|
|
586
|
+
pattern
|
|
587
|
+
and not pattern.startswith(("!", "#"))
|
|
588
|
+
and re.search(r"(?:^|[/,{])(?:\\?\.(?!\.?(?:/|$))|\[(?![!^])[^.\]]*\.[^\]]*\])", pattern)
|
|
589
|
+
)
|
|
590
|
+
cwd, target = (root, ".") if os.path.isdir(root) else os.path.split(root)
|
|
591
|
+
safe_cwd = _search_cwd(cwd)
|
|
592
|
+
if safe_cwd != cwd:
|
|
593
|
+
prefix = _to_posix(os.path.relpath(cwd, safe_cwd))
|
|
594
|
+
# Slash-containing globs are rooted at cwd. Move their anchor with the
|
|
595
|
+
# operand; basename-only globs already match at any depth.
|
|
596
|
+
if pattern:
|
|
597
|
+
negation = "!" if pattern.startswith("!") else ""
|
|
598
|
+
body = pattern.removeprefix("!")
|
|
599
|
+
if body.startswith("/") or "/" in body.removesuffix("/"):
|
|
600
|
+
escaped = "".join("\\" + char if char in "\\*?[]{}!#" else char for char in prefix)
|
|
601
|
+
pattern = f"{negation}{escaped}/{body.removeprefix('/')}"
|
|
602
|
+
target = os.path.relpath(root, safe_cwd)
|
|
603
|
+
cwd = safe_cwd
|
|
604
|
+
if target == "-":
|
|
605
|
+
target = os.path.join(".", target)
|
|
606
|
+
patterns = (pattern,) if explicit_name else ((None,) if pattern is None or pattern == "*" else (None, pattern))
|
|
607
|
+
for index, filename_pattern in enumerate(patterns):
|
|
608
|
+
filters = [] if filename_pattern is None else ["--glob", filename_pattern]
|
|
609
|
+
try:
|
|
610
|
+
stdout, stderr, code = await _run_rg(
|
|
611
|
+
[
|
|
612
|
+
"--files",
|
|
613
|
+
"--null",
|
|
614
|
+
*(["--hidden"] if hidden else []),
|
|
615
|
+
*filters,
|
|
616
|
+
*_ignore_args(respect_gitignore),
|
|
617
|
+
"--",
|
|
618
|
+
target,
|
|
619
|
+
],
|
|
620
|
+
cwd=cwd,
|
|
621
|
+
)
|
|
622
|
+
except _ListingTooLarge:
|
|
623
|
+
# Searching only part of the listing would hide matches without saying so.
|
|
624
|
+
return tool_error(
|
|
625
|
+
"search_too_broad",
|
|
626
|
+
f"too many files under {root} to list (over {_MAX_LISTING_BYTES // (1024 * 1024)} MiB of names)"
|
|
627
|
+
" — search a narrower path",
|
|
628
|
+
)
|
|
629
|
+
if code not in (0, 1):
|
|
630
|
+
errors = _SearchErrors()
|
|
631
|
+
errors.add(stderr, code)
|
|
632
|
+
record_process_result(code)
|
|
633
|
+
return tool_error("search_process_failed", errors.summary())
|
|
634
|
+
found = stdout.split("\0")
|
|
635
|
+
if index == 0:
|
|
636
|
+
files = [name for name in found if name]
|
|
637
|
+
else:
|
|
638
|
+
matches = set(found)
|
|
639
|
+
files = [name for name in files if name in matches]
|
|
640
|
+
return [os.path.normpath(os.path.join(cwd, name)) for name in files]
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _argument_size(argument: str) -> int:
|
|
644
|
+
"""Measure one appended argument, including its separator or terminator."""
|
|
645
|
+
if _PLATFORM.is_windows:
|
|
646
|
+
quoted = subprocess.list2cmdline([argument])
|
|
647
|
+
return len(quoted.encode("utf-16-le", errors="surrogatepass")) // 2 + 1
|
|
648
|
+
return len(os.fsencode(argument)) + 1 + (8 if _PLATFORM.is_linux else 0)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _command_size(command: list[str]) -> int:
|
|
652
|
+
"""Measure the executable and fixed options in the platform's budget units."""
|
|
653
|
+
if _PLATFORM.is_windows:
|
|
654
|
+
return len(subprocess.list2cmdline(command).encode("utf-16-le", errors="surrogatepass")) // 2 + 1
|
|
655
|
+
return sum(_argument_size(argument) for argument in command) + (8 if _PLATFORM.is_linux else 0)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _file_batches(files: list[str], *, command: list[str]) -> Iterator[list[str]]:
|
|
659
|
+
"""Fill the remaining command budget after the executable and fixed options."""
|
|
660
|
+
limit = _WINDOWS_COMMAND_LIMIT - _WINDOWS_COMMAND_HEADROOM if _PLATFORM.is_windows else _POSIX_COMMAND_LIMIT
|
|
661
|
+
fixed_size = _command_size(command)
|
|
662
|
+
batch: list[str] = []
|
|
663
|
+
size = fixed_size
|
|
664
|
+
for path in files:
|
|
665
|
+
cost = _argument_size(path)
|
|
666
|
+
if fixed_size + cost > limit:
|
|
667
|
+
raise ValueError(f"search command and file path exceed the platform command budget ({limit})")
|
|
668
|
+
if batch and size + cost > limit:
|
|
669
|
+
yield batch
|
|
670
|
+
batch = []
|
|
671
|
+
size = fixed_size
|
|
672
|
+
batch.append(path)
|
|
673
|
+
size += cost
|
|
674
|
+
if batch:
|
|
675
|
+
yield batch
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
# ---------------------------------------------------------------------------
|
|
679
|
+
# grep
|
|
680
|
+
# ---------------------------------------------------------------------------
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
_CUT_RECORD_HEAD = re.compile(rb'\{"type":"(match|context)","data":\{"path":')
|
|
684
|
+
"""How rg starts a match or context record: its file comes first, before the line."""
|
|
685
|
+
_CUT_RECORD_LINE = re.compile(rb',"line_number":(\d+)[,}]')
|
|
686
|
+
"""How rg gives a record's line number, after the line: a JSON string holds no unescaped quote."""
|
|
687
|
+
_CUT_RECORD_LINE_WINDOW = 64
|
|
688
|
+
"""Bytes kept from one chunk to the next, so a line number split between them is still found."""
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _whole_record(record: bytes | bytearray) -> tuple[str, dict[str, Any]]:
|
|
692
|
+
entry = json.loads(record)
|
|
693
|
+
return entry.get("type", ""), entry.get("data", {})
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _cut_record(head: bytes | bytearray) -> tuple[str, dict[str, Any]]:
|
|
697
|
+
"""Read the type and file of a record from the head kept of it."""
|
|
698
|
+
match = _CUT_RECORD_HEAD.match(head)
|
|
699
|
+
if match is None:
|
|
700
|
+
raise ValueError("not a match or context record")
|
|
701
|
+
path, _ = json.JSONDecoder().raw_decode(head[match.end() :].decode("utf-8", errors="replace"))
|
|
702
|
+
return match.group(1).decode("ascii"), {"path": path}
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
@dataclass(frozen=True, slots=True)
|
|
706
|
+
class _OversizedMatch:
|
|
707
|
+
"""A match whose record was too long to keep: its file and line number, without the line."""
|
|
708
|
+
|
|
709
|
+
rel_path: str
|
|
710
|
+
line_num: int
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
type _GrepItem = tuple[GrepEntry, LongLine | None] | _OversizedMatch
|
|
714
|
+
"""A parsed line and its long-line note, or a match too long to keep."""
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
class _GrepStream:
|
|
718
|
+
"""Parse ``rg --json`` output as it arrives, keeping only what the result can show.
|
|
719
|
+
|
|
720
|
+
Output stops being needed once one match more than *max_results* has
|
|
721
|
+
arrived; its caller stops rg there. Matches in *seen_matches* (found by an
|
|
722
|
+
earlier pass) are kept without being counted. A match too long to keep is
|
|
723
|
+
named in ``oversized`` by its file and line, and counts like any other.
|
|
724
|
+
With *skip_binary*, a file's entries wait
|
|
725
|
+
for its ``end`` record, which says whether rg found binary content, and
|
|
726
|
+
only as many wait as could still fill the result.
|
|
727
|
+
"""
|
|
728
|
+
|
|
729
|
+
def __init__(
|
|
730
|
+
self,
|
|
731
|
+
root: str,
|
|
732
|
+
max_results: int,
|
|
733
|
+
*,
|
|
734
|
+
seen_matches: set[tuple[str, int]] | None = None,
|
|
735
|
+
skip_binary: bool = False,
|
|
736
|
+
) -> None:
|
|
737
|
+
self.result = GrepParseResult()
|
|
738
|
+
self.full = False
|
|
739
|
+
self.saw_output = False
|
|
740
|
+
self._root = root
|
|
741
|
+
self._max_results = max_results
|
|
742
|
+
self._seen_matches = seen_matches
|
|
743
|
+
self._skip_binary = skip_binary
|
|
744
|
+
self._record = bytearray()
|
|
745
|
+
self._record_cut = False
|
|
746
|
+
self._cut_tail = bytearray()
|
|
747
|
+
self._cut_line: int | None = None
|
|
748
|
+
self._pending: dict[tuple[str | None, str | None], list[_GrepItem]] = {}
|
|
749
|
+
self._pending_matches: dict[tuple[str | None, str | None], int] = {}
|
|
750
|
+
|
|
751
|
+
def feed(self, chunk: bytes) -> bool:
|
|
752
|
+
"""Take the next chunk of output; False once the result needs no more."""
|
|
753
|
+
self.saw_output = self.saw_output or bool(chunk)
|
|
754
|
+
start = 0
|
|
755
|
+
while not self.full:
|
|
756
|
+
end = chunk.find(b"\n", start)
|
|
757
|
+
piece = chunk[start : len(chunk) if end < 0 else end]
|
|
758
|
+
if not self._record_cut:
|
|
759
|
+
self._record += piece
|
|
760
|
+
if len(self._record) > _MAX_RECORD_BYTES:
|
|
761
|
+
self._find_cut_line(self._record)
|
|
762
|
+
del self._record[_RECORD_HEAD_BYTES:]
|
|
763
|
+
self._record_cut = True
|
|
764
|
+
elif self._cut_line is None:
|
|
765
|
+
self._find_cut_line(piece)
|
|
766
|
+
if end < 0:
|
|
767
|
+
break
|
|
768
|
+
self._take(self._record, cut=self._record_cut)
|
|
769
|
+
self._record.clear()
|
|
770
|
+
self._record_cut = False
|
|
771
|
+
self._cut_tail.clear()
|
|
772
|
+
self._cut_line = None
|
|
773
|
+
start = end + 1
|
|
774
|
+
return not self.full
|
|
775
|
+
|
|
776
|
+
def _find_cut_line(self, data: bytes | bytearray) -> None:
|
|
777
|
+
self._cut_tail += data
|
|
778
|
+
found = _CUT_RECORD_LINE.search(self._cut_tail)
|
|
779
|
+
if found is not None:
|
|
780
|
+
self._cut_line = int(found.group(1))
|
|
781
|
+
del self._cut_tail[:-_CUT_RECORD_LINE_WINDOW]
|
|
782
|
+
|
|
783
|
+
def _take(self, record: bytearray, *, cut: bool) -> None:
|
|
784
|
+
try:
|
|
785
|
+
entry_type, data = _cut_record(record) if cut else _whole_record(record)
|
|
786
|
+
except ValueError:
|
|
787
|
+
return
|
|
788
|
+
path_data = data.get("path", {})
|
|
789
|
+
key = (path_data.get("text"), path_data.get("bytes"))
|
|
790
|
+
if entry_type == "end":
|
|
791
|
+
items = self._pending.pop(key, [])
|
|
792
|
+
self._pending_matches.pop(key, None)
|
|
793
|
+
if data.get("binary_offset") is None:
|
|
794
|
+
for item in items:
|
|
795
|
+
self._keep(item)
|
|
796
|
+
if self.full:
|
|
797
|
+
return
|
|
798
|
+
return
|
|
799
|
+
if entry_type not in ("match", "context"):
|
|
800
|
+
return
|
|
801
|
+
waiting = self._pending_matches.get(key, 0)
|
|
802
|
+
if self._skip_binary and waiting > self._max_results - self.result.match_count:
|
|
803
|
+
return # Enough of this file waits to fill the result; a dense file sends many more.
|
|
804
|
+
rel_path = self._rel_path(path_data)
|
|
805
|
+
if cut:
|
|
806
|
+
# The line itself was not kept: a match is named by its file and line, its context dropped.
|
|
807
|
+
if entry_type != "match" or self._cut_line is None:
|
|
808
|
+
return
|
|
809
|
+
item: _GrepItem = _OversizedMatch(rel_path, self._cut_line)
|
|
810
|
+
else:
|
|
811
|
+
item = self._entry(entry_type, data, rel_path)
|
|
812
|
+
if not self._skip_binary:
|
|
813
|
+
self._keep(item)
|
|
814
|
+
return
|
|
815
|
+
if self._counts(item):
|
|
816
|
+
self._pending_matches[key] = waiting + 1
|
|
817
|
+
self._pending.setdefault(key, []).append(item)
|
|
818
|
+
|
|
819
|
+
def _rel_path(self, path_data: dict[str, Any]) -> str:
|
|
820
|
+
file_path = (
|
|
821
|
+
os.fsdecode(base64.b64decode(path_data["bytes"])) if "bytes" in path_data else path_data.get("text", "")
|
|
822
|
+
)
|
|
823
|
+
if file_path and not os.path.isabs(file_path):
|
|
824
|
+
file_path = os.path.join(self._root, file_path)
|
|
825
|
+
return _to_posix(os.path.relpath(file_path, self._root)) if file_path else ""
|
|
826
|
+
|
|
827
|
+
def _entry(self, entry_type: str, data: dict[str, Any], rel_path: str) -> tuple[GrepEntry, LongLine | None]:
|
|
828
|
+
line_num = data.get("line_number", 0)
|
|
829
|
+
line_text = data.get("lines", {}).get("text", "").rstrip("\r\n")
|
|
830
|
+
long_line = None
|
|
831
|
+
if len(line_text) > _MAX_LINE_DISPLAY_CHARS:
|
|
832
|
+
long_line = LongLine(rel_path, line_num, len(line_text))
|
|
833
|
+
line_text = line_text[:_MAX_LINE_DISPLAY_CHARS] + "... [truncated]"
|
|
834
|
+
return GrepEntry(rel_path, line_num, ">" if entry_type == "match" else " ", line_text), long_line
|
|
835
|
+
|
|
836
|
+
def _counts(self, item: _GrepItem) -> bool:
|
|
837
|
+
if isinstance(item, _OversizedMatch):
|
|
838
|
+
key = (item.rel_path, item.line_num)
|
|
839
|
+
elif item[0].marker == ">":
|
|
840
|
+
key = (item[0].rel_path, item[0].line_num)
|
|
841
|
+
else:
|
|
842
|
+
return False
|
|
843
|
+
return self._seen_matches is None or key not in self._seen_matches
|
|
844
|
+
|
|
845
|
+
def _keep(self, item: _GrepItem) -> None:
|
|
846
|
+
counts = self._counts(item)
|
|
847
|
+
if counts:
|
|
848
|
+
self.result.match_count += 1
|
|
849
|
+
if self.result.match_count > self._max_results:
|
|
850
|
+
self.full = True
|
|
851
|
+
return
|
|
852
|
+
if isinstance(item, _OversizedMatch):
|
|
853
|
+
if counts: # Otherwise an earlier pass showed or named it.
|
|
854
|
+
self.result.oversized.add((item.rel_path, item.line_num))
|
|
855
|
+
return
|
|
856
|
+
entry, long_line = item
|
|
857
|
+
self.result.entries.append(entry)
|
|
858
|
+
if long_line is not None:
|
|
859
|
+
self.result.long_lines.append(long_line)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _oversized_note(lines: set[tuple[str, int]]) -> str:
|
|
863
|
+
if not lines:
|
|
864
|
+
return ""
|
|
865
|
+
ordered = sorted(lines)
|
|
866
|
+
shown = ", ".join(f"{rel_path}:{line_num}" for rel_path, line_num in ordered[:_MAX_OVERSIZED_LINES_SHOWN])
|
|
867
|
+
more = len(ordered) - _MAX_OVERSIZED_LINES_SHOWN
|
|
868
|
+
if more > 0:
|
|
869
|
+
shown += f" and {more} more line(s)"
|
|
870
|
+
limit = _MAX_RECORD_BYTES // (1024 * 1024)
|
|
871
|
+
return f"\n\n[Matching lines too long to show (over {limit} MiB of search output each): {shown}]"
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def _format_matches(entries: list[GrepEntry]) -> list[str]:
|
|
875
|
+
"""Group entries into context blocks and format for display."""
|
|
876
|
+
matches: list[str] = []
|
|
877
|
+
block: list[GrepEntry] = []
|
|
878
|
+
|
|
879
|
+
def _flush_block() -> None:
|
|
880
|
+
if not block:
|
|
881
|
+
return
|
|
882
|
+
first_match = next((b for b in block if b.marker == ">"), block[0])
|
|
883
|
+
header = f"{first_match.rel_path}:{first_match.line_num}"
|
|
884
|
+
lines = [f" {e.marker} {e.line_num:>4} | {e.text}" for e in block]
|
|
885
|
+
matches.append(header + "\n" + "\n".join(lines))
|
|
886
|
+
|
|
887
|
+
for entry in entries:
|
|
888
|
+
if block:
|
|
889
|
+
prev = block[-1]
|
|
890
|
+
if entry.rel_path != prev.rel_path or entry.line_num != prev.line_num + 1:
|
|
891
|
+
_flush_block()
|
|
892
|
+
block = []
|
|
893
|
+
block.append(entry)
|
|
894
|
+
|
|
895
|
+
_flush_block()
|
|
896
|
+
return matches
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
async def _grep_impl(
|
|
900
|
+
pattern: str,
|
|
901
|
+
path: str = ".",
|
|
902
|
+
glob: str | None = None,
|
|
903
|
+
context_lines: int = 2,
|
|
904
|
+
max_results: int = _MAX_RESULTS,
|
|
905
|
+
*,
|
|
906
|
+
base_cwd: str | None = None,
|
|
907
|
+
respect_gitignore: bool = True,
|
|
908
|
+
) -> str:
|
|
909
|
+
max_results = min(max_results, _MAX_RESULTS_HARD_LIMIT)
|
|
910
|
+
missing_base = missing_base_cwd_error(path, base_cwd)
|
|
911
|
+
if missing_base is not None:
|
|
912
|
+
return missing_base
|
|
913
|
+
root = resolve_workspace_path(path, base_cwd=base_cwd)
|
|
914
|
+
if not os.path.exists(root):
|
|
915
|
+
return tool_error("path_not_found", f"path not found — {root}", details={"path": path, "resolved_path": root})
|
|
916
|
+
|
|
917
|
+
base_args = [
|
|
918
|
+
"--json",
|
|
919
|
+
"-e",
|
|
920
|
+
pattern,
|
|
921
|
+
"-C",
|
|
922
|
+
str(context_lines),
|
|
923
|
+
*_ignore_args(respect_gitignore),
|
|
924
|
+
]
|
|
925
|
+
|
|
926
|
+
# Determine which encodings to search. For pure-ASCII patterns a single
|
|
927
|
+
# pass suffices since ASCII bytes are identical across all encodings.
|
|
928
|
+
encodings = ["utf-8"] if _is_ascii_only(pattern) else _get_encodings()
|
|
929
|
+
|
|
930
|
+
all_entries: dict[tuple[str, int], GrepEntry] = {}
|
|
931
|
+
all_long_lines: list[LongLine] = []
|
|
932
|
+
seen_long: set[tuple[str, int]] = set()
|
|
933
|
+
oversized: set[tuple[str, int]] = set()
|
|
934
|
+
total_matches = 0
|
|
935
|
+
# Matches too long to show count toward the limit too, so a full search can show fewer.
|
|
936
|
+
limited = False
|
|
937
|
+
errors = _SearchErrors()
|
|
938
|
+
|
|
939
|
+
try:
|
|
940
|
+
async with asyncio.timeout(_TIMEOUT):
|
|
941
|
+
files = [root]
|
|
942
|
+
globbed_directory = bool(glob and glob != "*" and os.path.isdir(root))
|
|
943
|
+
if (
|
|
944
|
+
not globbed_directory
|
|
945
|
+
and respect_gitignore
|
|
946
|
+
and os.path.isdir(root)
|
|
947
|
+
and await _directory_is_gitignored(root)
|
|
948
|
+
):
|
|
949
|
+
base_args.append("--no-ignore-vcs")
|
|
950
|
+
search_cwd = root if globbed_directory and _search_cwd(root) == root else None
|
|
951
|
+
if globbed_directory:
|
|
952
|
+
selected = await _search_files(root, glob, respect_gitignore)
|
|
953
|
+
if isinstance(selected, str):
|
|
954
|
+
return selected
|
|
955
|
+
files = []
|
|
956
|
+
for name in selected:
|
|
957
|
+
if search_cwd is None:
|
|
958
|
+
files.append(name)
|
|
959
|
+
continue
|
|
960
|
+
relative = os.path.relpath(name, root)
|
|
961
|
+
# Even after --, bare '-' means stdin to rg.
|
|
962
|
+
files.append(os.path.join(".", relative) if relative == "-" else relative)
|
|
963
|
+
encoding_args = {encoding: [*base_args, "-E", encoding, "--"] for encoding in encodings}
|
|
964
|
+
if not files:
|
|
965
|
+
# Validate with rg's own regex/flag parser against explicitly empty
|
|
966
|
+
# stdin. Never let an empty candidate set fall back to cwd traversal.
|
|
967
|
+
_, stderr, returncode = await _run_rg([*encoding_args["utf-8"], "-"])
|
|
968
|
+
if returncode not in (0, 1):
|
|
969
|
+
errors.add(stderr, returncode)
|
|
970
|
+
record_process_result(returncode)
|
|
971
|
+
return tool_error(
|
|
972
|
+
"search_process_failed", errors.summary(), details={"path": path, "pattern": pattern}
|
|
973
|
+
)
|
|
974
|
+
return surrogate_safe_text(f"No matches found for /{pattern}/ in {root}")
|
|
975
|
+
rg = _find_rg()
|
|
976
|
+
command = max(([rg, *args] for args in encoding_args.values()), key=_command_size)
|
|
977
|
+
fatal_error = False
|
|
978
|
+
# Never invoke rg with an empty argv file list: it would search cwd/stdin.
|
|
979
|
+
for batch in _file_batches(files, command=command):
|
|
980
|
+
utf8_searched = False
|
|
981
|
+
for encoding, args in tuple(encoding_args.items()):
|
|
982
|
+
parser = _GrepStream(
|
|
983
|
+
root,
|
|
984
|
+
max_results - total_matches - len(oversized),
|
|
985
|
+
seen_matches={key for key, entry in all_entries.items() if entry.marker == ">"} | oversized,
|
|
986
|
+
# Enumerated paths are explicit to rg, which enables binary searching.
|
|
987
|
+
# Filter before counting matches; user-supplied files retain rg's defaults.
|
|
988
|
+
skip_binary=globbed_directory,
|
|
989
|
+
)
|
|
990
|
+
_, stderr, returncode = await _run_rg([*args, *batch], cwd=search_cwd, consume=parser.feed)
|
|
991
|
+
# A full result already says it is limited. The rg stopped for it was killed: its exit code
|
|
992
|
+
# is the kill's, and its stderr names only the files it reached before then.
|
|
993
|
+
if not parser.full and returncode not in (0, 1):
|
|
994
|
+
errors.add(stderr, returncode)
|
|
995
|
+
if returncode == 2 and not parser.saw_output:
|
|
996
|
+
# File-read errors still emit a JSON summary. No stdout
|
|
997
|
+
# means rg failed before searching; repeating cannot help.
|
|
998
|
+
# UTF-8 runs first. Its completed search validates the
|
|
999
|
+
# shared args; only -E changes in subsequent passes.
|
|
1000
|
+
if utf8_searched:
|
|
1001
|
+
del encoding_args[encoding]
|
|
1002
|
+
continue
|
|
1003
|
+
fatal_error = True
|
|
1004
|
+
break
|
|
1005
|
+
if returncode != 2:
|
|
1006
|
+
continue
|
|
1007
|
+
if encoding == "utf-8":
|
|
1008
|
+
utf8_searched = True
|
|
1009
|
+
|
|
1010
|
+
parsed = parser.result
|
|
1011
|
+
limited = limited or parser.full
|
|
1012
|
+
oversized |= parsed.oversized
|
|
1013
|
+
for entry in parsed.entries:
|
|
1014
|
+
key = (entry.rel_path, entry.line_num)
|
|
1015
|
+
previous = all_entries.get(key)
|
|
1016
|
+
if previous is None or (previous.marker != ">" and entry.marker == ">"):
|
|
1017
|
+
all_entries[key] = entry
|
|
1018
|
+
if entry.marker == ">":
|
|
1019
|
+
total_matches += 1
|
|
1020
|
+
oversized.discard(key) # Too long to show under another encoding only.
|
|
1021
|
+
for ll in parsed.long_lines:
|
|
1022
|
+
key = (ll.rel_path, ll.line_num)
|
|
1023
|
+
if key not in seen_long:
|
|
1024
|
+
seen_long.add(key)
|
|
1025
|
+
all_long_lines.append(ll)
|
|
1026
|
+
if limited or total_matches + len(oversized) >= max_results:
|
|
1027
|
+
break
|
|
1028
|
+
if fatal_error or not encoding_args or limited or total_matches + len(oversized) >= max_results:
|
|
1029
|
+
break
|
|
1030
|
+
except NotADirectoryError as exc:
|
|
1031
|
+
# The session directory can vanish between the check above and rg's spawn.
|
|
1032
|
+
missing_base = missing_base_cwd_error(path, base_cwd)
|
|
1033
|
+
if missing_base is not None:
|
|
1034
|
+
return missing_base
|
|
1035
|
+
return tool_error("path_not_found", str(exc), details={"path": path, "resolved_path": root})
|
|
1036
|
+
except FileNotFoundError:
|
|
1037
|
+
return tool_error("ripgrep_not_found", "ripgrep (rg) not found")
|
|
1038
|
+
except TimeoutError:
|
|
1039
|
+
record_process_timeout(_TIMEOUT)
|
|
1040
|
+
return tool_error("search_timeout", f"search timed out after {_TIMEOUT}s", retryable=True)
|
|
1041
|
+
except Exception as e:
|
|
1042
|
+
return tool_error("search_failed", f"search failed — {e}", details={"path": path, "pattern": pattern})
|
|
1043
|
+
|
|
1044
|
+
if not total_matches and not oversized:
|
|
1045
|
+
if errors.exit_code is not None:
|
|
1046
|
+
record_process_result(errors.exit_code)
|
|
1047
|
+
# rg reports a session directory deleted after the check above as its own IO error.
|
|
1048
|
+
missing_base = missing_base_cwd_error(path, base_cwd)
|
|
1049
|
+
if missing_base is not None:
|
|
1050
|
+
return missing_base
|
|
1051
|
+
return tool_error("search_process_failed", errors.summary(), details={"path": path, "pattern": pattern})
|
|
1052
|
+
return surrogate_safe_text(f"No matches found for /{pattern}/ in {root}")
|
|
1053
|
+
|
|
1054
|
+
# Sort by (file, line_number) for consistent display
|
|
1055
|
+
matches = _format_matches(sorted(all_entries.values(), key=lambda e: (e.rel_path, e.line_num)))
|
|
1056
|
+
header = (
|
|
1057
|
+
f"Found {total_matches} match(es) in {root}"
|
|
1058
|
+
if total_matches
|
|
1059
|
+
else f"Found matches for /{pattern}/ in {root}, but every matching line is too long to show"
|
|
1060
|
+
)
|
|
1061
|
+
if limited or total_matches + len(oversized) >= max_results:
|
|
1062
|
+
header += f" (limited to {max_results})"
|
|
1063
|
+
result = "\n\n".join([header, *matches])
|
|
1064
|
+
if all_long_lines:
|
|
1065
|
+
details = ", ".join(f"{ll.rel_path}:{ll.line_num} ({ll.actual_length} chars)" for ll in all_long_lines)
|
|
1066
|
+
result += f"\n\n[Long lines truncated to {_MAX_LINE_DISPLAY_CHARS} chars: {details}]"
|
|
1067
|
+
result += _oversized_note(oversized)
|
|
1068
|
+
if errors.exit_code is not None:
|
|
1069
|
+
record_process_result(errors.exit_code)
|
|
1070
|
+
result += "\n\n" + tool_error(
|
|
1071
|
+
"search_incomplete",
|
|
1072
|
+
f"search results may be incomplete — {errors.summary()}",
|
|
1073
|
+
details={"path": path, "pattern": pattern},
|
|
1074
|
+
)
|
|
1075
|
+
# Keep raw path identities through filtering/deduplication; escape only display text.
|
|
1076
|
+
return surrogate_safe_text(result)
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
@tool(kind=KIND_SEARCH)
|
|
1080
|
+
async def grep(
|
|
1081
|
+
pattern: Annotated[str, "Regex pattern to search for in file contents."],
|
|
1082
|
+
path: Annotated[str, "Directory or file path to search in."] = ".",
|
|
1083
|
+
glob: Annotated[
|
|
1084
|
+
str | None, "Glob pattern to filter files (e.g. '*.py', '**/*.ts'). Omit to search all files."
|
|
1085
|
+
] = None,
|
|
1086
|
+
context_lines: Annotated[int, "Number of context lines before and after each match."] = 2,
|
|
1087
|
+
max_results: Annotated[int, "Maximum number of matches to return."] = _MAX_RESULTS,
|
|
1088
|
+
) -> str:
|
|
1089
|
+
"""Search file contents using a regex pattern. Returns matching lines with context.
|
|
1090
|
+
|
|
1091
|
+
Powered by ripgrep (rg --json -e PATTERN -C CONTEXT_LINES -E ENCODING PATH).
|
|
1092
|
+
The pattern is a Rust-flavor regex (PCRE-like, not Python re). Glob filtering
|
|
1093
|
+
uses gitignore-style rules: patterns without '/' match the filename at any
|
|
1094
|
+
depth (e.g. '*.py' matches 'src/foo.py'), patterns with '/' match against
|
|
1095
|
+
the relative path. max_results is capped at 100 regardless of the value provided.
|
|
1096
|
+
Directory searches respect Git ignore rules and ripgrep hidden-file rules.
|
|
1097
|
+
Explicit file names and directory prefixes can select hidden or ignored paths.
|
|
1098
|
+
Explicit ignored directories bypass Git ignore rules within that directory.
|
|
1099
|
+
"""
|
|
1100
|
+
return await _grep_impl(
|
|
1101
|
+
pattern,
|
|
1102
|
+
path=path,
|
|
1103
|
+
glob=glob,
|
|
1104
|
+
context_lines=context_lines,
|
|
1105
|
+
max_results=max_results,
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
# ---------------------------------------------------------------------------
|
|
1110
|
+
# glob
|
|
1111
|
+
# ---------------------------------------------------------------------------
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
async def _glob_impl(
|
|
1115
|
+
pattern: str,
|
|
1116
|
+
path: str = ".",
|
|
1117
|
+
max_results: int = _MAX_RESULTS,
|
|
1118
|
+
*,
|
|
1119
|
+
base_cwd: str | None = None,
|
|
1120
|
+
respect_gitignore: bool = True,
|
|
1121
|
+
) -> str:
|
|
1122
|
+
max_results = min(max_results, _MAX_RESULTS_HARD_LIMIT)
|
|
1123
|
+
missing_base = missing_base_cwd_error(path, base_cwd)
|
|
1124
|
+
if missing_base is not None:
|
|
1125
|
+
return missing_base
|
|
1126
|
+
root = resolve_workspace_path(path, base_cwd=base_cwd)
|
|
1127
|
+
if not os.path.exists(root):
|
|
1128
|
+
return tool_error("path_not_found", f"path not found — {root}", details={"path": path, "resolved_path": root})
|
|
1129
|
+
|
|
1130
|
+
try:
|
|
1131
|
+
async with asyncio.timeout(_TIMEOUT):
|
|
1132
|
+
files = await _search_files(root, pattern, respect_gitignore)
|
|
1133
|
+
except NotADirectoryError as exc:
|
|
1134
|
+
# The session directory can vanish between the check above and rg's spawn.
|
|
1135
|
+
missing_base = missing_base_cwd_error(path, base_cwd)
|
|
1136
|
+
if missing_base is not None:
|
|
1137
|
+
return missing_base
|
|
1138
|
+
return tool_error("path_not_found", str(exc), details={"path": path, "resolved_path": root})
|
|
1139
|
+
except FileNotFoundError:
|
|
1140
|
+
return tool_error("ripgrep_not_found", "ripgrep (rg) not found")
|
|
1141
|
+
except TimeoutError:
|
|
1142
|
+
record_process_timeout(_TIMEOUT)
|
|
1143
|
+
return tool_error("search_timeout", f"search timed out after {_TIMEOUT}s", retryable=True)
|
|
1144
|
+
except Exception as e:
|
|
1145
|
+
return tool_error("glob_failed", f"glob failed — {e}", details={"path": path, "pattern": pattern})
|
|
1146
|
+
|
|
1147
|
+
if isinstance(files, str):
|
|
1148
|
+
return files
|
|
1149
|
+
if not files:
|
|
1150
|
+
return surrogate_safe_text(f"No files matching '{pattern}' found in {root}")
|
|
1151
|
+
|
|
1152
|
+
# Convert to relative paths and limit results
|
|
1153
|
+
results = []
|
|
1154
|
+
for name in files:
|
|
1155
|
+
rel = _to_posix(os.path.relpath(name, root))
|
|
1156
|
+
results.append(rel)
|
|
1157
|
+
if len(results) >= max_results:
|
|
1158
|
+
break
|
|
1159
|
+
|
|
1160
|
+
header = f"Found {len(results)} file(s) matching '{pattern}' in {root}"
|
|
1161
|
+
if len(results) >= max_results:
|
|
1162
|
+
header += f" (limited to {max_results})"
|
|
1163
|
+
return surrogate_safe_text(header + "\n" + "\n".join(f" {r}" for r in results))
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
@tool(kind=KIND_SEARCH)
|
|
1167
|
+
async def glob(
|
|
1168
|
+
pattern: Annotated[str, "Glob pattern to match file names (e.g. '*.py', 'test_*.py', '**/*.ts')."],
|
|
1169
|
+
path: Annotated[str, "Directory to search in."] = ".",
|
|
1170
|
+
max_results: Annotated[int, "Maximum number of results to return."] = _MAX_RESULTS,
|
|
1171
|
+
) -> str:
|
|
1172
|
+
"""Find files by name pattern. Returns relative paths of matching files.
|
|
1173
|
+
|
|
1174
|
+
Powered by ripgrep (rg --files --glob PATTERN PATH). The pattern uses
|
|
1175
|
+
gitignore-style glob rules: patterns without '/' match the filename at any
|
|
1176
|
+
depth (e.g. '*.py' matches 'src/foo.py'), patterns with '/' match against
|
|
1177
|
+
the relative path. Searches recursively through all subdirectories.
|
|
1178
|
+
max_results is capped at 100 regardless of the value provided.
|
|
1179
|
+
Discovery respects Git ignore rules and ripgrep hidden-file rules.
|
|
1180
|
+
Explicit file names and directory prefixes can select hidden or ignored paths.
|
|
1181
|
+
Explicit ignored directories bypass Git ignore rules within that directory.
|
|
1182
|
+
"""
|
|
1183
|
+
return await _glob_impl(pattern, path=path, max_results=max_results)
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
class SearchTools:
|
|
1187
|
+
"""Runtime-bound search tools that resolve relative paths per session."""
|
|
1188
|
+
|
|
1189
|
+
def __init__(self, runtime: SessionEnvironment, *, respect_gitignore: bool = True) -> None:
|
|
1190
|
+
self._runtime = runtime
|
|
1191
|
+
self._respect_gitignore = respect_gitignore
|
|
1192
|
+
|
|
1193
|
+
def tools(self) -> list:
|
|
1194
|
+
"""Return search tools with descriptions reflecting this runtime's ignore policy."""
|
|
1195
|
+
if self._respect_gitignore:
|
|
1196
|
+
policy = (
|
|
1197
|
+
"Current setting: Respect .gitignore is enabled. Directory searches respect Git ignore rules.\n"
|
|
1198
|
+
"Explicit ignored directories bypass Git ignore rules within that directory."
|
|
1199
|
+
)
|
|
1200
|
+
else:
|
|
1201
|
+
policy = (
|
|
1202
|
+
"Current setting: Respect .gitignore is disabled. Directory searches do not respect Git ignore rules "
|
|
1203
|
+
"and can include Git-ignored files and directories."
|
|
1204
|
+
)
|
|
1205
|
+
guidance = (
|
|
1206
|
+
f"{policy}\n"
|
|
1207
|
+
"Ripgrep hidden-file rules and .ignore/.rgignore rules still apply.\n"
|
|
1208
|
+
"Explicit file names and directory prefixes can select hidden or ignored paths."
|
|
1209
|
+
)
|
|
1210
|
+
tools = [self.grep, self.glob]
|
|
1211
|
+
for search_tool in tools:
|
|
1212
|
+
search_tool.description = f"{search_tool.description.rstrip()}\n\n{guidance}"
|
|
1213
|
+
return tools
|
|
1214
|
+
|
|
1215
|
+
@tool(kind=KIND_SEARCH)
|
|
1216
|
+
async def grep(
|
|
1217
|
+
self,
|
|
1218
|
+
pattern: Annotated[str, "Regex pattern to search for in file contents."],
|
|
1219
|
+
path: Annotated[str, "Directory or file path to search in."] = ".",
|
|
1220
|
+
glob: Annotated[
|
|
1221
|
+
str | None, "Glob pattern to filter files (e.g. '*.py', '**/*.ts'). Omit to search all files."
|
|
1222
|
+
] = None,
|
|
1223
|
+
context_lines: Annotated[int, "Number of context lines before and after each match."] = 2,
|
|
1224
|
+
max_results: Annotated[int, "Maximum number of matches to return."] = _MAX_RESULTS,
|
|
1225
|
+
) -> str:
|
|
1226
|
+
"""Search file contents using a regex pattern. Returns matching lines with context.
|
|
1227
|
+
|
|
1228
|
+
Powered by ripgrep (rg --json -e PATTERN -C CONTEXT_LINES -E ENCODING PATH).
|
|
1229
|
+
The pattern is a Rust-flavor regex (PCRE-like, not Python re). Glob filtering
|
|
1230
|
+
uses gitignore-style rules: patterns without '/' match the filename at any
|
|
1231
|
+
depth (e.g. '*.py' matches 'src/foo.py'), patterns with '/' match against
|
|
1232
|
+
the relative path. max_results is capped at 100 regardless of the value provided.
|
|
1233
|
+
"""
|
|
1234
|
+
return await _grep_impl(
|
|
1235
|
+
pattern,
|
|
1236
|
+
path=path,
|
|
1237
|
+
glob=glob,
|
|
1238
|
+
context_lines=context_lines,
|
|
1239
|
+
max_results=max_results,
|
|
1240
|
+
base_cwd=self._runtime.cwd,
|
|
1241
|
+
respect_gitignore=self._respect_gitignore,
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
@tool(kind=KIND_SEARCH)
|
|
1245
|
+
async def glob(
|
|
1246
|
+
self,
|
|
1247
|
+
pattern: Annotated[str, "Glob pattern to match file names (e.g. '*.py', 'test_*.py', '**/*.ts')."],
|
|
1248
|
+
path: Annotated[str, "Directory to search in."] = ".",
|
|
1249
|
+
max_results: Annotated[int, "Maximum number of results to return."] = _MAX_RESULTS,
|
|
1250
|
+
) -> str:
|
|
1251
|
+
"""Find files by name pattern. Returns relative paths of matching files.
|
|
1252
|
+
|
|
1253
|
+
Powered by ripgrep (rg --files --glob PATTERN PATH). The pattern uses
|
|
1254
|
+
gitignore-style glob rules: patterns without '/' match the filename at any
|
|
1255
|
+
depth (e.g. '*.py' matches 'src/foo.py'), patterns with '/' match against
|
|
1256
|
+
the relative path. Searches recursively through all subdirectories.
|
|
1257
|
+
max_results is capped at 100 regardless of the value provided.
|
|
1258
|
+
"""
|
|
1259
|
+
return await _glob_impl(
|
|
1260
|
+
pattern,
|
|
1261
|
+
path=path,
|
|
1262
|
+
max_results=max_results,
|
|
1263
|
+
base_cwd=self._runtime.cwd,
|
|
1264
|
+
respect_gitignore=self._respect_gitignore,
|
|
1265
|
+
)
|