pocketrb 0.1.0 → 0.3.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +98 -17
  4. data/lib/pocketrb/agent/compaction.rb +235 -22
  5. data/lib/pocketrb/agent/context.rb +26 -3
  6. data/lib/pocketrb/agent/loop.rb +53 -5
  7. data/lib/pocketrb/agent/spawn_tool.rb +14 -0
  8. data/lib/pocketrb/agent/subagent_manager.rb +24 -2
  9. data/lib/pocketrb/bus/events.rb +42 -1
  10. data/lib/pocketrb/bus/message_bus.rb +37 -0
  11. data/lib/pocketrb/channels/base.rb +13 -0
  12. data/lib/pocketrb/channels/cli.rb +16 -1
  13. data/lib/pocketrb/channels/telegram.rb +70 -4
  14. data/lib/pocketrb/channels/whatsapp.rb +49 -7
  15. data/lib/pocketrb/cli/chat.rb +7 -0
  16. data/lib/pocketrb/cli/config.rb +4 -0
  17. data/lib/pocketrb/cli/cron.rb +6 -0
  18. data/lib/pocketrb/cli.rb +0 -1
  19. data/lib/pocketrb/config.rb +55 -6
  20. data/lib/pocketrb/cron/job.rb +41 -0
  21. data/lib/pocketrb/cron/service.rb +44 -15
  22. data/lib/pocketrb/heartbeat/service.rb +21 -0
  23. data/lib/pocketrb/mcp/client.rb +41 -3
  24. data/lib/pocketrb/mcp/memory_tool.rb +33 -0
  25. data/lib/pocketrb/media/processor.rb +32 -1
  26. data/lib/pocketrb/memory.rb +12 -3
  27. data/lib/pocketrb/planning/manager.rb +29 -1
  28. data/lib/pocketrb/planning/plan.rb +52 -0
  29. data/lib/pocketrb/planning/tool.rb +45 -0
  30. data/lib/pocketrb/providers/anthropic.rb +113 -9
  31. data/lib/pocketrb/providers/base.rb +39 -4
  32. data/lib/pocketrb/providers/claude_cli.rb +79 -7
  33. data/lib/pocketrb/providers/claude_max_proxy.rb +72 -6
  34. data/lib/pocketrb/providers/openai_compatible.rb +79 -0
  35. data/lib/pocketrb/providers/openrouter.rb +72 -12
  36. data/lib/pocketrb/providers/registry.rb +24 -0
  37. data/lib/pocketrb/providers/ruby_llm_provider.rb +168 -54
  38. data/lib/pocketrb/providers/types.rb +51 -1
  39. data/lib/pocketrb/session/manager.rb +34 -2
  40. data/lib/pocketrb/session/session.rb +53 -11
  41. data/lib/pocketrb/skills/builtin/tmux/SKILL.md +9 -9
  42. data/lib/pocketrb/skills/create_tool.rb +28 -0
  43. data/lib/pocketrb/skills/loader.rb +22 -1
  44. data/lib/pocketrb/skills/modify_tool.rb +22 -0
  45. data/lib/pocketrb/skills/skill.rb +19 -3
  46. data/lib/pocketrb/tools/background_job_manager.rb +29 -0
  47. data/lib/pocketrb/tools/base.rb +16 -1
  48. data/lib/pocketrb/tools/browser.rb +40 -0
  49. data/lib/pocketrb/tools/browser_advanced.rb +94 -4
  50. data/lib/pocketrb/tools/browser_session.rb +18 -0
  51. data/lib/pocketrb/tools/cron.rb +46 -0
  52. data/lib/pocketrb/tools/edit_file.rb +16 -0
  53. data/lib/pocketrb/tools/exec.rb +46 -0
  54. data/lib/pocketrb/tools/jobs.rb +26 -2
  55. data/lib/pocketrb/tools/list_dir.rb +19 -0
  56. data/lib/pocketrb/tools/memory.rb +34 -0
  57. data/lib/pocketrb/tools/message.rb +15 -0
  58. data/lib/pocketrb/tools/para_memory.rb +47 -0
  59. data/lib/pocketrb/tools/read_file.rb +11 -0
  60. data/lib/pocketrb/tools/registry.rb +12 -4
  61. data/lib/pocketrb/tools/send_file.rb +29 -0
  62. data/lib/pocketrb/tools/think.rb +9 -0
  63. data/lib/pocketrb/tools/web_fetch.rb +58 -3
  64. data/lib/pocketrb/tools/web_search.rb +25 -0
  65. data/lib/pocketrb/tools/write_file.rb +10 -0
  66. data/lib/pocketrb/version.rb +2 -1
  67. data/lib/pocketrb.rb +4 -1
  68. data/pocketrb.gemspec +6 -8
  69. metadata +24 -44
@@ -7,6 +7,10 @@ module Pocketrb
7
7
  attr_reader :key, :metadata, :created_at
8
8
  attr_accessor :messages
9
9
 
10
+ # Initialize a new session
11
+ # @param key [String] Unique session identifier
12
+ # @param messages [Array<Message>] Initial messages (defaults to empty array)
13
+ # @param metadata [Hash] Session metadata (defaults to empty hash)
10
14
  def initialize(key:, messages: [], metadata: {})
11
15
  @key = key
12
16
  @messages = messages
@@ -16,15 +20,20 @@ module Pocketrb
16
20
  end
17
21
 
18
22
  # Add a message to the session
19
- # @param role [String] Message role
20
- # @param content [String] Message content
21
- # @param kwargs [Hash] Additional message attributes
22
- def add_message(role:, content:, **kwargs)
23
+ # @param role [String] Message role (user, assistant, tool, system)
24
+ # @param content [String, Array] Message content (text or content blocks)
25
+ # @param name [String, nil] Message name (for tool messages)
26
+ # @param tool_call_id [String, nil] Tool call identifier (for tool result messages)
27
+ # @param tool_calls [Array<ToolCall>, nil] Tool calls made by assistant
28
+ # @return [void]
29
+ def add_message(role:, content:, name: nil, tool_call_id: nil, tool_calls: nil)
23
30
  @mutex.synchronize do
24
31
  message = Providers::Message.new(
25
32
  role: role,
26
33
  content: content,
27
- **kwargs
34
+ name: name,
35
+ tool_call_id: tool_call_id,
36
+ tool_calls: tool_calls
28
37
  )
29
38
  @messages << message
30
39
  save_to_log(message)
@@ -61,6 +70,9 @@ module Pocketrb
61
70
  end
62
71
 
63
72
  # Add an assistant message
73
+ # @param content [String] Assistant response text
74
+ # @param tool_calls [Array<ToolCall>, nil] Tool calls made by the assistant
75
+ # @return [void]
64
76
  def add_assistant_message(content, tool_calls: nil)
65
77
  # Truncate large tool call arguments to prevent context bloat
66
78
  sanitized_calls = sanitize_tool_calls(tool_calls) if tool_calls
@@ -70,6 +82,11 @@ module Pocketrb
70
82
  # Add a tool result message
71
83
  MAX_TOOL_RESULT_LENGTH = 2000
72
84
 
85
+ # Add a tool execution result
86
+ # @param tool_call_id [String] Tool call identifier to link result to request
87
+ # @param name [String] Tool name
88
+ # @param content [String] Tool execution result or output
89
+ # @return [void]
73
90
  def add_tool_result(tool_call_id:, name:, content:)
74
91
  # Truncate large tool results to prevent context bloat
75
92
  truncated_content = if content.is_a?(String) && content.length > MAX_TOOL_RESULT_LENGTH
@@ -87,7 +104,7 @@ module Pocketrb
87
104
  end
88
105
 
89
106
  # Get message history (optionally limited)
90
- # @param max_messages [Integer|nil] Maximum messages to return
107
+ # @param max_messages [Integer, nil] Maximum messages to return
91
108
  # @return [Array<Message>]
92
109
  def get_history(max_messages: nil)
93
110
  @mutex.synchronize do
@@ -97,39 +114,56 @@ module Pocketrb
97
114
  end
98
115
  end
99
116
 
100
- # Clear all messages
117
+ # Clear all messages from session
118
+ # @return [void]
101
119
  def clear
102
120
  @mutex.synchronize do
103
121
  @messages.clear
104
122
  end
105
123
  end
106
124
 
107
- # Get the last message
125
+ # Get the last message in the session
126
+ # @return [Providers::Message, nil] last message or nil
108
127
  def last_message
109
128
  @mutex.synchronize { @messages.last }
110
129
  end
111
130
 
112
- # Number of messages
131
+ # Number of messages in session
132
+ # @return [Integer] message count
113
133
  def message_count
114
134
  @mutex.synchronize { @messages.size }
115
135
  end
116
136
 
117
- # Check if session is empty
137
+ # Check if session has no messages
138
+ # @return [Boolean] true if no messages
118
139
  def empty?
119
140
  @mutex.synchronize { @messages.empty? }
120
141
  end
121
142
 
143
+ # Yield to block while holding the session mutex
144
+ # @yield Block to execute under the session lock
145
+ # @return [Object] Return value of the block
146
+ def with_lock(&)
147
+ @mutex.synchronize(&)
148
+ end
149
+
122
150
  # Set metadata value
151
+ # @param key [String, Symbol] Metadata key
152
+ # @param value [Object] Metadata value (any serializable object)
153
+ # @return [void]
123
154
  def set_meta(key, value)
124
155
  @mutex.synchronize { @metadata[key] = value }
125
156
  end
126
157
 
127
158
  # Get metadata value
159
+ # @param key [String, Symbol] Metadata key
160
+ # @return [Object, nil] Stored value or nil
128
161
  def get_meta(key)
129
162
  @mutex.synchronize { @metadata[key] }
130
163
  end
131
164
 
132
- # Convert to hash for serialization
165
+ # Convert session to hash for serialization
166
+ # @return [Hash] serialized session data
133
167
  def to_h
134
168
  @mutex.synchronize do
135
169
  {
@@ -142,6 +176,8 @@ module Pocketrb
142
176
  end
143
177
 
144
178
  # Create from hash
179
+ # @param hash [Hash] Serialized session data
180
+ # @return [Session] Reconstructed session instance
145
181
  def self.from_h(hash)
146
182
  messages = (hash[:messages] || hash["messages"] || []).map do |m|
147
183
  Providers::Message.new(
@@ -174,6 +210,9 @@ module Pocketrb
174
210
  # Large content (scripts, files) shouldn't be stored in session history
175
211
  MAX_ARG_LENGTH = 500
176
212
 
213
+ # Truncate large arguments in tool calls to prevent context bloat
214
+ # @param tool_calls [Array<Providers::ToolCall>, nil] tool calls to sanitize
215
+ # @return [Array<Providers::ToolCall>, nil] sanitized tool calls
177
216
  def sanitize_tool_calls(tool_calls)
178
217
  return nil if tool_calls.nil?
179
218
 
@@ -195,6 +234,9 @@ module Pocketrb
195
234
  end
196
235
  end
197
236
 
237
+ # Hook for real-time logging of messages
238
+ # @param message [Providers::Message] newly appended conversation turn for real-time logging
239
+ # @return [void]
198
240
  def save_to_log(message)
199
241
  # Session manager handles persistence via JSONL
200
242
  # This is a hook for real-time logging if needed
@@ -40,7 +40,7 @@ SOCKET="$SOCKET_DIR/pocketrb.sock"
40
40
  tmux -S "$SOCKET" new-session -d -s mysession
41
41
 
42
42
  # Create session with initial command
43
- tmux -S "$SOCKET" new-session -d -s mysession "python3"
43
+ tmux -S "$SOCKET" new-session -d -s mysession "irb"
44
44
 
45
45
  # List sessions
46
46
  tmux -S "$SOCKET" list-sessions
@@ -86,7 +86,7 @@ tmux -S "$SOCKET" capture-pane -p -t mysession > output.txt
86
86
 
87
87
  ```bash
88
88
  # Send command
89
- tmux -S "$SOCKET" send-keys -t mysession "python3 script.py" Enter
89
+ tmux -S "$SOCKET" send-keys -t mysession "ruby script.rb" Enter
90
90
 
91
91
  # Wait for completion (simple approach)
92
92
  sleep 2
@@ -95,19 +95,19 @@ sleep 2
95
95
  tmux -S "$SOCKET" capture-pane -p -J -t mysession -S -50
96
96
  ```
97
97
 
98
- ### Interactive Python/REPL Session
98
+ ### Interactive Ruby/IRB Session
99
99
 
100
100
  ```bash
101
- # Start Python session
102
- tmux -S "$SOCKET" new-session -d -s python "python3"
101
+ # Start IRB session
102
+ tmux -S "$SOCKET" new-session -d -s ruby "irb"
103
103
 
104
- # Send Python commands
105
- tmux -S "$SOCKET" send-keys -t python "import os" Enter
106
- tmux -S "$SOCKET" send-keys -t python "print(os.getcwd())" Enter
104
+ # Send Ruby commands
105
+ tmux -S "$SOCKET" send-keys -t ruby "require 'pathname'" Enter
106
+ tmux -S "$SOCKET" send-keys -t ruby "puts Pathname.pwd" Enter
107
107
 
108
108
  # Get output
109
109
  sleep 1
110
- tmux -S "$SOCKET" capture-pane -p -t python
110
+ tmux -S "$SOCKET" capture-pane -p -t ruby
111
111
  ```
112
112
 
113
113
  ### SSH Session
@@ -1,17 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pocketrb
4
+ # Skills system for reusable prompts and capabilities
4
5
  module Skills
5
6
  # Tool for creating new skills at runtime
6
7
  class CreateTool < Tools::Base
8
+ # Tool name
9
+ # @return [String] Tool identifier
7
10
  def name
8
11
  "skill_create"
9
12
  end
10
13
 
14
+ # Tool description
15
+ # @return [String] Human-readable description
11
16
  def description
12
17
  "Create a new skill. Skills are reusable prompts/instructions that can be loaded into context. Use this to teach yourself new capabilities."
13
18
  end
14
19
 
20
+ # Tool parameters schema
21
+ # @return [Hash] JSON schema for tool parameters
15
22
  def parameters
16
23
  {
17
24
  type: "object",
@@ -42,6 +49,13 @@ module Pocketrb
42
49
  }
43
50
  end
44
51
 
52
+ # Execute skill creation
53
+ # @param skill_name [String] Skill name (lowercase, hyphens allowed)
54
+ # @param skill_description [String] Brief skill description
55
+ # @param content [String] Skill content/instructions in markdown
56
+ # @param triggers [Array<String>, nil] Keywords that trigger this skill
57
+ # @param always [Boolean] Whether skill should always be loaded (defaults to false)
58
+ # @return [String] Success or error message
45
59
  def execute(skill_name:, skill_description:, content:, triggers: nil, always: false)
46
60
  # Validate skill name
47
61
  unless valid_skill_name?(skill_name)
@@ -70,10 +84,20 @@ module Pocketrb
70
84
 
71
85
  private
72
86
 
87
+ # Validate skill name format (lowercase alphanumeric with hyphens)
88
+ # @param name [String] skill name to validate
89
+ # @return [Boolean] true if name is valid
73
90
  def valid_skill_name?(name)
74
91
  name.match?(/\A[a-z][a-z0-9-]*\z/)
75
92
  end
76
93
 
94
+ # Build SKILL.md content with YAML frontmatter
95
+ # @param name [String] skill name
96
+ # @param description [String] skill description
97
+ # @param content [String] skill body content
98
+ # @param triggers [Array<String>, nil] trigger keywords
99
+ # @param always [Boolean] whether to always load the skill
100
+ # @return [String] complete SKILL.md file content
77
101
  def build_skill_file(name, description, content, triggers, always)
78
102
  metadata = {
79
103
  "name" => name,
@@ -90,6 +114,10 @@ module Pocketrb
90
114
  MD
91
115
  end
92
116
 
117
+ # Append skill entry to TOOLS.md if it exists
118
+ # @param skill_name [String] skill name to add
119
+ # @param description [String] skill description
120
+ # @return [void]
93
121
  def update_tools_documentation(skill_name, description)
94
122
  tools_file = workspace.join("TOOLS.md")
95
123
  return unless tools_file.exist?
@@ -6,11 +6,16 @@ module Pocketrb
6
6
  module Skills
7
7
  # Loads and manages skills from SKILL.md files
8
8
  class Loader
9
+ # Filename for skill definition
9
10
  SKILL_FILE = "SKILL.md"
11
+ # Regex to parse YAML frontmatter in skill files
10
12
  FRONTMATTER_REGEX = /\A---\s*\n(.+?)\n---\s*\n/m
11
13
 
12
14
  attr_reader :workspace, :builtin_dir
13
15
 
16
+ # Initialize skills loader
17
+ # @param workspace [String, Pathname] Workspace directory containing skills/ folder
18
+ # @param builtin_dir [String, Pathname, nil] Directory with builtin skills (defaults to gem's builtin dir)
14
19
  def initialize(workspace:, builtin_dir: nil)
15
20
  @workspace = Pathname.new(workspace)
16
21
  @builtin_dir = if builtin_dir
@@ -43,7 +48,7 @@ module Pocketrb
43
48
 
44
49
  # Load a specific skill by name
45
50
  # @param name [String] Skill name
46
- # @return [Skill|nil]
51
+ # @return [Skill, nil]
47
52
  def load_skill(name)
48
53
  @skills_cache[name] ||= find_and_load_skill(name)
49
54
  end
@@ -84,12 +89,16 @@ module Pocketrb
84
89
  end
85
90
 
86
91
  # Clear the skills cache
92
+ # @return [void]
87
93
  def clear_cache!
88
94
  @skills_cache.clear
89
95
  end
90
96
 
91
97
  private
92
98
 
99
+ # Load all skills from a directory and its subdirectories
100
+ # @param dir [Pathname] directory to scan for SKILL.md files
101
+ # @return [Array<Skill>] loaded skills
93
102
  def load_from_directory(dir)
94
103
  return [] unless dir.exist?
95
104
 
@@ -108,6 +117,9 @@ module Pocketrb
108
117
  skills.compact
109
118
  end
110
119
 
120
+ # Find and load a skill by name from workspace or builtin directories
121
+ # @param name [String] skill name to find
122
+ # @return [Skill, nil] loaded skill or nil if not found
111
123
  def find_and_load_skill(name)
112
124
  # Check workspace skills
113
125
  skill_path = @workspace.join("skills", name, SKILL_FILE)
@@ -122,6 +134,9 @@ module Pocketrb
122
134
  nil
123
135
  end
124
136
 
137
+ # Parse a SKILL.md file into a Skill object
138
+ # @param path [Pathname] path to SKILL.md file
139
+ # @return [Skill, nil] parsed skill or nil on error
125
140
  def parse_skill_file(path)
126
141
  return nil unless path.exist?
127
142
 
@@ -143,6 +158,9 @@ module Pocketrb
143
158
  nil
144
159
  end
145
160
 
161
+ # Extract YAML frontmatter and body from markdown content
162
+ # @param content [String] raw file content
163
+ # @return [Array] two-element array of metadata Hash and body String
146
164
  def extract_frontmatter(content)
147
165
  if content.match?(FRONTMATTER_REGEX)
148
166
  match = content.match(FRONTMATTER_REGEX)
@@ -154,6 +172,9 @@ module Pocketrb
154
172
  end
155
173
  end
156
174
 
175
+ # Extract a description from the first line of markdown body
176
+ # @param body [String] markdown body text
177
+ # @return [String] extracted description
157
178
  def extract_description(body)
158
179
  # Extract first line or paragraph as description
159
180
  first_line = body.lines.first&.strip
@@ -4,14 +4,20 @@ module Pocketrb
4
4
  module Skills
5
5
  # Tool for modifying existing skills
6
6
  class ModifyTool < Tools::Base
7
+ # Tool name
8
+ # @return [String] Tool identifier
7
9
  def name
8
10
  "skill_modify"
9
11
  end
10
12
 
13
+ # Tool description
14
+ # @return [String] Human-readable description
11
15
  def description
12
16
  "Modify an existing skill. Can update the content, description, triggers, or other properties."
13
17
  end
14
18
 
19
+ # Tool parameters schema
20
+ # @return [Hash] JSON schema for tool parameters
15
21
  def parameters
16
22
  {
17
23
  type: "object",
@@ -51,6 +57,15 @@ module Pocketrb
51
57
  }
52
58
  end
53
59
 
60
+ # Execute skill modification
61
+ # @param skill_name [String] Name of skill to modify
62
+ # @param new_content [String, nil] Replace entire skill content
63
+ # @param append_content [String, nil] Append to existing content
64
+ # @param new_description [String, nil] Update skill description
65
+ # @param add_triggers [Array<String>, nil] Add trigger keywords
66
+ # @param remove_triggers [Array<String>, nil] Remove trigger keywords
67
+ # @param set_always [Boolean, nil] Set always-load flag
68
+ # @return [String] Success or error message
54
69
  def execute(
55
70
  skill_name:,
56
71
  new_content: nil,
@@ -99,6 +114,9 @@ module Pocketrb
99
114
 
100
115
  private
101
116
 
117
+ # Parse YAML frontmatter from skill file content
118
+ # @param content [String] raw file content with optional frontmatter
119
+ # @return [Array] two-element array of metadata Hash and body String
102
120
  def parse_frontmatter(content)
103
121
  if content.match?(/\A---\s*\n(.+?)\n---\s*\n/m)
104
122
  match = content.match(/\A---\s*\n(.+?)\n---\s*\n/m)
@@ -110,6 +128,10 @@ module Pocketrb
110
128
  end
111
129
  end
112
130
 
131
+ # Build SKILL.md content from metadata and body
132
+ # @param metadata [Hash] YAML frontmatter data
133
+ # @param body [String] markdown body content
134
+ # @return [String] complete SKILL.md file content
113
135
  def build_skill_file(metadata, body)
114
136
  <<~MD
115
137
  ---
@@ -6,6 +6,12 @@ module Pocketrb
6
6
  class Skill
7
7
  attr_reader :name, :description, :content, :path, :metadata
8
8
 
9
+ # Initialize skill
10
+ # @param name [String] Skill name
11
+ # @param description [String] Skill description
12
+ # @param content [String] Skill markdown content
13
+ # @param path [Pathname] Path to skill file
14
+ # @param metadata [Hash] Skill metadata (triggers, always, requires, etc.) (defaults to empty hash)
9
15
  def initialize(name:, description:, content:, path:, metadata: {})
10
16
  @name = name
11
17
  @description = description
@@ -15,11 +21,13 @@ module Pocketrb
15
21
  end
16
22
 
17
23
  # Check if skill should always be included in context
24
+ # @return [Boolean] true if always-load flag is set
18
25
  def always?
19
26
  metadata[:always] == true || metadata["always"] == true
20
27
  end
21
28
 
22
- # Check if skill is available (meets requirements)
29
+ # Check if skill is available (all requirements met)
30
+ # @return [Boolean] true if all requirements are satisfied
23
31
  def available?
24
32
  requires = metadata[:requires] || metadata["requires"]
25
33
  return true unless requires
@@ -27,13 +35,16 @@ module Pocketrb
27
35
  Array(requires).all? { |req| check_requirement(req) }
28
36
  end
29
37
 
30
- # Get skill triggers/keywords
38
+ # Get skill trigger keywords
39
+ # @return [Array<String>] trigger keywords
31
40
  def triggers
32
41
  triggers = metadata[:triggers] || metadata["triggers"] || []
33
42
  Array(triggers)
34
43
  end
35
44
 
36
45
  # Check if a message should trigger this skill
46
+ # @param text [String] Message text to check against triggers
47
+ # @return [Boolean] True if any trigger matches
37
48
  def matches?(text)
38
49
  return false if triggers.empty?
39
50
 
@@ -42,6 +53,7 @@ module Pocketrb
42
53
  end
43
54
 
44
55
  # Get full prompt content for this skill
56
+ # @return [String] XML-wrapped skill prompt
45
57
  def to_prompt
46
58
  <<~PROMPT
47
59
  <skill name="#{name}">
@@ -50,13 +62,17 @@ module Pocketrb
50
62
  PROMPT
51
63
  end
52
64
 
53
- # Summary for skills list
65
+ # Summary line for skills list
66
+ # @return [String] formatted summary
54
67
  def to_summary
55
68
  "- #{name}: #{description}"
56
69
  end
57
70
 
58
71
  private
59
72
 
73
+ # Check if a single requirement is satisfied
74
+ # @param req [String] requirement string (e.g., "env:VAR", "file:path", "tool:name")
75
+ # @return [Boolean] true if requirement is met
60
76
  def check_requirement(req)
61
77
  case req
62
78
  when /^env:(.+)/
@@ -6,7 +6,9 @@ module Pocketrb
6
6
  module Tools
7
7
  # Manages background jobs for long-running commands
8
8
  class BackgroundJobManager
9
+ # Maximum number of concurrent background jobs
9
10
  MAX_JOBS = 50
11
+ # Maximum number of completed job records to retain
10
12
  MAX_COMPLETED_JOBS = 20
11
13
  MAX_JOB_AGE = 24 * 60 * 60 # 24 hours
12
14
 
@@ -32,6 +34,8 @@ module Pocketrb
32
34
 
33
35
  attr_reader :jobs_dir
34
36
 
37
+ # Initialize background job manager
38
+ # @param workspace [String, Pathname] Workspace directory path for storing job state
35
39
  def initialize(workspace:)
36
40
  @workspace = Pathname.new(workspace)
37
41
  @jobs_dir = resolve_jobs_dir
@@ -40,12 +44,15 @@ module Pocketrb
40
44
  end
41
45
 
42
46
  # Check if job manager is available (directory accessible)
47
+ # @return [Boolean] true if the jobs directory is usable
43
48
  def available?
44
49
  @available
45
50
  end
46
51
 
47
52
  private
48
53
 
54
+ # Determine the directory for storing job state files
55
+ # @return [Pathname]
49
56
  def resolve_jobs_dir
50
57
  # Don't create .pocketrb in root filesystem
51
58
  if @workspace.to_s == "/" || !@workspace.writable?
@@ -55,6 +62,8 @@ module Pocketrb
55
62
  end
56
63
  end
57
64
 
65
+ # Create the jobs directory if it does not exist
66
+ # @return [Boolean] true if directory is accessible
58
67
  def setup_jobs_dir!
59
68
  FileUtils.mkdir_p(@jobs_dir)
60
69
  true
@@ -66,6 +75,8 @@ module Pocketrb
66
75
  public
67
76
 
68
77
  # Check if command should auto-run in background
78
+ # @param command [String] Shell command to analyze
79
+ # @return [Boolean] true if command matches long-running patterns
69
80
  def long_running?(command)
70
81
  return false if command.nil? || command.empty?
71
82
 
@@ -73,6 +84,10 @@ module Pocketrb
73
84
  end
74
85
 
75
86
  # Start a background job
87
+ # @param command [String] Shell command to execute in background
88
+ # @param working_dir [String, nil] Working directory for command execution
89
+ # @param name [String, nil] Human-readable job name
90
+ # @return [Hash] Job metadata including ID and status
76
91
  def start(command:, working_dir: nil, name: nil)
77
92
  cleanup_stale_jobs
78
93
 
@@ -138,6 +153,8 @@ module Pocketrb
138
153
  end
139
154
 
140
155
  # Get job status and output
156
+ # @param job_id [String] Job identifier
157
+ # @return [Hash, nil] Job metadata with status, output, and command info
141
158
  def status(job_id)
142
159
  job_dir = @jobs_dir.join(job_id)
143
160
  return nil unless job_dir.exist?
@@ -164,6 +181,9 @@ module Pocketrb
164
181
  end
165
182
 
166
183
  # Get job output (tail)
184
+ # @param job_id [String] Job identifier
185
+ # @param lines [Integer] Number of recent output lines to retrieve
186
+ # @return [String, nil] Recent output lines or nil if job not found
167
187
  def output(job_id, lines: 50)
168
188
  log_file = @jobs_dir.join(job_id, "output.log")
169
189
  return nil unless log_file.exist?
@@ -172,6 +192,8 @@ module Pocketrb
172
192
  end
173
193
 
174
194
  # Kill a job
195
+ # @param job_id [String] Job identifier
196
+ # @return [Boolean] true if job was killed successfully
175
197
  def kill(job_id)
176
198
  pid_file = @jobs_dir.join(job_id, "pid")
177
199
  return false unless pid_file.exist?
@@ -244,6 +266,9 @@ module Pocketrb
244
266
 
245
267
  private
246
268
 
269
+ # Check if a process is still running
270
+ # @param pid [Integer] process ID to check
271
+ # @return [Boolean] true if process exists and is running
247
272
  def process_running?(pid)
248
273
  Process.kill(0, pid)
249
274
  true
@@ -251,6 +276,10 @@ module Pocketrb
251
276
  false
252
277
  end
253
278
 
279
+ # Truncate output that exceeds the maximum size
280
+ # @param output [String] output text to truncate
281
+ # @param max_size [Integer] maximum allowed character count
282
+ # @return [String] truncated or original output
254
283
  def truncate_output(output, max_size: 100_000)
255
284
  return output if output.length <= max_size
256
285
 
@@ -6,6 +6,8 @@ module Pocketrb
6
6
  class Base
7
7
  attr_reader :context
8
8
 
9
+ # Initialize tool instance
10
+ # @param context [Hash] Shared context hash containing workspace, bus, and other runtime dependencies
9
11
  def initialize(context = {})
10
12
  @context = context
11
13
  end
@@ -29,8 +31,9 @@ module Pocketrb
29
31
  end
30
32
 
31
33
  # Execute the tool
32
- # @param kwargs [Hash] Tool arguments
34
+ # @option kwargs [Object] * Tool-specific arguments as defined in the parameters schema
33
35
  # @return [String] Result
36
+ # @note Subclasses should override this method and define specific parameters matching their schema
34
37
  def execute(**kwargs)
35
38
  raise NotImplementedError, "#{self.class}#execute must be implemented"
36
39
  end
@@ -67,11 +70,15 @@ module Pocketrb
67
70
  protected
68
71
 
69
72
  # Helper to build a successful result
73
+ # @param message [Object] content to convert to string
74
+ # @return [String]
70
75
  def success(message)
71
76
  message.to_s
72
77
  end
73
78
 
74
79
  # Helper to build an error result
80
+ # @param message [String] error description
81
+ # @return [String]
75
82
  def error(message)
76
83
  "Error: #{message}"
77
84
  end
@@ -87,6 +94,8 @@ module Pocketrb
87
94
  end
88
95
 
89
96
  # Resolve a path relative to workspace
97
+ # @param path [String] file or directory path
98
+ # @return [Pathname]
90
99
  def resolve_path(path)
91
100
  return Pathname.new(path) if Pathname.new(path).absolute?
92
101
 
@@ -94,6 +103,8 @@ module Pocketrb
94
103
  end
95
104
 
96
105
  # Check if path is within workspace (security)
106
+ # @param path [String] file or directory path to check
107
+ # @return [Boolean]
97
108
  def path_allowed?(path)
98
109
  return true unless workspace
99
110
 
@@ -104,6 +115,10 @@ module Pocketrb
104
115
  end
105
116
 
106
117
  # Validate path is allowed and exists
118
+ # @param path [String] file or directory path to validate
119
+ # @param must_exist [Boolean] whether the path must already exist
120
+ # @return [Pathname] resolved path
121
+ # @raise [ToolError] if path is outside workspace or does not exist
107
122
  def validate_path!(path, must_exist: true)
108
123
  resolved = resolve_path(path)
109
124