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,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
# Parser for .gitignore files to determine which files should be ignored
|
|
5
|
+
class GitignoreParser
|
|
6
|
+
attr_reader :patterns
|
|
7
|
+
|
|
8
|
+
def initialize(gitignore_path = nil)
|
|
9
|
+
@patterns = []
|
|
10
|
+
@negation_patterns = []
|
|
11
|
+
|
|
12
|
+
if gitignore_path && File.exist?(gitignore_path)
|
|
13
|
+
parse_gitignore(gitignore_path)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def merge!(other_gitignore_path, prefix: nil)
|
|
18
|
+
return unless other_gitignore_path && File.exist?(other_gitignore_path)
|
|
19
|
+
|
|
20
|
+
File.readlines(other_gitignore_path, chomp: true).each do |line|
|
|
21
|
+
next if line.strip.empty? || line.start_with?('#')
|
|
22
|
+
|
|
23
|
+
negation = line.start_with?('!')
|
|
24
|
+
raw = negation ? line[1..] : line
|
|
25
|
+
info = normalize_pattern(raw)
|
|
26
|
+
|
|
27
|
+
if prefix
|
|
28
|
+
original = info[:pattern]
|
|
29
|
+
original = original[1..] if info[:is_absolute]
|
|
30
|
+
info[:pattern] = "#{prefix}/#{original}"
|
|
31
|
+
info[:is_absolute] = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if negation
|
|
35
|
+
@negation_patterns << info
|
|
36
|
+
else
|
|
37
|
+
@patterns << info
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
warn "Warning: Failed to merge .gitignore: #{e.message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check if a file path should be ignored
|
|
45
|
+
def ignored?(path)
|
|
46
|
+
relative_path = path.start_with?('./') ? path[2..] : path
|
|
47
|
+
|
|
48
|
+
# Check negation patterns first (! prefix in .gitignore)
|
|
49
|
+
@negation_patterns.each do |pattern|
|
|
50
|
+
return false if match_pattern?(relative_path, pattern)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Then check ignore patterns
|
|
54
|
+
@patterns.each do |pattern|
|
|
55
|
+
return true if match_pattern?(relative_path, pattern)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_gitignore(path)
|
|
63
|
+
File.readlines(path, chomp: true).each do |line|
|
|
64
|
+
# Skip comments and empty lines
|
|
65
|
+
next if line.strip.empty? || line.start_with?('#')
|
|
66
|
+
|
|
67
|
+
# Handle negation patterns (lines starting with !)
|
|
68
|
+
if line.start_with?('!')
|
|
69
|
+
@negation_patterns << normalize_pattern(line[1..])
|
|
70
|
+
else
|
|
71
|
+
@patterns << normalize_pattern(line)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
# If we can't parse .gitignore, just continue with empty patterns
|
|
76
|
+
warn "Warning: Failed to parse .gitignore: #{e.message}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def normalize_pattern(pattern)
|
|
80
|
+
pattern = pattern.strip
|
|
81
|
+
|
|
82
|
+
# Remove trailing whitespace
|
|
83
|
+
pattern = pattern.rstrip
|
|
84
|
+
|
|
85
|
+
# Store original for directory detection
|
|
86
|
+
is_directory = pattern.end_with?('/')
|
|
87
|
+
pattern = pattern.chomp('/')
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
pattern: pattern,
|
|
91
|
+
is_directory: is_directory,
|
|
92
|
+
is_absolute: pattern.start_with?('/'),
|
|
93
|
+
has_wildcard: pattern.include?('*') || pattern.include?('?'),
|
|
94
|
+
has_double_star: pattern.include?('**')
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def match_pattern?(path, pattern_info)
|
|
99
|
+
pattern = pattern_info[:pattern]
|
|
100
|
+
is_absolute = pattern_info[:is_absolute]
|
|
101
|
+
|
|
102
|
+
# For absolute patterns (starting with /), remove the leading slash
|
|
103
|
+
# These patterns match from the root of the repository
|
|
104
|
+
if is_absolute
|
|
105
|
+
pattern = pattern[1..]
|
|
106
|
+
# Absolute patterns match exactly from the start of the path
|
|
107
|
+
return true if path == pattern
|
|
108
|
+
return true if path.start_with?("#{pattern}/")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Handle directory patterns
|
|
112
|
+
if pattern_info[:is_directory]
|
|
113
|
+
# Directory patterns should match the directory and all its contents
|
|
114
|
+
return true if path == pattern
|
|
115
|
+
return true if path.start_with?("#{pattern}/")
|
|
116
|
+
# Also check if any path component matches the directory pattern
|
|
117
|
+
return true if path.split('/').include?(pattern)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Handle different wildcard patterns
|
|
121
|
+
if pattern_info[:has_double_star]
|
|
122
|
+
# Convert ** to match any number of directories
|
|
123
|
+
regex_pattern = Regexp.escape(pattern)
|
|
124
|
+
.gsub('\*\*/', '(.*/)?') # **/ matches zero or more directories
|
|
125
|
+
.gsub('\*\*', '.*') # ** at end matches anything
|
|
126
|
+
.gsub('\*', '[^/]*') # * matches anything except /
|
|
127
|
+
.gsub('\?', '[^/]') # ? matches single character except /
|
|
128
|
+
|
|
129
|
+
regex = Regexp.new("^#{regex_pattern}$")
|
|
130
|
+
return true if path.match?(regex)
|
|
131
|
+
return true if path.split('/').any? { |part| part.match?(regex) }
|
|
132
|
+
elsif pattern_info[:has_wildcard]
|
|
133
|
+
# Convert glob pattern to regex
|
|
134
|
+
regex_pattern = Regexp.escape(pattern)
|
|
135
|
+
.gsub('\*', '[^/]*')
|
|
136
|
+
.gsub('\?', '[^/]')
|
|
137
|
+
|
|
138
|
+
regex = Regexp.new("^#{regex_pattern}$")
|
|
139
|
+
return true if path.match?(regex)
|
|
140
|
+
return true if File.basename(path).match?(regex)
|
|
141
|
+
else
|
|
142
|
+
# Exact match - pattern without wildcards
|
|
143
|
+
# Match as basename or as path prefix
|
|
144
|
+
return true if path == pattern
|
|
145
|
+
return true if path.start_with?("#{pattern}/")
|
|
146
|
+
return true if File.basename(path) == pattern
|
|
147
|
+
# Also check if pattern matches any path component
|
|
148
|
+
return true if path.split('/').include?(pattern)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
false
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
module Utils
|
|
5
|
+
# Auto-rolling fixed-size array.
|
|
6
|
+
# Automatically discards oldest elements when the line-count limit is exceeded.
|
|
7
|
+
#
|
|
8
|
+
# Optional limits (all default to nil = no limit):
|
|
9
|
+
# max_line_chars – truncate each individual line to this many characters on push
|
|
10
|
+
# max_chars – once the total accepted chars reach this threshold, further
|
|
11
|
+
# pushes are silently dropped (sets #truncated? = true)
|
|
12
|
+
#
|
|
13
|
+
# These extra limits are fully opt-in; existing callers that only pass max_size
|
|
14
|
+
# are completely unaffected.
|
|
15
|
+
class LimitStack
|
|
16
|
+
attr_reader :max_size, :items
|
|
17
|
+
|
|
18
|
+
def initialize(max_size: 5000, max_line_chars: nil, max_chars: nil)
|
|
19
|
+
@max_size = max_size
|
|
20
|
+
@max_line_chars = max_line_chars
|
|
21
|
+
@max_chars = max_chars
|
|
22
|
+
|
|
23
|
+
@items = []
|
|
24
|
+
@total_chars = 0 # chars currently stored in @items
|
|
25
|
+
@truncated = false
|
|
26
|
+
@chars_full = false # latched true once max_chars is reached
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# True if any content was dropped (lines rolled off the front OR
|
|
30
|
+
# chars budget was exceeded OR a line was truncated).
|
|
31
|
+
def truncated?
|
|
32
|
+
@truncated
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Add elements (supports single or multiple)
|
|
36
|
+
def push(*elements)
|
|
37
|
+
elements.each do |element|
|
|
38
|
+
_push_one(element)
|
|
39
|
+
end
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
alias_method :<<, :push
|
|
43
|
+
|
|
44
|
+
# Add multi-line text (split by lines and add)
|
|
45
|
+
def push_lines(text)
|
|
46
|
+
return self if text.nil? || text.empty?
|
|
47
|
+
|
|
48
|
+
lines = text.is_a?(Array) ? text : text.lines
|
|
49
|
+
lines.each { |line| _push_one(line) }
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Remove and return the last element
|
|
54
|
+
def pop
|
|
55
|
+
item = @items.pop
|
|
56
|
+
@total_chars -= item.length if item.is_a?(String)
|
|
57
|
+
item
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get last N elements
|
|
61
|
+
def last(n = nil)
|
|
62
|
+
n ? @items.last(n) : @items.last
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get all elements
|
|
66
|
+
def to_a
|
|
67
|
+
@items.dup
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Convert to string (for text content)
|
|
71
|
+
def to_s
|
|
72
|
+
@items.join
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Current size
|
|
76
|
+
def size
|
|
77
|
+
@items.size
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Check if empty
|
|
81
|
+
def empty?
|
|
82
|
+
@items.empty?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Clear all elements
|
|
86
|
+
def clear
|
|
87
|
+
@items.clear
|
|
88
|
+
@total_chars = 0
|
|
89
|
+
@truncated = false
|
|
90
|
+
@chars_full = false
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Iterate over elements
|
|
95
|
+
def each(&block)
|
|
96
|
+
@items.each(&block)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# kept for compatibility (called internally; public so subclasses can override)
|
|
100
|
+
def trim_if_needed
|
|
101
|
+
while @items.size > @max_size
|
|
102
|
+
removed = @items.shift
|
|
103
|
+
@total_chars -= removed.length if removed.is_a?(String)
|
|
104
|
+
@truncated = true
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private def _push_one(element)
|
|
109
|
+
# --- chars budget check ---
|
|
110
|
+
if @chars_full
|
|
111
|
+
@truncated = true
|
|
112
|
+
return
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
item = element
|
|
116
|
+
|
|
117
|
+
# --- per-line truncation ---
|
|
118
|
+
if @max_line_chars && item.is_a?(String) && item.length > @max_line_chars
|
|
119
|
+
item = item[0, @max_line_chars]
|
|
120
|
+
# Preserve trailing newline if original had one
|
|
121
|
+
item += "\n" if element.end_with?("\n") && !item.end_with?("\n")
|
|
122
|
+
@truncated = true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# --- total chars check ---
|
|
126
|
+
if @max_chars && item.is_a?(String)
|
|
127
|
+
remaining = @max_chars - @total_chars
|
|
128
|
+
if remaining <= 0
|
|
129
|
+
@chars_full = true
|
|
130
|
+
@truncated = true
|
|
131
|
+
return
|
|
132
|
+
end
|
|
133
|
+
if item.length > remaining
|
|
134
|
+
# If original line ends with \n we must preserve it, so reserve 1
|
|
135
|
+
# byte for it — this keeps total_chars strictly within max_chars.
|
|
136
|
+
needs_newline = element.is_a?(String) && element.end_with?("\n")
|
|
137
|
+
cut = needs_newline ? [remaining - 1, 0].max : remaining
|
|
138
|
+
item = item[0, cut]
|
|
139
|
+
item += "\n" if needs_newline && !item.end_with?("\n")
|
|
140
|
+
@chars_full = true
|
|
141
|
+
@truncated = true
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@items << item
|
|
146
|
+
@total_chars += item.length if item.is_a?(String)
|
|
147
|
+
|
|
148
|
+
trim_if_needed
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Octo
|
|
7
|
+
# Thread-safe daily-rotating file logger.
|
|
8
|
+
#
|
|
9
|
+
# Log files are written to ~/.octo/logger/octo-YYYY-MM-DD.log.
|
|
10
|
+
# At most 7 daily log files are kept; older ones are pruned automatically.
|
|
11
|
+
#
|
|
12
|
+
# Usage (anywhere in the codebase):
|
|
13
|
+
# Octo::Logger.info("server started")
|
|
14
|
+
# Octo::Logger.debug("tool result", tool: "shell", exit_code: 0)
|
|
15
|
+
# Octo::Logger.warn("retry attempt", n: 3)
|
|
16
|
+
# Octo::Logger.error("unhandled exception", error: e)
|
|
17
|
+
module Logger
|
|
18
|
+
LOG_DIR = File.join(Dir.home, ".octo", "logger").freeze
|
|
19
|
+
MAX_LOG_FILES = 7
|
|
20
|
+
MUTEX = Mutex.new
|
|
21
|
+
|
|
22
|
+
# Level constants (numeric, for future filtering)
|
|
23
|
+
LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }.freeze
|
|
24
|
+
|
|
25
|
+
# Minimum level to echo to $stderr when console output is enabled.
|
|
26
|
+
# :debug → all; :info → info/warn/error; :warn → warn/error only
|
|
27
|
+
CONSOLE_MIN_LEVEL = :info
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# Enable/disable echoing log lines to $stderr (in addition to the file).
|
|
31
|
+
# Call Octo::Logger.console = true from server startup to activate.
|
|
32
|
+
attr_writer :console
|
|
33
|
+
|
|
34
|
+
private def console?
|
|
35
|
+
@console ||= false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Path of the log file currently being written to (today's file).
|
|
39
|
+
# File may not exist yet if no log has been emitted today — callers
|
|
40
|
+
# should check File.exist? before reading.
|
|
41
|
+
def current_log_file
|
|
42
|
+
log_file_path(Time.now)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Log at DEBUG level.
|
|
46
|
+
def debug(message, **context)
|
|
47
|
+
write_log(:debug, message, context)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Log at INFO level.
|
|
51
|
+
def info(message, **context)
|
|
52
|
+
write_log(:info, message, context)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Log at WARN level.
|
|
56
|
+
def warn(message, **context)
|
|
57
|
+
write_log(:warn, message, context)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Log at ERROR level. Accepts an optional :error key that may be an
|
|
61
|
+
# Exception; its backtrace is appended automatically.
|
|
62
|
+
def error(message, **context)
|
|
63
|
+
write_log(:error, message, context)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private def write_log(level, message, context = {})
|
|
67
|
+
now = Time.now
|
|
68
|
+
line = format_line(now, level, message, context)
|
|
69
|
+
|
|
70
|
+
MUTEX.synchronize do
|
|
71
|
+
ensure_log_dir
|
|
72
|
+
File.open(log_file_path(now), "a") { |f| f.puts(line) }
|
|
73
|
+
prune_old_logs
|
|
74
|
+
echo_to_console(level, line) if console?
|
|
75
|
+
end
|
|
76
|
+
rescue StandardError
|
|
77
|
+
# Never let logger errors crash the main process.
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private def echo_to_console(level, line)
|
|
82
|
+
return if LEVELS[level] < LEVELS[CONSOLE_MIN_LEVEL]
|
|
83
|
+
$stderr.puts(line)
|
|
84
|
+
rescue StandardError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private def format_line(time, level, message, context)
|
|
89
|
+
timestamp = time.strftime("%Y-%m-%dT%H:%M:%S.%3N%z")
|
|
90
|
+
tag = level.to_s.upcase.ljust(5)
|
|
91
|
+
base = "[#{timestamp}] #{tag} #{message}"
|
|
92
|
+
|
|
93
|
+
if context.empty?
|
|
94
|
+
base
|
|
95
|
+
else
|
|
96
|
+
# Expand exception objects for :error key
|
|
97
|
+
if (err = context[:error]).is_a?(Exception)
|
|
98
|
+
context = context.merge(
|
|
99
|
+
error: "#{err.class}: #{err.message}",
|
|
100
|
+
backtrace: (err.backtrace || []).first(10).join(" | ")
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
pairs = context.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
|
|
104
|
+
"#{base} | #{pairs}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private def log_file_path(time)
|
|
109
|
+
File.join(LOG_DIR, "octo-#{time.strftime('%Y-%m-%d')}.log")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private def ensure_log_dir
|
|
113
|
+
FileUtils.mkdir_p(LOG_DIR) unless Dir.exist?(LOG_DIR)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Remove log files older than MAX_LOG_FILES days.
|
|
117
|
+
private def prune_old_logs
|
|
118
|
+
logs = Dir.glob(File.join(LOG_DIR, "octo-*.log")).sort
|
|
119
|
+
excess = logs.length - MAX_LOG_FILES
|
|
120
|
+
logs.first(excess).each { |f| File.delete(f) } if excess > 0
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
module Utils
|
|
5
|
+
# Spawn child processes in an environment that has the user's shell rc
|
|
6
|
+
# files sourced — so version managers (mise / rbenv / asdf / nvm) and
|
|
7
|
+
# custom PATH entries are active, even when the octo server itself
|
|
8
|
+
# was started by launchd / a desktop icon with a minimal PATH.
|
|
9
|
+
#
|
|
10
|
+
# ## Approach: manual `source` + `exec`
|
|
11
|
+
#
|
|
12
|
+
# Instead of using `$SHELL -l -i -c` (which prints rc banners, triggers
|
|
13
|
+
# job-control warnings in non-tty contexts, and may not even work as
|
|
14
|
+
# expected under launchd), we build an inline shell snippet:
|
|
15
|
+
#
|
|
16
|
+
# { source ~/.zshenv; source ~/.zprofile; source ~/.zshrc; } 1>&2
|
|
17
|
+
# exec <target-cmd>
|
|
18
|
+
#
|
|
19
|
+
# Then invoke it with plain `zsh -c <snippet>` (NO -l / -i flags).
|
|
20
|
+
#
|
|
21
|
+
# Why this wins:
|
|
22
|
+
#
|
|
23
|
+
# - `source ~/.zshrc` runs user's rc code including `eval "$(mise activate zsh)"`
|
|
24
|
+
# which injects the correct PATH (so `node`/`ruby`/`gem` resolve).
|
|
25
|
+
# - `{ … } 1>&2` redirects ALL rc-time output (banners, welcome msgs,
|
|
26
|
+
# mise warnings) to stderr, keeping target's stdout CLEAN — critical
|
|
27
|
+
# for JSON-RPC stdio channels like chrome-devtools-mcp.
|
|
28
|
+
# - `exec` replaces the shell with the target process, so our pipe's
|
|
29
|
+
# child is the target itself (pid / signals / waitpid all work).
|
|
30
|
+
# - No `-i`, so no "no job control in this shell" warnings.
|
|
31
|
+
# - No `-l` needed because we explicitly source what we need.
|
|
32
|
+
#
|
|
33
|
+
# ## Method: login_shell_command
|
|
34
|
+
#
|
|
35
|
+
# Build argv for `Open3.popen3` / `Process.spawn` that runs `command`
|
|
36
|
+
# with rc files pre-sourced. Returns argv, not a running process —
|
|
37
|
+
# caller picks the right Open3 method for their needs.
|
|
38
|
+
module LoginShell
|
|
39
|
+
# Build argv that runs `command` inside a shell with rc files sourced.
|
|
40
|
+
#
|
|
41
|
+
# @param command [String] shell-ready command (caller quotes user input).
|
|
42
|
+
# @return [Array<String>] argv for Open3.popen3 / Process.spawn.
|
|
43
|
+
def self.login_shell_command(command)
|
|
44
|
+
shell = ENV["SHELL"].to_s
|
|
45
|
+
shell = "/bin/bash" if shell.empty? || !File.executable?(shell)
|
|
46
|
+
name = File.basename(shell)
|
|
47
|
+
name = "bash" unless %w[zsh bash].include?(name)
|
|
48
|
+
shell = "/bin/bash" if name == "bash" && !File.executable?(shell)
|
|
49
|
+
|
|
50
|
+
rc_sources = rc_source_snippet(name)
|
|
51
|
+
|
|
52
|
+
# { rc_sources; } 1>&2 — send rc-time stdout to stderr so target's
|
|
53
|
+
# stdout is pristine. `exec` replaces the shell with target.
|
|
54
|
+
script = "{ #{rc_sources}; } 1>&2; exec #{command}"
|
|
55
|
+
[shell, "-c", script]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Per-shell rc source chain. Order matters:
|
|
59
|
+
# zsh: .zshenv → .zprofile → .zshrc (login + interactive equivalent)
|
|
60
|
+
# bash: .profile → .bash_profile → .bashrc
|
|
61
|
+
def self.rc_source_snippet(shell_name)
|
|
62
|
+
files =
|
|
63
|
+
case shell_name
|
|
64
|
+
when "zsh" then %w[.zshenv .zprofile .zshrc]
|
|
65
|
+
else %w[.profile .bash_profile .bashrc]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
files.map { |f| %([ -f "$HOME/#{f}" ] && . "$HOME/#{f}") }.join("; ")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|