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
@@ -4,19 +4,27 @@ module Pocketrb
4
4
  module MCP
5
5
  # Tool for interacting with memory via MCP
6
6
  class MemoryTool < Tools::Base
7
+ # Initialize memory tool
8
+ # @param context [Hash] Context hash containing :mcp_client (defaults to empty hash)
7
9
  def initialize(context = {})
8
10
  super
9
11
  @client = context[:mcp_client] || Client.new
10
12
  end
11
13
 
14
+ # Tool name
15
+ # @return [String] Tool identifier
12
16
  def name
13
17
  "memory"
14
18
  end
15
19
 
20
+ # Tool description
21
+ # @return [String] Human-readable description
16
22
  def description
17
23
  "Interact with long-term memory. Search for relevant information or store new knowledge for future reference."
18
24
  end
19
25
 
26
+ # Tool parameters schema
27
+ # @return [Hash] JSON schema
20
28
  def parameters
21
29
  {
22
30
  type: "object",
@@ -48,12 +56,21 @@ module Pocketrb
48
56
  }
49
57
  end
50
58
 
59
+ # Check if MCP client is available
60
+ # @return [Boolean] True if client can connect
51
61
  def available?
52
62
  @client.connected? || @client.connect
53
63
  rescue StandardError
54
64
  false
55
65
  end
56
66
 
67
+ # Execute memory operation
68
+ # @param action [String] Action to perform ("search" or "store")
69
+ # @param query [String, nil] Search query (for search action)
70
+ # @param content [String, nil] Content to store (for store action)
71
+ # @param tags [Array<String>, nil] Tags to associate with content (for store action)
72
+ # @param limit [Integer] Maximum search results (defaults to 5)
73
+ # @return [String] JSON result of operation
57
74
  def execute(action:, query: nil, content: nil, tags: nil, limit: 5)
58
75
  case action
59
76
  when "search"
@@ -67,6 +84,10 @@ module Pocketrb
67
84
 
68
85
  private
69
86
 
87
+ # Execute a memory search operation
88
+ # @param query [String] search query
89
+ # @param limit [Integer] maximum number of results
90
+ # @return [String] formatted search results or error message
70
91
  def execute_search(query, limit)
71
92
  return error("Query is required for search") if query.nil? || query.empty?
72
93
 
@@ -79,6 +100,10 @@ module Pocketrb
79
100
  error("Memory search failed: #{e.message}")
80
101
  end
81
102
 
103
+ # Execute a memory store operation
104
+ # @param content [String] text or fact to persist in the memory backend
105
+ # @param tags [Array<String>, nil] tags to associate with content
106
+ # @return [String] success or error message
82
107
  def execute_store(content, tags)
83
108
  return error("Content is required for store") if content.nil? || content.empty?
84
109
 
@@ -98,6 +123,10 @@ module Pocketrb
98
123
  error("Memory store failed: #{e.message}")
99
124
  end
100
125
 
126
+ # Format search results into a readable string
127
+ # @param results [String, Array, Hash] raw search results from MCP
128
+ # @param query [String] original search query
129
+ # @return [String] formatted results
101
130
  def format_search_results(results, query)
102
131
  output = ["Memory search results for: #{query}\n"]
103
132
 
@@ -117,6 +146,10 @@ module Pocketrb
117
146
  output.join("\n")
118
147
  end
119
148
 
149
+ # Format a single search result entry
150
+ # @param result [Hash] result hash with content, score, and tags
151
+ # @param index [Integer] display index (1-based)
152
+ # @return [String] formatted result line
120
153
  def format_result(result, index)
121
154
  content = result["content"] || result["text"] || result.to_s
122
155
  score = result["score"]
@@ -5,6 +5,7 @@ require "fileutils"
5
5
  require "tmpdir"
6
6
 
7
7
  module Pocketrb
8
+ # Media processing for images, files, and attachments
8
9
  module Media
9
10
  # Handles media processing: downloading, encoding, type detection
10
11
  class Processor
@@ -31,13 +32,16 @@ module Pocketrb
31
32
 
32
33
  attr_reader :cache_dir
33
34
 
35
+ # Initialize media processor
36
+ # @param cache_dir [String, Pathname, nil] Directory path for caching downloaded media (defaults to system temp
37
+ # dir)
34
38
  def initialize(cache_dir: nil)
35
39
  @cache_dir = cache_dir || default_cache_dir
36
40
  ensure_cache_dir!
37
41
  end
38
42
 
39
43
  # Download media from URL and return Media object
40
- # @param url [String] URL to download
44
+ # @param url [String] HTTP/HTTPS URL pointing to the media resource to fetch
41
45
  # @param filename [String, nil] Original filename
42
46
  # @param mime_type [String, nil] Known MIME type
43
47
  # @return [Bus::Media]
@@ -169,20 +173,30 @@ module Pocketrb
169
173
 
170
174
  private
171
175
 
176
+ # Default cache directory in system temp dir
177
+ # @return [Pathname] default cache directory path
172
178
  def default_cache_dir
173
179
  Pathname.new(Dir.tmpdir).join("pocketrb-media")
174
180
  end
175
181
 
182
+ # Ensure the cache directory exists
183
+ # @return [void]
176
184
  def ensure_cache_dir!
177
185
  FileUtils.mkdir_p(@cache_dir)
178
186
  end
179
187
 
188
+ # Build a timestamped cache file path for a given filename
189
+ # @param filename [String] original filename to sanitize and store
190
+ # @return [Pathname] full path to cached file
180
191
  def cache_file_path(filename)
181
192
  # Sanitize filename and add timestamp to avoid conflicts
182
193
  safe_name = filename.gsub(/[^a-zA-Z0-9._-]/, "_")
183
194
  @cache_dir.join("#{Time.now.to_i}_#{safe_name}")
184
195
  end
185
196
 
197
+ # Detect media type category from MIME type
198
+ # @param mime_type [String] MIME type string
199
+ # @return [Symbol] media type (:image, :audio, :video, or :file)
186
200
  def detect_type(mime_type)
187
201
  case mime_type
188
202
  when %r{^image/}
@@ -196,11 +210,18 @@ module Pocketrb
196
210
  end
197
211
  end
198
212
 
213
+ # Detect MIME type from file extension
214
+ # @param path [String] file path
215
+ # @return [String] MIME type string
199
216
  def detect_mime_type(path)
200
217
  ext = File.extname(path).downcase
201
218
  MIME_TYPES[ext] || "application/octet-stream"
202
219
  end
203
220
 
221
+ # Base64-encode content if it is a small vision-compatible image
222
+ # @param content [String] binary content
223
+ # @param mime_type [String] MIME type of the content
224
+ # @return [String, nil] base64 string or nil if too large or not an image
204
225
  def encode_if_small(content, mime_type)
205
226
  return nil if content.bytesize > MAX_INLINE_SIZE
206
227
  return nil unless VISION_IMAGE_TYPES.include?(mime_type)
@@ -208,6 +229,10 @@ module Pocketrb
208
229
  Base64.strict_encode64(content)
209
230
  end
210
231
 
232
+ # Extract or generate a filename from a URL
233
+ # @param url [String] source URL
234
+ # @param content_type [String] MIME type for extension fallback
235
+ # @return [String] extracted or generated filename
211
236
  def extract_filename(url, content_type)
212
237
  # Try to get filename from URL
213
238
  uri_path = begin
@@ -225,10 +250,14 @@ module Pocketrb
225
250
  name
226
251
  end
227
252
 
253
+ # Get file extension for a MIME type
254
+ # @param mime_type [String] MIME type string
255
+ # @return [String] file extension without dot
228
256
  def extension_for(mime_type)
229
257
  EXTENSIONS[mime_type] || "bin"
230
258
  end
231
259
 
260
+ # Mapping of file extensions to MIME types
232
261
  MIME_TYPES = {
233
262
  ".jpg" => "image/jpeg",
234
263
  ".jpeg" => "image/jpeg",
@@ -246,6 +275,7 @@ module Pocketrb
246
275
  ".json" => "application/json"
247
276
  }.freeze
248
277
 
278
+ # Mapping of MIME types to file extensions
249
279
  EXTENSIONS = MIME_TYPES.invert.merge(
250
280
  "image/jpeg" => "jpg",
251
281
  "audio/mpeg" => "mp3",
@@ -253,6 +283,7 @@ module Pocketrb
253
283
  ).freeze
254
284
  end
255
285
 
286
+ # Error raised when media processing fails
256
287
  class MediaError < Pocketrb::Error; end
257
288
  end
258
289
  end
@@ -4,8 +4,7 @@ require "json"
4
4
  require "fileutils"
5
5
 
6
6
  module Pocketrb
7
- # Simple memory system for facts and recent events
8
- # Inspired by nanobot - no vector DB, just JSON + keyword matching
7
+ # Simple memory system for facts and recent events. Inspired by nanobot - no vector DB, just JSON + keyword matching
9
8
  class Memory
10
9
  # Maximum number of recent events to keep
11
10
  MAX_RECENT = 50
@@ -241,6 +240,7 @@ module Pocketrb
241
240
  end
242
241
 
243
242
  # Get memory statistics
243
+ # @return [Hash] counts of learned topics, facts, preferences, and events
244
244
  def stats
245
245
  {
246
246
  learned_topics: @facts["learned"].keys.size,
@@ -252,7 +252,8 @@ module Pocketrb
252
252
  }
253
253
  end
254
254
 
255
- # Dump all memories (for debugging)
255
+ # Dump all memories for debugging
256
+ # @return [Hash] all facts and recent events
256
257
  def dump_all
257
258
  {
258
259
  "facts" => @facts,
@@ -262,6 +263,10 @@ module Pocketrb
262
263
 
263
264
  private
264
265
 
266
+ # Load and parse a JSON file
267
+ # @param path [Pathname] path to JSON file
268
+ # @return [Hash, Array, nil] parsed JSON data or nil if file does not exist
269
+ # @raise [Pocketrb::ConfigurationError] if JSON is invalid
265
270
  def load_json(path)
266
271
  return nil unless path.exist? # Missing file OK (first run)
267
272
 
@@ -272,10 +277,14 @@ module Pocketrb
272
277
  "Invalid JSON in #{path}: #{e.message}\nContent preview: #{content[0..100]}"
273
278
  end
274
279
 
280
+ # Persist facts to disk
281
+ # @return [void]
275
282
  def save_facts
276
283
  @facts_file.write(JSON.pretty_generate(@facts))
277
284
  end
278
285
 
286
+ # Persist recent events to disk
287
+ # @return [void]
279
288
  def save_recent
280
289
  @recent_file.write(JSON.pretty_generate(@recent))
281
290
  end
@@ -3,11 +3,14 @@
3
3
  require "json"
4
4
 
5
5
  module Pocketrb
6
+ # Planning system with step-by-step execution plans
6
7
  module Planning
7
8
  # Manages plans persistence and lifecycle
8
9
  class Manager
9
10
  attr_reader :workspace
10
11
 
12
+ # Initialize planning manager
13
+ # @param workspace [String, Pathname] Workspace directory path for storing plans
11
14
  def initialize(workspace:)
12
15
  @workspace = Pathname.new(workspace)
13
16
  @plans_dir = @workspace.join(".pocketrb", "plans")
@@ -36,7 +39,7 @@ module Pocketrb
36
39
 
37
40
  # Get a plan by name
38
41
  # @param name [String] Plan name
39
- # @return [Plan|nil]
42
+ # @return [Plan, nil]
40
43
  def get_plan(name)
41
44
  @plans_cache[name] ||= load_plan(name)
42
45
  end
@@ -62,6 +65,10 @@ module Pocketrb
62
65
  end
63
66
 
64
67
  # Fail a step in a plan
68
+ # @param name [String] Plan name
69
+ # @param step_index [Integer] Index of step to mark as failed
70
+ # @param notes [String, nil] Optional failure notes
71
+ # @return [Plan] Updated plan
65
72
  def fail_step(name:, step_index:, notes: nil)
66
73
  plan = get_plan(name)
67
74
  raise Error, "Plan '#{name}' not found" unless plan
@@ -72,6 +79,8 @@ module Pocketrb
72
79
  end
73
80
 
74
81
  # Activate a plan
82
+ # @param name [String] Plan name to activate
83
+ # @return [Plan] Activated plan
75
84
  def activate_plan(name)
76
85
  plan = get_plan(name)
77
86
  raise Error, "Plan '#{name}' not found" unless plan
@@ -82,6 +91,8 @@ module Pocketrb
82
91
  end
83
92
 
84
93
  # Mark a plan as complete
94
+ # @param name [String] Plan name to mark complete
95
+ # @return [Plan] Completed plan
85
96
  def mark_complete(name)
86
97
  plan = get_plan(name)
87
98
  raise Error, "Plan '#{name}' not found" unless plan
@@ -92,6 +103,8 @@ module Pocketrb
92
103
  end
93
104
 
94
105
  # Cancel a plan
106
+ # @param name [String] Plan name to cancel
107
+ # @return [Plan] Cancelled plan
95
108
  def cancel_plan(name)
96
109
  plan = get_plan(name)
97
110
  raise Error, "Plan '#{name}' not found" unless plan
@@ -102,6 +115,8 @@ module Pocketrb
102
115
  end
103
116
 
104
117
  # Delete a plan
118
+ # @param name [String] Plan name to delete
119
+ # @return [void]
105
120
  def delete_plan(name)
106
121
  file = plan_file(name)
107
122
  File.delete(file) if file.exist?
@@ -124,26 +139,39 @@ module Pocketrb
124
139
  end
125
140
 
126
141
  # Check if a plan exists
142
+ # @param name [String] Plan name to check
143
+ # @return [Boolean] true if plan exists
127
144
  def exists?(name)
128
145
  plan_file(name).exist?
129
146
  end
130
147
 
131
148
  private
132
149
 
150
+ # Ensure the plans directory exists
151
+ # @return [void]
133
152
  def ensure_plans_dir!
134
153
  FileUtils.mkdir_p(@plans_dir) unless @plans_dir.exist?
135
154
  end
136
155
 
156
+ # Build the file path for a plan by name
157
+ # @param name [String] plan name
158
+ # @return [Pathname] sanitized path to the plan JSON file
137
159
  def plan_file(name)
138
160
  safe_name = name.gsub(/[^a-zA-Z0-9_-]/, "_")
139
161
  @plans_dir.join("#{safe_name}.json")
140
162
  end
141
163
 
164
+ # Persist a plan to its JSON file
165
+ # @param plan [Plan] structured plan with steps to write as JSON
166
+ # @return [void]
142
167
  def save_plan(plan)
143
168
  file = plan_file(plan.name)
144
169
  File.write(file, JSON.pretty_generate(plan.to_h))
145
170
  end
146
171
 
172
+ # Load a plan from its JSON file
173
+ # @param name [String] plan name
174
+ # @return [Plan, nil] loaded plan or nil if not found
147
175
  def load_plan(name)
148
176
  file = plan_file(name)
149
177
  return nil unless file.exist?
@@ -9,39 +9,64 @@ module Pocketrb
9
9
 
10
10
  # Step statuses
11
11
  module StepStatus
12
+ # Step is waiting to be started
12
13
  PENDING = "pending"
14
+ # Step is currently being executed
13
15
  IN_PROGRESS = "in_progress"
16
+ # Step was completed successfully
14
17
  COMPLETED = "completed"
18
+ # Step execution failed
15
19
  FAILED = "failed"
20
+ # Step was skipped
16
21
  SKIPPED = "skipped"
17
22
  end
18
23
 
19
24
  # Plan statuses
20
25
  module PlanStatus
26
+ # Plan is being drafted
21
27
  DRAFT = "draft"
28
+ # Plan is actively being executed
22
29
  ACTIVE = "active"
30
+ # Plan completed successfully
23
31
  COMPLETED = "completed"
32
+ # Plan execution failed
24
33
  FAILED = "failed"
34
+ # Plan was cancelled
25
35
  CANCELLED = "cancelled"
26
36
  end
27
37
 
38
+ # Plan step data structure
28
39
  Step = Data.define(:index, :description, :status, :notes, :completed_at) do
40
+ # Initialize plan step
41
+ # @param index [Integer] Step position in the plan (zero-based)
42
+ # @param description [String] Step description or task to complete
43
+ # @param status [String] Step status (defaults to PENDING)
44
+ # @param notes [String, nil] Additional notes or execution details
45
+ # @param completed_at [Time, nil] Timestamp when step was completed
29
46
  def initialize(index:, description:, status: StepStatus::PENDING, notes: nil, completed_at: nil)
30
47
  super
31
48
  end
32
49
 
50
+ # Check if step is pending
51
+ # @return [Boolean] True if status is PENDING
33
52
  def pending?
34
53
  status == StepStatus::PENDING
35
54
  end
36
55
 
56
+ # Check if step is completed
57
+ # @return [Boolean] True if status is COMPLETED
37
58
  def completed?
38
59
  status == StepStatus::COMPLETED
39
60
  end
40
61
 
62
+ # Check if step failed
63
+ # @return [Boolean] True if status is FAILED
41
64
  def failed?
42
65
  status == StepStatus::FAILED
43
66
  end
44
67
 
68
+ # Convert step to hash
69
+ # @return [Hash] Step data as hash
45
70
  def to_h
46
71
  {
47
72
  index: index,
@@ -53,6 +78,12 @@ module Pocketrb
53
78
  end
54
79
  end
55
80
 
81
+ # Initialize plan
82
+ # @param name [String] Plan name
83
+ # @param description [String, nil] Plan description or goal
84
+ # @param steps [Array<Step, String>] Array of steps (Step objects or strings)
85
+ # @param status [String] Plan status (defaults to DRAFT)
86
+ # @param metadata [Hash] Additional metadata (defaults to empty hash)
56
87
  def initialize(name:, description: nil, steps: [], status: PlanStatus::DRAFT, metadata: {})
57
88
  @name = name
58
89
  @description = description
@@ -65,6 +96,8 @@ module Pocketrb
65
96
  end
66
97
 
67
98
  # Add a step to the plan
99
+ # @param description [String] Step description
100
+ # @return [Step] New step instance
68
101
  def add_step(description)
69
102
  step = Step.new(index: @steps.length, description: description)
70
103
  @steps << step
@@ -72,11 +105,17 @@ module Pocketrb
72
105
  end
73
106
 
74
107
  # Add multiple steps
108
+ # @param descriptions [Array<String>] Array of step descriptions
109
+ # @return [void]
75
110
  def add_steps(descriptions)
76
111
  descriptions.each { |d| add_step(d) }
77
112
  end
78
113
 
79
114
  # Update a step's status
115
+ # @param index [Integer] Step index
116
+ # @param status [String] New status value
117
+ # @param notes [String, nil] Optional notes to add
118
+ # @return [Step, nil] Updated step or nil if index invalid
80
119
  def update_step(index, status:, notes: nil)
81
120
  return nil unless @steps[index]
82
121
 
@@ -94,16 +133,25 @@ module Pocketrb
94
133
  end
95
134
 
96
135
  # Mark a step as completed
136
+ # @param index [Integer] Step index
137
+ # @param notes [String, nil] Optional completion notes
138
+ # @return [Step, nil] Updated step or nil if index invalid
97
139
  def complete_step(index, notes: nil)
98
140
  update_step(index, status: StepStatus::COMPLETED, notes: notes)
99
141
  end
100
142
 
101
143
  # Mark a step as failed
144
+ # @param index [Integer] Step index
145
+ # @param notes [String, nil] Optional failure notes
146
+ # @return [Step, nil] Updated step or nil if index invalid
102
147
  def fail_step(index, notes: nil)
103
148
  update_step(index, status: StepStatus::FAILED, notes: notes)
104
149
  end
105
150
 
106
151
  # Skip a step
152
+ # @param index [Integer] Step index
153
+ # @param notes [String, nil] Optional skip reason notes
154
+ # @return [Step, nil] Updated step or nil if index invalid
107
155
  def skip_step(index, notes: nil)
108
156
  update_step(index, status: StepStatus::SKIPPED, notes: notes)
109
157
  end
@@ -119,11 +167,13 @@ module Pocketrb
119
167
  end
120
168
 
121
169
  # Check if plan is complete
170
+ # @return [Boolean] true if all steps are completed
122
171
  def complete?
123
172
  @steps.all?(&:completed?)
124
173
  end
125
174
 
126
175
  # Check if plan has failed
176
+ # @return [Boolean] true if any step has failed
127
177
  def failed?
128
178
  @steps.any?(&:failed?)
129
179
  end
@@ -193,6 +243,8 @@ module Pocketrb
193
243
  end
194
244
 
195
245
  # Create from hash
246
+ # @param hash [Hash] Hash containing plan data
247
+ # @return [Plan] New plan instance
196
248
  def self.from_h(hash)
197
249
  steps = (hash[:steps] || hash["steps"] || []).map do |s|
198
250
  Step.new(
@@ -4,14 +4,20 @@ module Pocketrb
4
4
  module Planning
5
5
  # Tool for creating and managing execution plans
6
6
  class Tool < Tools::Base
7
+ # Tool name
8
+ # @return [String] Tool identifier
7
9
  def name
8
10
  "plan"
9
11
  end
10
12
 
13
+ # Tool description
14
+ # @return [String] Human-readable description
11
15
  def description
12
16
  "Create and manage execution plans for complex tasks. Plans help organize multi-step work and track progress."
13
17
  end
14
18
 
19
+ # Tool parameters schema
20
+ # @return [Hash] JSON schema
15
21
  def parameters
16
22
  {
17
23
  type: "object",
@@ -47,6 +53,14 @@ module Pocketrb
47
53
  }
48
54
  end
49
55
 
56
+ # Execute planning operation
57
+ # @param action [String] Action to perform (create, update, complete, fail, list, show)
58
+ # @param plan_name [String, nil] Plan name
59
+ # @param plan_description [String, nil] Plan description (for create action)
60
+ # @param steps [Array<String>, nil] Step descriptions (for create/update actions)
61
+ # @param step_index [Integer, nil] Step index (for complete/fail actions)
62
+ # @param notes [String, nil] Notes for step completion/failure
63
+ # @return [String] JSON result of operation
50
64
  def execute(
51
65
  action:,
52
66
  plan_name: nil,
@@ -77,10 +91,17 @@ module Pocketrb
77
91
 
78
92
  private
79
93
 
94
+ # Planning manager instance
95
+ # @return [Manager] lazily initialized manager
80
96
  def manager
81
97
  @manager ||= Manager.new(workspace: workspace)
82
98
  end
83
99
 
100
+ # Create and activate a new plan
101
+ # @param name [String] plan name
102
+ # @param description [String, nil] plan description
103
+ # @param steps [Array<String>] step descriptions
104
+ # @return [String] success or error message
84
105
  def create_plan(name, description, steps)
85
106
  return error("Plan name is required") unless name
86
107
  return error("At least one step is required") if steps.nil? || steps.empty?
@@ -94,6 +115,12 @@ module Pocketrb
94
115
  error(e.message)
95
116
  end
96
117
 
118
+ # Update an existing plan with new steps or step completion
119
+ # @param name [String] plan name
120
+ # @param new_steps [Array<String>, nil] steps to add
121
+ # @param step_index [Integer, nil] step index to mark complete
122
+ # @param notes [String, nil] notes for step completion
123
+ # @return [String] success or error message
97
124
  def update_plan(name, new_steps, step_index, notes)
98
125
  return error("Plan name is required") unless name
99
126
 
@@ -109,6 +136,11 @@ module Pocketrb
109
136
  error(e.message)
110
137
  end
111
138
 
139
+ # Mark a step as complete in a plan
140
+ # @param name [String] plan name
141
+ # @param step_index [Integer] step index to complete
142
+ # @param notes [String, nil] completion notes
143
+ # @return [String] success or error message
112
144
  def complete_step(name, step_index, notes)
113
145
  return error("Plan name is required") unless name
114
146
  return error("Step index is required") if step_index.nil?
@@ -125,6 +157,11 @@ module Pocketrb
125
157
  error(e.message)
126
158
  end
127
159
 
160
+ # Mark a step as failed in a plan
161
+ # @param name [String] plan name
162
+ # @param step_index [Integer] step index to fail
163
+ # @param notes [String, nil] failure notes
164
+ # @return [String] success or error message
128
165
  def fail_step(name, step_index, notes)
129
166
  return error("Plan name is required") unless name
130
167
  return error("Step index is required") if step_index.nil?
@@ -135,6 +172,8 @@ module Pocketrb
135
172
  error(e.message)
136
173
  end
137
174
 
175
+ # List all plans with their status
176
+ # @return [String] formatted plan listing
138
177
  def list_plans
139
178
  plans = manager.list_plans
140
179
 
@@ -156,6 +195,9 @@ module Pocketrb
156
195
  output.join("\n")
157
196
  end
158
197
 
198
+ # Show a plan's details in markdown format
199
+ # @param name [String] plan name
200
+ # @return [String] plan markdown or error message
159
201
  def show_plan(name)
160
202
  return error("Plan name is required") unless name
161
203
 
@@ -165,6 +207,9 @@ module Pocketrb
165
207
  plan.to_markdown
166
208
  end
167
209
 
210
+ # Delete a plan by name
211
+ # @param name [String] plan name
212
+ # @return [String] success or error message
168
213
  def delete_plan(name)
169
214
  return error("Plan name is required") unless name
170
215