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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +98 -17
- data/lib/pocketrb/agent/compaction.rb +235 -22
- data/lib/pocketrb/agent/context.rb +26 -3
- data/lib/pocketrb/agent/loop.rb +53 -5
- data/lib/pocketrb/agent/spawn_tool.rb +14 -0
- data/lib/pocketrb/agent/subagent_manager.rb +24 -2
- data/lib/pocketrb/bus/events.rb +42 -1
- data/lib/pocketrb/bus/message_bus.rb +37 -0
- data/lib/pocketrb/channels/base.rb +13 -0
- data/lib/pocketrb/channels/cli.rb +16 -1
- data/lib/pocketrb/channels/telegram.rb +70 -4
- data/lib/pocketrb/channels/whatsapp.rb +49 -7
- data/lib/pocketrb/cli/chat.rb +7 -0
- data/lib/pocketrb/cli/config.rb +4 -0
- data/lib/pocketrb/cli/cron.rb +6 -0
- data/lib/pocketrb/cli.rb +0 -1
- data/lib/pocketrb/config.rb +55 -6
- data/lib/pocketrb/cron/job.rb +41 -0
- data/lib/pocketrb/cron/service.rb +44 -15
- data/lib/pocketrb/heartbeat/service.rb +21 -0
- data/lib/pocketrb/mcp/client.rb +41 -3
- data/lib/pocketrb/mcp/memory_tool.rb +33 -0
- data/lib/pocketrb/media/processor.rb +32 -1
- data/lib/pocketrb/memory.rb +12 -3
- data/lib/pocketrb/planning/manager.rb +29 -1
- data/lib/pocketrb/planning/plan.rb +52 -0
- data/lib/pocketrb/planning/tool.rb +45 -0
- data/lib/pocketrb/providers/anthropic.rb +113 -9
- data/lib/pocketrb/providers/base.rb +39 -4
- data/lib/pocketrb/providers/claude_cli.rb +79 -7
- data/lib/pocketrb/providers/claude_max_proxy.rb +72 -6
- data/lib/pocketrb/providers/openai_compatible.rb +79 -0
- data/lib/pocketrb/providers/openrouter.rb +72 -12
- data/lib/pocketrb/providers/registry.rb +24 -0
- data/lib/pocketrb/providers/ruby_llm_provider.rb +168 -54
- data/lib/pocketrb/providers/types.rb +51 -1
- data/lib/pocketrb/session/manager.rb +34 -2
- data/lib/pocketrb/session/session.rb +53 -11
- data/lib/pocketrb/skills/builtin/tmux/SKILL.md +9 -9
- data/lib/pocketrb/skills/create_tool.rb +28 -0
- data/lib/pocketrb/skills/loader.rb +22 -1
- data/lib/pocketrb/skills/modify_tool.rb +22 -0
- data/lib/pocketrb/skills/skill.rb +19 -3
- data/lib/pocketrb/tools/background_job_manager.rb +29 -0
- data/lib/pocketrb/tools/base.rb +16 -1
- data/lib/pocketrb/tools/browser.rb +40 -0
- data/lib/pocketrb/tools/browser_advanced.rb +94 -4
- data/lib/pocketrb/tools/browser_session.rb +18 -0
- data/lib/pocketrb/tools/cron.rb +46 -0
- data/lib/pocketrb/tools/edit_file.rb +16 -0
- data/lib/pocketrb/tools/exec.rb +46 -0
- data/lib/pocketrb/tools/jobs.rb +26 -2
- data/lib/pocketrb/tools/list_dir.rb +19 -0
- data/lib/pocketrb/tools/memory.rb +34 -0
- data/lib/pocketrb/tools/message.rb +15 -0
- data/lib/pocketrb/tools/para_memory.rb +47 -0
- data/lib/pocketrb/tools/read_file.rb +11 -0
- data/lib/pocketrb/tools/registry.rb +12 -4
- data/lib/pocketrb/tools/send_file.rb +29 -0
- data/lib/pocketrb/tools/think.rb +9 -0
- data/lib/pocketrb/tools/web_fetch.rb +58 -3
- data/lib/pocketrb/tools/web_search.rb +25 -0
- data/lib/pocketrb/tools/write_file.rb +10 -0
- data/lib/pocketrb/version.rb +2 -1
- data/lib/pocketrb.rb +4 -1
- data/pocketrb.gemspec +6 -8
- 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
|
|
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
|
data/lib/pocketrb/memory.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|