dorian 2.1.0 → 2.2.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/dorian/bin.rb +325 -12
  4. metadata +19 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 926123b36e9d821e68bec3a83818a70f7a39f6f52c1fa5d19ac777dc98b64da0
4
- data.tar.gz: ae5fda1a6fb16ae0ae1d3fd61a329608586329fde537e4c4430513f0d2e1eb25
3
+ metadata.gz: dc5cf78891ac8f7ed7f4ebe350d639c6e42bc7cfa2a8bd1fa6ad526b3e9785db
4
+ data.tar.gz: '018eee86347e85d6e02b12d23f0a8eadb34a1ba429e0881d8c14fe656a3fa956'
5
5
  SHA512:
6
- metadata.gz: 5c9bbdc2e3951bd84159719b1292dce233c4092937cdb18054301e438517030bf4bf3b2352a89ec1cb705751130f8f374b1bc40b7496951be21b7744be36cf5c
7
- data.tar.gz: 1e9210a0bf87c799539ac9ce4c39dd774c4483facdd326718c31bb3655a895a08b1aae273e80eb87a0f32ad6aa46491bbe73a4a2ec5645102ee4c59665019f50
6
+ metadata.gz: 98bfeb69db7cc33735780171db3a4f507e6d13f233bd18b2ae0c0e1d264999bff7827fabb5cbcd4590afdc3bf870d4a6d1c701f2257b502bb72f9ad3ea01f11a
7
+ data.tar.gz: a3351fdbc3b191402813581845e15fd049276510365a13417d461a9806c1f018073e6c65ab2974167269386cab2a569ec70a4c94302ce8740b3ae1ccb0df6654
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.0
1
+ 2.2.1
data/lib/dorian/bin.rb CHANGED
@@ -5,8 +5,11 @@ require "dorian/arguments"
5
5
  require "dorian/eval"
6
6
  require "dorian/progress"
7
7
  require "dorian/to_struct"
8
+ require "git"
8
9
  require "json"
10
+ require "net/http"
9
11
  require "parallel"
12
+ require "uri"
10
13
  require "yaml"
11
14
 
12
15
  class Dorian
@@ -26,7 +29,7 @@ class Dorian
26
29
  "ymll" => :yamll
27
30
  }.freeze
28
31
 
29
- attr_reader :parsed, :command, :arguments, :ruby
32
+ attr_reader :parsed, :command, :arguments, :ruby, :ruby_before, :ruby_after
30
33
 
31
34
  def initialize
32
35
  @parsed =
@@ -116,15 +119,64 @@ class Dorian
116
119
  when :each
117
120
  arguments.delete("each")
118
121
  @command = :each
119
- @ruby = arguments.join(" ")
120
- @arguments = []
122
+ @ruby = arguments.delete_at(0)
121
123
  command_each
122
124
  when :all
123
125
  arguments.delete("all")
124
126
  @command = :all
125
- @ruby = arguments.join(" ")
126
- @arguments = []
127
+ @ruby = arguments.delete_at(0)
127
128
  command_all
129
+ when :before
130
+ arguments.delete("before")
131
+ @command = :before
132
+ @ruby = arguments.delete_at(0)
133
+ command_before
134
+ when :after
135
+ arguments.delete("after")
136
+ @command = :after
137
+ @ruby = arguments.delete_at(0)
138
+ command_after
139
+ when :between
140
+ arguments.delete("between")
141
+ @command = :between
142
+ @ruby_after = arguments.delete_at(0)
143
+ @ruby_before = arguments.delete_at(0)
144
+ command_between
145
+ when :select
146
+ arguments.delete("select")
147
+ @command = :select
148
+ @ruby = arguments.delete_at(0)
149
+ command_select
150
+ when :reject
151
+ arguments.delete("reject")
152
+ @command = :reject
153
+ @ruby = arguments.delete_at(0)
154
+ command_reject
155
+ when :tally
156
+ arguments.delete("tally")
157
+ @command = :tally
158
+ @ruby = arguments.delete_at(0)
159
+ command_tally
160
+ when :anonymize
161
+ arguments.delete("anonymize")
162
+ @command = :anonymize
163
+ command_anonymize
164
+ when :append
165
+ arguments.delete("append")
166
+ @command = :append
167
+ command_append
168
+ when :prepend
169
+ arguments.delete("prepend")
170
+ @command = :prepend
171
+ command_prepend
172
+ when :chat
173
+ arguments.delete("chat")
174
+ @command = :chat
175
+ command_chat
176
+ when :commit
177
+ arguments.delete("commit")
178
+ @command = :commit
179
+ command_commit
128
180
  else
129
181
  arguments.delete("read")
130
182
  @command = :read
@@ -136,6 +188,43 @@ class Dorian
136
188
  parsed.files
137
189
  end
138
190
 
191
+ def command_chat
192
+ puts completion(
193
+ token: token(".chat"),
194
+ model: "gpt-4o",
195
+ messages: [{ role: :user, content: everything.join("\n") }]
196
+ )
197
+ end
198
+
199
+ def command_commit
200
+ system_prompt = "simple, clear, short, lowercase commit message"
201
+ prompt_1 = "for the following diff:"
202
+ prompt_2 = "for the following git status:"
203
+ prompt_3 = "for the following comment:"
204
+
205
+ content_1 = short(`git diff --staged`)
206
+ content_2 = short(`git status`)
207
+ content_3 = short(arguments.join("\n"))
208
+
209
+ abort "no staged files" if content_1.empty?
210
+
211
+ messages = [
212
+ { role: :system, content: system_prompt },
213
+ { role: :system, content: prompt_1 },
214
+ { role: :user, content: content_1 },
215
+ { role: :system, content: prompt_2 },
216
+ { role: :user, content: content_2 },
217
+ { role: :system, content: prompt_3 },
218
+ { role: :user, content: content_3 }
219
+ ]
220
+
221
+ message = completion(token: token(".commit"), model: "gpt-4o", messages:)
222
+
223
+ Git.open(".").commit(message)
224
+
225
+ puts message
226
+ end
227
+
139
228
  def command_read
140
229
  each(stdin_files + files) do |input|
141
230
  outputs(reads(File.read(input)), file: input)
@@ -145,20 +234,106 @@ class Dorian
145
234
  end
146
235
 
147
236
  def everything
148
- read_stdin_files + read_files + stdin_arguments + arguments
237
+ read_stdin_files + stdin_arguments + read_files + arguments
149
238
  end
150
239
 
151
240
  def command_each
152
241
  each(everything) do |input|
153
- each(lines(reads(input)), progress: true) do |line|
154
- evaluates(ruby, it: line)
155
- end
242
+ each(lines(reads(input)), progress: true) { |line| evaluates(it: line) }
243
+ end
244
+ end
245
+
246
+ def command_tally
247
+ each(everything) do |input|
248
+ outputs(
249
+ JSON.pretty_generate(
250
+ map(lines(reads(input)), progress: true) do |element|
251
+ if ruby.to_s.empty?
252
+ element
253
+ else
254
+ evaluates(it: element, returns: true, stdout: false).returned
255
+ end
256
+ end.tally
257
+ )
258
+ )
156
259
  end
157
260
  end
158
261
 
159
262
  def command_all
160
- each(everything, progress: true) do |input|
161
- evaluates(ruby, it: reads(input))
263
+ each(everything, progress: true) { |input| evaluates(it: reads(input)) }
264
+ end
265
+
266
+ def command_append
267
+ outputs(everything.sum { |input| lines(reads(input)) })
268
+ end
269
+
270
+ def command_prepend
271
+ outputs(everything.reverse.sum { |input| lines(reads(input)) })
272
+ end
273
+
274
+ def command_select
275
+ each(stdin_files + files) do |input|
276
+ outputs(
277
+ select(lines(reads(File.read(input)))) { |element| match?(element) },
278
+ file: input
279
+ )
280
+ end
281
+
282
+ each(stdin_arguments + arguments) do |input|
283
+ outputs(select(lines(reads(input))) { |element| match?(element) })
284
+ end
285
+ end
286
+
287
+ def command_reject
288
+ each(stdin_files + files) do |input|
289
+ outputs(
290
+ reject(lines(reads(File.read(input)))) { |element| match?(element) },
291
+ file: input
292
+ )
293
+ end
294
+
295
+ each(stdin_arguments + arguments) do |input|
296
+ outputs(reject(lines(reads(input))) { |element| match?(element) })
297
+ end
298
+ end
299
+
300
+ def command_after
301
+ each(stdin_files + files) do |input|
302
+ outputs(after(lines(reads(File.read(input)))), file: input)
303
+ end
304
+
305
+ each(stdin_arguments + arguments) do |input|
306
+ outputs(after(lines(reads(input))))
307
+ end
308
+ end
309
+
310
+ def command_before
311
+ each(stdin_files + files) do |input|
312
+ outputs(before(reads(File.read(input))), file: input)
313
+ end
314
+
315
+ each(stdin_arguments + arguments) do |input|
316
+ outputs(before(lines(reads(input))))
317
+ end
318
+ end
319
+
320
+ def command_between
321
+ each(stdin_files + files) do |input|
322
+ outputs(between(lines(reads(File.read(input)))), file: input)
323
+ end
324
+
325
+ each(stdin_arguments + arguments) do |input|
326
+ outputs(between(lines(reads(input))))
327
+ end
328
+ end
329
+
330
+ def command_anonymize
331
+ each(stdin_files + files) do |input|
332
+ outputs(anonymize(reads(File.read(input))), file: input)
333
+ end
334
+
335
+ each(stdin_arguments + arguments) do |input|
336
+ outputs(anonymize(reads(input)))
162
337
  end
163
338
  end
164
339
 
@@ -400,6 +575,24 @@ class Dorian
400
575
  end
401
576
  end
402
577
 
578
+ def select(collection, progress: false, &)
579
+ collection = wrap(collection)
580
+ progress_bar = progress ? create_progress_bar(collection.size) : nil
581
+
582
+ collection.select do |element|
583
+ yield(element).tap { progress_bar&.increment }
584
+ end
585
+ end
586
+
587
+ def reject(collection, progress: false, &)
588
+ collection = wrap(collection)
589
+ progress_bar = progress ? create_progress_bar(collection.size) : nil
590
+
591
+ collection.reject do |element|
592
+ yield(element).tap { progress_bar&.increment }
593
+ end
594
+ end
595
+
403
596
  def lines(input)
404
597
  if input.is_a?(String)
405
598
  input.lines.map(&:rstrip)
@@ -441,8 +634,128 @@ class Dorian
441
634
  Dorian::Progress.create(total:, format: progress_format)
442
635
  end
443
636
 
637
+ def after(input, ruby: @ruby_after || @ruby)
638
+ if ruby.to_i.to_s == ruby
639
+ input[(ruby.to_i)..]
640
+ else
641
+ selected = false
642
+
643
+ input.select do |element|
644
+ selected = true if match?(element, ruby:)
645
+ selected
646
+ end
647
+ end
648
+ end
649
+
650
+ def before(input, ruby: @ruby_before || @ruby)
651
+ if ruby.to_i.to_s == ruby
652
+ input[..(ruby.to_i)]
653
+ else
654
+ selected = true
655
+
656
+ input.select do |element|
657
+ selected.tap { selected = false if match?(element, ruby:) }
658
+ end
659
+ end
660
+ end
661
+
662
+ def between(input, ruby_before: @ruby_before, ruby_after: @ruby_after)
663
+ if ruby_before.to_i.to_s == ruby_before &&
664
+ ruby_after.to_i.to_s == ruby_after
665
+ input[(ruby_after.to_i)..(ruby_before.to_i)]
666
+ else
667
+ selected = false
668
+
669
+ input.select do |element|
670
+ selected = true if match?(element, ruby: ruby_after)
671
+ selected.tap do
672
+ selected = false if match?(element, ruby: ruby_before)
673
+ end
674
+ end
675
+ end
676
+ end
677
+
678
+ def anonymize(input)
679
+ if input.is_a?(String)
680
+ input.gsub(/[a-z]/, "a").gsub(/[A-Z]/, "A").gsub(/[0-9]/, "0")
681
+ elsif input.is_a?(Integer)
682
+ 0
683
+ elsif input.is_a?(Float)
684
+ 0.0
685
+ elsif input.is_a?(TrueClass) || input.is_a?(FalseClass)
686
+ false
687
+ elsif input.nil?
688
+ nil
689
+ elsif input.is_a?(Hash)
690
+ input.transform_values { |value| anonymize(value) }
691
+ elsif input.is_a?(Array)
692
+ input.map { |element| anonymize(element) }
693
+ elsif input.is_a?(Struct)
694
+ anonymize(input.from_deep_struct).to_deep_struct
695
+ else
696
+ raise "#{input.class.inspect} not supported"
697
+ end
698
+ end
699
+
700
+ def match?(element, ruby: @ruby)
701
+ !!evaluates(ruby:, it: element, stdout: false, returns: true).returned
702
+ end
703
+
704
+ def token(file)
705
+ token_file = File.join(Dir.home, file)
706
+
707
+ if File.exist?(token_file)
708
+ token = File.read(token_file).strip
709
+ else
710
+ print "token: "
711
+ token = gets.strip
712
+ File.write(token_file, token)
713
+ puts "token written to #{token_file}"
714
+ end
715
+
716
+ token
717
+ end
718
+
719
+ def completion(token:, model:, messages:)
720
+ body =
721
+ post(
722
+ "https://api.openai.com/v1/chat/completions",
723
+ headers: {
724
+ "Content-Type" => "application/json",
725
+ "Authorization" => "Bearer #{token}"
726
+ },
727
+ body: { model:, messages: }.to_json
728
+ )
729
+
730
+ json = JSON.parse(body)
731
+ output = json.dig("choices", 0, "message", "content")
732
+
733
+ if output
734
+ output.strip
735
+ else
736
+ abort JSON.pretty_generate(json)
737
+ end
738
+ end
739
+
740
+ def post(url, headers: {}, body: {})
741
+ uri = URI.parse(url)
742
+ http = Net::HTTP.new(uri.host, uri.port)
743
+ http.use_ssl = true
744
+ request = Net::HTTP::Post.new(uri.path, headers)
745
+ request.body = body
746
+ http.request(request).body
747
+ end
748
+
749
+ def short(string)
750
+ string[0..5000]
751
+ end
752
+
753
+ def encoder
754
+ Tiktoken.encoding_for_model("gpt-4o")
755
+ end
756
+
444
757
  def evaluates(
445
- ruby,
758
+ ruby: @ruby,
446
759
  it: nil,
447
760
  debug: debug?,
448
761
  stdout: stdout?,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dorian
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dorian Marié
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-02 00:00:00.000000000 Z
11
+ date: 2024-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: csv
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: git
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'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: json
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -143,16 +157,16 @@ require_paths:
143
157
  - lib
144
158
  required_ruby_version: !ruby/object:Gem::Requirement
145
159
  requirements:
146
- - - '='
160
+ - - ">="
147
161
  - !ruby/object:Gem::Version
148
- version: 3.3.4
162
+ version: '3'
149
163
  required_rubygems_version: !ruby/object:Gem::Requirement
150
164
  requirements:
151
165
  - - ">="
152
166
  - !ruby/object:Gem::Version
153
167
  version: '0'
154
168
  requirements: []
155
- rubygems_version: 3.5.11
169
+ rubygems_version: 3.5.16
156
170
  signing_key:
157
171
  specification_version: 4
158
172
  summary: a collection of gems