aidp 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15737cc2920d4af142a562df51e1acd5951625e836584f62dca3d30f7eb51a81
4
- data.tar.gz: a8243c124ab94cc09f775088cbb62faf6f6fa558f3c6a78bbf52b455d3b446f5
3
+ metadata.gz: ceebdfcb742085ac8d9ea6b5db63557d5fe999f498e97699c199fc9651cf4654
4
+ data.tar.gz: f1b230dcc33692c4c0391464beaf305b76993dbe02964d8c80198a7a50294a5c
5
5
  SHA512:
6
- metadata.gz: db054e792e7c69e22ea2ccda883674e9045febe6907e7d8a9ffc925db952c98e253303d883b3dbb744b079255ea554b66448bdd3eaf6ce0a60130c09ddac6868
7
- data.tar.gz: 4ebe66fadbdef39a5ddad1df6fb7790b86a16a0fef306636b00b7154dc5191c403763484d1448b21c89daf0c5f22c9a39cda1bc7fe2668e632b817951a4d9a3a
6
+ metadata.gz: d20547fa251feb09a97e6795b04fc09dd13d0f0fd801c5abdc994bb27a20898a604692eb04b7ed8a370ac7619bcbaf89a17af60dc34c844e782e8b68087ef67a
7
+ data.tar.gz: cb8503f9b0a85d6113d9ab2dada1cb0087b9b9e6ab1abb44894aa5152d3596c782c08f4708e3f03f9b969a94736acb7d882350b6f0cb869787069c8b12ddde85
data/bin/aidp CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'aidp'
5
- Aidp::Shared::CLI.start(ARGV)
4
+ require "aidp"
5
+ Aidp::CLI.start(ARGV)
@@ -19,8 +19,8 @@ module Aidp
19
19
  # Default retention periods (in days)
20
20
  DEFAULT_RETENTION_PERIODS = {
21
21
  "analysis_results" => 90, # 3 months
22
- "execution_logs" => 30, # 1 month
23
- "temporary_data" => 1 # 1 day
22
+ "execution_logs" => 30, # 1 month
23
+ "temporary_data" => 1 # 1 day
24
24
  }.freeze
25
25
 
26
26
  def initialize(project_dir = Dir.pwd, config = {})
@@ -13,7 +13,7 @@ module Aidp
13
13
  # Default configuration
14
14
  DEFAULT_CONFIG = {
15
15
  checkpoint_interval: 100, # Save progress every 100 items
16
- max_checkpoints: 50, # Keep last 50 checkpoints
16
+ max_checkpoints: 50, # Keep last 50 checkpoints
17
17
  progress_file: ".aidp-large-analysis-progress.yml",
18
18
  auto_save: true,
19
19
  detailed_logging: false
@@ -280,7 +280,7 @@ module Aidp
280
280
  when "json"
281
281
  JSON.parse(data)
282
282
  when "yaml"
283
- YAML.load(data)
283
+ YAML.safe_load(data)
284
284
  else
285
285
  raise "Unsupported import format: #{format}"
286
286
  end
@@ -281,9 +281,9 @@ module Aidp
281
281
 
282
282
  # Adjust for authorship patterns
283
283
  if authorship_file[:author_count] == 1
284
- base_score *= 1.5 # Knowledge silo penalty
284
+ base_score *= 1.5 # Knowledge silo penalty
285
285
  elsif authorship_file[:author_count] > 3
286
- base_score *= 1.2 # Coordination complexity penalty
286
+ base_score *= 1.2 # Coordination complexity penalty
287
287
  end
288
288
 
289
289
  base_score
@@ -13,15 +13,15 @@ module Aidp
13
13
  DEFAULT_CHUNK_CONFIG = {
14
14
  "time_based" => {
15
15
  "chunk_size" => "30d", # 30 days
16
- "overlap" => "7d" # 7 days overlap
16
+ "overlap" => "7d" # 7 days overlap
17
17
  },
18
18
  "commit_count" => {
19
- "chunk_size" => 1000, # 1000 commits per chunk
20
- "overlap" => 100 # 100 commits overlap
19
+ "chunk_size" => 1000, # 1000 commits per chunk
20
+ "overlap" => 100 # 100 commits overlap
21
21
  },
22
22
  "size_based" => {
23
23
  "chunk_size" => "100MB", # 100MB per chunk
24
- "overlap" => "10MB" # 10MB overlap
24
+ "overlap" => "10MB" # 10MB overlap
25
25
  },
26
26
  "feature_based" => {
27
27
  "max_files_per_chunk" => 500,
@@ -358,14 +358,15 @@ module Aidp
358
358
 
359
359
  def parse_time_duration(duration_str)
360
360
  # Parse duration strings like "30d", "7d", "1w", etc.
361
- case duration_str
362
- when /(\d+)d/
361
+ # Use anchored patterns with limited digit repetition to prevent ReDoS
362
+ case duration_str.to_s.strip
363
+ when /\A(\d{1,6})d\z/
363
364
  ::Regexp.last_match(1).to_i * 24 * 60 * 60
364
- when /(\d+)w/
365
+ when /\A(\d{1,6})w\z/
365
366
  ::Regexp.last_match(1).to_i * 7 * 24 * 60 * 60
366
- when /(\d+)m/
367
+ when /\A(\d{1,6})m\z/
367
368
  ::Regexp.last_match(1).to_i * 30 * 24 * 60 * 60
368
- when /(\d+)y/
369
+ when /\A(\d{1,6})y\z/
369
370
  ::Regexp.last_match(1).to_i * 365 * 24 * 60 * 60
370
371
  else
371
372
  30 * 24 * 60 * 60 # Default to 30 days
@@ -374,12 +375,13 @@ module Aidp
374
375
 
375
376
  def parse_size(size_str)
376
377
  # Parse size strings like "100MB", "1GB", etc.
377
- case size_str
378
- when /(\d+)KB/i
378
+ # Use anchored patterns with limited digit repetition to prevent ReDoS
379
+ case size_str.to_s.strip
380
+ when /\A(\d{1,10})KB\z/i
379
381
  ::Regexp.last_match(1).to_i * 1024
380
- when /(\d+)MB/i
382
+ when /\A(\d{1,10})MB\z/i
381
383
  ::Regexp.last_match(1).to_i * 1024 * 1024
382
- when /(\d+)GB/i
384
+ when /\A(\d{1,10})GB\z/i
383
385
  ::Regexp.last_match(1).to_i * 1024 * 1024 * 1024
384
386
  else
385
387
  100 * 1024 * 1024 # Default to 100MB
@@ -288,7 +288,7 @@ module Aidp
288
288
  when "json"
289
289
  JSON.parse(data)
290
290
  when "yaml"
291
- YAML.load(data)
291
+ YAML.safe_load(data)
292
292
  else
293
293
  raise "Unsupported import format: #{format}"
294
294
  end
data/lib/aidp/cli.rb ADDED
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Aidp
6
+ # CLI interface for both execute and analyze modes
7
+ class CLI < Thor
8
+ desc "execute [STEP]", "Run execute mode step(s)"
9
+ option :force, type: :boolean, desc: "Force execution even if dependencies are not met"
10
+ option :rerun, type: :boolean, desc: "Re-run a completed step"
11
+ def execute(project_dir = Dir.pwd, step_name = nil, custom_options = {})
12
+ if step_name
13
+ runner = Aidp::Execute::Runner.new(project_dir)
14
+ # Merge Thor options with custom options
15
+ all_options = options.merge(custom_options)
16
+ runner.run_step(step_name, all_options)
17
+ else
18
+ puts "Available execute steps:"
19
+ Aidp::Execute::Steps::SPEC.keys.each { |step| puts " - #{step}" }
20
+ progress = Aidp::Execute::Progress.new(project_dir)
21
+ next_step = progress.next_step
22
+ {status: "success", message: "Available steps listed", next_step: next_step}
23
+ end
24
+ end
25
+
26
+ desc "analyze [STEP]", "Run analyze mode step(s)"
27
+ long_desc <<~DESC
28
+ Run analyze mode steps. STEP can be:
29
+ - A full step name (e.g., 01_REPOSITORY_ANALYSIS)
30
+ - A step number (e.g., 01, 02, 03)
31
+ - 'next' to run the next unfinished step
32
+ - 'current' to run the current step
33
+ - Empty to list available steps
34
+ DESC
35
+ option :force, type: :boolean, desc: "Force execution even if dependencies are not met"
36
+ option :rerun, type: :boolean, desc: "Re-run a completed step"
37
+ def analyze(project_dir = Dir.pwd, step_name = nil, custom_options = {})
38
+ progress = Aidp::Analyze::Progress.new(project_dir)
39
+
40
+ if step_name
41
+ # Resolve the step name
42
+ resolved_step = resolve_analyze_step(step_name, progress)
43
+
44
+ if resolved_step
45
+ runner = Aidp::Analyze::Runner.new(project_dir)
46
+ # Merge Thor options with custom options
47
+ all_options = options.merge(custom_options)
48
+ runner.run_step(resolved_step, all_options)
49
+ else
50
+ puts "❌ Step '#{step_name}' not found or not available"
51
+ puts "\nAvailable steps:"
52
+ Aidp::Analyze::Steps::SPEC.keys.each_with_index do |step, index|
53
+ status = progress.step_completed?(step) ? "✅" : "⏳"
54
+ puts " #{status} #{sprintf("%02d", index + 1)}: #{step}"
55
+ end
56
+ {status: "error", message: "Step not found"}
57
+ end
58
+ else
59
+ puts "Available analyze steps:"
60
+ Aidp::Analyze::Steps::SPEC.keys.each_with_index do |step, index|
61
+ status = progress.step_completed?(step) ? "✅" : "⏳"
62
+ puts " #{status} #{sprintf("%02d", index + 1)}: #{step}"
63
+ end
64
+
65
+ next_step = progress.next_step
66
+ if next_step
67
+ puts "\n💡 Run 'aidp analyze next' or 'aidp analyze #{next_step.match(/^(\d+)/)[1]}' to run the next step"
68
+ end
69
+
70
+ {status: "success", message: "Available steps listed", next_step: next_step,
71
+ completed_steps: progress.completed_steps}
72
+ end
73
+ end
74
+
75
+ desc "analyze-approve STEP", "Approve a completed analyze gate step"
76
+ def analyze_approve(project_dir = Dir.pwd, step_name = nil)
77
+ progress = Aidp::Analyze::Progress.new(project_dir)
78
+ progress.mark_step_completed(step_name)
79
+ puts "✅ Approved analyze step: #{step_name}"
80
+ {status: "success", step: step_name}
81
+ end
82
+
83
+ desc "analyze-reset", "Reset analyze mode progress"
84
+ def analyze_reset(project_dir = Dir.pwd)
85
+ progress = Aidp::Analyze::Progress.new(project_dir)
86
+ progress.reset
87
+ puts "🔄 Reset analyze mode progress"
88
+ {status: "success", message: "Progress reset"}
89
+ end
90
+
91
+ desc "execute-approve STEP", "Approve a completed execute gate step"
92
+ def execute_approve(project_dir = Dir.pwd, step_name = nil)
93
+ progress = Aidp::Execute::Progress.new(project_dir)
94
+ progress.mark_step_completed(step_name)
95
+ puts "✅ Approved execute step: #{step_name}"
96
+ {status: "success", step: step_name}
97
+ end
98
+
99
+ desc "execute-reset", "Reset execute mode progress"
100
+ def execute_reset(project_dir = Dir.pwd)
101
+ progress = Aidp::Execute::Progress.new(project_dir)
102
+ progress.reset
103
+ puts "🔄 Reset execute mode progress"
104
+ {status: "success", message: "Progress reset"}
105
+ end
106
+
107
+ # Backward compatibility aliases
108
+ desc "approve STEP", "Approve a completed execute gate step (alias for execute-approve)"
109
+ def approve(project_dir = Dir.pwd, step_name = nil)
110
+ execute_approve(project_dir, step_name)
111
+ end
112
+
113
+ desc "reset", "Reset execute mode progress (alias for execute-reset)"
114
+ def reset(project_dir = Dir.pwd)
115
+ execute_reset(project_dir)
116
+ end
117
+
118
+ desc "status", "Show current progress for both modes"
119
+ def status
120
+ puts "\n📊 AI Dev Pipeline Status"
121
+ puts "=" * 50
122
+
123
+ # Execute mode status
124
+ execute_progress = Aidp::Execute::Progress.new(Dir.pwd)
125
+ puts "\n🔧 Execute Mode:"
126
+ Aidp::Execute::Steps::SPEC.keys.each do |step|
127
+ status = execute_progress.step_completed?(step) ? "✅" : "⏳"
128
+ puts " #{status} #{step}"
129
+ end
130
+
131
+ # Analyze mode status
132
+ analyze_progress = Aidp::Analyze::Progress.new(Dir.pwd)
133
+ puts "\n🔍 Analyze Mode:"
134
+ Aidp::Analyze::Steps::SPEC.keys.each do |step|
135
+ status = analyze_progress.step_completed?(step) ? "✅" : "⏳"
136
+ puts " #{status} #{step}"
137
+ end
138
+ end
139
+
140
+ desc "version", "Show version information"
141
+ def version
142
+ puts "Aidp version #{Aidp::VERSION}"
143
+ end
144
+
145
+ private
146
+
147
+ def resolve_analyze_step(step_input, progress)
148
+ step_input = step_input.to_s.downcase.strip
149
+
150
+ case step_input
151
+ when "next"
152
+ progress.next_step
153
+ when "current"
154
+ progress.current_step || progress.next_step
155
+ else
156
+ # Check if it's a step number (e.g., "01", "02", "1", "2")
157
+ if step_input.match?(/^\d{1,2}$/)
158
+ step_number = sprintf("%02d", step_input.to_i)
159
+ # Find step that starts with this number
160
+ Aidp::Analyze::Steps::SPEC.keys.find { |step| step.start_with?(step_number) }
161
+ else
162
+ # Check if it's a full step name (case insensitive)
163
+ Aidp::Analyze::Steps::SPEC.keys.find { |step| step.downcase == step_input }
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Aidp
6
+ # Configuration management for both execute and analyze modes
7
+ class Config
8
+ def self.load(project_dir = Dir.pwd)
9
+ config_file = File.join(project_dir, ".aidp.yml")
10
+ if File.exist?(config_file)
11
+ YAML.load_file(config_file) || {}
12
+ else
13
+ {}
14
+ end
15
+ end
16
+
17
+ def self.templates_root
18
+ File.join(Dir.pwd, "templates")
19
+ end
20
+
21
+ def self.analyze_templates_root
22
+ File.join(Dir.pwd, "templates", "ANALYZE")
23
+ end
24
+
25
+ def self.execute_templates_root
26
+ File.join(Dir.pwd, "templates", "EXECUTE")
27
+ end
28
+
29
+ def self.common_templates_root
30
+ File.join(Dir.pwd, "templates", "COMMON")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+
6
+ module Aidp
7
+ # Detects project type, language, framework, and other characteristics
8
+ class ProjectDetector
9
+ attr_reader :project_dir
10
+
11
+ def initialize(project_dir = Dir.pwd)
12
+ @project_dir = project_dir
13
+ end
14
+
15
+ def detect
16
+ {
17
+ language: detect_language,
18
+ framework: detect_framework,
19
+ build_system: detect_build_system,
20
+ package_manager: detect_package_manager,
21
+ static_analysis_tools: detect_static_analysis_tools,
22
+ test_framework: detect_test_framework,
23
+ database: detect_database,
24
+ deployment: detect_deployment
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ def detect_language
31
+ return "ruby" if File.exist?(File.join(@project_dir, "Gemfile"))
32
+ return "javascript" if File.exist?(File.join(@project_dir, "package.json"))
33
+ return "python" if File.exist?(File.join(@project_dir, "requirements.txt")) || File.exist?(File.join(@project_dir, "pyproject.toml"))
34
+ return "java" if File.exist?(File.join(@project_dir, "pom.xml")) || File.exist?(File.join(@project_dir, "build.gradle"))
35
+ return "go" if File.exist?(File.join(@project_dir, "go.mod"))
36
+ return "rust" if File.exist?(File.join(@project_dir, "Cargo.toml"))
37
+ return "csharp" if File.exist?(File.join(@project_dir, "*.csproj"))
38
+ "unknown"
39
+ end
40
+
41
+ def detect_framework
42
+ case detect_language
43
+ when "ruby"
44
+ return "rails" if File.exist?(File.join(@project_dir, "config", "application.rb"))
45
+ return "sinatra" if File.exist?(File.join(@project_dir, "app.rb")) && File.read(File.join(@project_dir, "app.rb")).include?("Sinatra")
46
+ when "javascript"
47
+ return "react" if File.exist?(File.join(@project_dir, "package.json")) && File.read(File.join(@project_dir, "package.json")).include?("react")
48
+ return "vue" if File.exist?(File.join(@project_dir, "package.json")) && File.read(File.join(@project_dir, "package.json")).include?("vue")
49
+ return "angular" if File.exist?(File.join(@project_dir, "angular.json"))
50
+ return "express" if File.exist?(File.join(@project_dir, "package.json")) && File.read(File.join(@project_dir, "package.json")).include?("express")
51
+ when "python"
52
+ return "django" if File.exist?(File.join(@project_dir, "manage.py"))
53
+ return "flask" if File.exist?(File.join(@project_dir, "app.py")) && File.read(File.join(@project_dir, "app.py")).include?("Flask")
54
+ when "java"
55
+ return "spring" if File.exist?(File.join(@project_dir, "pom.xml")) && File.read(File.join(@project_dir, "pom.xml")).include?("spring-boot")
56
+ end
57
+ "unknown"
58
+ end
59
+
60
+ def detect_build_system
61
+ return "maven" if File.exist?(File.join(@project_dir, "pom.xml"))
62
+ return "gradle" if File.exist?(File.join(@project_dir, "build.gradle"))
63
+ return "npm" if File.exist?(File.join(@project_dir, "package.json"))
64
+ return "bundler" if File.exist?(File.join(@project_dir, "Gemfile"))
65
+ return "pip" if File.exist?(File.join(@project_dir, "requirements.txt"))
66
+ return "cargo" if File.exist?(File.join(@project_dir, "Cargo.toml"))
67
+ return "go" if File.exist?(File.join(@project_dir, "go.mod"))
68
+ "unknown"
69
+ end
70
+
71
+ def detect_package_manager
72
+ detect_build_system
73
+ end
74
+
75
+ def detect_static_analysis_tools
76
+ tools = []
77
+ tools << "rubocop" if File.exist?(File.join(@project_dir, ".rubocop.yml"))
78
+ tools << "eslint" if File.exist?(File.join(@project_dir, ".eslintrc"))
79
+ tools << "flake8" if File.exist?(File.join(@project_dir, ".flake8"))
80
+ tools << "checkstyle" if File.exist?(File.join(@project_dir, "checkstyle.xml"))
81
+ tools << "clippy" if File.exist?(File.join(@project_dir, "Cargo.toml"))
82
+ tools
83
+ end
84
+
85
+ def detect_test_framework
86
+ case detect_language
87
+ when "ruby"
88
+ return "rspec" if File.exist?(File.join(@project_dir, "spec"))
89
+ return "minitest" if File.exist?(File.join(@project_dir, "test"))
90
+ when "javascript"
91
+ return "jest" if File.exist?(File.join(@project_dir, "package.json")) && File.read(File.join(@project_dir, "package.json")).include?("jest")
92
+ return "mocha" if File.exist?(File.join(@project_dir, "package.json")) && File.read(File.join(@project_dir, "package.json")).include?("mocha")
93
+ when "python"
94
+ return "pytest" if File.exist?(File.join(@project_dir, "pytest.ini"))
95
+ return "unittest" if Dir.exist?(File.join(@project_dir, "tests"))
96
+ when "java"
97
+ return "junit" if File.exist?(File.join(@project_dir, "src", "test"))
98
+ end
99
+ "unknown"
100
+ end
101
+
102
+ def detect_database
103
+ return "postgresql" if File.exist?(File.join(@project_dir, "config", "database.yml")) && File.read(File.join(@project_dir, "config", "database.yml")).include?("postgresql")
104
+ return "mysql" if File.exist?(File.join(@project_dir, "config", "database.yml")) && File.read(File.join(@project_dir, "config", "database.yml")).include?("mysql")
105
+ return "sqlite" if File.exist?(File.join(@project_dir, "config", "database.yml")) && File.read(File.join(@project_dir, "config", "database.yml")).include?("sqlite")
106
+ "unknown"
107
+ end
108
+
109
+ def detect_deployment
110
+ return "docker" if File.exist?(File.join(@project_dir, "Dockerfile"))
111
+ return "kubernetes" if File.exist?(File.join(@project_dir, "k8s")) || File.exist?(File.join(@project_dir, "kubernetes"))
112
+ return "heroku" if File.exist?(File.join(@project_dir, "Procfile"))
113
+ return "aws" if File.exist?(File.join(@project_dir, "serverless.yml")) || File.exist?(File.join(@project_dir, "template.yaml"))
114
+ "unknown"
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Aidp
6
+ module Providers
7
+ class Anthropic < Base
8
+ def self.available?
9
+ !!Aidp::Util.which("claude")
10
+ end
11
+
12
+ def name = "anthropic"
13
+
14
+ def send(prompt:, session: nil)
15
+ raise "claude CLI not available" unless self.class.available?
16
+
17
+ # Use Claude CLI for non-interactive mode
18
+ cmd = ["claude", "compose", "--prompt", prompt]
19
+ system(*cmd)
20
+ :ok
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Providers
5
+ class Base
6
+ def name = raise(NotImplementedError)
7
+
8
+ # Send a composed prompt string to the provider.
9
+ # Return :ok when command completed successfully,
10
+ # Return :interactive when starting an interactive session (for gate steps),
11
+ # or return a string if we captured output and the caller should write to a file.
12
+ def send(prompt:, session: nil) = raise(NotImplementedError)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "timeout"
5
+ require_relative "base"
6
+ require_relative "../util"
7
+
8
+ module Aidp
9
+ module Providers
10
+ class Cursor < Base
11
+ def self.available?
12
+ !!Aidp::Util.which("cursor-agent")
13
+ end
14
+
15
+ def name = "cursor"
16
+
17
+ def send(prompt:, session: nil)
18
+ raise "cursor-agent not available" unless self.class.available?
19
+
20
+ # Always use non-interactive mode with -p flag
21
+ cmd = ["cursor-agent", "-p"]
22
+ puts "📝 Sending prompt to cursor-agent"
23
+
24
+ # Enable debug output if requested
25
+ if ENV["AIDP_DEBUG"]
26
+ puts "🔍 Debug mode enabled - showing cursor-agent output"
27
+ end
28
+
29
+ # Setup logging if log file is specified
30
+ log_file = ENV["AIDP_LOG_FILE"]
31
+ if log_file
32
+ puts "📝 Logging to: #{log_file}"
33
+ end
34
+
35
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait|
36
+ # Send the prompt to stdin
37
+ stdin.puts prompt
38
+ stdin.close
39
+
40
+ # Log the prompt if debugging
41
+ if ENV["AIDP_DEBUG"] || log_file
42
+ prompt_log = "📝 Sending prompt to cursor-agent:\n#{prompt}"
43
+ puts prompt_log if ENV["AIDP_DEBUG"]
44
+ File.write(log_file, "#{Time.now.iso8601} #{prompt_log}\n", mode: "a") if log_file
45
+ end
46
+
47
+ # Handle debug output and logging
48
+ if ENV["AIDP_DEBUG"] || log_file
49
+ # Start threads to capture and display output in real-time
50
+ stdout_thread = Thread.new do
51
+ stdout.each_line do |line|
52
+ output = "📤 cursor-agent: #{line.chomp}"
53
+ puts output if ENV["AIDP_DEBUG"]
54
+ File.write(log_file, "#{Time.now.iso8601} #{output}\n", mode: "a") if log_file
55
+ end
56
+ end
57
+
58
+ stderr_thread = Thread.new do
59
+ stderr.each_line do |line|
60
+ output = "❌ cursor-agent error: #{line.chomp}"
61
+ puts output if ENV["AIDP_DEBUG"]
62
+ File.write(log_file, "#{Time.now.iso8601} #{output}\n", mode: "a") if log_file
63
+ end
64
+ end
65
+ end
66
+
67
+ # Wait for completion with a reasonable timeout
68
+ begin
69
+ Timeout.timeout(300) do # 5 minutes timeout
70
+ result = wait.value
71
+
72
+ # Stop debug threads
73
+ if ENV["AIDP_DEBUG"]
74
+ stdout_thread&.kill
75
+ stderr_thread&.kill
76
+ end
77
+
78
+ return :ok if result.success?
79
+ raise "cursor-agent failed with exit code #{result.exitstatus}"
80
+ end
81
+ rescue Timeout::Error
82
+ # Stop debug threads
83
+ if ENV["AIDP_DEBUG"]
84
+ stdout_thread&.kill
85
+ stderr_thread&.kill
86
+ end
87
+
88
+ # Kill the process if it's taking too long
89
+ begin
90
+ Process.kill("TERM", wait.pid)
91
+ rescue
92
+ nil
93
+ end
94
+ raise Timeout::Error, "cursor-agent timed out after 5 minutes"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Aidp
6
+ module Providers
7
+ class Gemini < Base
8
+ def self.available?
9
+ !!Aidp::Util.which("gemini")
10
+ end
11
+
12
+ def name = "gemini"
13
+
14
+ def send(prompt:, session: nil)
15
+ raise "gemini CLI not available" unless self.class.available?
16
+
17
+ # Use Gemini CLI for non-interactive mode
18
+ cmd = ["gemini", "chat", "--prompt", prompt]
19
+ system(*cmd)
20
+ :ok
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Aidp
6
+ module Providers
7
+ class MacOSUI < Base
8
+ def self.available?
9
+ RUBY_PLATFORM.include?("darwin")
10
+ end
11
+
12
+ def name = "macos"
13
+
14
+ def send(prompt:, session: nil)
15
+ raise "macOS UI not available on this platform" unless self.class.available?
16
+
17
+ # Use macOS UI for interactive mode
18
+ cmd = ["osascript", "-e", "display dialog \"#{prompt}\" with title \"Aidp\" buttons {\"OK\"} default button \"OK\""]
19
+ system(*cmd)
20
+ :ok
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/aidp/sync.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Aidp
6
+ # Synchronization utilities
7
+ class Sync
8
+ def self.ensure_workspace_sync
9
+ # Placeholder for workspace synchronization logic
10
+ true
11
+ end
12
+ end
13
+ end