octo-agent 0.11.2
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.
- checksums.yaml +7 -0
- data/.clacky/skills/commit/SKILL.md +423 -0
- data/.clacky/skills/gem-release/SKILL.md +199 -0
- data/.clacky/skills/gem-release/scripts/release.sh +304 -0
- data/.clacky/skills/oss-upload/SKILL.md +47 -0
- data/.octorules +106 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +76 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +92 -0
- data/Dockerfile +28 -0
- data/LICENSE.txt +22 -0
- data/POSITIONING.md +46 -0
- data/README.md +134 -0
- data/README_CN.md +134 -0
- data/Rakefile +34 -0
- data/benchmark/fixtures/sample_project/Gemfile +3 -0
- data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
- data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
- data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
- data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
- data/benchmark/results/EVALUATION_REPORT.md +165 -0
- data/benchmark/results/baseline_20260511_174424.json +128 -0
- data/benchmark/results/report_20260511_175256.json +271 -0
- data/benchmark/results/report_20260511_175444.json +271 -0
- data/benchmark/results/treatment_20260511_175103.json +130 -0
- data/benchmark/runner.rb +441 -0
- data/bin/octo +7 -0
- data/docs/agent-first-ui-design.md +77 -0
- data/docs/billing-system.md +318 -0
- data/docs/channel-architecture.md +235 -0
- data/docs/engineering-article.md +343 -0
- data/docs/session-skill-invocation.md +69 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/ui2-architecture.md +124 -0
- data/homebrew/README.md +96 -0
- data/homebrew/openocto.rb +24 -0
- data/lib/octo/agent/hook_manager.rb +61 -0
- data/lib/octo/agent/llm_caller.rb +800 -0
- data/lib/octo/agent/memory_updater.rb +246 -0
- data/lib/octo/agent/message_compressor.rb +225 -0
- data/lib/octo/agent/message_compressor_helper.rb +869 -0
- data/lib/octo/agent/next_message_suggester.rb +215 -0
- data/lib/octo/agent/session_serializer.rb +685 -0
- data/lib/octo/agent/skill_auto_creator.rb +114 -0
- data/lib/octo/agent/skill_evolution.rb +61 -0
- data/lib/octo/agent/skill_manager.rb +466 -0
- data/lib/octo/agent/skill_reflector.rb +89 -0
- data/lib/octo/agent/system_prompt_builder.rb +101 -0
- data/lib/octo/agent/time_machine.rb +214 -0
- data/lib/octo/agent/tool_executor.rb +454 -0
- data/lib/octo/agent/tool_registry.rb +150 -0
- data/lib/octo/agent.rb +2180 -0
- data/lib/octo/agent_config.rb +989 -0
- data/lib/octo/agent_profile.rb +112 -0
- data/lib/octo/anthropic_stream_aggregator.rb +137 -0
- data/lib/octo/background_task_registry.rb +324 -0
- data/lib/octo/banner.rb +34 -0
- data/lib/octo/bedrock_stream_aggregator.rb +137 -0
- data/lib/octo/block_font.rb +331 -0
- data/lib/octo/cli.rb +968 -0
- data/lib/octo/client.rb +623 -0
- data/lib/octo/default_agents/SOUL.md +3 -0
- data/lib/octo/default_agents/USER.md +1 -0
- data/lib/octo/default_agents/base_prompt.md +66 -0
- data/lib/octo/default_agents/coding/profile.yml +2 -0
- data/lib/octo/default_agents/coding/system_prompt.md +67 -0
- data/lib/octo/default_agents/general/profile.yml +2 -0
- data/lib/octo/default_agents/general/system_prompt.md +16 -0
- data/lib/octo/default_parsers/doc_parser.rb +69 -0
- data/lib/octo/default_parsers/docx_parser.rb +188 -0
- data/lib/octo/default_parsers/pdf_parser.rb +120 -0
- data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/octo/default_parsers/pptx_parser.rb +140 -0
- data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
- data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
- data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
- data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
- data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
- data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
- data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
- data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
- data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
- data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
- data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
- data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
- data/lib/octo/default_skills/onboard/SKILL.md +578 -0
- data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
- data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
- data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
- data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
- data/lib/octo/default_skills/personal-website/publish.rb +235 -0
- data/lib/octo/default_skills/product-help/SKILL.md +123 -0
- data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
- data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
- data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
- data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
- data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
- data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
- data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
- data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
- data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
- data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
- data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
- data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
- data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
- data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
- data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
- data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
- data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
- data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
- data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
- data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
- data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
- data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
- data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
- data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
- data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
- data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
- data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
- data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
- data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
- data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
- data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/octo/idle_compression_timer.rb +115 -0
- data/lib/octo/json_ui_controller.rb +204 -0
- data/lib/octo/message_format/anthropic.rb +409 -0
- data/lib/octo/message_format/bedrock.rb +361 -0
- data/lib/octo/message_format/open_ai.rb +222 -0
- data/lib/octo/message_history.rb +373 -0
- data/lib/octo/openai_stream_aggregator.rb +130 -0
- data/lib/octo/plain_ui_controller.rb +166 -0
- data/lib/octo/providers.rb +534 -0
- data/lib/octo/server/browser_manager.rb +397 -0
- data/lib/octo/server/channel/adapters/base.rb +82 -0
- data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
- data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
- data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
- data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
- data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
- data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
- data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
- data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
- data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
- data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
- data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
- data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
- data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
- data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
- data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
- data/lib/octo/server/channel/channel_config.rb +178 -0
- data/lib/octo/server/channel/channel_manager.rb +468 -0
- data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
- data/lib/octo/server/channel.rb +33 -0
- data/lib/octo/server/discover.rb +77 -0
- data/lib/octo/server/epipe_safe_io.rb +105 -0
- data/lib/octo/server/http_server.rb +3554 -0
- data/lib/octo/server/scheduler.rb +317 -0
- data/lib/octo/server/server_master.rb +325 -0
- data/lib/octo/server/session_registry.rb +431 -0
- data/lib/octo/server/web_ui_controller.rb +487 -0
- data/lib/octo/session_manager.rb +385 -0
- data/lib/octo/skill.rb +466 -0
- data/lib/octo/skill_loader.rb +328 -0
- data/lib/octo/tools/base.rb +118 -0
- data/lib/octo/tools/browser.rb +625 -0
- data/lib/octo/tools/edit.rb +165 -0
- data/lib/octo/tools/file_reader.rb +549 -0
- data/lib/octo/tools/glob.rb +162 -0
- data/lib/octo/tools/grep.rb +356 -0
- data/lib/octo/tools/invoke_skill.rb +96 -0
- data/lib/octo/tools/list_tasks.rb +54 -0
- data/lib/octo/tools/redo_task.rb +41 -0
- data/lib/octo/tools/request_user_feedback.rb +84 -0
- data/lib/octo/tools/security.rb +333 -0
- data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
- data/lib/octo/tools/terminal/persistent_session.rb +268 -0
- data/lib/octo/tools/terminal/safe_rm.sh +106 -0
- data/lib/octo/tools/terminal/session_manager.rb +213 -0
- data/lib/octo/tools/terminal.rb +1828 -0
- data/lib/octo/tools/todo_manager.rb +374 -0
- data/lib/octo/tools/trash_manager.rb +388 -0
- data/lib/octo/tools/undo_task.rb +35 -0
- data/lib/octo/tools/web_fetch.rb +242 -0
- data/lib/octo/tools/web_search.rb +260 -0
- data/lib/octo/tools/write.rb +77 -0
- data/lib/octo/ui2/block_font.rb +10 -0
- data/lib/octo/ui2/components/base_component.rb +163 -0
- data/lib/octo/ui2/components/command_suggestions.rb +290 -0
- data/lib/octo/ui2/components/common_component.rb +96 -0
- data/lib/octo/ui2/components/inline_input.rb +226 -0
- data/lib/octo/ui2/components/input_area.rb +1338 -0
- data/lib/octo/ui2/components/message_component.rb +99 -0
- data/lib/octo/ui2/components/modal_component.rb +419 -0
- data/lib/octo/ui2/components/todo_area.rb +149 -0
- data/lib/octo/ui2/components/tool_component.rb +107 -0
- data/lib/octo/ui2/components/welcome_banner.rb +139 -0
- data/lib/octo/ui2/layout_manager.rb +807 -0
- data/lib/octo/ui2/line_editor.rb +363 -0
- data/lib/octo/ui2/markdown_renderer.rb +100 -0
- data/lib/octo/ui2/output_buffer.rb +370 -0
- data/lib/octo/ui2/progress_handle.rb +362 -0
- data/lib/octo/ui2/progress_indicator.rb +55 -0
- data/lib/octo/ui2/screen_buffer.rb +273 -0
- data/lib/octo/ui2/terminal_detector.rb +119 -0
- data/lib/octo/ui2/theme_manager.rb +85 -0
- data/lib/octo/ui2/themes/base_theme.rb +105 -0
- data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
- data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
- data/lib/octo/ui2/thinking_verbs.rb +26 -0
- data/lib/octo/ui2/ui_controller.rb +1625 -0
- data/lib/octo/ui2/view_renderer.rb +177 -0
- data/lib/octo/ui2.rb +40 -0
- data/lib/octo/ui_interface.rb +154 -0
- data/lib/octo/utils/arguments_parser.rb +191 -0
- data/lib/octo/utils/browser_detector.rb +195 -0
- data/lib/octo/utils/encoding.rb +92 -0
- data/lib/octo/utils/environment_detector.rb +140 -0
- data/lib/octo/utils/file_ignore_helper.rb +170 -0
- data/lib/octo/utils/file_processor.rb +601 -0
- data/lib/octo/utils/gitignore_parser.rb +154 -0
- data/lib/octo/utils/limit_stack.rb +152 -0
- data/lib/octo/utils/logger.rb +124 -0
- data/lib/octo/utils/login_shell.rb +72 -0
- data/lib/octo/utils/model_pricing.rb +646 -0
- data/lib/octo/utils/parser_manager.rb +165 -0
- data/lib/octo/utils/path_helper.rb +15 -0
- data/lib/octo/utils/scripts_manager.rb +59 -0
- data/lib/octo/utils/string_matcher.rb +158 -0
- data/lib/octo/utils/trash_directory.rb +112 -0
- data/lib/octo/utils/workspace_rules.rb +46 -0
- data/lib/octo/version.rb +5 -0
- data/lib/octo/web/app.css +7141 -0
- data/lib/octo/web/app.js +543 -0
- data/lib/octo/web/apple-touch-icon.png +0 -0
- data/lib/octo/web/auth.js +150 -0
- data/lib/octo/web/channels.js +276 -0
- data/lib/octo/web/datepicker.js +205 -0
- data/lib/octo/web/favicon.png +0 -0
- data/lib/octo/web/i18n.js +1073 -0
- data/lib/octo/web/icon-512.png +0 -0
- data/lib/octo/web/icon-dark.svg +25 -0
- data/lib/octo/web/icon.svg +29 -0
- data/lib/octo/web/index.html +871 -0
- data/lib/octo/web/marked.min.js +69 -0
- data/lib/octo/web/onboard.js +491 -0
- data/lib/octo/web/profile.js +442 -0
- data/lib/octo/web/sessions.js +4421 -0
- data/lib/octo/web/settings.js +913 -0
- data/lib/octo/web/sidebar.js +32 -0
- data/lib/octo/web/skills.js +885 -0
- data/lib/octo/web/tasks.js +297 -0
- data/lib/octo/web/theme.js +105 -0
- data/lib/octo/web/trash.js +343 -0
- data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
- data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
- data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/katex.min.css +1 -0
- data/lib/octo/web/vendor/katex/katex.min.js +1 -0
- data/lib/octo/web/version.js +449 -0
- data/lib/octo/web/weixin-qr.html +209 -0
- data/lib/octo/web/ws-dispatcher.js +357 -0
- data/lib/octo/web/ws.js +128 -0
- data/lib/octo.rb +145 -0
- data/scripts/build/build.sh +329 -0
- data/scripts/build/lib/apt.sh +56 -0
- data/scripts/build/lib/brew.sh +89 -0
- data/scripts/build/lib/colors.sh +17 -0
- data/scripts/build/lib/gem.sh +95 -0
- data/scripts/build/lib/mise.sh +125 -0
- data/scripts/build/lib/network.sh +157 -0
- data/scripts/build/lib/os.sh +57 -0
- data/scripts/build/lib/shell.sh +37 -0
- data/scripts/build/src/install.sh.cc +174 -0
- data/scripts/build/src/install_browser.sh.cc +101 -0
- data/scripts/build/src/install_full.sh.cc +290 -0
- data/scripts/build/src/install_rails_deps.sh.cc +145 -0
- data/scripts/build/src/install_system_deps.sh.cc +123 -0
- data/scripts/build/src/uninstall.sh.cc +101 -0
- data/scripts/install.ps1 +532 -0
- data/scripts/install.sh +567 -0
- data/scripts/install_browser.sh +479 -0
- data/scripts/install_full.sh +838 -0
- data/scripts/install_rails_deps.sh +746 -0
- data/scripts/install_system_deps.sh +518 -0
- data/scripts/uninstall.sh +287 -0
- data/sig/octo.rbs +4 -0
- metadata +614 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for redoing a task after undo (Time Machine feature)
|
|
6
|
+
class RedoTask < Base
|
|
7
|
+
self.tool_name = "redo_task"
|
|
8
|
+
self.tool_description = "Redo to a specific task after undo. Restores files to that task's state. " \
|
|
9
|
+
"Use when user wants to go forward to a future task or switch to a different branch."
|
|
10
|
+
self.tool_category = "time_machine"
|
|
11
|
+
self.tool_parameters = {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
task_id: {
|
|
15
|
+
type: "integer",
|
|
16
|
+
description: "The task ID to redo to (must be greater than current active task)"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
required: ["task_id"]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def execute(agent:, task_id:, **_args)
|
|
23
|
+
result = agent.switch_to_task(task_id)
|
|
24
|
+
|
|
25
|
+
if result[:success]
|
|
26
|
+
result[:message]
|
|
27
|
+
else
|
|
28
|
+
"Error: #{result[:message]}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def format_call(task_id:, **_args)
|
|
33
|
+
"Redoing to task #{task_id}..."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def format_result(result)
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
module Tools
|
|
5
|
+
class RequestUserFeedback < Base
|
|
6
|
+
self.tool_name = "request_user_feedback"
|
|
7
|
+
self.tool_description = <<~DESC
|
|
8
|
+
Request feedback or clarification from the user when you need more information to complete a task.
|
|
9
|
+
Use this tool when:
|
|
10
|
+
- You need clarification on ambiguous requirements
|
|
11
|
+
- You need the user to choose between multiple options
|
|
12
|
+
- You need additional information that you cannot infer
|
|
13
|
+
- You want to confirm your understanding before proceeding
|
|
14
|
+
|
|
15
|
+
After calling this tool, STOP and wait for the user's response.
|
|
16
|
+
Do NOT continue with other actions until you receive user feedback.
|
|
17
|
+
DESC
|
|
18
|
+
|
|
19
|
+
self.tool_parameters = {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
question: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "The question or clarification request to ask the user"
|
|
25
|
+
},
|
|
26
|
+
context: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Optional context explaining why you need this information (helps user understand)"
|
|
29
|
+
},
|
|
30
|
+
options: {
|
|
31
|
+
type: "array",
|
|
32
|
+
items: { type: "string" },
|
|
33
|
+
description: "Optional array of choices/options if asking user to select from predefined options"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
required: ["question"]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
self.tool_category = "interaction"
|
|
40
|
+
|
|
41
|
+
def execute(question:, context: nil, options: nil, working_dir: nil)
|
|
42
|
+
# Build the feedback request message
|
|
43
|
+
message_parts = []
|
|
44
|
+
|
|
45
|
+
if context && !context.strip.empty?
|
|
46
|
+
message_parts << "**Context:** #{context.strip}"
|
|
47
|
+
message_parts << ""
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
message_parts << "**Question:** #{question.strip}"
|
|
51
|
+
|
|
52
|
+
if options && !options.empty?
|
|
53
|
+
message_parts << ""
|
|
54
|
+
message_parts << "**Options:**"
|
|
55
|
+
options.each_with_index do |option, index|
|
|
56
|
+
message_parts << " #{index + 1}. #{option}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
formatted_message = message_parts.join("\n")
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
success: true,
|
|
64
|
+
message: formatted_message,
|
|
65
|
+
awaiting_feedback: true # Special flag to indicate we're waiting for user
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def format_call(args)
|
|
70
|
+
question = args[:question] || args["question"]
|
|
71
|
+
preview = question.length > 60 ? "#{question[0..60]}..." : question
|
|
72
|
+
"request_user_feedback(\"#{preview}\")"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def format_result(result)
|
|
76
|
+
if result.is_a?(Hash) && result[:message]
|
|
77
|
+
result[:message]
|
|
78
|
+
else
|
|
79
|
+
"Waiting for user feedback..."
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require "json"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require_relative "../utils/trash_directory"
|
|
7
|
+
require_relative "../utils/encoding"
|
|
8
|
+
|
|
9
|
+
module Octo
|
|
10
|
+
module Tools
|
|
11
|
+
# Pre-execution safety layer for shell-style commands.
|
|
12
|
+
#
|
|
13
|
+
# Design principle: protect against the handful of commands that are
|
|
14
|
+
# irreversibly destructive or can compromise the host. Everything else
|
|
15
|
+
# is the user's (or agent's) business. Over-protection burns tool-call
|
|
16
|
+
# rounds and forces awkward work-arounds (e.g. the infamous "cp ~/.octo
|
|
17
|
+
# /xxx ./ ... Blocked: outside project directory" dance).
|
|
18
|
+
#
|
|
19
|
+
# Responsibilities (applied to the `command` string BEFORE it is handed
|
|
20
|
+
# to a shell / PTY for execution):
|
|
21
|
+
#
|
|
22
|
+
# 1. Block hard-dangerous commands: sudo, pkill octo, eval, exec,
|
|
23
|
+
# `...`, $(...), | sh, | bash,
|
|
24
|
+
# redirect to /etc /usr /bin.
|
|
25
|
+
# 2. Rewrite `curl ... | bash` → save script to a file for manual
|
|
26
|
+
# review instead of exec.
|
|
27
|
+
# 3. Protect credential/secret files: .env, .ssh/, .aws/ — block
|
|
28
|
+
# writes to these only. Other
|
|
29
|
+
# "project" files (Gemfile,
|
|
30
|
+
# README.md, package.json, …)
|
|
31
|
+
# are NOT protected — editing
|
|
32
|
+
# them is a normal dev task.
|
|
33
|
+
#
|
|
34
|
+
# Note on `rm`:
|
|
35
|
+
# `rm` is NOT rewritten here — it's intercepted at runtime by a shell
|
|
36
|
+
# function installed in each PTY session (see Terminal::SAFE_RM_BASH
|
|
37
|
+
# and Terminal#install_marker). This lets the shell's own parser
|
|
38
|
+
# handle heredocs / multi-line / globs / variables correctly. A
|
|
39
|
+
# static Ruby-side rewrite cannot — it would mis-parse heredoc
|
|
40
|
+
# bodies and destroy legitimate commands.
|
|
41
|
+
#
|
|
42
|
+
# Notes:
|
|
43
|
+
# - `cp`, `mv`, `mkdir`, `touch`, `echo` are allowed to touch ANY path
|
|
44
|
+
# (including outside the project root). The source of a `cp` is
|
|
45
|
+
# read-only to the FS, and writing to arbitrary dirs is a legitimate
|
|
46
|
+
# need (copying from ~/.octo/skills/..., writing to /tmp, etc.).
|
|
47
|
+
#
|
|
48
|
+
# Raises SecurityError on block. Returns a (possibly rewritten) command
|
|
49
|
+
# string on success.
|
|
50
|
+
#
|
|
51
|
+
# This module was extracted from the former `SafeShell` tool. It is now
|
|
52
|
+
# shared by any tool that executes shell-style commands (currently:
|
|
53
|
+
# `terminal`).
|
|
54
|
+
module Security
|
|
55
|
+
# Raised when a command cannot be made safe.
|
|
56
|
+
class Blocked < StandardError; end
|
|
57
|
+
|
|
58
|
+
# Read-only commands that are considered safe for auto-execution
|
|
59
|
+
# (permission mode :confirm_safes).
|
|
60
|
+
SAFE_READONLY_COMMANDS = %w[
|
|
61
|
+
ls pwd cat less more head tail
|
|
62
|
+
grep find which whereis whoami
|
|
63
|
+
ps top htop df du
|
|
64
|
+
git echo printf wc
|
|
65
|
+
date file stat
|
|
66
|
+
env printenv
|
|
67
|
+
curl wget
|
|
68
|
+
].freeze
|
|
69
|
+
|
|
70
|
+
class << self
|
|
71
|
+
# Process `command` and return a (possibly rewritten) safe version.
|
|
72
|
+
# Raises SecurityError when the command cannot be made safe.
|
|
73
|
+
#
|
|
74
|
+
# @param command [String] command to check
|
|
75
|
+
# @param project_root [String] path treated as the allowed root for writes
|
|
76
|
+
# @return [String] safe command to execute
|
|
77
|
+
def make_safe(command, project_root: Dir.pwd)
|
|
78
|
+
Replacer.new(project_root).make_command_safe(command)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# True iff the command is safe to auto-execute in :confirm_safes mode.
|
|
82
|
+
# (Either a known read-only command, or one that Security.make_safe
|
|
83
|
+
# returns unchanged.)
|
|
84
|
+
def command_safe_for_auto_execution?(command)
|
|
85
|
+
return false unless command
|
|
86
|
+
|
|
87
|
+
cmd_name = command.strip.split.first
|
|
88
|
+
return true if SAFE_READONLY_COMMANDS.include?(cmd_name)
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
safe = make_safe(command, project_root: Dir.pwd)
|
|
92
|
+
command.strip == safe.strip
|
|
93
|
+
rescue SecurityError
|
|
94
|
+
false
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Internal class that owns per-project state (trash dir, log dir, ...).
|
|
100
|
+
# Extracted almost verbatim from the old SafeShell::CommandSafetyReplacer.
|
|
101
|
+
class Replacer
|
|
102
|
+
def initialize(project_root)
|
|
103
|
+
@project_root = File.expand_path(project_root)
|
|
104
|
+
|
|
105
|
+
trash_directory = Octo::TrashDirectory.new(@project_root)
|
|
106
|
+
@backup_dir = trash_directory.backup_dir
|
|
107
|
+
|
|
108
|
+
@project_hash = trash_directory.generate_project_hash(@project_root)
|
|
109
|
+
@safety_log_dir = File.join(Dir.home, ".octo", "safety_logs", @project_hash)
|
|
110
|
+
FileUtils.mkdir_p(@safety_log_dir) unless Dir.exist?(@safety_log_dir)
|
|
111
|
+
@safety_log_file = File.join(@safety_log_dir, "safety.log")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def make_command_safe(command)
|
|
115
|
+
command = command.strip
|
|
116
|
+
|
|
117
|
+
# Use a UTF-8-scrubbed copy ONLY for regex checks. The original
|
|
118
|
+
# bytes are returned unchanged so the shell receives exact paths
|
|
119
|
+
# (e.g. GBK-encoded Chinese filenames in zip archives).
|
|
120
|
+
@safe_check_command = Octo::Utils::Encoding.safe_check(command)
|
|
121
|
+
|
|
122
|
+
case @safe_check_command
|
|
123
|
+
# Block attempts to terminate the octo server process.
|
|
124
|
+
# IMPORTANT: each verb is anchored with \b so substrings like
|
|
125
|
+
# "Skill" (contains "kill") or "Bill Killalina" don't trigger
|
|
126
|
+
# false positives. We also require `octo` to appear as a whole
|
|
127
|
+
# word AND within a reasonable distance (same logical command,
|
|
128
|
+
# not hundreds of chars later in an unrelated echo string).
|
|
129
|
+
when /\bpkill\b[^\n;|&]{0,80}\bocto\b|\bkillall\b[^\n;|&]{0,80}\bocto\b|\bkill\s+(?:-\S+\s+)*[^\n;|&]{0,40}\bocto\b/i
|
|
130
|
+
raise SecurityError, "Killing the octo server process is not allowed. To restart, use: #{restart_hint}"
|
|
131
|
+
when /\bocto\s+server\b/
|
|
132
|
+
raise SecurityError, "Managing the octo server from within a session is not allowed. To restart, use: #{restart_hint}"
|
|
133
|
+
when /^chmod\s+x/
|
|
134
|
+
replace_chmod_command(command)
|
|
135
|
+
when /^curl.*\|\s*(sh|bash)/
|
|
136
|
+
replace_curl_pipe_command(command)
|
|
137
|
+
when /^sudo\s+/
|
|
138
|
+
block_sudo_command(command)
|
|
139
|
+
when />\s*\/dev\/null\s*$/
|
|
140
|
+
allow_dev_null_redirect(command)
|
|
141
|
+
when /^(mv|cp|mkdir|touch|echo)\s+/
|
|
142
|
+
validate_and_allow(command)
|
|
143
|
+
else
|
|
144
|
+
validate_general_command(@safe_check_command)
|
|
145
|
+
command
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def replace_chmod_command(command)
|
|
150
|
+
begin
|
|
151
|
+
parts = Shellwords.split(command)
|
|
152
|
+
rescue ArgumentError
|
|
153
|
+
parts = command.split(/\s+/)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
files = parts[2..-1] || []
|
|
157
|
+
files.each { |file| validate_file_path(file) unless file.start_with?('-') }
|
|
158
|
+
|
|
159
|
+
log_replacement("chmod", command, "chmod +x is allowed - file permissions will be modified")
|
|
160
|
+
command
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def replace_curl_pipe_command(command)
|
|
164
|
+
if command.match(/curl\s+(.*?)\s*\|\s*(sh|bash)/)
|
|
165
|
+
url = $1
|
|
166
|
+
shell_type = $2
|
|
167
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
168
|
+
safe_file = File.join(@backup_dir, "downloaded_script_#{timestamp}.sh")
|
|
169
|
+
|
|
170
|
+
result = "curl #{url} -o #{Shellwords.escape(safe_file)} && echo '🔒 Script downloaded to #{safe_file} for manual review. Run: cat #{safe_file}'"
|
|
171
|
+
log_replacement("curl | #{shell_type}", result, "Script saved for manual review instead of automatic execution")
|
|
172
|
+
result
|
|
173
|
+
else
|
|
174
|
+
command
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def block_sudo_command(_command)
|
|
179
|
+
raise SecurityError, "sudo commands are not allowed for security reasons"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def allow_dev_null_redirect(command)
|
|
183
|
+
command
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Build a copy-pasteable "how to restart octo server" hint.
|
|
187
|
+
# When running inside a octo server worker, `OCTO_MASTER_PID` is
|
|
188
|
+
# injected by ServerMaster (see server_master.rb). We keep the
|
|
189
|
+
# variable name in the hint (so the AI / user learns the standard
|
|
190
|
+
# convention) AND append the resolved PID in parentheses so it's
|
|
191
|
+
# immediately actionable. When the variable isn't set (e.g. one-shot
|
|
192
|
+
# CLI invocation), we just show the variable name.
|
|
193
|
+
def restart_hint
|
|
194
|
+
pid = ENV["OCTO_MASTER_PID"].to_s
|
|
195
|
+
if pid =~ /\A\d+\z/
|
|
196
|
+
"kill -USR1 $OCTO_MASTER_PID (current master PID: #{pid})"
|
|
197
|
+
else
|
|
198
|
+
"kill -USR1 $OCTO_MASTER_PID"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Relaxed validator for mv / cp / mkdir / touch / echo.
|
|
203
|
+
#
|
|
204
|
+
# Historical behavior was to forbid any path outside @project_root,
|
|
205
|
+
# which broke legitimate workflows like copying skill templates from
|
|
206
|
+
# ~/.octo/skills/... into the project. We now only block writes to
|
|
207
|
+
# true credential directories (.ssh, .aws) and .env files. Everything
|
|
208
|
+
# else is allowed.
|
|
209
|
+
def validate_and_allow(command)
|
|
210
|
+
begin
|
|
211
|
+
parts = Shellwords.split(command)
|
|
212
|
+
rescue ArgumentError
|
|
213
|
+
parts = command.split(/\s+/)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
cmd = parts.first
|
|
217
|
+
args = parts[1..-1] || []
|
|
218
|
+
|
|
219
|
+
case cmd
|
|
220
|
+
when 'mv', 'cp'
|
|
221
|
+
# For mv/cp only the DESTINATION (last non-flag arg) is a write
|
|
222
|
+
# target; earlier args are sources and are read-only to the FS.
|
|
223
|
+
write_targets = args.reject { |a| a.start_with?('-') }
|
|
224
|
+
dest = write_targets.last
|
|
225
|
+
validate_secret_write(dest) if dest
|
|
226
|
+
when 'mkdir', 'touch'
|
|
227
|
+
args.each { |path| validate_secret_write(path) unless path.start_with?('-') }
|
|
228
|
+
when 'echo'
|
|
229
|
+
# `echo foo > path` — best-effort: block only if redirecting to a
|
|
230
|
+
# secret path. The redirect target will also be caught by
|
|
231
|
+
# validate_general_command for /etc /usr /bin; here we add .env,
|
|
232
|
+
# .ssh/, .aws/.
|
|
233
|
+
if command =~ />\s*([^\s|&;]+)/
|
|
234
|
+
validate_secret_write(Regexp.last_match(1))
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
command
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def validate_general_command(command)
|
|
242
|
+
cmd_without_quotes = command.gsub(/'[^']*'|"[^"]*"/, '')
|
|
243
|
+
|
|
244
|
+
dangerous_patterns = [
|
|
245
|
+
/eval\s*\(/,
|
|
246
|
+
/exec\s*\(/,
|
|
247
|
+
/system\s*\(/,
|
|
248
|
+
/`[^`]+`/,
|
|
249
|
+
/\$\([^)]+\)/,
|
|
250
|
+
/\|\s*sh\s*$/,
|
|
251
|
+
/\|\s*bash\s*$/,
|
|
252
|
+
/>\s*\/etc\//,
|
|
253
|
+
/>\s*\/usr\//,
|
|
254
|
+
/>\s*\/bin\//
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
dangerous_patterns.each do |pattern|
|
|
258
|
+
if cmd_without_quotes.match?(pattern)
|
|
259
|
+
raise SecurityError, "Dangerous command pattern detected: #{pattern.source}"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
command
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Block writes that would clobber credentials / secrets.
|
|
267
|
+
# These are the only paths truly dangerous to write to by accident:
|
|
268
|
+
# - ~/.ssh/* (SSH private keys)
|
|
269
|
+
# - ~/.aws/* (AWS credentials)
|
|
270
|
+
# - any *.env file (API keys, DB URLs, etc.)
|
|
271
|
+
#
|
|
272
|
+
# Paths in / outside the project root, Gemfile, README, package.json,
|
|
273
|
+
# etc. are all allowed — the agent is expected to edit them normally.
|
|
274
|
+
SECRET_WRITE_PATTERNS = [
|
|
275
|
+
%r{(?:\A|/)\.ssh/},
|
|
276
|
+
%r{(?:\A|/)\.aws/},
|
|
277
|
+
/(?:\A|\/)\.env(?:\.|\z)/,
|
|
278
|
+
/\.env\z/
|
|
279
|
+
].freeze
|
|
280
|
+
|
|
281
|
+
def validate_secret_write(path)
|
|
282
|
+
return if path.nil? || path.empty? || path.start_with?('-')
|
|
283
|
+
|
|
284
|
+
expanded_path = File.expand_path(path)
|
|
285
|
+
|
|
286
|
+
SECRET_WRITE_PATTERNS.each do |pattern|
|
|
287
|
+
if expanded_path.match?(pattern)
|
|
288
|
+
raise SecurityError,
|
|
289
|
+
"Write to credential/secret path blocked: #{path} " \
|
|
290
|
+
"(matched protected pattern). If intentional, edit the " \
|
|
291
|
+
"file manually outside the agent."
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Alias retained for readability — chmod handler validates that
|
|
297
|
+
# the target is not a credential/secret file.
|
|
298
|
+
def validate_file_path(path)
|
|
299
|
+
validate_secret_write(path)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def log_replacement(original, replacement, reason)
|
|
303
|
+
write_log(
|
|
304
|
+
action: 'command_replacement',
|
|
305
|
+
original_command: original,
|
|
306
|
+
safe_replacement: replacement,
|
|
307
|
+
reason: reason
|
|
308
|
+
)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def log_warning(message)
|
|
312
|
+
write_log(action: 'warning', message: message)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def write_log(**fields)
|
|
316
|
+
log_entry = { timestamp: Time.now.iso8601 }.merge(fields)
|
|
317
|
+
File.open(@safety_log_file, 'a') { |f| f.puts JSON.generate(log_entry) }
|
|
318
|
+
rescue StandardError
|
|
319
|
+
# Logging must never break main functionality.
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
private :replace_chmod_command,
|
|
323
|
+
:replace_curl_pipe_command, :block_sudo_command,
|
|
324
|
+
:allow_dev_null_redirect, :validate_and_allow,
|
|
325
|
+
:validate_general_command,
|
|
326
|
+
:validate_file_path, :validate_secret_write,
|
|
327
|
+
:restart_hint,
|
|
328
|
+
:log_replacement,
|
|
329
|
+
:log_warning, :write_log
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
module Tools
|
|
5
|
+
class Terminal < Base
|
|
6
|
+
# Output cleaning for raw PTY bytes.
|
|
7
|
+
#
|
|
8
|
+
# A PTY emits whatever the child writes plus terminal control codes.
|
|
9
|
+
# Since the Terminal tool is targeted at LINE-BASED interactive shells
|
|
10
|
+
# (not full-screen TUIs like vim/top), we aggressively strip visual
|
|
11
|
+
# control sequences rather than maintain a screen model.
|
|
12
|
+
#
|
|
13
|
+
# Cleaning steps (in order):
|
|
14
|
+
# 1. Strip CSI sequences (ESC[...letter) — colors, cursor, SGR
|
|
15
|
+
# 2. Strip OSC sequences (ESC]...BEL/ST) — window title, etc.
|
|
16
|
+
# 3. Strip simple 2-byte esc (ESC= / ESC>) — keypad modes
|
|
17
|
+
# 4. Collapse \r-overwrites (spinner/progress)
|
|
18
|
+
# 5. Drop backspace erase (char + \x08)
|
|
19
|
+
# 6. Normalize CRLF → LF
|
|
20
|
+
#
|
|
21
|
+
# This is lossy for full-screen apps (you'll see a pile of text without
|
|
22
|
+
# cursor positioning), but for line-based commands it yields clean,
|
|
23
|
+
# diff-friendly output.
|
|
24
|
+
module OutputCleaner
|
|
25
|
+
CSI_REGEX = /\e\[[\d;?]*[a-zA-Z@]/.freeze
|
|
26
|
+
OSC_REGEX = /\e\].*?(\a|\e\\)/m.freeze
|
|
27
|
+
SIMPLE_ESC_REGEX = /\e[=>\(\)].?/.freeze
|
|
28
|
+
BACKSPACE_REGEX = /[^\x08]\x08/.freeze
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# Clean raw PTY bytes for LLM consumption.
|
|
33
|
+
# @param raw [String] raw PTY bytes
|
|
34
|
+
# @return [String] cleaned, UTF-8-safe text
|
|
35
|
+
def clean(raw)
|
|
36
|
+
return "" if raw.nil? || raw.empty?
|
|
37
|
+
|
|
38
|
+
s = raw.dup
|
|
39
|
+
s.force_encoding(Encoding::UTF_8)
|
|
40
|
+
s = s.scrub("?") unless s.valid_encoding?
|
|
41
|
+
|
|
42
|
+
s = s.gsub(CSI_REGEX, "")
|
|
43
|
+
s = s.gsub(OSC_REGEX, "")
|
|
44
|
+
s = s.gsub(SIMPLE_ESC_REGEX, "")
|
|
45
|
+
|
|
46
|
+
# Handle \r overwrites within each line. "50%\r100%" → "100%".
|
|
47
|
+
# Split on \n KEEPING the terminators (-1 preserves trailing empty),
|
|
48
|
+
# then for each segment keep only the portion after the last \r
|
|
49
|
+
# (which is what would actually be visible).
|
|
50
|
+
s = s.split("\n", -1).map { |line| line.split("\r").last || "" }.join("\n")
|
|
51
|
+
|
|
52
|
+
# Erase "X\b" pairs repeatedly (readline rubout).
|
|
53
|
+
s = s.gsub(BACKSPACE_REGEX, "") while s =~ BACKSPACE_REGEX
|
|
54
|
+
|
|
55
|
+
# Normalize any leftover isolated \r.
|
|
56
|
+
s = s.gsub(/\r/, "")
|
|
57
|
+
|
|
58
|
+
s
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|