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 +4 -4
- data/lib/cowrite/cli.rb +93 -28
- data/lib/cowrite/version.rb +1 -1
- data/lib/cowrite.rb +24 -17
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2cffeb7bd5c44317c2084f80ef1e3e0c9c9356f203bce74b3d4538df3f546fc5
|
|
4
|
+
data.tar.gz: 8cbf2cebb9b34e5e4e9768f205a4d67714cdd256be12ba7443bb97515a9839fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
prompt
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
data/lib/cowrite/version.rb
CHANGED
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
|
|
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
|
-
|
|
22
|
+
Only reply with a list of files, newline separated, nothing else.
|
|
24
23
|
|
|
25
|
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
55
|
-
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
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-
|
|
11
|
+
date: 2024-10-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: parallel
|