agentf 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 +7 -0
- data/bin/agentf +8 -0
- data/lib/agentf/agent_policy.rb +54 -0
- data/lib/agentf/agents/architect.rb +67 -0
- data/lib/agentf/agents/base.rb +53 -0
- data/lib/agentf/agents/debugger.rb +75 -0
- data/lib/agentf/agents/designer.rb +69 -0
- data/lib/agentf/agents/documenter.rb +58 -0
- data/lib/agentf/agents/explorer.rb +65 -0
- data/lib/agentf/agents/reviewer.rb +64 -0
- data/lib/agentf/agents/security.rb +84 -0
- data/lib/agentf/agents/specialist.rb +68 -0
- data/lib/agentf/agents/tester.rb +79 -0
- data/lib/agentf/agents.rb +19 -0
- data/lib/agentf/cli/architecture.rb +83 -0
- data/lib/agentf/cli/arg_parser.rb +50 -0
- data/lib/agentf/cli/code.rb +165 -0
- data/lib/agentf/cli/install.rb +112 -0
- data/lib/agentf/cli/memory.rb +393 -0
- data/lib/agentf/cli/metrics.rb +103 -0
- data/lib/agentf/cli/router.rb +111 -0
- data/lib/agentf/cli/update.rb +204 -0
- data/lib/agentf/commands/architecture.rb +183 -0
- data/lib/agentf/commands/debugger.rb +238 -0
- data/lib/agentf/commands/designer.rb +179 -0
- data/lib/agentf/commands/explorer.rb +208 -0
- data/lib/agentf/commands/memory_reviewer.rb +186 -0
- data/lib/agentf/commands/metrics.rb +272 -0
- data/lib/agentf/commands/security_scanner.rb +98 -0
- data/lib/agentf/commands/tester.rb +232 -0
- data/lib/agentf/commands.rb +17 -0
- data/lib/agentf/context_builder.rb +35 -0
- data/lib/agentf/installer.rb +580 -0
- data/lib/agentf/mcp/server.rb +310 -0
- data/lib/agentf/memory.rb +530 -0
- data/lib/agentf/packs.rb +74 -0
- data/lib/agentf/service/providers.rb +158 -0
- data/lib/agentf/tools/component_spec.rb +28 -0
- data/lib/agentf/tools/error_analysis.rb +19 -0
- data/lib/agentf/tools/file_match.rb +21 -0
- data/lib/agentf/tools/test_template.rb +17 -0
- data/lib/agentf/tools.rb +12 -0
- data/lib/agentf/version.rb +5 -0
- data/lib/agentf/workflow_contract.rb +158 -0
- data/lib/agentf/workflow_engine.rb +424 -0
- data/lib/agentf.rb +87 -0
- metadata +164 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Agentf
|
|
7
|
+
module Commands
|
|
8
|
+
class Designer
|
|
9
|
+
NAME = "designer"
|
|
10
|
+
|
|
11
|
+
def self.manifest
|
|
12
|
+
{
|
|
13
|
+
"name" => NAME,
|
|
14
|
+
"description" => "Generate components from design specs and validate design systems.",
|
|
15
|
+
"commands" => [
|
|
16
|
+
{ "name" => "generate_component", "type" => "function" },
|
|
17
|
+
{ "name" => "validate_design_system", "type" => "function" }
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(base_path: nil)
|
|
23
|
+
@base_path = base_path || Agentf.config.base_path
|
|
24
|
+
detect_design_system
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Generate component code from design spec
|
|
28
|
+
def generate_component(name, design_spec, style_system: nil, framework: nil)
|
|
29
|
+
style = style_system || @style_system
|
|
30
|
+
fw = framework || @framework
|
|
31
|
+
|
|
32
|
+
props = parse_design_spec(design_spec)
|
|
33
|
+
|
|
34
|
+
code = if fw == "vue"
|
|
35
|
+
generate_vue(name, props, design_spec)
|
|
36
|
+
elsif style == "tailwind" && fw == "react"
|
|
37
|
+
generate_react_tailwind(name, props, design_spec)
|
|
38
|
+
elsif style == "css" && fw == "react"
|
|
39
|
+
generate_react_css(name, props, design_spec)
|
|
40
|
+
else
|
|
41
|
+
generate_react_css(name, props, design_spec)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Agentf::Tools::ComponentSpec.new(
|
|
45
|
+
name: name,
|
|
46
|
+
code: code,
|
|
47
|
+
framework: fw,
|
|
48
|
+
style: style,
|
|
49
|
+
props: props
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check design system consistency
|
|
54
|
+
def validate_design_system
|
|
55
|
+
base = Pathname.new(@base_path)
|
|
56
|
+
|
|
57
|
+
results = {
|
|
58
|
+
"framework" => @framework,
|
|
59
|
+
"style_system" => @style_system,
|
|
60
|
+
"components_found" => [],
|
|
61
|
+
"issues" => []
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
%w[.tsx .jsx .vue .js].each do |ext|
|
|
65
|
+
base.glob("**/*#{ext}").first(10).each do |p|
|
|
66
|
+
results["components_found"] << p.relative_path_from(base).to_s
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
results
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def detect_design_system
|
|
76
|
+
base = Pathname.new(@base_path)
|
|
77
|
+
@framework = "react"
|
|
78
|
+
@style_system = "css"
|
|
79
|
+
|
|
80
|
+
# Check package.json
|
|
81
|
+
pkg_path = base.join("package.json")
|
|
82
|
+
if pkg_path.exist?
|
|
83
|
+
pkg = JSON.parse(pkg_path.read)
|
|
84
|
+
deps = pkg.fetch("dependencies", {}).merge(pkg.fetch("devDependencies", {}))
|
|
85
|
+
|
|
86
|
+
@framework = "vue" if deps.key?("vue")
|
|
87
|
+
@style_system = "tailwind" if deps.key?("tailwindcss")
|
|
88
|
+
@style_system = "material-ui" if deps.key?("@mui/material")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check for Rails
|
|
92
|
+
if (base / "Gemfile").exist? && base.join("Gemfile").read.include?("rails")
|
|
93
|
+
@framework = "rails"
|
|
94
|
+
@style_system = "css"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def parse_design_spec(spec)
|
|
99
|
+
props = []
|
|
100
|
+
|
|
101
|
+
spec.scan(/(\w+):\s*(\w+)/) do |prop_name, prop_type|
|
|
102
|
+
next if %w[class style on ref].include?(prop_name)
|
|
103
|
+
props << { "name" => prop_name, "type" => prop_type, "required" => spec.downcase.include?("required").to_s }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
props
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def generate_react_tailwind(name, props, _spec)
|
|
110
|
+
props_interface = props.map { |p| " #{p['name']}: #{p['type']};" }.join("\n")
|
|
111
|
+
props_destructure = props.map { |p| p['name'] }.join(", ")
|
|
112
|
+
|
|
113
|
+
<<~RUBY
|
|
114
|
+
import React from 'react';
|
|
115
|
+
|
|
116
|
+
interface #{name}Props {
|
|
117
|
+
#{props_interface.empty? ? " // No props" : props_interface}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function #{name}({ #{props_destructure} }: #{name}Props) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="flex flex-col gap-4 p-4">
|
|
123
|
+
{/* Content here */}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
RUBY
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def generate_react_css(name, props, _spec)
|
|
131
|
+
props_interface = props.map { |p| " #{p['name']}: #{p['type']};" }.join("\n")
|
|
132
|
+
props_destructure = props.map { |p| p['name'] }.join(", ")
|
|
133
|
+
|
|
134
|
+
<<~RUBY
|
|
135
|
+
import React from 'react';
|
|
136
|
+
import './#{name}.css';
|
|
137
|
+
|
|
138
|
+
interface #{name}Props {
|
|
139
|
+
#{props_interface.empty? ? " // No props" : props_interface}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function #{name}({ #{props_destructure} }: #{name}Props) {
|
|
143
|
+
return (
|
|
144
|
+
<div className="#{name}">
|
|
145
|
+
{/* Content here */}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
RUBY
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def generate_vue(name, props, _spec)
|
|
153
|
+
props_interface = props.map { |p| " #{p['name']}: #{p['type']};" }.join("\n")
|
|
154
|
+
|
|
155
|
+
<<~RUBY
|
|
156
|
+
<template>
|
|
157
|
+
<div class="#{name}">
|
|
158
|
+
<!-- Content here -->
|
|
159
|
+
</div>
|
|
160
|
+
</template>
|
|
161
|
+
|
|
162
|
+
<script setup lang="ts">
|
|
163
|
+
interface Props {
|
|
164
|
+
#{props_interface.empty? ? " // No props" : props_interface}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const props = defineProps<Props>();
|
|
168
|
+
</script>
|
|
169
|
+
|
|
170
|
+
<style scoped>
|
|
171
|
+
.#{name} {
|
|
172
|
+
/* styles here */
|
|
173
|
+
}
|
|
174
|
+
</style>
|
|
175
|
+
RUBY
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Agentf
|
|
7
|
+
module Commands
|
|
8
|
+
class Explorer
|
|
9
|
+
NAME = "explorer"
|
|
10
|
+
|
|
11
|
+
def self.manifest
|
|
12
|
+
{
|
|
13
|
+
"name" => NAME,
|
|
14
|
+
"description" => "Find files, search code, and explore project structure.",
|
|
15
|
+
"commands" => [
|
|
16
|
+
{ "name" => "glob", "type" => "function" },
|
|
17
|
+
{ "name" => "grep", "type" => "function" },
|
|
18
|
+
{ "name" => "get_file_tree", "type" => "function" },
|
|
19
|
+
{ "name" => "find_related_files", "type" => "function" }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(base_path: nil)
|
|
25
|
+
@base_path = base_path || Agentf.config.base_path
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Find files matching a glob pattern
|
|
29
|
+
def glob(pattern, file_types: nil)
|
|
30
|
+
base = Pathname.new(@base_path)
|
|
31
|
+
matches = []
|
|
32
|
+
|
|
33
|
+
base.glob(pattern).each do |match|
|
|
34
|
+
next unless match.file?
|
|
35
|
+
|
|
36
|
+
if file_types
|
|
37
|
+
matches << match.relative_path_from(base).to_s if file_types.include?(match.extname.delete("."))
|
|
38
|
+
else
|
|
39
|
+
matches << match.relative_path_from(base).to_s
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
matches.sort
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
[{ "error" => e.message }]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Search for pattern in files
|
|
49
|
+
def grep(pattern, file_pattern: nil, context_lines: 2)
|
|
50
|
+
cmd = ["grep", "-rn"]
|
|
51
|
+
cmd << "--include=#{file_pattern || '*'}" if file_pattern
|
|
52
|
+
cmd << pattern
|
|
53
|
+
cmd << @base_path
|
|
54
|
+
|
|
55
|
+
stdout, _stderr, _status = Open3.capture3(*cmd)
|
|
56
|
+
|
|
57
|
+
matches = []
|
|
58
|
+
stdout.lines.each do |line|
|
|
59
|
+
parts = line.split(":", 3)
|
|
60
|
+
next unless parts.length >= 3
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
line_num = Integer(parts[1])
|
|
64
|
+
matches << Agentf::Tools::FileMatch.new(
|
|
65
|
+
path: parts[0],
|
|
66
|
+
line_number: line_num,
|
|
67
|
+
content: parts[2].strip,
|
|
68
|
+
match_type: "pattern"
|
|
69
|
+
)
|
|
70
|
+
rescue ArgumentError
|
|
71
|
+
# Skip invalid line numbers
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
matches
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
[{ "error" => e.message }]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Get directory tree structure
|
|
81
|
+
def get_file_tree(max_depth: 3, exclude_dirs: nil)
|
|
82
|
+
exclude_dirs ||= %w[node_modules .git __pycache__ .venv venv dist build]
|
|
83
|
+
base = Pathname.new(@base_path)
|
|
84
|
+
|
|
85
|
+
build_tree(base, 0, max_depth, exclude_dirs)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Find related files based on imports/exports
|
|
89
|
+
def find_related_files(target_file)
|
|
90
|
+
base = Pathname.new(@base_path)
|
|
91
|
+
target = base + target_file
|
|
92
|
+
|
|
93
|
+
return { "error" => "File not found" } unless target.exist?
|
|
94
|
+
|
|
95
|
+
content = target.read
|
|
96
|
+
result = { "imports" => [], "imported_by" => [], "tests" => [], "similar" => [] }
|
|
97
|
+
|
|
98
|
+
# Ruby imports
|
|
99
|
+
if target.extname == ".rb"
|
|
100
|
+
content.scan(/^(?:require|require_relative)\s+['"]([^'"]+)['"]/) do |match|
|
|
101
|
+
result["imports"] << match[0]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Python imports
|
|
106
|
+
if target.extname == ".py"
|
|
107
|
+
content.scan(/^(?:from|import)\s+([\w.]+)/) do |match|
|
|
108
|
+
result["imports"] << match[0]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# JS/TS imports
|
|
113
|
+
if %w[.js .ts .jsx .tsx].include?(target.extname)
|
|
114
|
+
content.scan(/(?:import|require)\s+['"]([^'"]+)['"]/) do |match|
|
|
115
|
+
result["imports"] << match[0]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Discover files that reference the target file by basename or relative path fragment
|
|
120
|
+
target_stem = target.basename.sub_ext("").to_s
|
|
121
|
+
search_tokens = [target_stem, target_file.gsub(target.extname, "")].uniq
|
|
122
|
+
base.glob("**/*#{target.extname}").each do |file|
|
|
123
|
+
next if file == target
|
|
124
|
+
next unless file.file?
|
|
125
|
+
|
|
126
|
+
file_content = safe_read(file)
|
|
127
|
+
next if file_content.empty?
|
|
128
|
+
|
|
129
|
+
if search_tokens.any? { |token| file_content.include?(token) }
|
|
130
|
+
result["imported_by"] << file.relative_path_from(base).to_s
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Test companions by naming convention
|
|
135
|
+
target_dir = target.dirname.relative_path_from(base).to_s
|
|
136
|
+
target_name = target.basename.sub_ext("").to_s
|
|
137
|
+
test_patterns = [
|
|
138
|
+
"spec/**/*#{target_name}*_spec.rb",
|
|
139
|
+
"test/**/*#{target_name}*_test.rb",
|
|
140
|
+
"**/*#{target_name}.test.js",
|
|
141
|
+
"**/*#{target_name}.test.ts",
|
|
142
|
+
"**/*#{target_name}.spec.js",
|
|
143
|
+
"**/*#{target_name}.spec.ts"
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
test_patterns.each do |pattern|
|
|
147
|
+
base.glob(pattern).each do |match|
|
|
148
|
+
rel = match.relative_path_from(base).to_s
|
|
149
|
+
result["tests"] << rel if rel.include?(target_name) || rel.include?(target_dir)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Similar files in same directory by prefix
|
|
154
|
+
sibling_prefix = target_name.split("_").first
|
|
155
|
+
target.dirname.children.each do |child|
|
|
156
|
+
next unless child.file?
|
|
157
|
+
next if child == target
|
|
158
|
+
|
|
159
|
+
name = child.basename.sub_ext("").to_s
|
|
160
|
+
result["similar"] << child.relative_path_from(base).to_s if name.start_with?(sibling_prefix)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
result["imports"].uniq!
|
|
164
|
+
result["imported_by"].uniq!
|
|
165
|
+
result["tests"].uniq!
|
|
166
|
+
result["similar"].uniq!
|
|
167
|
+
|
|
168
|
+
result
|
|
169
|
+
rescue StandardError => e
|
|
170
|
+
{ "error" => e.message }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def build_tree(path, depth, max_depth, exclude_dirs)
|
|
176
|
+
return {} if depth > max_depth
|
|
177
|
+
|
|
178
|
+
tree = { "type" => "directory", "name" => path.basename.to_s, "children" => [] }
|
|
179
|
+
|
|
180
|
+
begin
|
|
181
|
+
path.children.sort.each do |item|
|
|
182
|
+
next if exclude_dirs.include?(item.basename.to_s)
|
|
183
|
+
|
|
184
|
+
if item.directory?
|
|
185
|
+
tree["children"] << build_tree(item, depth + 1, max_depth, exclude_dirs)
|
|
186
|
+
else
|
|
187
|
+
tree["children"] << {
|
|
188
|
+
"type" => "file",
|
|
189
|
+
"name" => item.basename.to_s,
|
|
190
|
+
"ext" => item.extname
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
rescue Errno::EACCES
|
|
195
|
+
# Permission denied, skip
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
tree
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def safe_read(path)
|
|
202
|
+
path.read
|
|
203
|
+
rescue StandardError
|
|
204
|
+
""
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Agentf
|
|
7
|
+
module Commands
|
|
8
|
+
# Tool for reviewing Redis-stored memories and learnings
|
|
9
|
+
class MemoryReviewer
|
|
10
|
+
NAME = "memory"
|
|
11
|
+
|
|
12
|
+
def self.manifest
|
|
13
|
+
{
|
|
14
|
+
"name" => NAME,
|
|
15
|
+
"description" => "Review and query Redis-stored memories, pitfalls, and learnings.",
|
|
16
|
+
"commands" => [
|
|
17
|
+
{ "name" => "get_recent_memories", "type" => "function" },
|
|
18
|
+
{ "name" => "get_pitfalls", "type" => "function" },
|
|
19
|
+
{ "name" => "get_lessons", "type" => "function" },
|
|
20
|
+
{ "name" => "get_successes", "type" => "function" },
|
|
21
|
+
{ "name" => "get_all_tags", "type" => "function" },
|
|
22
|
+
{ "name" => "get_by_tag", "type" => "function" },
|
|
23
|
+
{ "name" => "get_by_type", "type" => "function" },
|
|
24
|
+
{ "name" => "get_by_agent", "type" => "function" },
|
|
25
|
+
{ "name" => "search", "type" => "function" },
|
|
26
|
+
{ "name" => "get_summary", "type" => "function" }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(project: nil)
|
|
32
|
+
@project = project || Agentf.config.project_name
|
|
33
|
+
@memory = Agentf::Memory::RedisMemory.new(project: @project)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get recent memories
|
|
37
|
+
def get_recent_memories(limit: 10)
|
|
38
|
+
memories = @memory.get_recent_memories(limit: limit)
|
|
39
|
+
format_memories(memories)
|
|
40
|
+
rescue => e
|
|
41
|
+
{ "error" => e.message }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get all pitfalls (things that went wrong)
|
|
45
|
+
def get_pitfalls(limit: 10)
|
|
46
|
+
pitfalls = @memory.get_pitfalls(limit: limit)
|
|
47
|
+
format_memories(pitfalls)
|
|
48
|
+
rescue => e
|
|
49
|
+
{ "error" => e.message }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get all lessons learned
|
|
53
|
+
def get_lessons(limit: 10)
|
|
54
|
+
lessons = @memory.get_memories_by_type(type: "lesson", limit: limit)
|
|
55
|
+
format_memories(lessons)
|
|
56
|
+
rescue => e
|
|
57
|
+
{ "error" => e.message }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get all successes
|
|
61
|
+
def get_successes(limit: 10)
|
|
62
|
+
successes = @memory.get_memories_by_type(type: "success", limit: limit)
|
|
63
|
+
format_memories(successes)
|
|
64
|
+
rescue => e
|
|
65
|
+
{ "error" => e.message }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def get_business_intents(limit: 10)
|
|
69
|
+
intents = @memory.get_intents(kind: "business", limit: limit)
|
|
70
|
+
format_memories(intents)
|
|
71
|
+
rescue => e
|
|
72
|
+
{ "error" => e.message }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def get_feature_intents(limit: 10)
|
|
76
|
+
intents = @memory.get_intents(kind: "feature", limit: limit)
|
|
77
|
+
format_memories(intents)
|
|
78
|
+
rescue => e
|
|
79
|
+
{ "error" => e.message }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get all unique tags from memories
|
|
83
|
+
def get_all_tags
|
|
84
|
+
tags = @memory.get_all_tags
|
|
85
|
+
{ "tags" => tags.sort, "count" => tags.length }
|
|
86
|
+
rescue => e
|
|
87
|
+
{ "error" => e.message }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get memories by tag
|
|
91
|
+
def get_by_tag(tag, limit: 10)
|
|
92
|
+
memories = @memory.get_recent_memories(limit: 100)
|
|
93
|
+
filtered = memories.select { |m| m["tags"]&.include?(tag) }
|
|
94
|
+
format_memories(filtered.first(limit))
|
|
95
|
+
rescue => e
|
|
96
|
+
{ "error" => e.message }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get memories by type (pitfall, lesson, success)
|
|
100
|
+
def get_by_type(type, limit: 10)
|
|
101
|
+
memories = @memory.get_recent_memories(limit: 100)
|
|
102
|
+
filtered = memories.select { |m| m["type"] == type }
|
|
103
|
+
format_memories(filtered.first(limit))
|
|
104
|
+
rescue => e
|
|
105
|
+
{ "error" => e.message }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get memories by agent
|
|
109
|
+
def get_by_agent(agent, limit: 10)
|
|
110
|
+
memories = @memory.get_recent_memories(limit: 100)
|
|
111
|
+
filtered = memories.select { |m| m["agent"] == agent }
|
|
112
|
+
format_memories(filtered.first(limit))
|
|
113
|
+
rescue => e
|
|
114
|
+
{ "error" => e.message }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Search memories by keyword in title or description
|
|
118
|
+
def search(query, limit: 10)
|
|
119
|
+
memories = @memory.get_recent_memories(limit: 100)
|
|
120
|
+
q = query.downcase
|
|
121
|
+
filtered = memories.select do |m|
|
|
122
|
+
m["title"]&.downcase&.include?(q) ||
|
|
123
|
+
m["description"]&.downcase&.include?(q) ||
|
|
124
|
+
m["context"]&.downcase&.include?(q)
|
|
125
|
+
end
|
|
126
|
+
format_memories(filtered.first(limit))
|
|
127
|
+
rescue => e
|
|
128
|
+
{ "error" => e.message }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Get summary statistics
|
|
132
|
+
def get_summary
|
|
133
|
+
memories = @memory.get_recent_memories(limit: 100)
|
|
134
|
+
tags = @memory.get_all_tags
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
"total_memories" => memories.length,
|
|
138
|
+
"by_type" => {
|
|
139
|
+
"pitfall" => memories.count { |m| m["type"] == "pitfall" },
|
|
140
|
+
"lesson" => memories.count { |m| m["type"] == "lesson" },
|
|
141
|
+
"success" => memories.count { |m| m["type"] == "success" },
|
|
142
|
+
"business_intent" => memories.count { |m| m["type"] == "business_intent" },
|
|
143
|
+
"feature_intent" => memories.count { |m| m["type"] == "feature_intent" }
|
|
144
|
+
},
|
|
145
|
+
"by_agent" => memories.each_with_object(Hash.new(0)) { |m, h| h[m["agent"]] += 1 },
|
|
146
|
+
"unique_tags" => tags.length,
|
|
147
|
+
"project" => @project
|
|
148
|
+
}
|
|
149
|
+
rescue => e
|
|
150
|
+
{ "error" => e.message }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
def format_memories(memories)
|
|
156
|
+
return { "memories" => [], "count" => 0 } if memories.empty?
|
|
157
|
+
|
|
158
|
+
formatted = memories.map { |m| format_single_memory(m) }
|
|
159
|
+
{ "memories" => formatted, "count" => formatted.length }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def format_single_memory(m)
|
|
163
|
+
{
|
|
164
|
+
"id" => m["id"],
|
|
165
|
+
"type" => m["type"],
|
|
166
|
+
"title" => m["title"],
|
|
167
|
+
"description" => m["description"],
|
|
168
|
+
"context" => m["context"],
|
|
169
|
+
"code_snippet" => m["code_snippet"],
|
|
170
|
+
"tags" => m["tags"],
|
|
171
|
+
"agent" => m["agent"],
|
|
172
|
+
"created_at" => format_time(m["created_at"]),
|
|
173
|
+
"created_at_unix" => m["created_at"]
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def format_time(unix_time)
|
|
178
|
+
return nil unless unix_time
|
|
179
|
+
|
|
180
|
+
Time.at(unix_time).strftime("%Y-%m-%d %H:%M:%S")
|
|
181
|
+
rescue
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|