pocketrb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +32 -0
- data/LICENSE.txt +21 -0
- data/README.md +456 -0
- data/exe/pocketrb +6 -0
- data/lib/pocketrb/agent/compaction.rb +187 -0
- data/lib/pocketrb/agent/context.rb +171 -0
- data/lib/pocketrb/agent/loop.rb +276 -0
- data/lib/pocketrb/agent/spawn_tool.rb +72 -0
- data/lib/pocketrb/agent/subagent_manager.rb +196 -0
- data/lib/pocketrb/bus/events.rb +99 -0
- data/lib/pocketrb/bus/message_bus.rb +148 -0
- data/lib/pocketrb/channels/base.rb +69 -0
- data/lib/pocketrb/channels/cli.rb +109 -0
- data/lib/pocketrb/channels/telegram.rb +607 -0
- data/lib/pocketrb/channels/whatsapp.rb +242 -0
- data/lib/pocketrb/cli/base.rb +119 -0
- data/lib/pocketrb/cli/chat.rb +67 -0
- data/lib/pocketrb/cli/config.rb +52 -0
- data/lib/pocketrb/cli/cron.rb +144 -0
- data/lib/pocketrb/cli/gateway.rb +132 -0
- data/lib/pocketrb/cli/init.rb +39 -0
- data/lib/pocketrb/cli/plans.rb +28 -0
- data/lib/pocketrb/cli/skills.rb +34 -0
- data/lib/pocketrb/cli/start.rb +55 -0
- data/lib/pocketrb/cli/telegram.rb +93 -0
- data/lib/pocketrb/cli/version.rb +18 -0
- data/lib/pocketrb/cli/whatsapp.rb +60 -0
- data/lib/pocketrb/cli.rb +124 -0
- data/lib/pocketrb/config.rb +190 -0
- data/lib/pocketrb/cron/job.rb +155 -0
- data/lib/pocketrb/cron/service.rb +395 -0
- data/lib/pocketrb/heartbeat/service.rb +175 -0
- data/lib/pocketrb/mcp/client.rb +172 -0
- data/lib/pocketrb/mcp/memory_tool.rb +133 -0
- data/lib/pocketrb/media/processor.rb +258 -0
- data/lib/pocketrb/memory.rb +283 -0
- data/lib/pocketrb/planning/manager.rb +159 -0
- data/lib/pocketrb/planning/plan.rb +223 -0
- data/lib/pocketrb/planning/tool.rb +176 -0
- data/lib/pocketrb/providers/anthropic.rb +333 -0
- data/lib/pocketrb/providers/base.rb +98 -0
- data/lib/pocketrb/providers/claude_cli.rb +412 -0
- data/lib/pocketrb/providers/claude_max_proxy.rb +347 -0
- data/lib/pocketrb/providers/openrouter.rb +205 -0
- data/lib/pocketrb/providers/registry.rb +59 -0
- data/lib/pocketrb/providers/ruby_llm_provider.rb +136 -0
- data/lib/pocketrb/providers/types.rb +111 -0
- data/lib/pocketrb/session/manager.rb +192 -0
- data/lib/pocketrb/session/session.rb +204 -0
- data/lib/pocketrb/skills/builtin/github/SKILL.md +113 -0
- data/lib/pocketrb/skills/builtin/proactive/SKILL.md +101 -0
- data/lib/pocketrb/skills/builtin/reflection/SKILL.md +109 -0
- data/lib/pocketrb/skills/builtin/tmux/SKILL.md +130 -0
- data/lib/pocketrb/skills/builtin/weather/SKILL.md +130 -0
- data/lib/pocketrb/skills/create_tool.rb +115 -0
- data/lib/pocketrb/skills/loader.rb +164 -0
- data/lib/pocketrb/skills/modify_tool.rb +123 -0
- data/lib/pocketrb/skills/skill.rb +75 -0
- data/lib/pocketrb/tools/background_job_manager.rb +261 -0
- data/lib/pocketrb/tools/base.rb +118 -0
- data/lib/pocketrb/tools/browser.rb +152 -0
- data/lib/pocketrb/tools/browser_advanced.rb +470 -0
- data/lib/pocketrb/tools/browser_session.rb +167 -0
- data/lib/pocketrb/tools/cron.rb +222 -0
- data/lib/pocketrb/tools/edit_file.rb +101 -0
- data/lib/pocketrb/tools/exec.rb +194 -0
- data/lib/pocketrb/tools/jobs.rb +127 -0
- data/lib/pocketrb/tools/list_dir.rb +102 -0
- data/lib/pocketrb/tools/memory.rb +167 -0
- data/lib/pocketrb/tools/message.rb +70 -0
- data/lib/pocketrb/tools/para_memory.rb +264 -0
- data/lib/pocketrb/tools/read_file.rb +65 -0
- data/lib/pocketrb/tools/registry.rb +160 -0
- data/lib/pocketrb/tools/send_file.rb +158 -0
- data/lib/pocketrb/tools/think.rb +35 -0
- data/lib/pocketrb/tools/web_fetch.rb +150 -0
- data/lib/pocketrb/tools/web_search.rb +102 -0
- data/lib/pocketrb/tools/write_file.rb +55 -0
- data/lib/pocketrb/version.rb +5 -0
- data/lib/pocketrb.rb +75 -0
- data/pocketrb.gemspec +60 -0
- metadata +327 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Pocketrb
|
|
6
|
+
# Configuration management
|
|
7
|
+
class Config
|
|
8
|
+
CONFIG_FILE = "config.yml"
|
|
9
|
+
CONFIG_DIR = ".pocketrb"
|
|
10
|
+
|
|
11
|
+
DEFAULTS = {
|
|
12
|
+
provider: "anthropic",
|
|
13
|
+
model: "claude-sonnet-4-20250514",
|
|
14
|
+
max_iterations: 50,
|
|
15
|
+
heartbeat_interval: 1800, # 30 minutes (1800s)
|
|
16
|
+
mcp_endpoint: "http://localhost:7878",
|
|
17
|
+
log_level: "info",
|
|
18
|
+
session_history_limit: 100,
|
|
19
|
+
tool_timeout: 120
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :workspace, :data
|
|
23
|
+
|
|
24
|
+
def initialize(workspace: nil)
|
|
25
|
+
@workspace = workspace ? Pathname.new(workspace) : nil
|
|
26
|
+
@data = DEFAULTS.dup
|
|
27
|
+
load_config!
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get a config value
|
|
31
|
+
def [](key)
|
|
32
|
+
@data[key.to_sym] || @data[key.to_s]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Set a config value
|
|
36
|
+
def []=(key, value)
|
|
37
|
+
@data[key.to_sym] = value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get with default
|
|
41
|
+
def get(key, default = nil)
|
|
42
|
+
self[key] || default
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Set a value and save
|
|
46
|
+
def set(key, value)
|
|
47
|
+
self[key] = value
|
|
48
|
+
save!
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check if key exists
|
|
52
|
+
def key?(key)
|
|
53
|
+
@data.key?(key.to_sym) || @data.key?(key.to_s)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get provider configuration
|
|
57
|
+
def provider_config
|
|
58
|
+
# Warn about deprecated API key environment variables
|
|
59
|
+
warn_env_deprecated("ANTHROPIC_API_KEY", "anthropic_api_key") if ENV["ANTHROPIC_API_KEY"]
|
|
60
|
+
warn_env_deprecated("OPENROUTER_API_KEY", "openrouter_api_key") if ENV["OPENROUTER_API_KEY"]
|
|
61
|
+
warn_env_deprecated("OPENAI_API_KEY", "openai_api_key") if ENV["OPENAI_API_KEY"]
|
|
62
|
+
warn_env_deprecated("BRAVE_API_KEY", "brave_api_key") if ENV["BRAVE_API_KEY"]
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
anthropic_api_key: ENV["ANTHROPIC_API_KEY"] || self[:anthropic_api_key],
|
|
66
|
+
openrouter_api_key: ENV["OPENROUTER_API_KEY"] || self[:openrouter_api_key],
|
|
67
|
+
openai_api_key: ENV["OPENAI_API_KEY"] || self[:openai_api_key],
|
|
68
|
+
brave_api_key: ENV["BRAVE_API_KEY"] || self[:brave_api_key],
|
|
69
|
+
model: self[:model],
|
|
70
|
+
autonomous: self[:autonomous],
|
|
71
|
+
dangerously_skip_permissions: self[:dangerously_skip_permissions],
|
|
72
|
+
permission_mode: self[:permission_mode],
|
|
73
|
+
system_prompt: self[:system_prompt]
|
|
74
|
+
}.compact
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Save configuration
|
|
78
|
+
def save!
|
|
79
|
+
return unless @workspace
|
|
80
|
+
|
|
81
|
+
config_dir = @workspace.join(CONFIG_DIR)
|
|
82
|
+
FileUtils.mkdir_p(config_dir)
|
|
83
|
+
|
|
84
|
+
config_file = config_dir.join(CONFIG_FILE)
|
|
85
|
+
File.write(config_file, @data.to_yaml)
|
|
86
|
+
|
|
87
|
+
Pocketrb.logger.debug("Saved config to #{config_file}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Reload configuration
|
|
91
|
+
def reload!
|
|
92
|
+
@data = DEFAULTS.dup
|
|
93
|
+
load_config!
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Merge configuration
|
|
97
|
+
def merge!(hash)
|
|
98
|
+
hash.each do |key, value|
|
|
99
|
+
@data[key.to_sym] = value
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Convert to hash
|
|
104
|
+
def to_h
|
|
105
|
+
@data.dup
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Class method to load config
|
|
109
|
+
def self.load(workspace)
|
|
110
|
+
new(workspace: workspace)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Global default config
|
|
114
|
+
def self.default
|
|
115
|
+
@default ||= new
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def load_config!
|
|
121
|
+
load_workspace_config if @workspace
|
|
122
|
+
load_global_config
|
|
123
|
+
load_env_overrides
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def load_workspace_config
|
|
127
|
+
config_file = @workspace.join(CONFIG_DIR, CONFIG_FILE)
|
|
128
|
+
return unless config_file.exist?
|
|
129
|
+
|
|
130
|
+
data = YAML.safe_load_file(config_file, permitted_classes: [Symbol])
|
|
131
|
+
merge!(data) if data.is_a?(Hash)
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
Pocketrb.logger.warn("Failed to load workspace config: #{e.message}")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def load_global_config
|
|
137
|
+
global_config = Pathname.new(Dir.home).join(".pocketrb", CONFIG_FILE)
|
|
138
|
+
return unless global_config.exist?
|
|
139
|
+
|
|
140
|
+
data = YAML.safe_load_file(global_config, permitted_classes: [Symbol])
|
|
141
|
+
# Global config has lower priority than workspace config
|
|
142
|
+
data.each { |k, v| @data[k.to_sym] ||= v } if data.is_a?(Hash)
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
Pocketrb.logger.warn("Failed to load global config: #{e.message}")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def load_env_overrides
|
|
148
|
+
# Environment variables override config file
|
|
149
|
+
warn_env_deprecated("POCKETRB_PROVIDER", "provider") if ENV["POCKETRB_PROVIDER"]
|
|
150
|
+
@data[:provider] = ENV["POCKETRB_PROVIDER"] if ENV["POCKETRB_PROVIDER"]
|
|
151
|
+
|
|
152
|
+
warn_env_deprecated("POCKETRB_MODEL", "model") if ENV["POCKETRB_MODEL"]
|
|
153
|
+
@data[:model] = ENV["POCKETRB_MODEL"] if ENV["POCKETRB_MODEL"]
|
|
154
|
+
|
|
155
|
+
if ENV["POCKETRB_MAX_ITERATIONS"]
|
|
156
|
+
warn_env_deprecated("POCKETRB_MAX_ITERATIONS", "max_iterations")
|
|
157
|
+
@data[:max_iterations] = ENV["POCKETRB_MAX_ITERATIONS"].to_i
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
warn_env_deprecated("MCP_ENDPOINT", "mcp_endpoint") if ENV["MCP_ENDPOINT"]
|
|
161
|
+
@data[:mcp_endpoint] = ENV["MCP_ENDPOINT"] if ENV["MCP_ENDPOINT"]
|
|
162
|
+
|
|
163
|
+
# Autonomous mode (for sandboxed environments)
|
|
164
|
+
if %w[1 true].include?(ENV["POCKETRB_AUTONOMOUS"])
|
|
165
|
+
warn_env_deprecated("POCKETRB_AUTONOMOUS", "autonomous")
|
|
166
|
+
@data[:autonomous] = true
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Log level
|
|
170
|
+
return unless ENV["POCKETRB_LOG_LEVEL"]
|
|
171
|
+
|
|
172
|
+
warn_env_deprecated("POCKETRB_LOG_LEVEL", "log_level")
|
|
173
|
+
@data[:log_level] = ENV.fetch("POCKETRB_LOG_LEVEL", nil)
|
|
174
|
+
Pocketrb.logger.level = Logger.const_get(ENV["POCKETRB_LOG_LEVEL"].upcase)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Warn about deprecated environment variable usage
|
|
178
|
+
# @param env_var [String] environment variable name
|
|
179
|
+
# @param config_key [String] recommended config key
|
|
180
|
+
def warn_env_deprecated(env_var, config_key)
|
|
181
|
+
return if @warned_vars&.include?(env_var)
|
|
182
|
+
|
|
183
|
+
@warned_vars ||= Set.new
|
|
184
|
+
@warned_vars << env_var
|
|
185
|
+
|
|
186
|
+
Pocketrb.logger.warn("[DEPRECATION] ENV['#{env_var}'] is deprecated. " \
|
|
187
|
+
"Use config.yml: #{config_key} = value")
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pocketrb
|
|
4
|
+
module Cron
|
|
5
|
+
# Schedule types
|
|
6
|
+
# - :at - one-time execution at specific timestamp
|
|
7
|
+
# - :every - interval-based execution (every N seconds)
|
|
8
|
+
# - :cron - cron expression-based execution
|
|
9
|
+
Schedule = Data.define(:kind, :at_ms, :every_ms, :expr, :tz) do
|
|
10
|
+
def initialize(kind:, at_ms: nil, every_ms: nil, expr: nil, tz: nil)
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def one_time?
|
|
15
|
+
kind == :at
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def interval?
|
|
19
|
+
kind == :every
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cron?
|
|
23
|
+
kind == :cron
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Payload for job execution
|
|
28
|
+
Payload = Data.define(:message, :deliver, :channel, :to) do
|
|
29
|
+
def initialize(message:, deliver: false, channel: nil, to: nil)
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Job execution state
|
|
35
|
+
JobState = Data.define(:next_run_at_ms, :last_run_at_ms, :last_status, :last_error) do
|
|
36
|
+
def initialize(next_run_at_ms: nil, last_run_at_ms: nil, last_status: nil, last_error: nil)
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A scheduled cron job
|
|
42
|
+
Job = Data.define(
|
|
43
|
+
:id,
|
|
44
|
+
:name,
|
|
45
|
+
:enabled,
|
|
46
|
+
:schedule,
|
|
47
|
+
:payload,
|
|
48
|
+
:state,
|
|
49
|
+
:created_at_ms,
|
|
50
|
+
:updated_at_ms,
|
|
51
|
+
:delete_after_run
|
|
52
|
+
) do
|
|
53
|
+
def initialize(
|
|
54
|
+
id:,
|
|
55
|
+
name:,
|
|
56
|
+
schedule:,
|
|
57
|
+
payload:,
|
|
58
|
+
enabled: true,
|
|
59
|
+
state: JobState.new,
|
|
60
|
+
created_at_ms: nil,
|
|
61
|
+
updated_at_ms: nil,
|
|
62
|
+
delete_after_run: false
|
|
63
|
+
)
|
|
64
|
+
super(
|
|
65
|
+
id: id,
|
|
66
|
+
name: name,
|
|
67
|
+
enabled: enabled,
|
|
68
|
+
schedule: schedule,
|
|
69
|
+
payload: payload,
|
|
70
|
+
state: state,
|
|
71
|
+
created_at_ms: created_at_ms || (Time.now.to_f * 1000).to_i,
|
|
72
|
+
updated_at_ms: updated_at_ms || (Time.now.to_f * 1000).to_i,
|
|
73
|
+
delete_after_run: delete_after_run
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def due?(now_ms = nil)
|
|
78
|
+
return false unless enabled
|
|
79
|
+
return false unless state.next_run_at_ms
|
|
80
|
+
|
|
81
|
+
now_ms ||= (Time.now.to_f * 1000).to_i
|
|
82
|
+
state.next_run_at_ms <= now_ms
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def to_h
|
|
86
|
+
{
|
|
87
|
+
"id" => id,
|
|
88
|
+
"name" => name,
|
|
89
|
+
"enabled" => enabled,
|
|
90
|
+
"schedule" => {
|
|
91
|
+
"kind" => schedule.kind.to_s,
|
|
92
|
+
"at_ms" => schedule.at_ms,
|
|
93
|
+
"every_ms" => schedule.every_ms,
|
|
94
|
+
"expr" => schedule.expr,
|
|
95
|
+
"tz" => schedule.tz
|
|
96
|
+
},
|
|
97
|
+
"payload" => {
|
|
98
|
+
"message" => payload.message,
|
|
99
|
+
"deliver" => payload.deliver,
|
|
100
|
+
"channel" => payload.channel,
|
|
101
|
+
"to" => payload.to
|
|
102
|
+
},
|
|
103
|
+
"state" => {
|
|
104
|
+
"next_run_at_ms" => state.next_run_at_ms,
|
|
105
|
+
"last_run_at_ms" => state.last_run_at_ms,
|
|
106
|
+
"last_status" => state.last_status,
|
|
107
|
+
"last_error" => state.last_error
|
|
108
|
+
},
|
|
109
|
+
"created_at_ms" => created_at_ms,
|
|
110
|
+
"updated_at_ms" => updated_at_ms,
|
|
111
|
+
"delete_after_run" => delete_after_run
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.from_h(hash)
|
|
116
|
+
schedule_h = hash["schedule"]
|
|
117
|
+
schedule = Schedule.new(
|
|
118
|
+
kind: schedule_h["kind"].to_sym,
|
|
119
|
+
at_ms: schedule_h["at_ms"],
|
|
120
|
+
every_ms: schedule_h["every_ms"],
|
|
121
|
+
expr: schedule_h["expr"],
|
|
122
|
+
tz: schedule_h["tz"]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
payload_h = hash["payload"]
|
|
126
|
+
payload = Payload.new(
|
|
127
|
+
message: payload_h["message"],
|
|
128
|
+
deliver: payload_h["deliver"],
|
|
129
|
+
channel: payload_h["channel"],
|
|
130
|
+
to: payload_h["to"]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
state_h = hash["state"] || {}
|
|
134
|
+
state = JobState.new(
|
|
135
|
+
next_run_at_ms: state_h["next_run_at_ms"],
|
|
136
|
+
last_run_at_ms: state_h["last_run_at_ms"],
|
|
137
|
+
last_status: state_h["last_status"],
|
|
138
|
+
last_error: state_h["last_error"]
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
Job.new(
|
|
142
|
+
id: hash["id"],
|
|
143
|
+
name: hash["name"],
|
|
144
|
+
enabled: hash["enabled"],
|
|
145
|
+
schedule: schedule,
|
|
146
|
+
payload: payload,
|
|
147
|
+
state: state,
|
|
148
|
+
created_at_ms: hash["created_at_ms"],
|
|
149
|
+
updated_at_ms: hash["updated_at_ms"],
|
|
150
|
+
delete_after_run: hash["delete_after_run"]
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|