pocketrb 0.1.0

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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +32 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +456 -0
  5. data/exe/pocketrb +6 -0
  6. data/lib/pocketrb/agent/compaction.rb +187 -0
  7. data/lib/pocketrb/agent/context.rb +171 -0
  8. data/lib/pocketrb/agent/loop.rb +276 -0
  9. data/lib/pocketrb/agent/spawn_tool.rb +72 -0
  10. data/lib/pocketrb/agent/subagent_manager.rb +196 -0
  11. data/lib/pocketrb/bus/events.rb +99 -0
  12. data/lib/pocketrb/bus/message_bus.rb +148 -0
  13. data/lib/pocketrb/channels/base.rb +69 -0
  14. data/lib/pocketrb/channels/cli.rb +109 -0
  15. data/lib/pocketrb/channels/telegram.rb +607 -0
  16. data/lib/pocketrb/channels/whatsapp.rb +242 -0
  17. data/lib/pocketrb/cli/base.rb +119 -0
  18. data/lib/pocketrb/cli/chat.rb +67 -0
  19. data/lib/pocketrb/cli/config.rb +52 -0
  20. data/lib/pocketrb/cli/cron.rb +144 -0
  21. data/lib/pocketrb/cli/gateway.rb +132 -0
  22. data/lib/pocketrb/cli/init.rb +39 -0
  23. data/lib/pocketrb/cli/plans.rb +28 -0
  24. data/lib/pocketrb/cli/skills.rb +34 -0
  25. data/lib/pocketrb/cli/start.rb +55 -0
  26. data/lib/pocketrb/cli/telegram.rb +93 -0
  27. data/lib/pocketrb/cli/version.rb +18 -0
  28. data/lib/pocketrb/cli/whatsapp.rb +60 -0
  29. data/lib/pocketrb/cli.rb +124 -0
  30. data/lib/pocketrb/config.rb +190 -0
  31. data/lib/pocketrb/cron/job.rb +155 -0
  32. data/lib/pocketrb/cron/service.rb +395 -0
  33. data/lib/pocketrb/heartbeat/service.rb +175 -0
  34. data/lib/pocketrb/mcp/client.rb +172 -0
  35. data/lib/pocketrb/mcp/memory_tool.rb +133 -0
  36. data/lib/pocketrb/media/processor.rb +258 -0
  37. data/lib/pocketrb/memory.rb +283 -0
  38. data/lib/pocketrb/planning/manager.rb +159 -0
  39. data/lib/pocketrb/planning/plan.rb +223 -0
  40. data/lib/pocketrb/planning/tool.rb +176 -0
  41. data/lib/pocketrb/providers/anthropic.rb +333 -0
  42. data/lib/pocketrb/providers/base.rb +98 -0
  43. data/lib/pocketrb/providers/claude_cli.rb +412 -0
  44. data/lib/pocketrb/providers/claude_max_proxy.rb +347 -0
  45. data/lib/pocketrb/providers/openrouter.rb +205 -0
  46. data/lib/pocketrb/providers/registry.rb +59 -0
  47. data/lib/pocketrb/providers/ruby_llm_provider.rb +136 -0
  48. data/lib/pocketrb/providers/types.rb +111 -0
  49. data/lib/pocketrb/session/manager.rb +192 -0
  50. data/lib/pocketrb/session/session.rb +204 -0
  51. data/lib/pocketrb/skills/builtin/github/SKILL.md +113 -0
  52. data/lib/pocketrb/skills/builtin/proactive/SKILL.md +101 -0
  53. data/lib/pocketrb/skills/builtin/reflection/SKILL.md +109 -0
  54. data/lib/pocketrb/skills/builtin/tmux/SKILL.md +130 -0
  55. data/lib/pocketrb/skills/builtin/weather/SKILL.md +130 -0
  56. data/lib/pocketrb/skills/create_tool.rb +115 -0
  57. data/lib/pocketrb/skills/loader.rb +164 -0
  58. data/lib/pocketrb/skills/modify_tool.rb +123 -0
  59. data/lib/pocketrb/skills/skill.rb +75 -0
  60. data/lib/pocketrb/tools/background_job_manager.rb +261 -0
  61. data/lib/pocketrb/tools/base.rb +118 -0
  62. data/lib/pocketrb/tools/browser.rb +152 -0
  63. data/lib/pocketrb/tools/browser_advanced.rb +470 -0
  64. data/lib/pocketrb/tools/browser_session.rb +167 -0
  65. data/lib/pocketrb/tools/cron.rb +222 -0
  66. data/lib/pocketrb/tools/edit_file.rb +101 -0
  67. data/lib/pocketrb/tools/exec.rb +194 -0
  68. data/lib/pocketrb/tools/jobs.rb +127 -0
  69. data/lib/pocketrb/tools/list_dir.rb +102 -0
  70. data/lib/pocketrb/tools/memory.rb +167 -0
  71. data/lib/pocketrb/tools/message.rb +70 -0
  72. data/lib/pocketrb/tools/para_memory.rb +264 -0
  73. data/lib/pocketrb/tools/read_file.rb +65 -0
  74. data/lib/pocketrb/tools/registry.rb +160 -0
  75. data/lib/pocketrb/tools/send_file.rb +158 -0
  76. data/lib/pocketrb/tools/think.rb +35 -0
  77. data/lib/pocketrb/tools/web_fetch.rb +150 -0
  78. data/lib/pocketrb/tools/web_search.rb +102 -0
  79. data/lib/pocketrb/tools/write_file.rb +55 -0
  80. data/lib/pocketrb/version.rb +5 -0
  81. data/lib/pocketrb.rb +75 -0
  82. data/pocketrb.gemspec +60 -0
  83. metadata +327 -0
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pocketrb
4
+ module Tools
5
+ # Base class for all tools
6
+ class Base
7
+ attr_reader :context
8
+
9
+ def initialize(context = {})
10
+ @context = context
11
+ end
12
+
13
+ # Tool name (must be unique)
14
+ # @return [String]
15
+ def name
16
+ raise NotImplementedError, "#{self.class}#name must be implemented"
17
+ end
18
+
19
+ # Human-readable description
20
+ # @return [String]
21
+ def description
22
+ raise NotImplementedError, "#{self.class}#description must be implemented"
23
+ end
24
+
25
+ # JSON Schema for parameters
26
+ # @return [Hash]
27
+ def parameters
28
+ { type: "object", properties: {}, required: [] }
29
+ end
30
+
31
+ # Execute the tool
32
+ # @param kwargs [Hash] Tool arguments
33
+ # @return [String] Result
34
+ def execute(**kwargs)
35
+ raise NotImplementedError, "#{self.class}#execute must be implemented"
36
+ end
37
+
38
+ # Check if tool is available in current context
39
+ # @return [Boolean]
40
+ def available?
41
+ true
42
+ end
43
+
44
+ # Convert to OpenAI/Anthropic tool definition format
45
+ # @return [Hash]
46
+ def to_definition
47
+ {
48
+ type: "function",
49
+ function: {
50
+ name: name,
51
+ description: description,
52
+ parameters: parameters
53
+ }
54
+ }
55
+ end
56
+
57
+ # Convert to Anthropic-native tool format
58
+ # @return [Hash]
59
+ def to_anthropic_definition
60
+ {
61
+ name: name,
62
+ description: description,
63
+ input_schema: parameters
64
+ }
65
+ end
66
+
67
+ protected
68
+
69
+ # Helper to build a successful result
70
+ def success(message)
71
+ message.to_s
72
+ end
73
+
74
+ # Helper to build an error result
75
+ def error(message)
76
+ "Error: #{message}"
77
+ end
78
+
79
+ # Access workspace from context
80
+ def workspace
81
+ @context[:workspace]
82
+ end
83
+
84
+ # Access bus from context
85
+ def bus
86
+ @context[:bus]
87
+ end
88
+
89
+ # Resolve a path relative to workspace
90
+ def resolve_path(path)
91
+ return Pathname.new(path) if Pathname.new(path).absolute?
92
+
93
+ workspace ? workspace.join(path) : Pathname.new(path)
94
+ end
95
+
96
+ # Check if path is within workspace (security)
97
+ def path_allowed?(path)
98
+ return true unless workspace
99
+
100
+ resolved = resolve_path(path).expand_path
101
+ workspace_expanded = workspace.expand_path
102
+
103
+ resolved.to_s.start_with?(workspace_expanded.to_s)
104
+ end
105
+
106
+ # Validate path is allowed and exists
107
+ def validate_path!(path, must_exist: true)
108
+ resolved = resolve_path(path)
109
+
110
+ raise ToolError, "Path #{path} is outside workspace" unless path_allowed?(path)
111
+
112
+ raise ToolError, "Path does not exist: #{path}" if must_exist && !resolved.exist?
113
+
114
+ resolved
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "playwright"
4
+
5
+ module Pocketrb
6
+ module Tools
7
+ # Browser automation tool using Playwright
8
+ class Browser < Base
9
+ def name
10
+ "browser"
11
+ end
12
+
13
+ def description
14
+ "Automate browser interactions using headless Chromium. Can navigate to URLs, extract text content, take screenshots, interact with elements, and execute JavaScript. Use this for web scraping, research, testing, or any task requiring web browsing."
15
+ end
16
+
17
+ def parameters
18
+ {
19
+ type: "object",
20
+ properties: {
21
+ action: {
22
+ type: "string",
23
+ description: "Action to perform",
24
+ enum: %w[navigate extract_text screenshot click type execute_js]
25
+ },
26
+ url: {
27
+ type: "string",
28
+ description: "URL to navigate to (required for navigate action)"
29
+ },
30
+ selector: {
31
+ type: "string",
32
+ description: "CSS selector for element interaction (required for click, type actions)"
33
+ },
34
+ text: {
35
+ type: "string",
36
+ description: "Text to type (required for type action)"
37
+ },
38
+ javascript: {
39
+ type: "string",
40
+ description: "JavaScript code to execute (required for execute_js action)"
41
+ },
42
+ screenshot_path: {
43
+ type: "string",
44
+ description: "Path to save screenshot (optional, defaults to workspace/screenshot.png)"
45
+ },
46
+ wait_time: {
47
+ type: "integer",
48
+ description: "Milliseconds to wait after navigation (default: 1000)"
49
+ }
50
+ },
51
+ required: ["action"]
52
+ }
53
+ end
54
+
55
+ def execute(action:, url: nil, selector: nil, text: nil, javascript: nil, screenshot_path: nil, wait_time: 1000,
56
+ **_kwargs)
57
+ case action
58
+ when "navigate"
59
+ return error("URL required for navigate action") unless url
60
+
61
+ navigate_and_extract(url, wait_time)
62
+ when "extract_text"
63
+ extract_text_from_page
64
+ when "screenshot"
65
+ take_screenshot(screenshot_path || "screenshot.png")
66
+ when "click"
67
+ return error("Selector required for click action") unless selector
68
+
69
+ click_element(selector, wait_time)
70
+ when "type"
71
+ return error("Selector and text required for type action") unless selector && text
72
+
73
+ type_into_element(selector, text, wait_time)
74
+ when "execute_js"
75
+ return error("JavaScript required for execute_js action") unless javascript
76
+
77
+ execute_javascript(javascript)
78
+ else
79
+ error("Unknown action: #{action}")
80
+ end
81
+ rescue Playwright::Error => e
82
+ error("Playwright error: #{e.message}")
83
+ rescue StandardError => e
84
+ error("Unexpected error: #{e.message}")
85
+ end
86
+
87
+ private
88
+
89
+ def with_browser
90
+ Playwright.create(playwright_cli_executable_path: "npx playwright") do |playwright|
91
+ playwright.chromium.launch(headless: true) do |browser|
92
+ page = browser.new_page
93
+ @current_page = page
94
+ yield page
95
+ end
96
+ end
97
+ ensure
98
+ @current_page = nil
99
+ end
100
+
101
+ def navigate_and_extract(url, wait_time)
102
+ with_browser do |page|
103
+ page.goto(url)
104
+ page.wait_for_timeout(wait_time)
105
+
106
+ title = page.title
107
+ content = page.text_content("body")
108
+
109
+ success("Title: #{title}\n\nContent:\n#{content[0..2000]}#{"...(truncated)" if content.length > 2000}")
110
+ end
111
+ end
112
+
113
+ def extract_text_from_page
114
+ return error("No page is currently loaded. Use navigate action first.") unless @current_page
115
+
116
+ content = @current_page.text_content("body")
117
+ success(content[0..5000] + (content.length > 5000 ? "...(truncated)" : ""))
118
+ end
119
+
120
+ def take_screenshot(path)
121
+ with_browser do |page|
122
+ screenshot_path = resolve_path(path)
123
+ page.screenshot(path: screenshot_path.to_s)
124
+ success("Screenshot saved to #{screenshot_path}")
125
+ end
126
+ end
127
+
128
+ def click_element(selector, wait_time)
129
+ with_browser do |page|
130
+ page.click(selector)
131
+ page.wait_for_timeout(wait_time)
132
+ success("Clicked element: #{selector}")
133
+ end
134
+ end
135
+
136
+ def type_into_element(selector, text, wait_time)
137
+ with_browser do |page|
138
+ page.fill(selector, text)
139
+ page.wait_for_timeout(wait_time)
140
+ success("Typed into element: #{selector}")
141
+ end
142
+ end
143
+
144
+ def execute_javascript(javascript)
145
+ with_browser do |page|
146
+ result = page.evaluate(javascript)
147
+ success("JavaScript executed. Result: #{result.inspect}")
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end