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,1071 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Workspace tools — read_file, write_file, edit_file.
|
|
4
|
+
|
|
5
|
+
Approval for ``write_file`` and ``edit_file`` is handled by
|
|
6
|
+
``ApprovalMiddleware`` (a ``FunctionMiddleware``); the tool layer itself never
|
|
7
|
+
pauses for approval.
|
|
8
|
+
|
|
9
|
+
Shell execution tools are in shell.py as instance tools.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import secrets
|
|
16
|
+
import stat
|
|
17
|
+
import tempfile
|
|
18
|
+
import threading
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import TYPE_CHECKING, Annotated
|
|
21
|
+
|
|
22
|
+
from chrys.foundation.platform import get_platform
|
|
23
|
+
from chrys.foundation.platform.files import replace_with_retry, surrogate_safe_text
|
|
24
|
+
from chrys.foundation.platform.paths import resolve_existing_path, resolve_workspace_path
|
|
25
|
+
from chrys.foundation.text.images import ImageProcessingError, load_image_file
|
|
26
|
+
from chrys.foundation.text.lines import normalize_line_endings, split_lines
|
|
27
|
+
from chrys.kernel import Content
|
|
28
|
+
from chrys.service.tools.approval_targets import ApprovedTargets, approved_write_path, file_write_target
|
|
29
|
+
from chrys.service.tools.kinds import KIND_FILESYSTEM_READ, KIND_FILESYSTEM_WRITE, tool
|
|
30
|
+
from chrys.service.tools.result_metadata import tool_error
|
|
31
|
+
from chrys.service.tools.session_artifacts import (
|
|
32
|
+
is_document_artifact_handle,
|
|
33
|
+
resolve_document_image_artifact_handle,
|
|
34
|
+
resolve_document_markdown_artifact_handle,
|
|
35
|
+
resolve_tool_session_dir,
|
|
36
|
+
)
|
|
37
|
+
from chrys.service.tools.workspace_paths import missing_base_cwd_error
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
from chrys.foundation.models.session_env import SessionEnvironment
|
|
43
|
+
|
|
44
|
+
_DEFAULT_MAX_TOKENS = 5000
|
|
45
|
+
_MAX_LINE_DISPLAY_CHARS = 2048
|
|
46
|
+
_NEW_FILE_MODE = 0o644
|
|
47
|
+
_NEW_EXECUTABLE_FILE_MODE = 0o755
|
|
48
|
+
_ATOMIC_TEMP_CREATE_ATTEMPTS = 100
|
|
49
|
+
_SHELL_SCRIPT_EXTENSIONS = frozenset({".sh", ".bash", ".zsh", ".fish", ".ksh", ".csh", ".tcsh", ".command"})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(slots=True)
|
|
53
|
+
class FileToolPreviewError:
|
|
54
|
+
"""A deterministic file-tool failure discovered before execution."""
|
|
55
|
+
|
|
56
|
+
kind: str
|
|
57
|
+
message: str
|
|
58
|
+
resolved_path: str = ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(slots=True)
|
|
62
|
+
class WriteFilePlan:
|
|
63
|
+
"""Prepared write_file operation with final in-memory content."""
|
|
64
|
+
|
|
65
|
+
resolved_path: str
|
|
66
|
+
write_content: str
|
|
67
|
+
display_content: str
|
|
68
|
+
before_display_content: str = ""
|
|
69
|
+
overwrites_existing: bool = False
|
|
70
|
+
content_only_reason: str = ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(slots=True)
|
|
74
|
+
class EditFilePlan:
|
|
75
|
+
"""Prepared edit_file operation with before/after preview and write data."""
|
|
76
|
+
|
|
77
|
+
resolved_path: str
|
|
78
|
+
encoding: str
|
|
79
|
+
decode_errors: str
|
|
80
|
+
before_text: str
|
|
81
|
+
after_text: str
|
|
82
|
+
write_content: str
|
|
83
|
+
replacements: int
|
|
84
|
+
snippet: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _new_posix_file_mode(path: str, content: str) -> int:
|
|
88
|
+
extension = os.path.splitext(path)[1].lower()
|
|
89
|
+
if content.startswith("#!") or extension in _SHELL_SCRIPT_EXTENSIONS:
|
|
90
|
+
return _NEW_EXECUTABLE_FILE_MODE
|
|
91
|
+
return _NEW_FILE_MODE
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _posix_write_mode(path: str, content: str) -> tuple[int, bool]:
|
|
95
|
+
"""Return requested rwx mode and whether it must exactly match an existing file."""
|
|
96
|
+
try:
|
|
97
|
+
# Preserve existing user/group/other rwx bits. Special bits are
|
|
98
|
+
# deliberately cleared when replacing file contents.
|
|
99
|
+
return stat.S_IMODE(os.stat(path).st_mode) & 0o777, True
|
|
100
|
+
except OSError:
|
|
101
|
+
# A dangling, looping, or inaccessible symlink was replaceable before
|
|
102
|
+
# mode preservation was added. Treat every stat failure as a new-file
|
|
103
|
+
# replacement and let the actual temp creation/replace report any real
|
|
104
|
+
# parent-directory failure.
|
|
105
|
+
return _new_posix_file_mode(path, content), False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _open_posix_atomic_temp(parent: str, mode: int) -> tuple[int, str]:
|
|
109
|
+
"""Securely create a same-directory temp file with *mode* filtered by umask."""
|
|
110
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
111
|
+
for _ in range(_ATOMIC_TEMP_CREATE_ATTEMPTS):
|
|
112
|
+
tmp_path = os.path.join(parent, f".chrys-{secrets.token_hex(12)}.tmp")
|
|
113
|
+
try:
|
|
114
|
+
# Unlike tempfile.mkstemp's fixed 0600, os.open applies the
|
|
115
|
+
# process umask to the requested new-file mode without reading or
|
|
116
|
+
# mutating that process-global setting.
|
|
117
|
+
return os.open(tmp_path, flags, mode), tmp_path
|
|
118
|
+
except FileExistsError:
|
|
119
|
+
continue
|
|
120
|
+
raise FileExistsError("failed to allocate a unique atomic-write temporary file")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _atomic_write(path: str, content: str, encoding: str = "utf-8", errors: str = "strict") -> None:
|
|
124
|
+
"""Write *content* to *path* atomically via temp-file + rename.
|
|
125
|
+
|
|
126
|
+
Writes to a temporary file in the same directory (same filesystem) and
|
|
127
|
+
then calls ``os.replace()`` which is atomic on POSIX and Windows/NTFS.
|
|
128
|
+
If the write or flush fails, the temp file is removed and the original
|
|
129
|
+
file is left untouched.
|
|
130
|
+
"""
|
|
131
|
+
path = approved_write_path(path)
|
|
132
|
+
parent = os.path.dirname(path) or "."
|
|
133
|
+
platform = get_platform()
|
|
134
|
+
target_mode: int | None = None
|
|
135
|
+
preserve_existing_mode = False
|
|
136
|
+
if not platform.is_windows:
|
|
137
|
+
target_mode, preserve_existing_mode = _posix_write_mode(path, content)
|
|
138
|
+
fd = None
|
|
139
|
+
tmp_path = ""
|
|
140
|
+
try:
|
|
141
|
+
if platform.is_windows:
|
|
142
|
+
fd, tmp_path = tempfile.mkstemp(dir=parent, suffix=".tmp")
|
|
143
|
+
else:
|
|
144
|
+
if target_mode is None:
|
|
145
|
+
raise RuntimeError("A POSIX atomic write requires a target file mode.")
|
|
146
|
+
fd, tmp_path = _open_posix_atomic_temp(parent, target_mode)
|
|
147
|
+
with os.fdopen(fd, "w", encoding=encoding, errors=errors, newline="") as f:
|
|
148
|
+
fd = None # os.fdopen takes ownership of the fd
|
|
149
|
+
f.write(content)
|
|
150
|
+
f.flush()
|
|
151
|
+
if preserve_existing_mode:
|
|
152
|
+
# Existing modes override umask and must be restored exactly.
|
|
153
|
+
# This must run while fdopen still owns an open descriptor.
|
|
154
|
+
if target_mode is None:
|
|
155
|
+
raise RuntimeError("A POSIX atomic write requires a target file mode.")
|
|
156
|
+
os.fchmod(f.fileno(), target_mode)
|
|
157
|
+
os.fsync(f.fileno())
|
|
158
|
+
replace_with_retry(tmp_path, path)
|
|
159
|
+
except BaseException:
|
|
160
|
+
# Clean up the temp file on any failure
|
|
161
|
+
if fd is not None:
|
|
162
|
+
os.close(fd)
|
|
163
|
+
if tmp_path and os.path.exists(tmp_path):
|
|
164
|
+
os.unlink(tmp_path)
|
|
165
|
+
raise
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _detect_line_ending(raw: bytes, path: str = "") -> str:
|
|
169
|
+
"""Detect the predominant line ending in raw file bytes.
|
|
170
|
+
|
|
171
|
+
Falls back to ``_default_eol_for_new_file(path)`` when the file contains
|
|
172
|
+
no line endings (e.g. single-line or empty files).
|
|
173
|
+
"""
|
|
174
|
+
crlf = raw.count(b"\r\n")
|
|
175
|
+
cr = raw.count(b"\r") - crlf # standalone \r only
|
|
176
|
+
lf = raw.count(b"\n") - crlf # standalone \n only
|
|
177
|
+
if crlf >= lf and crlf >= cr and crlf > 0:
|
|
178
|
+
return "\r\n"
|
|
179
|
+
if cr > lf and cr > 0:
|
|
180
|
+
return "\r"
|
|
181
|
+
if lf > 0:
|
|
182
|
+
return "\n"
|
|
183
|
+
# No line endings found — use the smart default for this file type / platform
|
|
184
|
+
return _default_eol_for_new_file(path)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# File extensions that MUST use a specific line ending regardless of platform.
|
|
188
|
+
_FORCE_LF_EXTENSIONS = _SHELL_SCRIPT_EXTENSIONS | frozenset(
|
|
189
|
+
{
|
|
190
|
+
".py",
|
|
191
|
+
".rb",
|
|
192
|
+
".pl",
|
|
193
|
+
".php", # interpreters with shebang
|
|
194
|
+
".yml",
|
|
195
|
+
".yaml", # YAML is LF-only by spec
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
_FORCE_CRLF_EXTENSIONS = frozenset(
|
|
199
|
+
{
|
|
200
|
+
".bat",
|
|
201
|
+
".cmd", # Windows batch
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _default_eol_for_new_file(path: str) -> str:
|
|
207
|
+
"""Choose the line ending for a newly created file.
|
|
208
|
+
|
|
209
|
+
Priority:
|
|
210
|
+
1. File extension overrides (e.g. .sh → LF, .bat → CRLF)
|
|
211
|
+
2. Platform default (CRLF on Windows, LF elsewhere)
|
|
212
|
+
"""
|
|
213
|
+
ext = os.path.splitext(path)[1].lower()
|
|
214
|
+
if ext in _FORCE_LF_EXTENSIONS:
|
|
215
|
+
return "\n"
|
|
216
|
+
if ext in _FORCE_CRLF_EXTENSIONS:
|
|
217
|
+
return "\r\n"
|
|
218
|
+
return os.linesep
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _decode_write_preview_before(raw: bytes) -> tuple[str, str]:
|
|
222
|
+
"""Return display-safe existing content for overwrite approval previews."""
|
|
223
|
+
from chrys.foundation.text.encoding import EncodingDetector
|
|
224
|
+
|
|
225
|
+
if EncodingDetector.looks_binary(raw):
|
|
226
|
+
return "", f"content-only; existing binary file ({len(raw)} bytes) not shown"
|
|
227
|
+
|
|
228
|
+
detector = EncodingDetector()
|
|
229
|
+
detection = detector.detect_from_bytes(raw)
|
|
230
|
+
if detection.is_binary:
|
|
231
|
+
return "", f"content-only; existing binary file ({len(raw)} bytes) not shown"
|
|
232
|
+
|
|
233
|
+
encoding = detection.encoding or "utf-8"
|
|
234
|
+
normalized_encoding = encoding.lower().replace("_", "-")
|
|
235
|
+
decode_errors = "replace" if normalized_encoding.startswith(("utf-16", "utf-32")) else "surrogateescape"
|
|
236
|
+
try:
|
|
237
|
+
before_text = raw.decode(encoding, errors=decode_errors)
|
|
238
|
+
except LookupError, UnicodeDecodeError:
|
|
239
|
+
return "", "content-only; existing file could not be decoded"
|
|
240
|
+
return _replace_surrogate_escapes(normalize_line_endings(before_text)), ""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def plan_write_file(
|
|
244
|
+
path: str,
|
|
245
|
+
content: str,
|
|
246
|
+
overwrite: bool = False,
|
|
247
|
+
*,
|
|
248
|
+
base_cwd: str | None = None,
|
|
249
|
+
) -> WriteFilePlan | FileToolPreviewError:
|
|
250
|
+
"""Prepare a ``write_file`` operation without touching the destination."""
|
|
251
|
+
if is_document_artifact_handle(path):
|
|
252
|
+
return FileToolPreviewError(
|
|
253
|
+
kind="read_only_handle",
|
|
254
|
+
message=tool_error(
|
|
255
|
+
"session_artifact_handle_is_read_only",
|
|
256
|
+
"session document handles are read-only; use a regular filesystem path for write_file.",
|
|
257
|
+
details={"path": path},
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
try:
|
|
261
|
+
resolved = resolve_workspace_path(path, base_cwd=base_cwd)
|
|
262
|
+
if os.path.isdir(resolved):
|
|
263
|
+
return FileToolPreviewError(
|
|
264
|
+
kind="directory",
|
|
265
|
+
message=tool_error(
|
|
266
|
+
"path_is_directory",
|
|
267
|
+
f"path is a directory, not a file — {resolved}",
|
|
268
|
+
details={"path": path, "resolved_path": resolved},
|
|
269
|
+
),
|
|
270
|
+
resolved_path=resolved,
|
|
271
|
+
)
|
|
272
|
+
if os.path.exists(resolved) and not overwrite:
|
|
273
|
+
return FileToolPreviewError(
|
|
274
|
+
kind="exists",
|
|
275
|
+
message=tool_error(
|
|
276
|
+
"file_exists",
|
|
277
|
+
f"file already exists — {resolved}. Set overwrite=true to replace it.",
|
|
278
|
+
details={"path": path, "resolved_path": resolved},
|
|
279
|
+
),
|
|
280
|
+
resolved_path=resolved,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
before_display_content = ""
|
|
284
|
+
content_only_reason = ""
|
|
285
|
+
overwrites_existing = overwrite and os.path.isfile(resolved)
|
|
286
|
+
# Determine line ending: preserve existing file's style, or pick a smart default.
|
|
287
|
+
if overwrites_existing:
|
|
288
|
+
with open(resolved, "rb") as f:
|
|
289
|
+
raw = f.read()
|
|
290
|
+
eol = _detect_line_ending(raw, resolved)
|
|
291
|
+
before_display_content, content_only_reason = _decode_write_preview_before(raw)
|
|
292
|
+
else:
|
|
293
|
+
eol = _default_eol_for_new_file(resolved)
|
|
294
|
+
|
|
295
|
+
write_content = normalize_line_endings(content)
|
|
296
|
+
if eol != "\n":
|
|
297
|
+
write_content = write_content.replace("\n", eol)
|
|
298
|
+
return WriteFilePlan(
|
|
299
|
+
resolved_path=resolved,
|
|
300
|
+
write_content=write_content,
|
|
301
|
+
display_content=_replace_surrogate_escapes(write_content),
|
|
302
|
+
before_display_content=before_display_content,
|
|
303
|
+
overwrites_existing=overwrites_existing,
|
|
304
|
+
content_only_reason=content_only_reason,
|
|
305
|
+
)
|
|
306
|
+
except Exception as e:
|
|
307
|
+
return FileToolPreviewError(
|
|
308
|
+
kind="prepare_failed",
|
|
309
|
+
message=tool_error("write_failed", f"failed to write file — {e}", details={"path": path}),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _read_file_impl(
|
|
314
|
+
path: str,
|
|
315
|
+
max_tokens: int = _DEFAULT_MAX_TOKENS,
|
|
316
|
+
line_range: list[int] | None = None,
|
|
317
|
+
*,
|
|
318
|
+
base_cwd: str | None = None,
|
|
319
|
+
session_dir: Path | None = None,
|
|
320
|
+
) -> str:
|
|
321
|
+
from pathlib import Path
|
|
322
|
+
|
|
323
|
+
from chrys.foundation.text.encoding import EncodingDetector
|
|
324
|
+
from chrys.foundation.text.tokenizer import MixedLanguageTokenizer
|
|
325
|
+
|
|
326
|
+
missing_base = missing_base_cwd_error(path, base_cwd)
|
|
327
|
+
if missing_base is not None:
|
|
328
|
+
return missing_base
|
|
329
|
+
|
|
330
|
+
tokenizer = MixedLanguageTokenizer()
|
|
331
|
+
detector = EncodingDetector()
|
|
332
|
+
effective_path = path
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
try:
|
|
336
|
+
artifact_path = resolve_document_markdown_artifact_handle(path, session_dir)
|
|
337
|
+
except ValueError as e:
|
|
338
|
+
return tool_error(
|
|
339
|
+
"invalid_session_artifact_handle",
|
|
340
|
+
f"invalid session artifact handle — {surrogate_safe_text(str(e))}",
|
|
341
|
+
details={"path": path},
|
|
342
|
+
)
|
|
343
|
+
if artifact_path is None:
|
|
344
|
+
resolved = resolve_existing_path(effective_path, base_cwd=base_cwd)
|
|
345
|
+
else:
|
|
346
|
+
effective_path = artifact_path
|
|
347
|
+
# Handles name one exact session artifact. Do not feed them back
|
|
348
|
+
# into the user-path convenience resolver: its Unicode/whitespace
|
|
349
|
+
# fallbacks may select a different sibling than the handle names.
|
|
350
|
+
resolved = artifact_path if os.path.exists(artifact_path) else None
|
|
351
|
+
if resolved is None:
|
|
352
|
+
resolved = resolve_workspace_path(effective_path, base_cwd=base_cwd)
|
|
353
|
+
return tool_error(
|
|
354
|
+
"file_not_found",
|
|
355
|
+
f"file not found — {surrogate_safe_text(resolved)}",
|
|
356
|
+
details={"path": path, "resolved_path": resolved},
|
|
357
|
+
)
|
|
358
|
+
display_resolved = surrogate_safe_text(resolved)
|
|
359
|
+
detection = detector.detect_file(Path(resolved))
|
|
360
|
+
if detection.is_binary:
|
|
361
|
+
return tool_error(
|
|
362
|
+
"binary_file",
|
|
363
|
+
f"file appears to be binary — {display_resolved}",
|
|
364
|
+
details={"path": path, "resolved_path": resolved},
|
|
365
|
+
)
|
|
366
|
+
encoding = detection.encoding or "utf-8"
|
|
367
|
+
with open(resolved, encoding=encoding, errors="replace") as f:
|
|
368
|
+
content = f.read()
|
|
369
|
+
|
|
370
|
+
lines = split_lines(content)
|
|
371
|
+
total_lines = len(lines)
|
|
372
|
+
total_chars = len(content)
|
|
373
|
+
|
|
374
|
+
# Validate and apply line_range
|
|
375
|
+
offset = 0 # 0-indexed offset into `lines`
|
|
376
|
+
if line_range is not None:
|
|
377
|
+
if len(line_range) != 2:
|
|
378
|
+
return tool_error(
|
|
379
|
+
"invalid_line_range",
|
|
380
|
+
"line_range must be a list of exactly 2 integers [start, end].",
|
|
381
|
+
details={"line_range": line_range},
|
|
382
|
+
)
|
|
383
|
+
start, end = line_range
|
|
384
|
+
if start < 1:
|
|
385
|
+
return tool_error(
|
|
386
|
+
"invalid_line_range",
|
|
387
|
+
"line_range start must be >= 1.",
|
|
388
|
+
details={"line_range": line_range},
|
|
389
|
+
)
|
|
390
|
+
if end != -1 and end < start:
|
|
391
|
+
return tool_error(
|
|
392
|
+
"invalid_line_range",
|
|
393
|
+
"line_range end must be >= start or -1 for EOF.",
|
|
394
|
+
details={"line_range": line_range},
|
|
395
|
+
)
|
|
396
|
+
if start > total_lines:
|
|
397
|
+
return tool_error(
|
|
398
|
+
"invalid_line_range",
|
|
399
|
+
f"line_range start ({start}) is beyond the end of the file ({total_lines} lines).",
|
|
400
|
+
details={"line_range": line_range, "total_lines": total_lines},
|
|
401
|
+
)
|
|
402
|
+
offset = start - 1
|
|
403
|
+
end_idx = total_lines if end == -1 else min(end, total_lines)
|
|
404
|
+
lines = lines[offset:end_idx]
|
|
405
|
+
|
|
406
|
+
header = f"File: {display_resolved} ({total_lines} lines, {total_chars} chars)\n"
|
|
407
|
+
header_tokens = tokenizer.count_tokens(header)
|
|
408
|
+
remaining = max_tokens - header_tokens
|
|
409
|
+
|
|
410
|
+
output_lines: list[str] = []
|
|
411
|
+
truncated = False
|
|
412
|
+
last_line_num = 0
|
|
413
|
+
long_lines: list[tuple[int, int]] = []
|
|
414
|
+
|
|
415
|
+
for i, line in enumerate(lines):
|
|
416
|
+
line_num = offset + i + 1
|
|
417
|
+
if line_range is None and len(line) > _MAX_LINE_DISPLAY_CHARS:
|
|
418
|
+
long_lines.append((line_num, len(line)))
|
|
419
|
+
line = line[:_MAX_LINE_DISPLAY_CHARS] + "... [truncated]"
|
|
420
|
+
formatted = f"{line_num}|{line}\n"
|
|
421
|
+
line_tokens = tokenizer.count_tokens(formatted)
|
|
422
|
+
if line_tokens > remaining:
|
|
423
|
+
# Include a partial line instead of dropping it entirely
|
|
424
|
+
if remaining > 0:
|
|
425
|
+
fraction = remaining / line_tokens
|
|
426
|
+
cut = max(int(len(line) * fraction), 1)
|
|
427
|
+
output_lines.append(f"{line_num}|{line[:cut]}... [truncated]\n")
|
|
428
|
+
# Record for targeted reads so the LLM knows the actual length.
|
|
429
|
+
# Without line_range the char-level truncation (above) or the
|
|
430
|
+
# standard "[Truncated at line N]" message already covers it.
|
|
431
|
+
if line_range is not None:
|
|
432
|
+
long_lines.append((line_num, len(line)))
|
|
433
|
+
last_line_num = line_num
|
|
434
|
+
truncated = True
|
|
435
|
+
if last_line_num == 0:
|
|
436
|
+
last_line_num = line_num - 1 if i > 0 else offset
|
|
437
|
+
break
|
|
438
|
+
remaining -= line_tokens
|
|
439
|
+
output_lines.append(formatted)
|
|
440
|
+
last_line_num = line_num
|
|
441
|
+
|
|
442
|
+
result = header + "".join(output_lines)
|
|
443
|
+
|
|
444
|
+
if long_lines:
|
|
445
|
+
details = ", ".join(f"line {ln} ({length} chars)" for ln, length in long_lines)
|
|
446
|
+
result += (
|
|
447
|
+
f"\n[Long lines truncated to {_MAX_LINE_DISPLAY_CHARS} chars: {details}."
|
|
448
|
+
" Use line_range to read specific lines in full.]"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
if truncated:
|
|
452
|
+
next_line = last_line_num + 1
|
|
453
|
+
result += (
|
|
454
|
+
f"\n[Truncated at line {last_line_num}, file has {total_lines} lines. "
|
|
455
|
+
f"Content exceeds max_tokens={max_tokens}. "
|
|
456
|
+
f"Use line_range=[{next_line}, -1] to continue reading.]"
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
return result
|
|
460
|
+
except FileNotFoundError:
|
|
461
|
+
resolved = resolve_workspace_path(effective_path, base_cwd=base_cwd)
|
|
462
|
+
return tool_error(
|
|
463
|
+
"file_not_found",
|
|
464
|
+
f"file not found — {surrogate_safe_text(resolved)}",
|
|
465
|
+
details={"path": path, "resolved_path": resolved},
|
|
466
|
+
)
|
|
467
|
+
except IsADirectoryError:
|
|
468
|
+
resolved = resolve_workspace_path(effective_path, base_cwd=base_cwd)
|
|
469
|
+
return tool_error(
|
|
470
|
+
"path_is_directory",
|
|
471
|
+
f"path is a directory, not a file — {surrogate_safe_text(resolved)}",
|
|
472
|
+
details={"path": path, "resolved_path": resolved},
|
|
473
|
+
)
|
|
474
|
+
except Exception as e:
|
|
475
|
+
return tool_error("read_failed", f"failed to read file — {surrogate_safe_text(str(e))}", details={"path": path})
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
@tool(kind=KIND_FILESYSTEM_READ)
|
|
479
|
+
def read_file(
|
|
480
|
+
path: Annotated[str, "Absolute or relative path to the file to read."],
|
|
481
|
+
max_tokens: Annotated[
|
|
482
|
+
int,
|
|
483
|
+
"Token budget for the returned content. Large files are truncated to fit.",
|
|
484
|
+
] = _DEFAULT_MAX_TOKENS,
|
|
485
|
+
line_range: Annotated[
|
|
486
|
+
list[int] | None,
|
|
487
|
+
"Optional [start, end] 1-indexed line range. Use -1 for end to read to EOF.",
|
|
488
|
+
] = None,
|
|
489
|
+
) -> str:
|
|
490
|
+
"""Read the contents of a text file and return them with line numbers."""
|
|
491
|
+
return _read_file_impl(path, max_tokens=max_tokens, line_range=line_range)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _view_image_impl(
|
|
495
|
+
path: str,
|
|
496
|
+
*,
|
|
497
|
+
base_cwd: str | None = None,
|
|
498
|
+
session_dir: Path | None = None,
|
|
499
|
+
) -> list[Content]:
|
|
500
|
+
from pathlib import Path
|
|
501
|
+
|
|
502
|
+
missing_base = missing_base_cwd_error(path, base_cwd)
|
|
503
|
+
if missing_base is not None:
|
|
504
|
+
return [Content.from_text(missing_base)]
|
|
505
|
+
effective_path = path
|
|
506
|
+
try:
|
|
507
|
+
try:
|
|
508
|
+
artifact_path = resolve_document_image_artifact_handle(path, session_dir)
|
|
509
|
+
except ValueError as exc:
|
|
510
|
+
return [
|
|
511
|
+
Content.from_text(
|
|
512
|
+
tool_error(
|
|
513
|
+
"invalid_session_artifact_handle",
|
|
514
|
+
f"invalid session artifact handle — {surrogate_safe_text(str(exc))}",
|
|
515
|
+
details={"path": path},
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
]
|
|
519
|
+
if artifact_path is None:
|
|
520
|
+
resolved = resolve_existing_path(effective_path, base_cwd=base_cwd)
|
|
521
|
+
else:
|
|
522
|
+
effective_path = artifact_path
|
|
523
|
+
resolved = artifact_path if os.path.exists(artifact_path) else None
|
|
524
|
+
if resolved is None:
|
|
525
|
+
resolved = resolve_workspace_path(effective_path, base_cwd=base_cwd)
|
|
526
|
+
return [
|
|
527
|
+
Content.from_text(
|
|
528
|
+
tool_error(
|
|
529
|
+
"file_not_found",
|
|
530
|
+
f"file not found — {surrogate_safe_text(resolved)}",
|
|
531
|
+
details={"path": path, "resolved_path": resolved},
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
]
|
|
535
|
+
source_path = Path(resolved)
|
|
536
|
+
if source_path.is_dir():
|
|
537
|
+
return [
|
|
538
|
+
Content.from_text(
|
|
539
|
+
tool_error(
|
|
540
|
+
"path_is_directory",
|
|
541
|
+
f"path is a directory, not a file — {surrogate_safe_text(resolved)}",
|
|
542
|
+
details={"path": path, "resolved_path": resolved},
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
]
|
|
546
|
+
|
|
547
|
+
loaded = load_image_file(source_path)
|
|
548
|
+
metadata = {
|
|
549
|
+
"width": loaded.width,
|
|
550
|
+
"height": loaded.height,
|
|
551
|
+
"media_type": loaded.media_type,
|
|
552
|
+
"size": loaded.size,
|
|
553
|
+
"source_path": surrogate_safe_text(resolved),
|
|
554
|
+
}
|
|
555
|
+
return [Content.from_data(data=loaded.data, media_type=loaded.media_type, additional_properties=metadata)]
|
|
556
|
+
except ImageProcessingError as exc:
|
|
557
|
+
return [
|
|
558
|
+
Content.from_text(tool_error("view_image_failed", f"failed to view image — {exc}", details={"path": path}))
|
|
559
|
+
]
|
|
560
|
+
except Exception as exc:
|
|
561
|
+
return [
|
|
562
|
+
Content.from_text(tool_error("view_image_failed", f"failed to view image — {exc}", details={"path": path}))
|
|
563
|
+
]
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@tool(kind=KIND_FILESYSTEM_READ)
|
|
567
|
+
def view_image(
|
|
568
|
+
path: Annotated[str, "Absolute or relative path to the image file to inspect."],
|
|
569
|
+
) -> list[Content]:
|
|
570
|
+
"""Read an image file and return it as model-visible image content.
|
|
571
|
+
|
|
572
|
+
Images the user attached to a message are already visible to you, at the
|
|
573
|
+
same size this tool returns; call it for one only when it is not in the
|
|
574
|
+
conversation.
|
|
575
|
+
"""
|
|
576
|
+
return _view_image_impl(path)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
_FS_WRITE_LOCKS: dict[str, threading.Lock] = {}
|
|
580
|
+
_FS_WRITE_LOCKS_GUARD = threading.Lock()
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _fs_write_lock(path: str, base_cwd: str | None) -> threading.Lock:
|
|
584
|
+
"""Return the lock that serializes write/edit operations on *path*.
|
|
585
|
+
|
|
586
|
+
Chrys runs sync ``FunctionTool``s through ``asyncio.to_thread``, so two
|
|
587
|
+
write/edit calls in one tool batch can run concurrently in worker threads.
|
|
588
|
+
``edit_file`` (and overwrite-aware ``write_file``) is read-modify-write, so
|
|
589
|
+
concurrent calls on the same file would race and lose updates. Lock per
|
|
590
|
+
resolved path so same-file writes stay deterministic while distinct files
|
|
591
|
+
still proceed in parallel; reads remain lock-free.
|
|
592
|
+
"""
|
|
593
|
+
try:
|
|
594
|
+
# realpath (not just normpath) so symlink aliases to the same physical
|
|
595
|
+
# file share one lock — matching chrys's path-identity convention
|
|
596
|
+
# (runtime_paths.paths_equal, approval middleware).
|
|
597
|
+
key = os.path.normcase(os.path.realpath(resolve_workspace_path(path, base_cwd=base_cwd)))
|
|
598
|
+
except Exception:
|
|
599
|
+
key = "\x00unresolved"
|
|
600
|
+
with _FS_WRITE_LOCKS_GUARD:
|
|
601
|
+
lock = _FS_WRITE_LOCKS.get(key)
|
|
602
|
+
if lock is None:
|
|
603
|
+
lock = threading.Lock()
|
|
604
|
+
_FS_WRITE_LOCKS[key] = lock
|
|
605
|
+
return lock
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _write_file_impl(
|
|
609
|
+
path: str,
|
|
610
|
+
content: str,
|
|
611
|
+
overwrite: bool = False,
|
|
612
|
+
*,
|
|
613
|
+
base_cwd: str | None = None,
|
|
614
|
+
) -> str:
|
|
615
|
+
# Checked first: a relative write would otherwise recreate the deleted
|
|
616
|
+
# working directory through ``os.makedirs`` below.
|
|
617
|
+
missing_base = missing_base_cwd_error(path, base_cwd)
|
|
618
|
+
if missing_base is not None:
|
|
619
|
+
return missing_base
|
|
620
|
+
try:
|
|
621
|
+
path = approved_write_path(path, base_cwd=base_cwd)
|
|
622
|
+
with _fs_write_lock(path, base_cwd):
|
|
623
|
+
plan = plan_write_file(path, content, overwrite=overwrite, base_cwd=base_cwd)
|
|
624
|
+
if isinstance(plan, FileToolPreviewError):
|
|
625
|
+
return plan.message
|
|
626
|
+
|
|
627
|
+
# Ensure parent directory exists
|
|
628
|
+
parent = os.path.dirname(plan.resolved_path)
|
|
629
|
+
if parent:
|
|
630
|
+
os.makedirs(parent, exist_ok=True)
|
|
631
|
+
|
|
632
|
+
_atomic_write(plan.resolved_path, plan.write_content)
|
|
633
|
+
line_count = len(split_lines(normalize_line_endings(content)))
|
|
634
|
+
return f"Written {len(content)} chars ({line_count} lines) to {surrogate_safe_text(plan.resolved_path)}."
|
|
635
|
+
except Exception as e:
|
|
636
|
+
return tool_error("write_failed", f"failed to write file — {e}", details={"path": path})
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
@tool(kind=KIND_FILESYSTEM_WRITE)
|
|
640
|
+
def write_file(
|
|
641
|
+
path: Annotated[str, "Absolute or relative path to the file to write."],
|
|
642
|
+
content: Annotated[str, "The full file content to write."],
|
|
643
|
+
overwrite: Annotated[
|
|
644
|
+
bool,
|
|
645
|
+
"Set to true to overwrite an existing file. "
|
|
646
|
+
"If false (default) and the file already exists, "
|
|
647
|
+
"the write will be rejected.",
|
|
648
|
+
] = False,
|
|
649
|
+
) -> str:
|
|
650
|
+
"""Write content to a file. Creates parent directories if needed.
|
|
651
|
+
|
|
652
|
+
Use ``\\n`` for line breaks — the tool automatically applies the correct
|
|
653
|
+
line ending for the platform and file type. When overwriting an existing
|
|
654
|
+
file, the original line ending style is preserved. For new files, the
|
|
655
|
+
line ending is chosen by file extension (e.g. ``.sh`` → LF, ``.bat`` →
|
|
656
|
+
CRLF) and platform default (CRLF on Windows, LF on macOS/Linux).
|
|
657
|
+
|
|
658
|
+
On POSIX, new regular files request mode 0644 and new shell/shebang scripts
|
|
659
|
+
request mode 0755, with both filtered by the process umask. Overwriting an
|
|
660
|
+
existing file preserves its user/group/other rwx bits.
|
|
661
|
+
|
|
662
|
+
By default, refuses to overwrite existing files unless ``overwrite=true``.
|
|
663
|
+
"""
|
|
664
|
+
return _write_file_impl(path, content, overwrite=overwrite)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
_CONTEXT_LINES = 2
|
|
668
|
+
# A side of the snippet longer than this shows only its first and last halves.
|
|
669
|
+
_MAX_SNIPPET_SIDE_LINES = 30
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _format_diff_context(content: str, match_start: int, old_string: str, new_string: str) -> str:
|
|
673
|
+
"""Return a before/after snippet with line numbers and surrounding context."""
|
|
674
|
+
lines = split_lines(content)
|
|
675
|
+
|
|
676
|
+
# Find which lines the old_string spans by counting newlines before the match
|
|
677
|
+
prefix = content[:match_start]
|
|
678
|
+
old_start_line = prefix.count("\n")
|
|
679
|
+
old_end_line = old_start_line + old_string.count("\n")
|
|
680
|
+
|
|
681
|
+
ctx_start = max(0, old_start_line - _CONTEXT_LINES)
|
|
682
|
+
ctx_end = min(len(lines), old_end_line + 1 + _CONTEXT_LINES)
|
|
683
|
+
|
|
684
|
+
# "Before" — lines from original content
|
|
685
|
+
before = _snippet_side(lines, ctx_start, ctx_end)
|
|
686
|
+
|
|
687
|
+
# "After" — apply replacement and show the same region
|
|
688
|
+
after_content = content[:match_start] + new_string + content[match_start + len(old_string) :]
|
|
689
|
+
after_lines = split_lines(after_content)
|
|
690
|
+
new_end_line = old_start_line + new_string.count("\n")
|
|
691
|
+
after_ctx_end = min(len(after_lines), new_end_line + 1 + _CONTEXT_LINES)
|
|
692
|
+
|
|
693
|
+
after = _snippet_side(after_lines, ctx_start, after_ctx_end)
|
|
694
|
+
|
|
695
|
+
return f"Before:\n{before}\nAfter:\n{after}"
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _snippet_side(lines: list[str], start: int, end: int) -> str:
|
|
699
|
+
"""Number ``lines[start:end]``, eliding the middle of a side too long to read at a glance."""
|
|
700
|
+
numbered = [f" {index + 1}|{lines[index]}" for index in range(start, end)]
|
|
701
|
+
# The marker takes a line's place, so hiding a single line would save nothing.
|
|
702
|
+
if len(numbered) <= _MAX_SNIPPET_SIDE_LINES + 1:
|
|
703
|
+
return "\n".join(numbered)
|
|
704
|
+
keep = _MAX_SNIPPET_SIDE_LINES // 2
|
|
705
|
+
first_omitted = start + keep + 1
|
|
706
|
+
last_omitted = end - keep
|
|
707
|
+
omitted = last_omitted - first_omitted + 1
|
|
708
|
+
marker = f" [... {omitted} lines omitted ({first_omitted}-{last_omitted}) ...]"
|
|
709
|
+
return "\n".join([*numbered[:keep], marker, *numbered[-keep:]])
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _replace_surrogate_escapes(text: str) -> str:
|
|
713
|
+
"""Return display-safe text with one output character per input character.
|
|
714
|
+
|
|
715
|
+
This must remain length-preserving: ``plan_edit_file`` passes a raw-content
|
|
716
|
+
offset to ``_format_diff_context`` for indexing the display-safe copy.
|
|
717
|
+
"""
|
|
718
|
+
return "".join("\ufffd" if "\ud800" <= char <= "\udfff" else char for char in text)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def plan_edit_file(
|
|
722
|
+
path: str,
|
|
723
|
+
old_string: str,
|
|
724
|
+
new_string: str,
|
|
725
|
+
replace_all: bool = False,
|
|
726
|
+
*,
|
|
727
|
+
base_cwd: str | None = None,
|
|
728
|
+
) -> EditFilePlan | FileToolPreviewError:
|
|
729
|
+
"""Prepare an ``edit_file`` operation in memory without writing the file."""
|
|
730
|
+
from pathlib import Path
|
|
731
|
+
|
|
732
|
+
from chrys.foundation.text.encoding import EncodingDetector
|
|
733
|
+
|
|
734
|
+
detector = EncodingDetector()
|
|
735
|
+
|
|
736
|
+
if is_document_artifact_handle(path):
|
|
737
|
+
return FileToolPreviewError(
|
|
738
|
+
kind="read_only_handle",
|
|
739
|
+
message=tool_error(
|
|
740
|
+
"session_artifact_handle_is_read_only",
|
|
741
|
+
"session document handles are read-only; use a regular filesystem path for edit_file.",
|
|
742
|
+
details={"path": path},
|
|
743
|
+
),
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# An empty old_string "matches" between every two characters: it can
|
|
747
|
+
# neither name one place nor create a file.
|
|
748
|
+
if not old_string:
|
|
749
|
+
return FileToolPreviewError(
|
|
750
|
+
kind="empty_old_string",
|
|
751
|
+
message=tool_error(
|
|
752
|
+
"empty_old_string",
|
|
753
|
+
"old_string is empty. To create a file or replace a file's whole content, use write_file "
|
|
754
|
+
"(overwrite=true for an existing file). To insert text, put an existing neighbouring line "
|
|
755
|
+
"in old_string and repeat it in new_string.",
|
|
756
|
+
details={"path": path},
|
|
757
|
+
),
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
try:
|
|
761
|
+
resolved = resolve_workspace_path(path, base_cwd=base_cwd)
|
|
762
|
+
if not os.path.exists(resolved):
|
|
763
|
+
return FileToolPreviewError(
|
|
764
|
+
kind="missing",
|
|
765
|
+
message=tool_error(
|
|
766
|
+
"file_not_found",
|
|
767
|
+
f"file not found — {resolved}",
|
|
768
|
+
details={"path": path, "resolved_path": resolved},
|
|
769
|
+
),
|
|
770
|
+
resolved_path=resolved,
|
|
771
|
+
)
|
|
772
|
+
if os.path.isdir(resolved):
|
|
773
|
+
return FileToolPreviewError(
|
|
774
|
+
kind="directory",
|
|
775
|
+
message=tool_error(
|
|
776
|
+
"path_is_directory",
|
|
777
|
+
f"path is a directory, not a file — {resolved}",
|
|
778
|
+
details={"path": path, "resolved_path": resolved},
|
|
779
|
+
),
|
|
780
|
+
resolved_path=resolved,
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
detection = detector.detect_file(Path(resolved))
|
|
784
|
+
if detection.is_binary:
|
|
785
|
+
return FileToolPreviewError(
|
|
786
|
+
kind="binary",
|
|
787
|
+
message=tool_error(
|
|
788
|
+
"binary_file",
|
|
789
|
+
f"file appears to be binary — {resolved}",
|
|
790
|
+
details={"path": path, "resolved_path": resolved},
|
|
791
|
+
),
|
|
792
|
+
resolved_path=resolved,
|
|
793
|
+
)
|
|
794
|
+
encoding = detection.encoding or "utf-8"
|
|
795
|
+
|
|
796
|
+
# Read once in binary — detect EOL from raw bytes, then decode.
|
|
797
|
+
with open(resolved, "rb") as f:
|
|
798
|
+
raw = f.read()
|
|
799
|
+
original_eol = _detect_line_ending(raw, resolved)
|
|
800
|
+
decode_errors = "replace" if encoding.startswith(("utf-16", "utf-32")) else "surrogateescape"
|
|
801
|
+
content = normalize_line_endings(raw.decode(encoding, errors=decode_errors))
|
|
802
|
+
|
|
803
|
+
normalized_old = normalize_line_endings(old_string)
|
|
804
|
+
normalized_new = normalize_line_endings(new_string)
|
|
805
|
+
|
|
806
|
+
if normalized_old == normalized_new:
|
|
807
|
+
return FileToolPreviewError(
|
|
808
|
+
kind="identical",
|
|
809
|
+
message=tool_error("no_change", "old_string and new_string are identical — nothing to change."),
|
|
810
|
+
resolved_path=resolved,
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
count = content.count(normalized_old)
|
|
814
|
+
if count == 0:
|
|
815
|
+
return FileToolPreviewError(
|
|
816
|
+
kind="not_found",
|
|
817
|
+
message=tool_error(
|
|
818
|
+
"old_string_not_found",
|
|
819
|
+
f"old_string not found in {resolved}. "
|
|
820
|
+
"Make sure the string matches exactly (including whitespace and indentation). "
|
|
821
|
+
"Do not include line number prefixes (e.g. '13|') from read_file output.",
|
|
822
|
+
details={"path": path, "resolved_path": resolved},
|
|
823
|
+
),
|
|
824
|
+
resolved_path=resolved,
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
if not replace_all and count > 1:
|
|
828
|
+
return FileToolPreviewError(
|
|
829
|
+
kind="ambiguous",
|
|
830
|
+
message=tool_error(
|
|
831
|
+
"ambiguous_match",
|
|
832
|
+
f"old_string appears {count} times in {resolved}. "
|
|
833
|
+
"Include more surrounding context in old_string to make the match unique, "
|
|
834
|
+
"or set replace_all=true to replace every occurrence.",
|
|
835
|
+
details={"path": path, "resolved_path": resolved, "matches": count},
|
|
836
|
+
),
|
|
837
|
+
resolved_path=resolved,
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
match_start = content.find(normalized_old)
|
|
841
|
+
display_content = _replace_surrogate_escapes(content)
|
|
842
|
+
display_old = _replace_surrogate_escapes(normalized_old)
|
|
843
|
+
display_new = _replace_surrogate_escapes(normalized_new)
|
|
844
|
+
snippet = _format_diff_context(display_content, match_start, display_old, display_new)
|
|
845
|
+
|
|
846
|
+
new_content = (
|
|
847
|
+
content.replace(normalized_old, normalized_new)
|
|
848
|
+
if replace_all
|
|
849
|
+
else content.replace(normalized_old, normalized_new, 1)
|
|
850
|
+
)
|
|
851
|
+
write_content = new_content.replace("\n", original_eol) if original_eol != "\n" else new_content
|
|
852
|
+
replacements = count if replace_all else 1
|
|
853
|
+
return EditFilePlan(
|
|
854
|
+
resolved_path=resolved,
|
|
855
|
+
encoding=encoding,
|
|
856
|
+
decode_errors=decode_errors,
|
|
857
|
+
before_text=display_content,
|
|
858
|
+
after_text=_replace_surrogate_escapes(new_content),
|
|
859
|
+
write_content=write_content,
|
|
860
|
+
replacements=replacements,
|
|
861
|
+
snippet=snippet,
|
|
862
|
+
)
|
|
863
|
+
except Exception as e:
|
|
864
|
+
return FileToolPreviewError(
|
|
865
|
+
kind="prepare_failed",
|
|
866
|
+
message=tool_error("edit_failed", f"failed to edit file — {e}", details={"path": path}),
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def _edit_file_impl(
|
|
871
|
+
path: str,
|
|
872
|
+
old_string: str,
|
|
873
|
+
new_string: str,
|
|
874
|
+
replace_all: bool = False,
|
|
875
|
+
*,
|
|
876
|
+
base_cwd: str | None = None,
|
|
877
|
+
) -> str:
|
|
878
|
+
missing_base = missing_base_cwd_error(path, base_cwd)
|
|
879
|
+
if missing_base is not None:
|
|
880
|
+
return missing_base
|
|
881
|
+
try:
|
|
882
|
+
path = approved_write_path(path, base_cwd=base_cwd)
|
|
883
|
+
with _fs_write_lock(path, base_cwd):
|
|
884
|
+
plan = plan_edit_file(path, old_string, new_string, replace_all=replace_all, base_cwd=base_cwd)
|
|
885
|
+
if isinstance(plan, FileToolPreviewError):
|
|
886
|
+
return plan.message
|
|
887
|
+
|
|
888
|
+
_atomic_write(plan.resolved_path, plan.write_content, encoding=plan.encoding, errors=plan.decode_errors)
|
|
889
|
+
|
|
890
|
+
plural = "s" if plan.replacements != 1 else ""
|
|
891
|
+
return (
|
|
892
|
+
f"Replaced {plan.replacements} occurrence{plural} in {surrogate_safe_text(plan.resolved_path)}.\n"
|
|
893
|
+
f"{plan.snippet}"
|
|
894
|
+
)
|
|
895
|
+
except Exception as e:
|
|
896
|
+
return tool_error("edit_failed", f"failed to edit file — {e}", details={"path": path})
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
@tool(kind=KIND_FILESYSTEM_WRITE)
|
|
900
|
+
def edit_file(
|
|
901
|
+
path: Annotated[str, "Absolute or relative path to the file to edit."],
|
|
902
|
+
old_string: Annotated[
|
|
903
|
+
str,
|
|
904
|
+
"The exact text to find and replace. "
|
|
905
|
+
"Match the file content exactly — preserve indentation (tabs/spaces) as shown "
|
|
906
|
+
"by read_file. The line number prefix (e.g. '13|') is NOT part of the file "
|
|
907
|
+
"content; never include it in old_string or new_string. "
|
|
908
|
+
"When the text contains unicode escape sequences like \\u2192, "
|
|
909
|
+
"use the actual Unicode character (e.g. →) instead of the escape "
|
|
910
|
+
"sequence to avoid JSON encoding ambiguity.",
|
|
911
|
+
],
|
|
912
|
+
new_string: Annotated[
|
|
913
|
+
str,
|
|
914
|
+
"The replacement text. "
|
|
915
|
+
"Preserve the surrounding indentation style (tabs or spaces). "
|
|
916
|
+
"Use actual Unicode characters instead of \\uXXXX escape sequences.",
|
|
917
|
+
],
|
|
918
|
+
replace_all: Annotated[
|
|
919
|
+
bool,
|
|
920
|
+
"If true, replace all occurrences. If false (default), replace only "
|
|
921
|
+
"one occurrence and require it to be unique.",
|
|
922
|
+
] = False,
|
|
923
|
+
) -> str:
|
|
924
|
+
"""Replace exact string occurrences in a file.
|
|
925
|
+
|
|
926
|
+
Line endings are handled automatically — use ``\\n`` in old_string and
|
|
927
|
+
new_string regardless of the file's actual line ending style. The original
|
|
928
|
+
line ending style is detected and preserved on write. On POSIX, the file's
|
|
929
|
+
existing user/group/other rwx bits are also preserved.
|
|
930
|
+
|
|
931
|
+
When ``replace_all=false`` (default), the ``old_string`` must appear exactly
|
|
932
|
+
once. If it appears multiple times, the edit is rejected — include more
|
|
933
|
+
surrounding context in ``old_string`` to make the match unique.
|
|
934
|
+
"""
|
|
935
|
+
return _edit_file_impl(path, old_string, new_string, replace_all=replace_all)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
class FilesystemTools:
|
|
939
|
+
"""Runtime-bound filesystem tools that resolve relative paths per session."""
|
|
940
|
+
|
|
941
|
+
def __init__(
|
|
942
|
+
self,
|
|
943
|
+
runtime: SessionEnvironment,
|
|
944
|
+
session_dir: Path | None = None,
|
|
945
|
+
session_id: str = "",
|
|
946
|
+
) -> None:
|
|
947
|
+
self._runtime = runtime
|
|
948
|
+
self._session_dir = resolve_tool_session_dir(
|
|
949
|
+
runtime.platform.config_dir,
|
|
950
|
+
session_id=session_id,
|
|
951
|
+
session_dir=session_dir,
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
def tools(self) -> list:
|
|
955
|
+
"""Return filesystem tools for this runtime context."""
|
|
956
|
+
return [self.read_file, self.view_image, self.write_file, self.edit_file]
|
|
957
|
+
|
|
958
|
+
def approval_targets(self, arguments: dict[str, object]) -> ApprovedTargets:
|
|
959
|
+
"""The entries a write/edit replaces; an unresolved path authorizes no write."""
|
|
960
|
+
path = arguments.get("path")
|
|
961
|
+
if isinstance(path, str) and is_document_artifact_handle(path):
|
|
962
|
+
# The tool refuses to write a session document; nothing to pin.
|
|
963
|
+
return ApprovedTargets()
|
|
964
|
+
if not isinstance(path, str) or not path or "\0" in path:
|
|
965
|
+
return ApprovedTargets(files=())
|
|
966
|
+
try:
|
|
967
|
+
return ApprovedTargets(files=(file_write_target(path, base_cwd=self._runtime.cwd),))
|
|
968
|
+
except OSError, ValueError:
|
|
969
|
+
return ApprovedTargets(files=())
|
|
970
|
+
|
|
971
|
+
@tool(kind=KIND_FILESYSTEM_READ)
|
|
972
|
+
def read_file(
|
|
973
|
+
self,
|
|
974
|
+
path: Annotated[str, "Absolute or relative path to the file to read."],
|
|
975
|
+
max_tokens: Annotated[
|
|
976
|
+
int,
|
|
977
|
+
"Token budget for the returned content. Large files are truncated to fit.",
|
|
978
|
+
] = _DEFAULT_MAX_TOKENS,
|
|
979
|
+
line_range: Annotated[
|
|
980
|
+
list[int] | None,
|
|
981
|
+
"Optional [start, end] 1-indexed line range. Use -1 for end to read to EOF.",
|
|
982
|
+
] = None,
|
|
983
|
+
) -> str:
|
|
984
|
+
"""Read the contents of a text file and return them with line numbers."""
|
|
985
|
+
return _read_file_impl(
|
|
986
|
+
path,
|
|
987
|
+
max_tokens=max_tokens,
|
|
988
|
+
line_range=line_range,
|
|
989
|
+
base_cwd=self._runtime.cwd,
|
|
990
|
+
session_dir=self._session_dir,
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
@tool(kind=KIND_FILESYSTEM_READ)
|
|
994
|
+
def view_image(
|
|
995
|
+
self,
|
|
996
|
+
path: Annotated[str, "Absolute or relative path to the image file to inspect."],
|
|
997
|
+
) -> list[Content]:
|
|
998
|
+
"""Read an image file and return it as model-visible image content.
|
|
999
|
+
|
|
1000
|
+
Images the user attached to a message are already visible to you, at the
|
|
1001
|
+
same size this tool returns; call it for one only when it is not in the
|
|
1002
|
+
conversation.
|
|
1003
|
+
"""
|
|
1004
|
+
return _view_image_impl(path, base_cwd=self._runtime.cwd, session_dir=self._session_dir)
|
|
1005
|
+
|
|
1006
|
+
@tool(kind=KIND_FILESYSTEM_WRITE)
|
|
1007
|
+
def write_file(
|
|
1008
|
+
self,
|
|
1009
|
+
path: Annotated[str, "Absolute or relative path to the file to write."],
|
|
1010
|
+
content: Annotated[str, "The full file content to write."],
|
|
1011
|
+
overwrite: Annotated[
|
|
1012
|
+
bool,
|
|
1013
|
+
"Set to true to overwrite an existing file. "
|
|
1014
|
+
"If false (default) and the file already exists, "
|
|
1015
|
+
"the write will be rejected.",
|
|
1016
|
+
] = False,
|
|
1017
|
+
) -> str:
|
|
1018
|
+
"""Write content to a file. Creates parent directories if needed.
|
|
1019
|
+
|
|
1020
|
+
Use ``\\n`` for line breaks — the tool automatically applies the correct
|
|
1021
|
+
line ending for the platform and file type. When overwriting an existing
|
|
1022
|
+
file, the original line ending style is preserved. For new files, the
|
|
1023
|
+
line ending is chosen by file extension (e.g. ``.sh`` → LF, ``.bat`` →
|
|
1024
|
+
CRLF) and platform default (CRLF on Windows, LF on macOS/Linux).
|
|
1025
|
+
|
|
1026
|
+
On POSIX, new regular files request mode 0644 and new shell/shebang scripts
|
|
1027
|
+
request mode 0755, with both filtered by the process umask. Overwriting an
|
|
1028
|
+
existing file preserves its user/group/other rwx bits.
|
|
1029
|
+
|
|
1030
|
+
By default, refuses to overwrite existing files unless ``overwrite=true``.
|
|
1031
|
+
"""
|
|
1032
|
+
return _write_file_impl(path, content, overwrite=overwrite, base_cwd=self._runtime.cwd)
|
|
1033
|
+
|
|
1034
|
+
@tool(kind=KIND_FILESYSTEM_WRITE)
|
|
1035
|
+
def edit_file(
|
|
1036
|
+
self,
|
|
1037
|
+
path: Annotated[str, "Absolute or relative path to the file to edit."],
|
|
1038
|
+
old_string: Annotated[
|
|
1039
|
+
str,
|
|
1040
|
+
"The exact text to find and replace. "
|
|
1041
|
+
"Match the file content exactly — preserve indentation (tabs/spaces) as shown "
|
|
1042
|
+
"by read_file. The line number prefix (e.g. '13|') is NOT part of the file "
|
|
1043
|
+
"content; never include it in old_string or new_string. "
|
|
1044
|
+
"When the text contains unicode escape sequences like \\u2192, "
|
|
1045
|
+
"use the actual Unicode character (e.g. →) instead of the escape "
|
|
1046
|
+
"sequence to avoid JSON encoding ambiguity.",
|
|
1047
|
+
],
|
|
1048
|
+
new_string: Annotated[
|
|
1049
|
+
str,
|
|
1050
|
+
"The replacement text. "
|
|
1051
|
+
"Preserve the surrounding indentation style (tabs or spaces). "
|
|
1052
|
+
"Use actual Unicode characters instead of \\uXXXX escape sequences.",
|
|
1053
|
+
],
|
|
1054
|
+
replace_all: Annotated[
|
|
1055
|
+
bool,
|
|
1056
|
+
"If true, replace all occurrences. If false (default), replace only "
|
|
1057
|
+
"one occurrence and require it to be unique.",
|
|
1058
|
+
] = False,
|
|
1059
|
+
) -> str:
|
|
1060
|
+
"""Replace exact string occurrences in a file.
|
|
1061
|
+
|
|
1062
|
+
Line endings are handled automatically — use ``\\n`` in old_string and
|
|
1063
|
+
new_string regardless of the file's actual line ending style. The original
|
|
1064
|
+
line ending style is detected and preserved on write. On POSIX, the file's
|
|
1065
|
+
existing user/group/other rwx bits are also preserved.
|
|
1066
|
+
|
|
1067
|
+
When ``replace_all=false`` (default), the ``old_string`` must appear exactly
|
|
1068
|
+
once. If it appears multiple times, the edit is rejected — include more
|
|
1069
|
+
surrounding context in ``old_string`` to make the match unique.
|
|
1070
|
+
"""
|
|
1071
|
+
return _edit_file_impl(path, old_string, new_string, replace_all=replace_all, base_cwd=self._runtime.cwd)
|