cowrite 0.2.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: '0802510583d99ef115f4b8b21bcbf2ecb6d9924f0b8da3e5e494c47c74e7f5f3'
4
- data.tar.gz: 591ba65a82edf2263336d8fe1cc1114c0462eac8138d0d587160187e6ea72b52
3
+ metadata.gz: 2cffeb7bd5c44317c2084f80ef1e3e0c9c9356f203bce74b3d4538df3f546fc5
4
+ data.tar.gz: 8cbf2cebb9b34e5e4e9768f205a4d67714cdd256be12ba7443bb97515a9839fe
5
5
  SHA512:
6
- metadata.gz: 7c5af9c56e2198b2976af8137f984e99632d52978b1002639e5ab8c6e23e7094b8acef41865d75e04a6802d4463eda1cf041790b0ce91c2c890b47f5c7784c76
7
- data.tar.gz: 32aa39788c1ea19c2acb3eb0af9ea836d2d0a54688eac498a915248b2b2446f6ee645cd0ed582572488190adf0dfd3eff53ad35a031906fc379ecda3a92ec360
6
+ metadata.gz: e723beeae2cbd86dbd381a8ebb7747226b68efec1fac667b2ec0e1455da35549a46007f4b3ecd422bf297b656d5f5dbc5ee2cba5c3b0cc31484732ecdefaf77d
7
+ data.tar.gz: 19f32448a142bcb65b413168314eceeb0231dfb5db6705a8ee350dd7918a0406035ea5b49ddb2977e73c667827a3420ec77a33026bf68949bc00e75c140aa6d3
data/lib/cowrite/cli.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "shellwords"
4
4
  require "parallel"
5
5
  require "tempfile"
6
+ require "optparse"
6
7
 
7
8
  class Cowrite
8
9
  class CLI
@@ -19,38 +20,73 @@ class Cowrite
19
20
  end
20
21
 
21
22
  def run(argv)
22
- argv, files = parse_argv(argv)
23
-
24
- abort "Use only first argument for prompt" if argv.size != 1
25
- prompt = argv[0]
23
+ prompt, files, options = parse_argv(argv)
26
24
 
27
25
  files = find_files prompt if files.empty?
28
26
 
29
- # prompting on main thread so we can go 1-by-1
30
- finish = lambda do |file, i, diff|
31
- # ask user if diff is fine (TODO: add a "no" option and re-prompt somehow)
32
- prompt "Diff for #{file}:\n#{color_diff(diff)}Apply diff to #{file}?", ["yes"]
27
+ if (parallel = options[:parallel])
28
+ prompt_for_each_file_in_parallel prompt, files, parallel
29
+ else
30
+ prompt_with_all_files_in_context prompt, files
31
+ end
32
+ end
33
+
34
+ private
33
35
 
34
- with_content_in_file(diff) do |path|
35
- # apply diff (force sus changes, do not make backups)
36
- cmd = "patch --posix #{file} < #{path}"
37
- out = `#{cmd}`
38
- return if $?.success?
36
+ # send all files at once to have a bigger context
37
+ # TODO: might break if context gets too big
38
+ def prompt_with_all_files_in_context(prompt, files)
39
+ diffs = @cowrite.diffs(prompt, files)
40
+ diffs.each_with_index do |(file, diff), i|
41
+ last = (i + 1 == diffs.size)
42
+ prompt_to_apply_diff file, diff, last:
43
+ end
44
+ end
39
45
 
40
- # give the user a chance to copy the tempfile or modify it
41
- warn "Patch failed:\n#{cmd}\n#{out}"
42
- prompt "Continue ?", ["yes"] unless i + 1 == files.size
46
+ # run each file in parallel (faster) or all at once ?
47
+ # prompting on main thread so we don't get parallel prompts
48
+ def prompt_for_each_file_in_parallel(prompt, files, parallel)
49
+ finish = lambda do |file, i, diffs|
50
+ diffs.each_with_index do |(_, diff), j|
51
+ last = (i + 1 == files.size && j + 1 == diffs.size)
52
+ prompt_to_apply_diff file, diff, last:
43
53
  end
44
54
  end
45
55
 
46
- # TODO: --parallel instead of env
47
- # produce diffs in parallel since it is slow
48
- Parallel.each files, finish:, threads: Integer(ENV["PARALLEL"] || "10"), progress: true do |file|
49
- @cowrite.diff file, prompt
56
+ # produce diffs in parallel since multiple files will be slow and can confuse the model
57
+ Parallel.each files, finish:, threads: parallel, progress: true do |file, _i|
58
+ @cowrite.diffs prompt, [file]
50
59
  end
51
60
  end
52
61
 
53
- private
62
+ def prompt_to_apply_diff(file, diff, last:)
63
+ # ask user if diff is fine (TODO: add a "no" option and re-prompt somehow)
64
+ prompt "Diff for #{file}:\n#{color_diff(diff)}Apply diff to #{file}?", ["yes"]
65
+ return if apply_diff(file, diff)
66
+
67
+ # ask user to continue if diff failed to apply (to give time for manual fixes or abort)
68
+ prompt "Continue ?", ["yes"] unless last
69
+ end
70
+
71
+ # apply diff (force sus changes, do not make backups)
72
+ def apply_diff(file, diff)
73
+ with_content_in_file(diff) do |path|
74
+ ensure_file_exists(file)
75
+ cmd = "patch --posix #{file} < #{path}"
76
+ out = `#{cmd}`
77
+ return true if $?.success?
78
+
79
+ # give the user a chance to copy the tempfile or modify it
80
+ warn "Patch failed:\n#{cmd}\n#{out}"
81
+ false
82
+ end
83
+ end
84
+
85
+ def ensure_file_exists(file)
86
+ return if File.exist?(file)
87
+ FileUtils.mkdir_p(File.dirname(file))
88
+ File.write file, ""
89
+ end
54
90
 
55
91
  def with_content_in_file(content)
56
92
  Tempfile.create("cowrite-diff") do |f|
@@ -76,7 +112,7 @@ class Cowrite
76
112
  files.each_with_index.filter_map { |f, i| f if chosen.include?(i.to_s) } + # index
77
113
  chosen.grep_v(/^\d+$/) # path
78
114
  missing = chosen.reject { |f| File.exist?(f) }
79
- abort "Files #{missing} do not exist" if missing.any?
115
+ warn "Files #{missing} do not exist, assuming they will be created" if missing.any?
80
116
  chosen
81
117
  end
82
118
 
@@ -147,13 +183,42 @@ class Cowrite
147
183
  string.gsub(/\e\[(\d+)(;\d+)*m/, "")
148
184
  end
149
185
 
150
- # allow passing files after --
151
186
  def parse_argv(argv)
152
- if (dash_index = argv.index("--"))
153
- [argv[0...dash_index], argv[dash_index + 1..]]
154
- else
155
- [argv, []]
156
- end
187
+ # allow passing files after --
188
+ argv, files =
189
+ if (dash_index = argv.index("--"))
190
+ [argv[0...dash_index], argv[dash_index + 1..]]
191
+ else
192
+ [argv, []]
193
+ end
194
+
195
+ options = {}
196
+ OptionParser.new do |opts|
197
+ opts.banner = <<-BANNER.gsub(/^ {10}/, "")
198
+ WWTD: Travis simulator - faster + no more waiting for build emails
199
+
200
+ Usage:
201
+ wwtd
202
+
203
+ Options:
204
+ BANNER
205
+ opts.on("-p", "--parallel [COUNT]", Integer, "Run each file on its own in parallel") do |c|
206
+ options[:parallel] = c
207
+ end
208
+ opts.on("-h", "--help", "Show this.") do
209
+ puts opts
210
+ exit
211
+ end
212
+ opts.on("-v", "--version", "Show Version") do
213
+ puts WWTD::VERSION
214
+ exit
215
+ end
216
+ end.parse!(argv)
217
+
218
+ abort "Use only first argument for prompt" if argv.size != 1
219
+ prompt = argv[0]
220
+
221
+ [prompt, files, options]
157
222
  end
158
223
  end
159
224
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  class Cowrite
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.0"
4
4
  end
data/lib/cowrite.rb CHANGED
@@ -17,12 +17,13 @@ class Cowrite
17
17
  abort files unless $?.success?
18
18
 
19
19
  prompt = <<~MSG
20
- Your task is to find a subset of files from a given list of file names.
21
- Only reply with the subset of files, newline separated, nothing else.
20
+ Your task is to find files that need to be written or read to solve an LLM-prompt.
22
21
 
23
- Given this list of files: #{files.split("\n").inspect}
22
+ Only reply with a list of files, newline separated, nothing else.
24
23
 
25
- Which subset of files would be useful for this LLM prompt:
24
+ List of local files: #{files.split("\n").inspect}
25
+
26
+ LLM-prompt:
26
27
  ```
27
28
  #{prompt}
28
29
  ```
@@ -33,38 +34,44 @@ class Cowrite
33
34
  without_quotes(answer).split("\n").map(&:strip) # llms like to add extra spaces
34
35
  end
35
36
 
36
- def diff(file, prompt)
37
+ def diffs(prompt, files)
37
38
  # - tried "patch format" but that is often invalid
38
39
  # - tied "full fixed content" but that is always missing the fix
39
40
  # - need "ONLY" or it adds comments
40
41
  # - tried asking for a diff but it's always in the wrong direction or has sublet bugs that make it unusable
41
- content = File.read file
42
42
  prompt = <<~MSG
43
43
  Solve this prompt:
44
44
  ```
45
45
  #{prompt}
46
46
  ```
47
47
 
48
- By changing the content of the file #{file}:
49
- ```
50
- #{File.read file}
51
- ```
48
+ By changing the content of these files:
49
+ #{files.map { |f| "#{f}:\n```#{file_read_or_empty f}```" }.join("\n")}
52
50
 
53
- Reply with ONLY:
54
- - all changed content inside a single ``` block
55
- - what range lines from the original content need to be removed as "LINES: <start>-<end>"
51
+ Reply with ONLY a newline separated list of changes in the format:
52
+ - what range lines from the original content need to be removed as "FILE: <file> LINES: <start>-<end>"
53
+ - changed chunk inside a "```code" block
56
54
  MSG
55
+
57
56
  puts "prompt:#{prompt}" if ENV["DEBUG"]
58
57
  answer = send_to_openai(prompt)
59
58
  puts "answer:\n#{answer}" if ENV["DEBUG"]
60
- lines = answer.match(/LINES: (\d+)-(\d+)/)
61
- answer.sub!(/.*\z/, "") # remove "LINES" line
62
- section = without_quotes(answer).chomp # remove trailing newline since it always emits with an extra one
63
- generate_diff content, section, from: Integer(lines[1]), to: Integer(lines[2])
59
+
60
+ # - also getting leading "- " since sometimes the model prefixes that
61
+ # - getting any kind of block since sometimes the model uses "```go"
62
+ changes = answer.scan(/-? ?FILE: (\S+) LINES: (\d+)-(\d+)\n```\S+\n(.*?)\n```/m)
63
+ changes.map do |file, start, finish, diff|
64
+ content = file_read_or_empty file
65
+ [file, generate_diff(content, diff, from: Integer(start), to: Integer(finish))]
66
+ end
64
67
  end
65
68
 
66
69
  private
67
70
 
71
+ def file_read_or_empty(file)
72
+ File.exist?(file) ? File.read(file) : ""
73
+ end
74
+
68
75
  # remove ```foo<content>``` wrapping
69
76
  def without_quotes(answer)
70
77
  answer.strip.sub(/\A```\S*\n(.*)```\z/m, "\\1")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cowrite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-24 00:00:00.000000000 Z
11
+ date: 2024-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parallel