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,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pocketrb
|
|
4
|
+
module Tools
|
|
5
|
+
# List directory contents
|
|
6
|
+
class ListDir < Base
|
|
7
|
+
def name
|
|
8
|
+
"list_dir"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def description
|
|
12
|
+
"List the contents of a directory. Shows files and subdirectories with basic metadata."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parameters
|
|
16
|
+
{
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
path: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Path to the directory to list (defaults to workspace root)"
|
|
22
|
+
},
|
|
23
|
+
pattern: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Glob pattern to filter results (e.g., '*.rb', '**/*.ts')"
|
|
26
|
+
},
|
|
27
|
+
recursive: {
|
|
28
|
+
type: "boolean",
|
|
29
|
+
description: "List subdirectories recursively (default: false)"
|
|
30
|
+
},
|
|
31
|
+
include_hidden: {
|
|
32
|
+
type: "boolean",
|
|
33
|
+
description: "Include hidden files (starting with .) (default: false)"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
required: []
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def execute(path: nil, pattern: nil, recursive: false, include_hidden: false)
|
|
41
|
+
resolved = path ? validate_path!(path) : workspace
|
|
42
|
+
|
|
43
|
+
return error("Not a directory: #{path || "workspace"}") unless resolved&.directory?
|
|
44
|
+
|
|
45
|
+
entries = if pattern
|
|
46
|
+
glob_pattern = resolved.join(recursive ? "**" : "", pattern)
|
|
47
|
+
Dir.glob(glob_pattern)
|
|
48
|
+
elsif recursive
|
|
49
|
+
Dir.glob(resolved.join("**", "*"))
|
|
50
|
+
else
|
|
51
|
+
Dir.children(resolved).map { |e| resolved.join(e).to_s }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Filter hidden files
|
|
55
|
+
entries = entries.reject { |e| File.basename(e).start_with?(".") } unless include_hidden
|
|
56
|
+
|
|
57
|
+
# Sort entries
|
|
58
|
+
entries.sort!
|
|
59
|
+
|
|
60
|
+
# Format output
|
|
61
|
+
output = entries.map do |entry|
|
|
62
|
+
path_obj = Pathname.new(entry)
|
|
63
|
+
relative = workspace ? path_obj.relative_path_from(workspace) : path_obj
|
|
64
|
+
format_entry(path_obj, relative)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if output.empty?
|
|
68
|
+
"Directory is empty or no matches found"
|
|
69
|
+
else
|
|
70
|
+
output.join("\n")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def format_entry(path, relative)
|
|
77
|
+
if path.directory?
|
|
78
|
+
"#{relative}/"
|
|
79
|
+
else
|
|
80
|
+
size = format_size(path.size)
|
|
81
|
+
mtime = path.mtime.strftime("%Y-%m-%d %H:%M")
|
|
82
|
+
"#{relative} (#{size}, #{mtime})"
|
|
83
|
+
end
|
|
84
|
+
rescue StandardError
|
|
85
|
+
relative.to_s
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def format_size(bytes)
|
|
89
|
+
units = %w[B KB MB GB]
|
|
90
|
+
unit_index = 0
|
|
91
|
+
|
|
92
|
+
size = bytes.to_f
|
|
93
|
+
while size >= 1024 && unit_index < units.length - 1
|
|
94
|
+
size /= 1024
|
|
95
|
+
unit_index += 1
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
format("%.1f%s", size, units[unit_index])
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pocketrb
|
|
4
|
+
module Tools
|
|
5
|
+
# Simple memory tool - store and recall facts
|
|
6
|
+
class Memory < Base
|
|
7
|
+
ACTIONS = %w[store recall search recent stats].freeze
|
|
8
|
+
CATEGORIES = %w[learned user preference context].freeze
|
|
9
|
+
|
|
10
|
+
def name
|
|
11
|
+
"memory"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def description
|
|
15
|
+
<<~DESC.strip
|
|
16
|
+
Store and recall memories. Use this to remember important facts, preferences, and learnings.
|
|
17
|
+
|
|
18
|
+
Actions:
|
|
19
|
+
- store: Save a fact (categories: learned, user, preference, context)
|
|
20
|
+
- recall: Search for memories matching a query
|
|
21
|
+
- search: Deep search across all memory categories
|
|
22
|
+
- recent: Show recent events
|
|
23
|
+
- stats: Memory statistics
|
|
24
|
+
DESC
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def parameters
|
|
28
|
+
{
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
action: {
|
|
32
|
+
type: "string",
|
|
33
|
+
enum: ACTIONS,
|
|
34
|
+
description: "Memory action to perform"
|
|
35
|
+
},
|
|
36
|
+
category: {
|
|
37
|
+
type: "string",
|
|
38
|
+
enum: CATEGORIES,
|
|
39
|
+
description: "Category for storing (learned, user, preference, context)"
|
|
40
|
+
},
|
|
41
|
+
key: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Key/topic for the memory"
|
|
44
|
+
},
|
|
45
|
+
value: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Value/content to store"
|
|
48
|
+
},
|
|
49
|
+
query: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Search query for recall/search actions"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
required: ["action"]
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def available?
|
|
59
|
+
!memory_instance.nil?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def execute(action:, category: nil, key: nil, value: nil, query: nil, **)
|
|
63
|
+
return error("Memory not initialized") unless memory_instance
|
|
64
|
+
|
|
65
|
+
case action
|
|
66
|
+
when "store"
|
|
67
|
+
store_memory(category, key, value)
|
|
68
|
+
when "recall"
|
|
69
|
+
recall_memory(query)
|
|
70
|
+
when "search"
|
|
71
|
+
search_memory(query)
|
|
72
|
+
when "recent"
|
|
73
|
+
show_recent
|
|
74
|
+
when "stats"
|
|
75
|
+
show_stats
|
|
76
|
+
else
|
|
77
|
+
error("Unknown action: #{action}")
|
|
78
|
+
end
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
error("Memory error: #{e.message}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def memory_instance
|
|
86
|
+
@context[:memory]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def store_memory(category, key, value)
|
|
90
|
+
return error("Category, key, and value required") unless category && key && value
|
|
91
|
+
|
|
92
|
+
result = case category
|
|
93
|
+
when "learned"
|
|
94
|
+
memory_instance.remember_learned(key, value)
|
|
95
|
+
when "user"
|
|
96
|
+
memory_instance.remember_user(key, value)
|
|
97
|
+
when "preference"
|
|
98
|
+
memory_instance.remember_preference(key, value)
|
|
99
|
+
when "context"
|
|
100
|
+
memory_instance.remember_context(key, value)
|
|
101
|
+
else
|
|
102
|
+
return error("Invalid category. Use: learned, user, preference, or context")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
success(result)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def recall_memory(query)
|
|
109
|
+
return error("Query required") unless query
|
|
110
|
+
|
|
111
|
+
context = memory_instance.relevant_context(query, max_facts: 10)
|
|
112
|
+
|
|
113
|
+
if context.empty?
|
|
114
|
+
"No relevant memories found for: #{query}"
|
|
115
|
+
else
|
|
116
|
+
"Relevant memories:\n\n#{context}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def search_memory(query)
|
|
121
|
+
return error("Query required") unless query
|
|
122
|
+
|
|
123
|
+
results = memory_instance.search(query)
|
|
124
|
+
|
|
125
|
+
return "No memories found matching: #{query}" if results.empty?
|
|
126
|
+
|
|
127
|
+
lines = ["Found #{results.size} memories:"]
|
|
128
|
+
results.each_with_index do |result, i|
|
|
129
|
+
lines << "\n#{i + 1}. [#{result[:type]}] #{result[:topic] || result[:key]}"
|
|
130
|
+
lines << " #{result[:content] || result[:value]}"
|
|
131
|
+
lines << " (#{result[:date]})" if result[:date]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
lines.join("\n")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def show_recent
|
|
138
|
+
events = memory_instance.recent_events(10)
|
|
139
|
+
|
|
140
|
+
return "No recent events recorded." if events.empty?
|
|
141
|
+
|
|
142
|
+
lines = ["Recent events (#{events.size}):"]
|
|
143
|
+
events.each do |event|
|
|
144
|
+
timestamp = Time.parse(event["timestamp"]).strftime("%Y-%m-%d %H:%M")
|
|
145
|
+
lines << "- [#{timestamp}] #{event["description"]}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
lines.join("\n")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def show_stats
|
|
152
|
+
stats = memory_instance.stats
|
|
153
|
+
|
|
154
|
+
lines = [
|
|
155
|
+
"Memory Statistics:",
|
|
156
|
+
"- Learned topics: #{stats[:learned_topics]} (#{stats[:total_learned]} total facts)",
|
|
157
|
+
"- User facts: #{stats[:user_facts]}",
|
|
158
|
+
"- Preferences: #{stats[:preferences]}",
|
|
159
|
+
"- Context items: #{stats[:context_items]}",
|
|
160
|
+
"- Recent events: #{stats[:recent_events]}"
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
lines.join("\n")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pocketrb
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for sending messages to chat channels programmatically
|
|
6
|
+
class Message < Base
|
|
7
|
+
def name
|
|
8
|
+
"message"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def description
|
|
12
|
+
"Send a message to a chat channel. Use this to proactively communicate with users, send notifications, or respond on specific channels."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parameters
|
|
16
|
+
{
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
content: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "The message content to send"
|
|
22
|
+
},
|
|
23
|
+
channel: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Target channel (telegram, whatsapp, cli). Uses default if not specified."
|
|
26
|
+
},
|
|
27
|
+
chat_id: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Recipient chat ID. Uses default if not specified."
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
required: ["content"]
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def execute(content:, channel: nil, chat_id: nil)
|
|
37
|
+
# Use defaults from context if not provided
|
|
38
|
+
channel = (channel || @context[:default_channel])&.to_sym
|
|
39
|
+
chat_id ||= @context[:default_chat_id]
|
|
40
|
+
|
|
41
|
+
return error("No channel specified and no default channel in context") unless channel
|
|
42
|
+
|
|
43
|
+
return error("No chat_id specified and no default chat_id in context") unless chat_id
|
|
44
|
+
|
|
45
|
+
return error("Message bus not available in context") unless bus
|
|
46
|
+
|
|
47
|
+
outbound = Bus::OutboundMessage.new(
|
|
48
|
+
channel: channel,
|
|
49
|
+
chat_id: chat_id,
|
|
50
|
+
content: content
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
bus.publish_outbound(outbound)
|
|
54
|
+
Pocketrb.logger.info("Message sent to #{channel}:#{chat_id}")
|
|
55
|
+
|
|
56
|
+
success("Message sent to #{channel}:#{chat_id}")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def available?
|
|
60
|
+
!bus.nil?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def bus
|
|
66
|
+
@context[:bus]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pocketrb
|
|
4
|
+
module Tools
|
|
5
|
+
# PARA-based memory tool for structured knowledge management
|
|
6
|
+
class ParaMemory < Base
|
|
7
|
+
def name
|
|
8
|
+
"para_memory"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def description
|
|
12
|
+
<<~DESC.strip
|
|
13
|
+
Structured memory using PARA method. Store and retrieve facts about people, projects,
|
|
14
|
+
companies, and topics. Facts are organized, decay over time, and build a knowledge graph.
|
|
15
|
+
Use this for durable information that should persist across conversations.
|
|
16
|
+
DESC
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parameters
|
|
20
|
+
{
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
action: {
|
|
24
|
+
type: "string",
|
|
25
|
+
enum: %w[store search entity_info create_entity list_entities context preferences learn_preference],
|
|
26
|
+
description: "Action to perform"
|
|
27
|
+
},
|
|
28
|
+
# For store action
|
|
29
|
+
content: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Fact content to store"
|
|
32
|
+
},
|
|
33
|
+
category: {
|
|
34
|
+
type: "string",
|
|
35
|
+
enum: %w[relationship milestone status preference context],
|
|
36
|
+
description: "Fact category (for store)"
|
|
37
|
+
},
|
|
38
|
+
entity_type: {
|
|
39
|
+
type: "string",
|
|
40
|
+
enum: %w[projects areas resources archives],
|
|
41
|
+
description: "PARA entity type"
|
|
42
|
+
},
|
|
43
|
+
entity_name: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Entity name (e.g., 'zakiya', 'people/john', 'companies/acme')"
|
|
46
|
+
},
|
|
47
|
+
# For search action
|
|
48
|
+
query: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Search query"
|
|
51
|
+
},
|
|
52
|
+
# For create_entity
|
|
53
|
+
entity_subtype: {
|
|
54
|
+
type: "string",
|
|
55
|
+
enum: %w[project person company topic],
|
|
56
|
+
description: "What kind of entity to create"
|
|
57
|
+
},
|
|
58
|
+
# For learn_preference
|
|
59
|
+
preference_category: {
|
|
60
|
+
type: "string",
|
|
61
|
+
enum: %w[communication_preferences working_style tool_preferences rules_and_boundaries],
|
|
62
|
+
description: "Preference category"
|
|
63
|
+
},
|
|
64
|
+
preference_key: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Preference key (e.g., 'verbosity', 'timezone')"
|
|
67
|
+
},
|
|
68
|
+
preference_value: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "Preference value"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
required: ["action"]
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def available?
|
|
78
|
+
!para_manager.nil?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def execute(action:, **args)
|
|
82
|
+
return error("PARA memory not available. Configure memory_dir in agent settings.") unless para_manager
|
|
83
|
+
|
|
84
|
+
case action
|
|
85
|
+
when "store"
|
|
86
|
+
store_fact(args)
|
|
87
|
+
when "search"
|
|
88
|
+
search_memory(args[:query])
|
|
89
|
+
when "entity_info"
|
|
90
|
+
get_entity_info(args[:entity_type], args[:entity_name])
|
|
91
|
+
when "create_entity"
|
|
92
|
+
create_entity(args)
|
|
93
|
+
when "list_entities"
|
|
94
|
+
list_entities(args[:entity_type])
|
|
95
|
+
when "context"
|
|
96
|
+
get_context(args[:query])
|
|
97
|
+
when "preferences"
|
|
98
|
+
get_preferences
|
|
99
|
+
when "learn_preference"
|
|
100
|
+
learn_preference(args)
|
|
101
|
+
else
|
|
102
|
+
error("Unknown action: #{action}")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def para_manager
|
|
109
|
+
@context[:para_manager]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def store_fact(args)
|
|
113
|
+
content = args[:content]
|
|
114
|
+
return error("Content required") if content.nil? || content.empty?
|
|
115
|
+
|
|
116
|
+
category = (args[:category] || "context").to_sym
|
|
117
|
+
entity_type = args[:entity_type]&.to_sym
|
|
118
|
+
entity_name = args[:entity_name]
|
|
119
|
+
|
|
120
|
+
if entity_type && entity_name
|
|
121
|
+
fact = para_manager.store_fact(
|
|
122
|
+
entity_type: entity_type,
|
|
123
|
+
entity_name: entity_name,
|
|
124
|
+
content: content,
|
|
125
|
+
category: category
|
|
126
|
+
)
|
|
127
|
+
success("Stored fact in #{entity_type}/#{entity_name}: #{content[0..50]}... (ID: #{fact.id})")
|
|
128
|
+
else
|
|
129
|
+
para_manager.remember(content: content, category: category)
|
|
130
|
+
success("Stored fact: #{content[0..50]}...")
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def search_memory(query)
|
|
135
|
+
return error("Query required") if query.nil? || query.empty?
|
|
136
|
+
|
|
137
|
+
results = para_manager.search(query)
|
|
138
|
+
|
|
139
|
+
lines = ["Search results for: #{query}\n"]
|
|
140
|
+
|
|
141
|
+
if results[:knowledge_graph].any?
|
|
142
|
+
lines << "**Knowledge Graph:**"
|
|
143
|
+
results[:knowledge_graph].each do |r|
|
|
144
|
+
lines << " #{r[:entity]}:"
|
|
145
|
+
r[:facts].first(3).each { |f| lines << " - #{f}" }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
if results[:daily_notes].any?
|
|
150
|
+
lines << "\n**Daily Notes:**"
|
|
151
|
+
results[:daily_notes].first(5).each do |r|
|
|
152
|
+
lines << " #{r[:date]}:"
|
|
153
|
+
r[:matches].first(2).each { |m| lines << " - #{m}" }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
if results[:tacit].any?
|
|
158
|
+
lines << "\n**Preferences:**"
|
|
159
|
+
results[:tacit].each do |r|
|
|
160
|
+
lines << " #{r[:category]}/#{r[:key]}: #{r[:value]}"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
lines << "No results found." if lines.size == 1
|
|
165
|
+
|
|
166
|
+
lines.join("\n")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def get_entity_info(entity_type, entity_name)
|
|
170
|
+
return error("Entity type and name required") unless entity_type && entity_name
|
|
171
|
+
|
|
172
|
+
summary = para_manager.entity_summary(type: entity_type.to_sym, name: entity_name)
|
|
173
|
+
return error("Entity not found: #{entity_type}/#{entity_name}") unless summary
|
|
174
|
+
|
|
175
|
+
summary
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def create_entity(args)
|
|
179
|
+
subtype = args[:entity_subtype]
|
|
180
|
+
name = args[:entity_name]
|
|
181
|
+
content = args[:content]
|
|
182
|
+
|
|
183
|
+
return error("Entity subtype and name required") unless subtype && name
|
|
184
|
+
|
|
185
|
+
case subtype
|
|
186
|
+
when "project"
|
|
187
|
+
para_manager.create_project(name: name, description: content)
|
|
188
|
+
success("Created project: #{name}")
|
|
189
|
+
when "person"
|
|
190
|
+
para_manager.create_person(name: name, relationship: content)
|
|
191
|
+
success("Created person: #{name}")
|
|
192
|
+
when "company"
|
|
193
|
+
para_manager.create_company(name: name, context: content)
|
|
194
|
+
success("Created company: #{name}")
|
|
195
|
+
when "topic"
|
|
196
|
+
para_manager.knowledge_graph.create_entity(
|
|
197
|
+
type: :resources,
|
|
198
|
+
name: name,
|
|
199
|
+
initial_facts: content ? [{ content: content, category: :context }] : []
|
|
200
|
+
)
|
|
201
|
+
success("Created topic: #{name}")
|
|
202
|
+
else
|
|
203
|
+
error("Unknown entity subtype: #{subtype}")
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def list_entities(entity_type)
|
|
208
|
+
if entity_type
|
|
209
|
+
entities = para_manager.knowledge_graph.list_entities(entity_type.to_sym)
|
|
210
|
+
return "No #{entity_type} entities found." if entities.empty?
|
|
211
|
+
|
|
212
|
+
lines = ["#{entity_type.capitalize}:"]
|
|
213
|
+
entities.each do |e|
|
|
214
|
+
e.load!
|
|
215
|
+
lines << " - #{e.name} (#{e.active_facts.size} facts)"
|
|
216
|
+
end
|
|
217
|
+
lines.join("\n")
|
|
218
|
+
else
|
|
219
|
+
para_manager.full_context_summary
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def get_context(query)
|
|
224
|
+
if query && !query.empty?
|
|
225
|
+
para_manager.relevant_context(query)
|
|
226
|
+
else
|
|
227
|
+
para_manager.full_context_summary
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def get_preferences
|
|
232
|
+
prefs = para_manager.get_preferences
|
|
233
|
+
return "No learned preferences yet." if prefs.empty?
|
|
234
|
+
|
|
235
|
+
lines = ["User Preferences:"]
|
|
236
|
+
prefs.each do |category, items|
|
|
237
|
+
lines << "\n#{category.to_s.tr("_", " ").capitalize}:"
|
|
238
|
+
items.each do |key, pref|
|
|
239
|
+
conf = (pref[:confidence] * 100).to_i
|
|
240
|
+
lines << " - #{key}: #{pref[:value]} (#{conf}% confident)"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
lines.join("\n")
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def learn_preference(args)
|
|
247
|
+
category = args[:preference_category]
|
|
248
|
+
key = args[:preference_key]
|
|
249
|
+
value = args[:preference_value]
|
|
250
|
+
|
|
251
|
+
return error("Category, key, and value required") unless category && key && value
|
|
252
|
+
|
|
253
|
+
para_manager.learn_preference(
|
|
254
|
+
category: category.to_sym,
|
|
255
|
+
key: key.to_sym,
|
|
256
|
+
value: value,
|
|
257
|
+
source: "conversation"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
success("Learned preference: #{key} = #{value}")
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pocketrb
|
|
4
|
+
module Tools
|
|
5
|
+
# Read file contents
|
|
6
|
+
class ReadFile < Base
|
|
7
|
+
def name
|
|
8
|
+
"read_file"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def description
|
|
12
|
+
"Read the contents of a file. Returns the file content as text."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parameters
|
|
16
|
+
{
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
path: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Path to the file to read (relative to workspace or absolute)"
|
|
22
|
+
},
|
|
23
|
+
offset: {
|
|
24
|
+
type: "integer",
|
|
25
|
+
description: "Line number to start reading from (1-indexed, optional)"
|
|
26
|
+
},
|
|
27
|
+
limit: {
|
|
28
|
+
type: "integer",
|
|
29
|
+
description: "Maximum number of lines to read (optional)"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
required: ["path"]
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def execute(path:, offset: nil, limit: nil)
|
|
37
|
+
resolved = validate_path!(path)
|
|
38
|
+
|
|
39
|
+
return error("Not a file: #{path}") unless resolved.file?
|
|
40
|
+
|
|
41
|
+
content = File.read(resolved)
|
|
42
|
+
lines = content.lines
|
|
43
|
+
|
|
44
|
+
# Apply offset and limit
|
|
45
|
+
lines = lines[(offset - 1)..] if offset && offset > 1
|
|
46
|
+
|
|
47
|
+
lines = lines.first(limit) if limit
|
|
48
|
+
|
|
49
|
+
# Add line numbers
|
|
50
|
+
start_line = offset || 1
|
|
51
|
+
numbered_lines = lines.each_with_index.map do |line, idx|
|
|
52
|
+
"#{start_line + idx}: #{line}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
numbered_lines.join
|
|
56
|
+
rescue Errno::ENOENT
|
|
57
|
+
error("File not found: #{path}")
|
|
58
|
+
rescue Errno::EACCES
|
|
59
|
+
error("Permission denied: #{path}")
|
|
60
|
+
rescue Errno::EISDIR
|
|
61
|
+
error("Is a directory: #{path}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|