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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/bin/agentf +8 -0
  3. data/lib/agentf/agent_policy.rb +54 -0
  4. data/lib/agentf/agents/architect.rb +67 -0
  5. data/lib/agentf/agents/base.rb +53 -0
  6. data/lib/agentf/agents/debugger.rb +75 -0
  7. data/lib/agentf/agents/designer.rb +69 -0
  8. data/lib/agentf/agents/documenter.rb +58 -0
  9. data/lib/agentf/agents/explorer.rb +65 -0
  10. data/lib/agentf/agents/reviewer.rb +64 -0
  11. data/lib/agentf/agents/security.rb +84 -0
  12. data/lib/agentf/agents/specialist.rb +68 -0
  13. data/lib/agentf/agents/tester.rb +79 -0
  14. data/lib/agentf/agents.rb +19 -0
  15. data/lib/agentf/cli/architecture.rb +83 -0
  16. data/lib/agentf/cli/arg_parser.rb +50 -0
  17. data/lib/agentf/cli/code.rb +165 -0
  18. data/lib/agentf/cli/install.rb +112 -0
  19. data/lib/agentf/cli/memory.rb +393 -0
  20. data/lib/agentf/cli/metrics.rb +103 -0
  21. data/lib/agentf/cli/router.rb +111 -0
  22. data/lib/agentf/cli/update.rb +204 -0
  23. data/lib/agentf/commands/architecture.rb +183 -0
  24. data/lib/agentf/commands/debugger.rb +238 -0
  25. data/lib/agentf/commands/designer.rb +179 -0
  26. data/lib/agentf/commands/explorer.rb +208 -0
  27. data/lib/agentf/commands/memory_reviewer.rb +186 -0
  28. data/lib/agentf/commands/metrics.rb +272 -0
  29. data/lib/agentf/commands/security_scanner.rb +98 -0
  30. data/lib/agentf/commands/tester.rb +232 -0
  31. data/lib/agentf/commands.rb +17 -0
  32. data/lib/agentf/context_builder.rb +35 -0
  33. data/lib/agentf/installer.rb +580 -0
  34. data/lib/agentf/mcp/server.rb +310 -0
  35. data/lib/agentf/memory.rb +530 -0
  36. data/lib/agentf/packs.rb +74 -0
  37. data/lib/agentf/service/providers.rb +158 -0
  38. data/lib/agentf/tools/component_spec.rb +28 -0
  39. data/lib/agentf/tools/error_analysis.rb +19 -0
  40. data/lib/agentf/tools/file_match.rb +21 -0
  41. data/lib/agentf/tools/test_template.rb +17 -0
  42. data/lib/agentf/tools.rb +12 -0
  43. data/lib/agentf/version.rb +5 -0
  44. data/lib/agentf/workflow_contract.rb +158 -0
  45. data/lib/agentf/workflow_engine.rb +424 -0
  46. data/lib/agentf.rb +87 -0
  47. 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