compose 0.0.0.0 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d2431419357d7c817741c34e2f795e85ef015127e0e8d3e42b1f59c726b62f8f
4
+ data.tar.gz: 0e97b8ac1259e62671019195fe10425abfcfd87756a88673bf9c935eedb1f2ce
5
+ SHA512:
6
+ metadata.gz: 2aa977b296f3bb5ad630f8689727955ffcdc067254e5ffcc88491e84233f1814436bac94ab083211aa9871f0c3c334aeec982314799d9f23a4d5bb061d3367ac
7
+ data.tar.gz: 71c0bbccdc9da5a9edaad609a8afbac268734203e1f2f815c481c23137649a3ac60e54e2789a2e785b40b6ea79275b4e21fd6a486752ad41f2f1663e21e9ffa8
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # Compose
2
+
3
+ Compose is a Ruby gem that provides AI-assisted code editing capabilities.
4
+
5
+ ## Features
6
+
7
+ - Edit and create multiple files with a single prompt
8
+ - Ask questions about the codebase
9
+ - Uses multiple AI models (Claude and GPT)
10
+ - Verify diffs before applying changes
11
+ - Revert changes with `compose revert`
12
+ - Works with any codebase
13
+
14
+ ## Installation
15
+
16
+ ```
17
+ $ gem install compose
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ Run Compose with:
23
+
24
+ ```
25
+ compose [FILES] [OPTIONS]
26
+ ```
27
+
28
+ - `[FILES]`: Paths to individual files or folders you want to process
29
+ - `--model MODEL`: (Optional) AI model to use (e.g., 'claude-3-5-sonnet', 'gpt-4-turbo')
30
+ - `--include-imports`, `-a`: Include import statements when processing files
31
+
32
+ ## Contributing
33
+
34
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/compose.
35
+
36
+ ## License
37
+
38
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/bin/compose ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'compose'
5
+
6
+ Compose.run(ARGV)
data/config/models.yml ADDED
@@ -0,0 +1,20 @@
1
+ models:
2
+ - name: claude-3-5-sonnet-20240620
3
+ provider: anthropic
4
+ nickname: sonnet35
5
+ output_cpm: 15
6
+ input_cpm: 3
7
+ output_length: 8092
8
+ context_window: 200000
9
+ verify_model: false
10
+
11
+ - name: gpt-4o-mini
12
+ provider: openai
13
+ nickname: 4omini
14
+ output_cpm: 0.6
15
+ input_cpm: 0.15
16
+ output_length: 16384
17
+ context_window: 128000
18
+ verify_model: true
19
+
20
+ preferred_verifier_model: 4omini
@@ -0,0 +1,12 @@
1
+ Use the provided code to answer this question. Answer succinctly and provide code snippets if needed.
2
+
3
+ Use this format for code snippets:
4
+
5
+ ===
6
+ file_path.ext:123
7
+ \`\`\`language
8
+ // code goes here
9
+ \`\`\`
10
+ ===
11
+
12
+ Question: {{QUESTION}}
@@ -0,0 +1,30 @@
1
+ Task: {{TASK}}
2
+
3
+ Follow this spec and return ONLY VALID JSON to suggest additions or replacements in files to make this change in this codebase.
4
+
5
+ Facts:
6
+ 1. You can provide a new filename to create a file.
7
+ 2. Leave toLine empty for additions.
8
+ 3. Make sure the code snippet in the edit is complete. Feel free to make multiple edits, avoid repeating existing code if you can.
9
+ 4. Ensure the line numbers are accurate. Feel free to repeat existing code from previous or after lines to be sure.
10
+
11
+ ```json
12
+ {
13
+ "explain": string; // explain what you want to do and why you're making this change.
14
+ "filename": string;
15
+ "change":
16
+ | {
17
+ "type": "addition";
18
+ "atLine": number;
19
+ }
20
+ | {
21
+ "type": "replacement";
22
+ "fromLineNumber": number;
23
+ "toLineNumber": number;
24
+ };
25
+ "code": string; // Code to insert or replace, make sure \n and " are double escaped.
26
+ }
27
+ ```
28
+
29
+ Respond with a valid JSON object with the key 'edits' which contains an array
30
+ of edit objects following the spec above.
@@ -0,0 +1,33 @@
1
+ Edits:
2
+ ```json
3
+ {{EDIT}}
4
+ ```
5
+
6
+ Above is an edit to the provided code.
7
+ Verify this change:
8
+
9
+ ```json
10
+ {{CHANGE}}
11
+ ```
12
+
13
+ and make sure it's needed and it's only replacing the correct lines, or adding the code to the correct place. Feel free to change additions to replacements or vice versa, or skip edits if they're not needed.
14
+ Return only the fixed change object following this JSON spec:
15
+
16
+ ```json
17
+ {
18
+ "reason": string; // Explain why this change was made, write 'no change' if there's no need for a change
19
+ "type": "addition";
20
+ "atLine": number;
21
+ } | {
22
+ "reason": string;
23
+ "type": "replacement";
24
+ "fromLineNumber": number;
25
+ "toLineNumber": number;
26
+ } | {
27
+ "reason": string;
28
+ "type": "skip" // Means to skip this edit, no need to apply
29
+ };
30
+ ```
31
+
32
+ Respond with a valid JSON object following the spec above. Do not surround the
33
+ response with backticks.
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+ require 'anthropic'
3
+ require 'ruby/openai'
4
+ require 'whirly'
5
+ require 'paint'
6
+
7
+ module Compose
8
+ class AIClient
9
+ def initialize(model)
10
+ @model = model
11
+ end
12
+
13
+ def chat(system_prompt, user_prompt, temperature: 0)
14
+ case @model[:provider]
15
+ when 'anthropic'
16
+ response = anthropic_chat(system_prompt, user_prompt, temperature: temperature)
17
+ response.dig('content', 0, 'text')
18
+ when 'openai'
19
+ response = openai_chat(system_prompt, user_prompt, temperature: temperature)
20
+ response.dig('choices', 0, 'message', 'content')
21
+ else
22
+ raise "Unsupported model: #{@model[:name]}"
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def anthropic_chat(system_prompt, user_prompt, temperature: 0)
29
+ Whirly.start spinner: 'dots', status: 'Received 0 chunks'
30
+ token_count = 0
31
+ client = Anthropic::Client.new(access_token: ENV['ANTHROPIC_API_KEY'])
32
+ response = client.messages(
33
+ parameters: {
34
+ model: @model[:name],
35
+ system: system_prompt,
36
+ messages: [{ role: 'user', content: user_prompt }],
37
+ temperature: temperature,
38
+ max_tokens: @model[:output_length],
39
+ stream: Proc.new { |chunk| token_count += 1; Whirly.status = "Received #{token_count} chunks" }
40
+ }
41
+ )
42
+ Whirly.stop
43
+ response
44
+ end
45
+
46
+ def openai_chat(system_prompt, user_prompt, temperature: 0)
47
+ Whirly.start spinner: 'dots', status: 'Verifying change'
48
+ client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
49
+ response = client.chat(
50
+ parameters: {
51
+ model: @model[:name],
52
+ messages: [
53
+ { role: 'system', content: system_prompt },
54
+ { role: 'user', content: user_prompt }
55
+ ],
56
+ temperature: temperature
57
+ }
58
+ )
59
+ Whirly.stop
60
+ response
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotenv/load'
4
+
5
+ module Compose
6
+ class ApiKeyUtils
7
+ def self.setup(model)
8
+ env_var = env_var_name(model[:provider])
9
+ return unless ENV[env_var].nil?
10
+
11
+ puts "\n#{env_var} is not set in your environment.".yellow
12
+ exit
13
+ end
14
+
15
+ private
16
+
17
+ def self.env_var_name(provider)
18
+ case provider
19
+ when 'anthropic'
20
+ 'ANTHROPIC_API_KEY'
21
+ when 'openai'
22
+ 'OPENAI_API_KEY'
23
+ else
24
+ raise "Unsupported provider: #{provider}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'colorize'
5
+
6
+ module Compose
7
+ class CLI < Thor
8
+ desc 'edit [FILES]', 'Edit files using AI assistance'
9
+ method_option :model, type: :string, default: 'sonnet35', desc: 'AI model to use'
10
+ method_option :include_imports, type: :boolean, default: true, aliases: '-a', desc: 'Include import statements when processing files'
11
+ def edit(*files)
12
+ puts 'Welcome to Compose!'.green
13
+
14
+ if files.empty?
15
+ puts 'Error: No files or folders to process'.red
16
+ exit(1)
17
+ end
18
+
19
+ code_model = Model.find(options[:model])
20
+ ApiKeyUtils.setup(code_model)
21
+ verifier_model = Model.preferred_verifier_model
22
+ ApiKeyUtils.setup(verifier_model)
23
+
24
+ file_processor = FileProcessor.new(files, include_imports: options[:include_imports], model: code_model)
25
+ puts "Loaded #{file_processor.files.count} file(s)."
26
+
27
+ task = ask('What do you need me to do? (Type \'ask\' followed by your question to ask a question instead):')
28
+
29
+ if task.downcase.start_with?('ask ')
30
+ question = task[4..-1].strip
31
+ answer = file_processor.ask_files(question)
32
+ puts answer
33
+ else
34
+ edits = file_processor.edit_files(task)
35
+
36
+ edit_verifier = EditVerifier.new(edits, model: verifier_model)
37
+ verified_edits = edit_verifier.verify_edits
38
+
39
+ edit_processor = EditProcessor.new(verified_edits)
40
+ edit_processor.process_edits
41
+ end
42
+
43
+ puts 'Thank you for using Compose!'.green
44
+ end
45
+
46
+ desc 'version', 'Display the version of Compose'
47
+ def version
48
+ puts "Compose version #{Compose::VERSION}"
49
+ end
50
+
51
+ desc 'revert', 'Revert the last changes made by Compose'
52
+ def revert
53
+ edit_processor = EditProcessor.new
54
+ edit_processor.revert_last_changes
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'thor'
6
+ require 'colorize'
7
+ require 'diffy'
8
+
9
+ module Compose
10
+ class EditProcessor
11
+ include Thor::Shell
12
+ HISTORY_FILE = 'compose_history.json'
13
+
14
+ attr_reader :edits
15
+
16
+ def initialize(edits)
17
+ @edits = edits
18
+ @confirmed_edits = []
19
+ end
20
+
21
+ def process_edits
22
+ edits.each do |edit|
23
+ if confirm_edit(edit[:edit])
24
+ @confirmed_edits << edit[:edit]
25
+ end
26
+ end
27
+
28
+ if @confirmed_edits.any?
29
+ if ask('Do you want to apply all confirmed edits? (y/n)') == 'y'
30
+ apply_confirmed_edits
31
+ else
32
+ puts 'All changes discarded.'
33
+ end
34
+ else
35
+ puts 'No edits were confirmed.'
36
+ end
37
+ end
38
+
39
+ # def revert_last_changes
40
+ # history_path = File.join(Dir.pwd, HISTORY_FILE)
41
+ # unless File.exist?(history_path)
42
+ # puts 'No history file found. Nothing to revert.'
43
+ # return
44
+ # end
45
+
46
+ # history = JSON.parse(File.read(history_path))
47
+
48
+ # if history.empty?
49
+ # puts 'No changes to revert.'
50
+ # return
51
+ # end
52
+
53
+ # puts 'Recent changes:'
54
+ # history.each_with_index do |file_history, index|
55
+ # puts "#{index + 1}. #{file_history['filename']} (#{file_history['edits'].length} edits)"
56
+ # end
57
+
58
+ # if ask('Do you want to revert all changes? (y/n)') == 'y'
59
+ # history.each do |file_history|
60
+ # if file_history['original_content'].empty?
61
+ # File.delete(file_history['filename']) if File.exist?(file_history['filename'])
62
+ # puts "Deleted file: #{file_history['filename']}"
63
+ # else
64
+ # File.write(file_history['filename'], file_history['original_content'])
65
+ # puts "Reverted changes in: #{file_history['filename']}"
66
+ # end
67
+ # end
68
+
69
+ # File.write(history_path, '[]')
70
+ # puts 'All changes have been reverted and history has been cleared.'
71
+ # else
72
+ # puts 'Revert operation cancelled.'
73
+ # end
74
+ # end
75
+
76
+ private
77
+
78
+ def confirm_edit(edit)
79
+ puts '=' * 50
80
+ puts "Proposed change for #{edit[:filename]}:".cyan
81
+ puts edit[:explain]
82
+ puts '=' * 50
83
+
84
+ old_content = File.exist?(edit[:filename]) ? File.readlines(edit[:filename]) : []
85
+ new_content = old_content.dup
86
+
87
+ case edit[:change][:type]
88
+ when 'addition'
89
+ new_content.insert(edit[:change][:atLine] - 1, edit[:code])
90
+ when 'replacement'
91
+ new_content[edit[:change][:fromLineNumber] - 1..edit[:change][:toLineNumber] - 1] = edit[:code].split("\n").map { |line| line + "\n" }
92
+ end
93
+
94
+ diff = Diffy::Diff.new(old_content.join, new_content.join, context: 3)
95
+ puts diff.to_s(:color)
96
+ puts '=' * 50
97
+
98
+ ask('Do you want to confirm this change? (y/n)') == 'y'
99
+ end
100
+
101
+ def apply_confirmed_edits
102
+ file_edits = @confirmed_edits.group_by { |edit| edit[:filename] }
103
+
104
+ file_edits.each do |filename, edits|
105
+ content = File.exist?(filename) ? File.readlines(filename) : []
106
+ line_offset = 0
107
+
108
+ edits.sort_by { |edit| edit[:change][:type] == 'addition' ? edit[:change][:atLine] : edit[:change][:fromLineNumber] }.each do |edit|
109
+ case edit[:change][:type]
110
+ when 'addition'
111
+ adjusted_line = edit[:change][:atLine] + line_offset
112
+ content.insert(adjusted_line - 1, edit[:code])
113
+ line_offset += edit[:code].count("\n") + 1
114
+ when 'replacement'
115
+ from_line = edit[:change][:fromLineNumber] + line_offset
116
+ to_line = edit[:change][:toLineNumber] + line_offset
117
+ old_lines = to_line - from_line + 1
118
+ new_lines = edit[:code].count("\n") + 1
119
+ content[from_line - 1..to_line - 1] = edit[:code].split("\n").map { |line| line + "\n" }
120
+ line_offset += new_lines - old_lines
121
+ end
122
+ end
123
+
124
+ FileUtils.mkdir_p(File.dirname(filename))
125
+ File.write(filename, content.join)
126
+ puts "Applied changes to #{filename}".green
127
+ end
128
+
129
+ save_edit_history(@confirmed_edits)
130
+ puts 'All changes have been applied and saved.'.green
131
+ end
132
+
133
+ def save_edit_history(edits)
134
+ history_path = File.join(Dir.pwd, HISTORY_FILE)
135
+ history = File.exist?(history_path) ? JSON.parse(File.read(history_path)) : []
136
+
137
+ edits.each do |edit|
138
+ file_history = history.find { |h| h['filename'] == edit[:filename] }
139
+ if file_history
140
+ file_history['edits'] << edit
141
+ else
142
+ original_content = File.exist?(edit[:filename]) ? File.read(edit[:filename]) : ''
143
+ history << {
144
+ 'filename' => edit[:filename],
145
+ 'original_content' => original_content,
146
+ 'edits' => [edit]
147
+ }
148
+ end
149
+ end
150
+
151
+ File.write(history_path, JSON.pretty_generate(history))
152
+ puts 'Edit history has been updated.'.green
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Compose
4
+ class EditVerifier
5
+ attr_reader :ai_client, :edits
6
+
7
+ def initialize(edits, model:)
8
+ @ai_client = AIClient.new(model)
9
+ @edits = edits
10
+ end
11
+
12
+ def verify_edits
13
+ verified_edits = []
14
+
15
+ edits.each do |edit|
16
+ case edit[:type]
17
+ when 'edit'
18
+ verified_edit = verify_edit(edit[:edit])
19
+ verified_edits << { type: 'edit', edit: verified_edit } if verified_edit
20
+ end
21
+ end
22
+
23
+ verified_edits
24
+ end
25
+
26
+ def verify_edit(edit)
27
+ return edit unless File.exist?(edit[:filename])
28
+
29
+ prompt = File.read(File.expand_path("../../config/prompts/verify.txt", __dir__))
30
+ prompt.gsub!('{{EDIT}}', edit.to_json)
31
+ prompt.gsub!('{{CHANGE}}', edit[:change].to_json)
32
+
33
+ file_content = FileProcessor.load_file(File.expand_path(edit[:filename]))
34
+ system_prompt = "CODE:\n#{file_content}\n"
35
+
36
+ response = ai_client.chat(system_prompt, prompt)
37
+
38
+ begin
39
+ verified_change = JSON.parse(response)
40
+ verified_change = verified_change.transform_keys(&:to_sym)
41
+ return nil if verified_change[:type] == 'skip'
42
+
43
+ edit[:change] = verified_change
44
+ edit
45
+ rescue JSON::ParserError
46
+ puts "Failed to parse verification response. Using original edit."
47
+ edit
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'find'
4
+ require 'pathname'
5
+
6
+ module Compose
7
+ class FileProcessor
8
+ attr_reader :ai_client, :files, :tokens
9
+
10
+ def self.load_file(file_path, include_imports: true)
11
+ content = File.read(file_path)
12
+ processed_content = content.split("\n").map.with_index { |line, index| "L#{index + 1}: #{line}" }.join("\n")
13
+
14
+ unless include_imports
15
+ processed_content.gsub!(/L\d+:\s*require.*?\n/, '')
16
+ processed_content.gsub!(/L\d+:\s*import.*?\n/, '')
17
+ end
18
+
19
+ relative_path = Pathname.new(file_path).relative_path_from(Pathname.new(Dir.pwd)).to_s
20
+ "<#{relative_path}>\n#{processed_content}\n</#{relative_path}>"
21
+ end
22
+
23
+ def initialize(input_files, include_imports: true, model:)
24
+ @ai_client = AIClient.new(model)
25
+ @files = {}
26
+
27
+ input_files.each do |input|
28
+ if File.directory?(input)
29
+ find_files_in_directory(input).each do |file|
30
+ @files[File.expand_path(file)] = nil
31
+ end
32
+ elsif File.file?(input)
33
+ @files[File.expand_path(input)] = nil
34
+ else
35
+ puts "Skipping invalid input: #{input}".yellow
36
+ end
37
+ end
38
+
39
+ @files.each do |file, _|
40
+ @files[file] = FileProcessor.load_file(file, include_imports: include_imports)
41
+ end
42
+
43
+ self
44
+ end
45
+
46
+ def content
47
+ @files.values.join("\n\n")
48
+ end
49
+
50
+ def ask_files(question)
51
+ prompt = File.read(File.expand_path("../../config/prompts/ask.txt", __dir__))
52
+ prompt.gsub!('{{QUESTION}}', question)
53
+ system_prompt = "CODE:\n#{content}\n"
54
+
55
+ ai_client.chat(system_prompt, prompt)
56
+ end
57
+
58
+ def edit_files(task)
59
+ prompt = File.read(File.expand_path("../../config/prompts/task.txt", __dir__))
60
+ prompt.gsub!('{{TASK}}', task)
61
+ system_prompt = "CODE:\n#{content}\n"
62
+
63
+ response = ai_client.chat(system_prompt, prompt)
64
+
65
+ begin
66
+ edits = JSON.parse(response)['edits']
67
+ edits.map do |edit|
68
+ edit = edit.transform_keys(&:to_sym)
69
+ edit[:change] = edit[:change].transform_keys(&:to_sym)
70
+ {
71
+ type: 'edit',
72
+ edit: edit
73
+ }
74
+ end
75
+ rescue JSON::ParserError => e
76
+ [{ type: 'error', error: "Failed to parse AI response: #{e.message}" }]
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def find_files_in_directory(directory)
83
+ Find.find(directory).select { |path| File.file?(path) && path =~ /\.(rb|py|js|ts)$/ }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,25 @@
1
+ require 'yaml'
2
+
3
+ module Compose
4
+ class Model
5
+ def self.yaml
6
+ return @yaml if @yaml
7
+
8
+ yaml_content = File.read(File.expand_path("../../config/models.yml", __dir__))
9
+ @yaml = YAML.safe_load(yaml_content, symbolize_names: true)
10
+ end
11
+
12
+ def self.all
13
+ yaml[:models]
14
+ end
15
+
16
+ def self.find(name)
17
+ all.find { |model| model[:name] == name || model[:nickname] == name }
18
+ end
19
+
20
+ def self.preferred_verifier_model
21
+ preferred_nickname = yaml[:preferred_verifier_model]
22
+ all.find { |model| model[:nickname] == preferred_nickname }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Compose
4
+ VERSION = '0.1.4'
5
+ end
data/lib/compose.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'compose/version'
4
+ require_relative 'compose/cli'
5
+ require_relative 'compose/api_key_utils'
6
+ require_relative 'compose/file_processor'
7
+ require_relative 'compose/edit_processor'
8
+ require_relative 'compose/ai_client'
9
+ require_relative 'compose/edit_verifier'
10
+ require_relative 'compose/model'
11
+
12
+ module Compose
13
+ class Error < StandardError; end
14
+
15
+ def self.run(args)
16
+ CLI.start(args)
17
+ end
18
+ end
metadata CHANGED
@@ -1,56 +1,258 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: compose
3
- version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 0.0.0.0
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.4
6
5
  platform: ruby
7
- authors:
8
- - David Trasbo
9
- autorequire:
6
+ authors:
7
+ - Dreaming Tulpa
8
+ autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
-
13
- date: 2011-03-23 00:00:00 +01:00
14
- default_executable:
15
- dependencies: []
16
-
17
- description:
18
- email: me@dtrasbo.com
19
- executables: []
20
-
11
+ date: 2024-09-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby-openai
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: anthropic
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: colorize
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: dotenv
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: faraday
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: json
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
+ - !ruby/object:Gem::Dependency
98
+ name: thor
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: diffy
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: whirly
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: paint
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: fiddle
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: bundler
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '2.0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '2.0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rake
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '13.0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '13.0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: byebug
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ description: compose is a Ruby gem that provides AI-assisted code editing capabilities.
210
+ email:
211
+ - hey@dreamingtulpa.com
212
+ executables:
213
+ - compose
21
214
  extensions: []
22
-
23
215
  extra_rdoc_files: []
24
-
25
- files: []
26
-
27
- has_rdoc: true
28
- homepage:
29
- licenses: []
30
-
31
- post_install_message:
216
+ files:
217
+ - README.md
218
+ - bin/compose
219
+ - config/models.yml
220
+ - config/prompts/ask.txt
221
+ - config/prompts/task.txt
222
+ - config/prompts/verify.txt
223
+ - lib/compose.rb
224
+ - lib/compose/ai_client.rb
225
+ - lib/compose/api_key_utils.rb
226
+ - lib/compose/cli.rb
227
+ - lib/compose/edit_processor.rb
228
+ - lib/compose/edit_verifier.rb
229
+ - lib/compose/file_processor.rb
230
+ - lib/compose/model.rb
231
+ - lib/compose/version.rb
232
+ homepage: https://github.com/dreamingtulpa/compose
233
+ licenses:
234
+ - MIT
235
+ metadata:
236
+ homepage_uri: https://github.com/dreamingtulpa/compose
237
+ source_code_uri: https://github.com/dreamingtulpa/compose
238
+ changelog_uri: https://github.com/dreamingtulpa/compose/blob/main/CHANGELOG.md
239
+ post_install_message:
32
240
  rdoc_options: []
33
-
34
- require_paths:
241
+ require_paths:
35
242
  - lib
36
- required_ruby_version: !ruby/object:Gem::Requirement
37
- none: false
38
- requirements:
243
+ required_ruby_version: !ruby/object:Gem::Requirement
244
+ requirements:
39
245
  - - ">="
40
- - !ruby/object:Gem::Version
41
- version: "0"
42
- required_rubygems_version: !ruby/object:Gem::Requirement
43
- none: false
44
- requirements:
246
+ - !ruby/object:Gem::Version
247
+ version: 2.7.0
248
+ required_rubygems_version: !ruby/object:Gem::Requirement
249
+ requirements:
45
250
  - - ">="
46
- - !ruby/object:Gem::Version
47
- version: "0"
251
+ - !ruby/object:Gem::Version
252
+ version: '0'
48
253
  requirements: []
49
-
50
- rubyforge_project:
51
- rubygems_version: 1.6.2
52
- signing_key:
53
- specification_version: 3
54
- summary: Reserved gem name. Contact author if you want it, but there are no guarantees.
254
+ rubygems_version: 3.5.18
255
+ signing_key:
256
+ specification_version: 4
257
+ summary: A Ruby gem for AI-assisted code editing
55
258
  test_files: []
56
-