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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +32 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +456 -0
  5. data/exe/pocketrb +6 -0
  6. data/lib/pocketrb/agent/compaction.rb +187 -0
  7. data/lib/pocketrb/agent/context.rb +171 -0
  8. data/lib/pocketrb/agent/loop.rb +276 -0
  9. data/lib/pocketrb/agent/spawn_tool.rb +72 -0
  10. data/lib/pocketrb/agent/subagent_manager.rb +196 -0
  11. data/lib/pocketrb/bus/events.rb +99 -0
  12. data/lib/pocketrb/bus/message_bus.rb +148 -0
  13. data/lib/pocketrb/channels/base.rb +69 -0
  14. data/lib/pocketrb/channels/cli.rb +109 -0
  15. data/lib/pocketrb/channels/telegram.rb +607 -0
  16. data/lib/pocketrb/channels/whatsapp.rb +242 -0
  17. data/lib/pocketrb/cli/base.rb +119 -0
  18. data/lib/pocketrb/cli/chat.rb +67 -0
  19. data/lib/pocketrb/cli/config.rb +52 -0
  20. data/lib/pocketrb/cli/cron.rb +144 -0
  21. data/lib/pocketrb/cli/gateway.rb +132 -0
  22. data/lib/pocketrb/cli/init.rb +39 -0
  23. data/lib/pocketrb/cli/plans.rb +28 -0
  24. data/lib/pocketrb/cli/skills.rb +34 -0
  25. data/lib/pocketrb/cli/start.rb +55 -0
  26. data/lib/pocketrb/cli/telegram.rb +93 -0
  27. data/lib/pocketrb/cli/version.rb +18 -0
  28. data/lib/pocketrb/cli/whatsapp.rb +60 -0
  29. data/lib/pocketrb/cli.rb +124 -0
  30. data/lib/pocketrb/config.rb +190 -0
  31. data/lib/pocketrb/cron/job.rb +155 -0
  32. data/lib/pocketrb/cron/service.rb +395 -0
  33. data/lib/pocketrb/heartbeat/service.rb +175 -0
  34. data/lib/pocketrb/mcp/client.rb +172 -0
  35. data/lib/pocketrb/mcp/memory_tool.rb +133 -0
  36. data/lib/pocketrb/media/processor.rb +258 -0
  37. data/lib/pocketrb/memory.rb +283 -0
  38. data/lib/pocketrb/planning/manager.rb +159 -0
  39. data/lib/pocketrb/planning/plan.rb +223 -0
  40. data/lib/pocketrb/planning/tool.rb +176 -0
  41. data/lib/pocketrb/providers/anthropic.rb +333 -0
  42. data/lib/pocketrb/providers/base.rb +98 -0
  43. data/lib/pocketrb/providers/claude_cli.rb +412 -0
  44. data/lib/pocketrb/providers/claude_max_proxy.rb +347 -0
  45. data/lib/pocketrb/providers/openrouter.rb +205 -0
  46. data/lib/pocketrb/providers/registry.rb +59 -0
  47. data/lib/pocketrb/providers/ruby_llm_provider.rb +136 -0
  48. data/lib/pocketrb/providers/types.rb +111 -0
  49. data/lib/pocketrb/session/manager.rb +192 -0
  50. data/lib/pocketrb/session/session.rb +204 -0
  51. data/lib/pocketrb/skills/builtin/github/SKILL.md +113 -0
  52. data/lib/pocketrb/skills/builtin/proactive/SKILL.md +101 -0
  53. data/lib/pocketrb/skills/builtin/reflection/SKILL.md +109 -0
  54. data/lib/pocketrb/skills/builtin/tmux/SKILL.md +130 -0
  55. data/lib/pocketrb/skills/builtin/weather/SKILL.md +130 -0
  56. data/lib/pocketrb/skills/create_tool.rb +115 -0
  57. data/lib/pocketrb/skills/loader.rb +164 -0
  58. data/lib/pocketrb/skills/modify_tool.rb +123 -0
  59. data/lib/pocketrb/skills/skill.rb +75 -0
  60. data/lib/pocketrb/tools/background_job_manager.rb +261 -0
  61. data/lib/pocketrb/tools/base.rb +118 -0
  62. data/lib/pocketrb/tools/browser.rb +152 -0
  63. data/lib/pocketrb/tools/browser_advanced.rb +470 -0
  64. data/lib/pocketrb/tools/browser_session.rb +167 -0
  65. data/lib/pocketrb/tools/cron.rb +222 -0
  66. data/lib/pocketrb/tools/edit_file.rb +101 -0
  67. data/lib/pocketrb/tools/exec.rb +194 -0
  68. data/lib/pocketrb/tools/jobs.rb +127 -0
  69. data/lib/pocketrb/tools/list_dir.rb +102 -0
  70. data/lib/pocketrb/tools/memory.rb +167 -0
  71. data/lib/pocketrb/tools/message.rb +70 -0
  72. data/lib/pocketrb/tools/para_memory.rb +264 -0
  73. data/lib/pocketrb/tools/read_file.rb +65 -0
  74. data/lib/pocketrb/tools/registry.rb +160 -0
  75. data/lib/pocketrb/tools/send_file.rb +158 -0
  76. data/lib/pocketrb/tools/think.rb +35 -0
  77. data/lib/pocketrb/tools/web_fetch.rb +150 -0
  78. data/lib/pocketrb/tools/web_search.rb +102 -0
  79. data/lib/pocketrb/tools/write_file.rb +55 -0
  80. data/lib/pocketrb/version.rb +5 -0
  81. data/lib/pocketrb.rb +75 -0
  82. data/pocketrb.gemspec +60 -0
  83. 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