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
|
@@ -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
|
|
22
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 "
|
|
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 "
|
|
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
|
|
98
|
+
### Interactive Ruby/IRB Session
|
|
99
99
|
|
|
100
100
|
```bash
|
|
101
|
-
# Start
|
|
102
|
-
tmux -S "$SOCKET" new-session -d -s
|
|
101
|
+
# Start IRB session
|
|
102
|
+
tmux -S "$SOCKET" new-session -d -s ruby "irb"
|
|
103
103
|
|
|
104
|
-
# Send
|
|
105
|
-
tmux -S "$SOCKET" send-keys -t
|
|
106
|
-
tmux -S "$SOCKET" send-keys -t
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
data/lib/pocketrb/tools/base.rb
CHANGED
|
@@ -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
|
-
# @
|
|
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
|
|