pear-programmer 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5ff1857eefc3d9eb63642ff07a42536cec791067bbcaa27884586f37d8e3d1bb
4
+ data.tar.gz: 9639755ec441aa4ef3dca87d75dc71c742002386e275a4c406ba827269baf00e
5
+ SHA512:
6
+ metadata.gz: 65b97022ce92f9504703082db00c511eb94d8936e8cd1ce8b75c55c2e80aa70a55168cda8afed36458213ded9689e35f5ac66c841d845730f6ff6fc9fc9240f1
7
+ data.tar.gz: 9c59045e0bc0a77e6a5190fa5836720b9a5bd8ec7e330c7a84676015933aab9e362bb8ad9a3b84cfc5a14e225e75e305eb587884fa994e487e73b9789e72e9fd
data/bin/pear-on ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/cli'
@@ -0,0 +1,224 @@
1
+ require_relative "../pairprogrammer"
2
+ require_relative "display"
3
+ require_relative "version"
4
+ # need to require file
5
+
6
+ # TODO VALIDATIONS
7
+ module Cli
8
+ class Actions
9
+ def self.init
10
+ Cli::Display.info_message("Welcome to Pear Programmer!")
11
+ confirmation = Cli::Display.confirmation("Are you in the root directory of your project?")
12
+ if !confirmation
13
+ Cli::Display.error_message("Please change directory into the root of your project and try again.")
14
+ return
15
+ end
16
+
17
+ Cli::Display.info_message("creating #{Cli::Configuration::FILE_NAME} in #{Dir.pwd}")
18
+ Cli::Display.info_message("Context is any information about your project that you want the large language model to know about. A few sentences should suffice.")
19
+ Cli::Display.info_message("Include information about your framework (ie Ruby on Rails), your database, how you handle assets, authentication, etc.")
20
+ Cli::Display.info_message("The more detailed you are the better the LLM will perform.")
21
+ context = Cli::Display.get_input("context: ")
22
+
23
+ Cli::Display.info_message("If you haven't already, sign up for an API key at https://pairprogrammer.io")
24
+ Cli::Display.info_message("To skip for now and update later press enter")
25
+ api_key = Cli::Display.get_input("api_key: ")
26
+
27
+ Cli::Display.info_message("If you are running a python app, which python binary are you using? (python, python2, python3)")
28
+ Cli::Display.info_message("If you are not using python, press enter to skip")
29
+ python_command = Cli::Display.select("python command: (choose none if you are not using python)", {"python" => "python", "python2" => "python2", "python3" => "python3", "none" => ""})
30
+
31
+ Cli::Configuration.create(context, api_key, python_command)
32
+ Cli::Display.success_message("successfully created #{Cli::Configuration::FILE_NAME} - you can update this file at any time")
33
+ Cli::Display.info_message("Please add it to your .gitignore file")
34
+ end
35
+
36
+ def self.check_cli_version
37
+ versions = PairProgrammer::Api::System.versions
38
+ if versions["cli"] != Cli::Version::VERSION
39
+ Cli::Display.info_message("A new version of the CLI is available, installing update")
40
+ gem = PairProgrammer::Configuration.development? ? "pear-programmer-0.1.gem" : "pear-programmer"
41
+ Cli::Display.info_message("Running gem update #{gem}")
42
+ system("gem update #{gem}")
43
+ Cli::Display.success_message("Update complete")
44
+ end
45
+ end
46
+
47
+ def self.report_exception(command, e)
48
+ version = Cli::Version::VERSION
49
+ PairProgrammer::Api::System.client_exception(command, e, version)
50
+ Cli::Display.error_message("An error occurred")
51
+ Cli::Display.error_message(e.message)
52
+ end
53
+
54
+ def self.help
55
+ Cli::Display.info_message "Available Commands:"
56
+ Cli::Display.info_message " init - initialize pear-programmer from the root of your project"
57
+ Cli::Display.info_message " help"
58
+ Cli::Display.info_message " coding (new|start|list)"
59
+
60
+ Cli::Display.info_message "Usage examples:"
61
+ Cli::Display.info_message " pairprogrammer init"
62
+ Cli::Display.info_message " pairprogrammer help"
63
+ Cli::Display.info_message " pairprogrammer coding new"
64
+ Cli::Display.info_message " pairprogrammer coding start --id CODER_ID"
65
+ Cli::Display.info_message " pairprogrammer coding list"
66
+ end
67
+
68
+ # CODER
69
+ def self.create_coder(options)
70
+ config = Cli::Configuration.new
71
+ context = Cli::Display.get_input("context (press enter to use default): ")
72
+ if context.empty?
73
+ Cli::Display.info_message("using default context")
74
+ Cli::Display.info_message(config.default_context)
75
+ context = config.default_context
76
+ end
77
+ requirements = Cli::Display.get_input("requirements: ")
78
+ tasks = []
79
+ while true do
80
+ task = Cli::Display.get_input("task (press enter to complete): ")
81
+ if task.empty?
82
+ break
83
+ else
84
+ tasks << task
85
+ end
86
+ end
87
+
88
+ id = PairProgrammer::Api::Coder.create(tasks, context, requirements)
89
+ puts "Created coding assistant #{id}"
90
+ end
91
+
92
+ def self.run_coder(options)
93
+ config = Cli::Configuration.new
94
+ auto = !!options[:auto]
95
+
96
+ if options[:id]
97
+ id = options[:id]
98
+ else
99
+ coders = PairProgrammer::Api::Coder.list
100
+ id = Cli::Display.select("Select your coding assistant:", coders.inject({}) { |hash, coder| hash[coder["requirements"]] = coder["id"]; hash })
101
+ end
102
+
103
+ while true do
104
+ # RETRY when Net::ReadTimeout
105
+ spinner = Cli::Display.spinner
106
+ spinner.auto_spin
107
+ begin
108
+ response = PairProgrammer::Api::Coder.run(id)
109
+ rescue Net::ReadTimeout
110
+ Cli::Display.error_message("connection timed out but coder is still running. reconnecting...")
111
+ next
112
+ ensure
113
+ spinner.stop()
114
+ end
115
+
116
+ if response["running"]
117
+ Cli::Display.info_message("coder is still running, will try again in 20 seconds")
118
+ sleep(20)
119
+ next
120
+ elsif response["reached_max_length"]
121
+ Cli::Display.error_message("conversation has reached its context length due to limitations with LLMs")
122
+ Cli::Display.error_message("please create a new coder, this coder will no longer be able to run")
123
+ return
124
+ elsif response["error"]
125
+ Cli::Display.error_message("there was an error processing the assistant's response")
126
+ Cli::Display.info_message("retrying...")
127
+ next
128
+ end
129
+
130
+ if response["available_tokens"] && response["available_tokens"] < 500
131
+ Cli::Display.info_message("conversation is getting long and approaching context length limit")
132
+ Cli::Display.info_message("this conversation has #{response["available_tokens"]} tokens left")
133
+ end
134
+
135
+ system_message = response["system_message"]
136
+
137
+ response_required = true
138
+ # TODO if there is explanation but no command then response is required
139
+ if system_message["explanation"] && !system_message["explanation"].empty?
140
+ Cli::Display.message("assistant", system_message["explanation"])
141
+ end
142
+
143
+ if system_message["command"]
144
+ skip_command = false
145
+ command_display = PairProgrammer::Command.display_command(system_message["command"], system_message["arguments"])
146
+ Cli::Display.info_message(command_display) if command_display
147
+ response_required = false
148
+ # command overwrites
149
+ if system_message["command"] == "comment"
150
+ Cli::Display.message("assistant", system_message["arguments"]["comment"])
151
+ response_required = true
152
+ elsif system_message["command"] == "ask_question"
153
+ Cli::Display.message("assistant", system_message["arguments"]["question"])
154
+ response_required = true
155
+ else
156
+ if system_message["command"] == "write_file"
157
+ # this fails if file doesn't exist
158
+ file_path = PairProgrammer::Configuration.absolute_path(system_message["arguments"]["file_path"])
159
+ begin
160
+ original_content = File.read(file_path)
161
+ rescue Errno::ENOENT
162
+ Cli::Display.error_message("there was an error running the command, notifying assistant of error.")
163
+ PairProgrammer::Api::Coder.append_exception(id, e)
164
+ Cli::Display.info_message("retrying...")
165
+ next
166
+ end
167
+
168
+ Cli::Display.dispaly_diff(original_content, system_message["arguments"]["content"])
169
+
170
+ confirmation = Cli::Display.confirmation("Accept changes?")
171
+ if !confirmation
172
+ Cli::Display.info_message("changes rejected")
173
+ # PairProgrammer::Api::Coder.append_output(id, "COMMAND REJECTED")
174
+ skip_command = true
175
+ response_required = true
176
+ else
177
+ Cli::Display.info_message("changes accepted")
178
+ end
179
+ end
180
+
181
+ if !skip_command
182
+ output = nil
183
+ begin
184
+ output = PairProgrammer::Command.run(system_message["command"], system_message["arguments"])
185
+ rescue => e
186
+ Cli::Display.error_message("there was an error running the command, notifying assistant of error.")
187
+ PairProgrammer::Api::Coder.append_exception(id, e)
188
+ Cli::Display.info_message("retrying...")
189
+ next
190
+ end
191
+ PairProgrammer::Api::Coder.append_output(id, output)
192
+ end
193
+ end
194
+ end
195
+
196
+ while true do
197
+ # user does not have to respond if auto is true
198
+ if !response_required && auto
199
+ break
200
+ end
201
+
202
+ display = response_required ? "required" : "optional"
203
+ message = Cli::Display.get_input("response (#{display}): ")
204
+ if message.empty?
205
+ if response_required
206
+ Cli::Display.error_message("response required")
207
+ next
208
+ else
209
+ break
210
+ end
211
+ else
212
+ PairProgrammer::Api::Coder.add_user_message(id, message)
213
+ response_required = false
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ def self.list_coders(options)
220
+ coders = PairProgrammer::Api::Coder.list
221
+ Cli::Display.table(coders, ["id", "context", "requirements", "tasks", "created_at"])
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,73 @@
1
+ require 'yaml'
2
+
3
+ module Cli
4
+ class Configuration
5
+ FILE_NAME = ".pear-programmer.yml"
6
+
7
+ def self.create(context, api_key, python_command)
8
+ if !["python", "python2", "python3", ""].include?(python_command)
9
+ raise "Invalid python command, please choose python, python2, or python3"
10
+ end
11
+
12
+ # the version of the configuration file, not cli version
13
+ config = {
14
+ "version" => 1.0,
15
+ "project_settings" => {
16
+ "context" => context,
17
+ },
18
+ "auth" => {
19
+ "api_key" => api_key,
20
+ }
21
+ }
22
+
23
+ if !python_command.empty?
24
+ config["commands"] = {
25
+ "python" => python_command
26
+ }
27
+ end
28
+
29
+ File.open(File.join(Dir.pwd, FILE_NAME), "w") do |file|
30
+ file.write(config.to_yaml)
31
+ end
32
+ end
33
+
34
+ attr_accessor :root
35
+ def initialize
36
+ @root = Dir.pwd
37
+ @configuration_file_path = File.join(@root, FILE_NAME)
38
+ if !File.exists?(@configuration_file_path)
39
+ raise "Pear Programmer configuration file does not exist, please run 'pear-on init' or switch to working directory"
40
+ end
41
+ @configuration_file = YAML.load_file(@configuration_file_path)
42
+
43
+ # validations
44
+ if @configuration_file["auth"]&.[]("api_key").nil? || @configuration_file["auth"]["api_key"].empty?
45
+ raise "Pear Programmer api key is missing. Please add your api key to #{@configuration_file_path}"
46
+ end
47
+ end
48
+
49
+ def python_command
50
+ command = @configuration_file&.[]("commands")&.[]("python")
51
+ if command.nil? || command.empty?
52
+ nil
53
+ else
54
+ command
55
+ end
56
+ end
57
+
58
+ def api_key
59
+ @configuration_file["auth"]["api_key"]
60
+ end
61
+
62
+ def default_context
63
+ @configuration_file["project_settings"]["context"]
64
+ end
65
+
66
+ private
67
+
68
+ def update_file(config)
69
+ File.open(@configuration_file_path, "w") { |file| file.write(config.to_yaml) }
70
+ @configuration_file = config
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,83 @@
1
+ require 'colorize'
2
+ require 'tty-spinner'
3
+ require 'diffy'
4
+ require 'terminal-table'
5
+ require 'tty-prompt'
6
+
7
+ module Cli
8
+ class Display
9
+ def self.message(role, content)
10
+ if role == "user"
11
+ puts("you".colorize(background: :green) + ": " + content)
12
+ else
13
+ puts("assistant".colorize(background: :yellow) + ": " + content)
14
+ end
15
+ end
16
+
17
+ def self.table(rows, headers)
18
+ rows = rows.map(&:values)
19
+ table = Terminal::Table.new(
20
+ headings: headers,
21
+ rows: rows
22
+ )
23
+
24
+ puts table
25
+ end
26
+
27
+ def self.select(title, options)
28
+ prompt = TTY::Prompt.new
29
+ prompt.select(title.colorize(mode: :bold), options, per_page: 100, columnize: 2)
30
+ end
31
+
32
+ def self.confirmation(title)
33
+ while true do
34
+ puts(title.colorize(mode: :bold))
35
+ print("y/N: ".colorize(mode: :bold))
36
+ response = STDIN.gets.chomp.downcase
37
+
38
+ if response.empty?
39
+ puts("Response required".colorize(:red))
40
+ elsif response == "y"
41
+ return true
42
+ elsif response == "n"
43
+ return false
44
+ else
45
+ puts("Invalid response. Must be one letter, case insenstive".colorize(:red))
46
+ end
47
+ end
48
+ end
49
+
50
+ def self.get_input(input)
51
+ print(input.colorize(mode: :bold))
52
+ STDIN.gets.chomp
53
+ end
54
+
55
+ def self.success_message(message)
56
+ puts(message.colorize(:green))
57
+ end
58
+
59
+ def self.error_message(message)
60
+ puts(message.colorize(:red))
61
+ end
62
+
63
+ def self.info_message(message)
64
+ puts(message.colorize(color: :light_black))
65
+ end
66
+
67
+ def self.spinner(title="running")
68
+ TTY::Spinner.new("[:spinner] #{title}", format: :spin_2)
69
+ end
70
+
71
+ def self.dispaly_diff(original_content, new_content)
72
+ Diffy::Diff.new(original_content, new_content, source: 'strings').each do |line|
73
+ if line.start_with?("+")
74
+ puts line.colorize(:green)
75
+ elsif line.start_with?("-")
76
+ puts line.colorize(:red)
77
+ else
78
+ puts line
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,5 @@
1
+ module Cli
2
+ module Version
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
data/lib/cli.rb ADDED
@@ -0,0 +1,59 @@
1
+ require_relative 'cli/actions'
2
+ require_relative 'pairprogrammer/configuration'
3
+ require_relative 'cli/configuration'
4
+ require 'optionparser'
5
+ require_relative 'cli/display'
6
+
7
+
8
+ command = ARGV[0]
9
+
10
+ if command == "init"
11
+ Cli::Actions.init
12
+ return
13
+ elsif command == "help"
14
+ Cli::Actions.help
15
+ return
16
+ end
17
+
18
+ config = Cli::Configuration.new
19
+ PairProgrammer::Configuration.root = config.root
20
+ PairProgrammer::Configuration.api_key = config.api_key
21
+
22
+ begin
23
+ if config.python_command
24
+ PairProgrammer::Configuration.python_command = config.python_command
25
+ end
26
+
27
+ Cli::Actions.check_cli_version
28
+
29
+ options = {}
30
+ case command
31
+ when 'coding'
32
+ subcommand = ARGV[1]
33
+ case subcommand
34
+ when "new"
35
+ Cli::Actions.create_coder(options)
36
+ when "start"
37
+ OptionParser.new do |opts|
38
+ opts.banner = "Usage: coder run [options]"
39
+
40
+ opts.on('-a', '--auto', 'Specify auto') do
41
+ options[:auto] = true
42
+ end
43
+ end.parse!
44
+
45
+ Cli::Actions.run_coder(options)
46
+ when "list"
47
+ Cli::Actions.list_coders(options)
48
+ else
49
+ Cli::Display.error_message "Invalid coding command"
50
+ Cli::Actions.help
51
+ end
52
+ else
53
+ Cli::Display.error_message "Invalid command"
54
+ Cli::Actions.help
55
+ end
56
+ rescue => e
57
+ Cli::Actions.report_exception(ARGV.join(" "), e)
58
+ end
59
+
@@ -0,0 +1,73 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+ require_relative '../configuration'
5
+
6
+ module PairProgrammer
7
+ module Api
8
+ class Client
9
+ def initialize
10
+ @api_key = PairProgrammer::Configuration.api_key
11
+ # TODO api key is not always required, ie checking version
12
+ raise "Missing api key" if @api_key.nil?
13
+
14
+ @domain = PairProgrammer::Configuration.development? ? 'http://localhost:8000' : 'https://www.pearprogrammer.dev'
15
+ end
16
+
17
+ def get(endpoint, query_params={})
18
+ encoded_params = URI.encode_www_form(query_params)
19
+ uri = URI.parse(@domain + endpoint + "?" + encoded_params)
20
+
21
+ # Create a new instance of Net::HTTP for the specified URI
22
+ http = Net::HTTP.new(uri.host, uri.port)
23
+
24
+ # Use SSL if the URI scheme is HTTPS
25
+ http.use_ssl = true if uri.scheme == 'https'
26
+
27
+ # Set the open_timeout and read_timeout values (in seconds)
28
+ # http.open_timeout = 5 # Timeout for opening the connection
29
+ http.read_timeout = 605 # matches openai timeout
30
+
31
+ # Create a GET request
32
+ request = Net::HTTP::Get.new(uri)
33
+ request['Pairprogrammer-Api-Key'] = @api_key
34
+
35
+ # Make the HTTP GET request and handle the response
36
+ response = http.request(request)
37
+ handle_response(response)
38
+ end
39
+
40
+ def post(endpoint, body)
41
+ uri = URI.parse(@domain + endpoint)
42
+ request = Net::HTTP::Post.new(uri)
43
+ request.body = body.to_json
44
+ request['Content-Type'] = 'application/json'
45
+ request['Pairprogrammer-Api-Key'] = @api_key
46
+
47
+ # Create a new instance of Net::HTTP for the specified URI
48
+ http = Net::HTTP.new(uri.host, uri.port)
49
+
50
+ # Use SSL if the URI scheme is HTTPS
51
+ http.use_ssl = true if uri.scheme == 'https'
52
+
53
+ # Set the open_timeout and read_timeout values (in seconds)
54
+ # http.open_timeout = 5 # Timeout for opening the connection
55
+ http.read_timeout = 300 # Timeout for reading the response
56
+
57
+ # Make the HTTP POST request and handle the response
58
+ response = http.request(request)
59
+ handle_response(response)
60
+ end
61
+
62
+ private
63
+
64
+ def handle_response(response)
65
+ if response.code.to_i.between?(200, 299)
66
+ JSON.parse(response.body) if !response.body.empty?
67
+ else
68
+ raise "Error processing your request #{response.body}"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,62 @@
1
+ require_relative 'client'
2
+
3
+ module PairProgrammer
4
+ module Api
5
+ class Coder
6
+ def self.create(tasks, context, requirements)
7
+ body = {
8
+ tasks: tasks,
9
+ context: context,
10
+ requirements: requirements,
11
+ }
12
+ response = Client.new.post('/api/v1/coder', body)
13
+ response["id"]
14
+ end
15
+
16
+ def self.create_from_planner(planner_id)
17
+ body = {
18
+ from_planner: true,
19
+ planner_id: planner_id
20
+ }
21
+ response = Client.new.post('/api/v1/coder', body)
22
+ response["id"]
23
+ end
24
+
25
+ def self.run(id)
26
+ body = {
27
+ id: id
28
+ }
29
+ Client.new.post('/api/v1/coder/run', body)
30
+ end
31
+
32
+ def self.append_output(id, output)
33
+ body = {
34
+ id: id,
35
+ output: output
36
+ }
37
+ Client.new.post('/api/v1/coder/append_output', body)
38
+ end
39
+
40
+ def self.list
41
+ Client.new.get('/api/v1/coder/list', {})
42
+ end
43
+
44
+ def self.add_user_message(id, message)
45
+ body = {
46
+ id: id,
47
+ message: message
48
+ }
49
+ Client.new.post('/api/v1/coder/user_message', body)
50
+ end
51
+
52
+ def self.append_exception(id, e)
53
+ body = {
54
+ exception: e.class.to_s,
55
+ exception_message: e.message,
56
+ id: id
57
+ }
58
+ Client.new.post('/api/v1/coder/append_exception', body)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,48 @@
1
+
2
+ module PairProgrammer
3
+ module Api
4
+ class Planner
5
+ def self.create(context, requirements)
6
+ body = {
7
+ context: context,
8
+ requirements: requirements,
9
+ }
10
+ response = Client.new.post('/api/v1/planner', body)
11
+ response["id"]
12
+ end
13
+
14
+ def self.messages(id)
15
+ query = {
16
+ id: id
17
+ }
18
+ Client.new.get('/api/v1/planner/get_messages', query)
19
+ end
20
+
21
+ def self.run(id)
22
+ body = {
23
+ id: id
24
+ }
25
+ Client.new.post('/api/v1/planner/run', body)
26
+ end
27
+
28
+ def self.list
29
+ Client.new.get('/api/v1/planner/list', {})
30
+ end
31
+
32
+ def self.respond(id, content)
33
+ body = {
34
+ id: id,
35
+ content: content
36
+ }
37
+ Client.new.post('/api/v1/planner/respond', body)
38
+ end
39
+
40
+ def self.generate_tasks(id)
41
+ body = {
42
+ id: id
43
+ }
44
+ Client.new.post('/api/v1/planner/generate_tasks', body)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,22 @@
1
+ require_relative 'client'
2
+
3
+ module PairProgrammer
4
+ module Api
5
+ class System
6
+ def self.versions
7
+ Client.new.get('/api/v1/versions', {})
8
+ end
9
+
10
+ def self.client_exception(command, exception, version)
11
+ body = {
12
+ command: command,
13
+ exception: exception.class.to_s,
14
+ message: exception.message,
15
+ backtrace: exception.backtrace,
16
+ version: version
17
+ }
18
+ Client.new.post('/api/v1/client_exception', body)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,158 @@
1
+ require_relative 'configuration'
2
+ require 'fileutils'
3
+ require 'open3'
4
+
5
+ module PairProgrammer
6
+ class Command
7
+ def self.display_command(command, arguments)
8
+ case command
9
+ when "yarn"
10
+ "running `yarn #{arguments["command"]}`"
11
+ when "mv"
12
+ puts "moving #{arguments["source"]} to #{arguments["destination"]}"
13
+ when "python"
14
+ "running `python #{arguments["command"]}`"
15
+ when "ls"
16
+ "listing files and directories for #{arguments["directory_path"]}"
17
+ when "bundle"
18
+ puts "running `bundle #{arguments["command"]}`"
19
+ when "rails"
20
+ "running `rails #{arguments["command"]}`"
21
+ when "comment"
22
+ nil
23
+ when "write_file"
24
+ puts "updating #{arguments["file_path"]}"
25
+ when "create_directory"
26
+ puts "creating directory #{arguments["directory_path"]}"
27
+ when "delete_file"
28
+ puts "deleting #{arguments["file_path"]}"
29
+ when "view_changes"
30
+ # TODO
31
+ when "delete_lines"
32
+ puts "deleting lines #{line_numbers} from #{arguments["file_path"]}"
33
+ when "rspec"
34
+ puts "running `rspec #{arguments["file_path"]}`"
35
+ when "ask_question"
36
+ nil # will ask question
37
+ when "read_file"
38
+ puts "reading #{arguments["file_path"]}"
39
+ when "update_file"
40
+ puts "updating #{arguments["file_path"]}"
41
+ when "create_file"
42
+ puts "creating #{arguments["file_path"]}"
43
+ end
44
+ end
45
+
46
+ # default
47
+ @@default_commands = {
48
+ python: "python3",
49
+ }
50
+
51
+ def self.python_command
52
+ PairProgrammer::Configuration.python_command || @@default_commands[:python]
53
+ end
54
+
55
+ def self.run_shell(command)
56
+ output = ""
57
+ Open3.popen2e(command) do |stdin, stdout_err, wait_thr|
58
+ while line = stdout_err.gets
59
+ output += line
60
+ end
61
+ end
62
+ output
63
+ end
64
+
65
+ def self.run(command, arguments)
66
+ case command
67
+ when "update_file"
68
+ file_path = PairProgrammer::Configuration.absolute_path(arguments["file_path"])
69
+ file_content = File.readlines(file_path)
70
+ file_content.insert(arguments["line_number"], arguments["content"])
71
+ File.open(file_path, 'w') do |file|
72
+ file_content.each { |line| file.puts(line) }
73
+ end
74
+ when "yarn"
75
+ run_shell "cd #{PairProgrammer::Configuration.root} && yarn #{arguments["command"]}"
76
+ when "mv"
77
+ run_shell "cd #{PairProgrammer::Configuration.root} && mv #{arguments["source"]} #{arguments["destination"]}"
78
+ when "python"
79
+ run_shell "cd #{PairProgrammer::Configuration.root} && #{python_command} manage.py #{arguments["command"]}"
80
+ when "ls"
81
+ run_shell "cd #{PairProgrammer::Configuration.root} && ls #{arguments["directory_path"]}"
82
+ when "bundle"
83
+ run_shell "cd #{PairProgrammer::Configuration.root} && bundle #{arguments["command"]}"
84
+ when "rails"
85
+ run_shell "cd #{PairProgrammer::Configuration.root} && rails #{arguments["command"]}"
86
+ when "comment"
87
+ "comment received"
88
+ when "write_file"
89
+ file_path = PairProgrammer::Configuration.absolute_path(arguments["file_path"])
90
+ File.open(file_path, "w") do |file|
91
+ file.puts(arguments["content"])
92
+ end
93
+ when "create_directory"
94
+ directory_path = PairProgrammer::Configuration.absolute_path(arguments["directory_path"])
95
+ FileUtils.mkdir_p(directory_path)
96
+ when "delete_file"
97
+ file_path = PairProgrammer::Configuration.absolute_path(arguments["file_path"])
98
+ File.delete(file_path)
99
+ when "view_changes"
100
+ # TODO
101
+ when "delete_lines"
102
+ file_path = PairProgrammer::Configuration.absolute_path(arguments["file_path"])
103
+ line_numbers = arguments["line_numbers"]
104
+
105
+ # Read the contents of the file into memory
106
+ lines = File.readlines(file_path)
107
+
108
+ # Delete the specified lines from the contents
109
+ lines.delete_if.with_index { |line, index| line_numbers.include?(index) }
110
+
111
+ # Write the modified contents back to the file
112
+ File.write(file_path, lines.join)
113
+ when "rspec"
114
+ file_path = PairProgrammer::Configuration.absolute_path(arguments["file_path"])
115
+ run_shell "cd #{PairProgrammer::Configuration.root} && rspec #{file_path}}"
116
+ when "ask_question"
117
+ puts arguments["question"]
118
+ STDIN.gets.chomp
119
+ when "read_file"
120
+ file_path = PairProgrammer::Configuration.absolute_path(arguments["file_path"])
121
+ if File.exists?(file_path)
122
+ File.read(file_path)
123
+ else
124
+ "file does not exist"
125
+ end
126
+ when "create_file"
127
+ # TODO return response if file is already created
128
+ file_path = PairProgrammer::Configuration.absolute_path(arguments["file_path"])
129
+ FileUtils.touch(file_path)
130
+ else
131
+ raise "Invalid command: #{command}"
132
+ end
133
+ end
134
+
135
+ # TODO if a process hangs, ie it asks for user input, need to kill it or let the user interact with it
136
+ def self.run_shell(command)
137
+ logs = ""
138
+ Open3.popen2e(command) do |stdin, stdout_err, wait_thr|
139
+ logs = stdout_err.read
140
+ end
141
+ logs
142
+ end
143
+
144
+
145
+ def self.insert_content_at_line(file_path, content, line_number)
146
+ # Read the file's content into an array
147
+ file_content = File.readlines(file_path)
148
+
149
+ # Insert the new content at the desired line number
150
+ file_content.insert(line_number, content)
151
+
152
+ # Write the modified content back to the file
153
+ File.open(file_path, 'w') do |file|
154
+ file_content.each { |line| file.puts(line) }
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,43 @@
1
+ module PairProgrammer
2
+ class Configuration
3
+
4
+ @@api_key = nil
5
+
6
+ def self.root
7
+ @@root || ENV["PEAR_PROGRAMMER_ROOT"]
8
+ end
9
+
10
+ def self.api_key
11
+ @@api_key || ENV["PEAR_PROGRAMMER_API_KEY"]
12
+ end
13
+
14
+ def self.api_key=(api_key)
15
+ @@api_key = api_key
16
+ end
17
+
18
+ def self.root=(root)
19
+ @@root = root
20
+ end
21
+
22
+ def self.absolute_path(relative_path)
23
+ File.join(root, relative_path)
24
+ end
25
+
26
+ def self.python_command
27
+ @@python_command
28
+ end
29
+
30
+ def self.python_command=(python_command)
31
+ available_commands = ["python", "python2", "python3"]
32
+ if available_commands.include?(python_command)
33
+ @@python_command = python_command
34
+ else
35
+ raise "Invalid python command - #{python_command} - command must be one of #{available_commands}"
36
+ end
37
+ end
38
+
39
+ def self.development?
40
+ ENV["PEAR_PROGRAMMER_ENV"] == "development"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,4 @@
1
+ require_relative './pairprogrammer/configuration'
2
+ require_relative './pairprogrammer/command'
3
+ require_relative './pairprogrammer/api/coder'
4
+ require_relative './pairprogrammer/api/system'
@@ -0,0 +1,29 @@
1
+ require_relative '../spec_helper'
2
+ require 'cli/actions'
3
+ require 'cli/display'
4
+ require 'cli/configuration'
5
+
6
+ RSpec.describe Cli::Actions do
7
+ describe '.run_coder' do
8
+ let(:mock_api_response) {
9
+ { 'running' => false, 'system_message' => { 'explanation' => 'This is an explanation', 'command' => 'comment', 'arguments' => { 'comment' => 'This is a comment.' } } }
10
+ }
11
+
12
+ let(:coder_list_response) {
13
+ [{ "requirements": "test", "id": "abcde" }]
14
+ }
15
+
16
+ xit 'calls the coder API, displays messages, and shows a comment' do
17
+ config = instance_double('Cli::Configuration')
18
+ allow(Cli::Configuration).to receive(:new).and_return(config)
19
+
20
+ allow(PairProgrammer::Api::Coder).to receive(:list).and_return(coder_list_response)
21
+ allow(PairProgrammer::Api::Coder).to receive(:run).and_return(mock_api_response)
22
+
23
+ expect(Cli::Display).to receive(:info_message).with('This is an explanation').ordered
24
+ expect(Cli::Display).to receive(:message).with('assistant', 'This is a comment.').ordered
25
+
26
+ Cli::Actions.run_coder({})
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rspec.info/documentation/ for more information about RSpec
7
+
8
+ require 'webmock/rspec'
9
+
10
+ RSpec.configure do |config|
11
+ # Enable flags like --only-failures and --next-failure
12
+ config.example_status_persistence_file_path = "spec/examples.txt"
13
+
14
+ # Disable RSpec exposing methods globally on `Module` and `main`
15
+ config.disable_monkey_patching!
16
+
17
+ config.expect_with :rspec do |c|
18
+ c.syntax = :expect
19
+ end
20
+
21
+ ENV["PEAR_PROGRAMMER_ENV"] = "development"
22
+
23
+ config.before(:each) do
24
+ stub_request(:any, /localhost/)
25
+ .to_return { |_request|
26
+ {status: 200, body: {response: 'stubbed response'}.to_json, headers: {'Content-Type' => 'application/json'}}
27
+ }
28
+ allow(PairProgrammer::Configuration).to receive(:api_key).and_return('fake_api_key')
29
+ end
30
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pear-programmer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Sam Edelstein
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-06-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: open3
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: colorize
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-spinner
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: terminal-table
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: diffy
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: tty-prompt
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: A Ruby command line interface gem for integrating with the coding assistant
98
+ API for various tasks
99
+ email:
100
+ - your.email@example.com
101
+ executables:
102
+ - pear-on
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - bin/pear-on
107
+ - lib/cli.rb
108
+ - lib/cli/actions.rb
109
+ - lib/cli/configuration.rb
110
+ - lib/cli/display.rb
111
+ - lib/cli/version.rb
112
+ - lib/pairprogrammer.rb
113
+ - lib/pairprogrammer/api/client.rb
114
+ - lib/pairprogrammer/api/coder.rb
115
+ - lib/pairprogrammer/api/planner.rb
116
+ - lib/pairprogrammer/api/system.rb
117
+ - lib/pairprogrammer/command.rb
118
+ - lib/pairprogrammer/configuration.rb
119
+ - lib/spec/cli/actions_spec.rb
120
+ - lib/spec/spec_helper.rb
121
+ homepage: https://github.com/username/pairprogrammer
122
+ licenses:
123
+ - MIT
124
+ metadata: {}
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubygems_version: 3.3.7
141
+ signing_key:
142
+ specification_version: 4
143
+ summary: Ruby CLI for interacting with coding assistant API
144
+ test_files: []