ollama_chat 0.0.105 → 0.0.106

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: cbf0c7c0df08fa3ef8a15a9c62b7e092d035924c071302a4f37e01776dec2504
4
- data.tar.gz: '0996355cc3d0324936394a174aaeeee8b67b950d5306cf320fa22a70eef9ea0d'
3
+ metadata.gz: c735b80b131af33178cdbc883a31d6c0693371736cfc91366bcd0d9f42bf2ffb
4
+ data.tar.gz: 18a1019995611afe96a0fad541aad75324834ce9ac1828392b956988f7c916ff
5
5
  SHA512:
6
- metadata.gz: 879adc039769558c938911909dbfdfc9625d6b4774cd398d8b39b4ebeb7055d7f7eb1fdaa1c26caf79d7fab3330c57c95b1169927e34d6909518e5f30f1c9d74
7
- data.tar.gz: 7fbe2ad8b0f887cab6d83ece7efe118fd0189e1957fdbd5a40725200729b7e4d93e9c018c1a41cc8fc933dd2cee453a3a7b9090a7e647422314175d567e5496c
6
+ metadata.gz: db82070f04d57c6679e0ec52474d26b1240b9cea164170258e34916a66097b3c6d3fac51e78a428372377fa956dc7732964387062f1c56d0f04906920e6a3e4e
7
+ data.tar.gz: d049ed9ba805b0d2ff670ecc7c3427f5644373fab6a8bf877b0260edadabb777354d5d118fc40534801ea023a6f52b8467b705cfa126eb70115c9b420a1710ec
data/CHANGES.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-08-06 v0.0.106
4
+
5
+ ### Added
6
+
7
+ - New `/model options delete` subcommand to allow removing saved profiles.
8
+ - Implementation of `delete_model_options_profile` in `model_handling.rb`.
9
+ - New `/prompt rename` subcommand for prompt templates.
10
+ - Implementation of `rename_prompt` method in `prompt_management.rb`.
11
+ - Support for shell-style quoting for file pattern arguments using
12
+ `Shellwords.split`.
13
+
14
+ ### Changed
15
+
16
+ - Tightened validation for the `/conversation` command to restrict filenames to
17
+ `.json` or `.jsonl` extensions.
18
+ - Updated `options` string documentation to include the optional `FILENAME`
19
+ argument and added format hints to `save` and `load` subcommands.
20
+ - Added a `suggest` parameter to `choose_profile_for_model` to allow specific
21
+ profiles to be inserted at the top of selection lists.
22
+ - Updated `copy_model_options_profile` to use the source profile as the
23
+ suggested destination.
24
+ - Refactored error logging in `OllamaChat::Chat` to use a standardized `log`
25
+ method with `:warn` level instead of direct `STDOUT.puts`.
26
+ - Centralized pattern parsing logic by replacing `arg.scan(/(\S+)/).flatten`
27
+ with `extract_patterns` in `commands.rb`.
28
+ - Updated context generation in `input_content.rb` to dynamically call
29
+ `to_json` or `to_toon` based on the selected format.
30
+
31
+ ### Fixed
32
+
33
+ - Resolved a scope limitation in `Prompt#after_destroy` by replacing
34
+ `models::Favourite` with `OllamaChat::Database::Models::Favourite`.
35
+ - Ensured the `default` profile remains valid by resetting it to an empty
36
+ options `{}` instead of deleting it during model option removal.
37
+
38
+ ### Refactored
39
+
40
+ - Improved readability in `model_handling.rb` by wrapping long `STDOUT.puts`
41
+ strings.
42
+ - Wrapped prompt operations in `switch_history(:prompt)` for improved
43
+ consistency.
44
+ - Updated YARD documentation for `extract_patterns` to reflect new shell
45
+ semantics.
46
+
3
47
  ## 2026-08-04 v0.0.105
4
48
 
5
49
  ### New Features
@@ -535,7 +535,8 @@ class OllamaChat::Chat
535
535
  store_messages_in_session
536
536
  rescue Ollama::Errors::BadRequestError
537
537
  if (think? || tools_support.on?) && !retried
538
- STDOUT.puts "#{bold('Error')}: in think mode/with tool support, switch both off and retry."
538
+ msg = "Error in think mode/with tool support, switch both off and retry."
539
+ log(:warn, msg, warn: true)
539
540
  sleep 1
540
541
  think_mode.selected = 'disabled'
541
542
  tools_support.set false
@@ -139,13 +139,14 @@ module OllamaChat::Commands
139
139
 
140
140
  command(
141
141
  name: :model,
142
- regexp: %r(^/model(?:\s+(change|options(?: copy)?|options from session|options to session))?((?:\s+(?:-m))*)$),
143
- complete: [ 'model', %w[ change options options\ copy options\ from\ session options\ to\ session ] ],
142
+ regexp: %r(^/model(?:\s+(change|options(?: copy| delete)?|options from session|options to session))?((?:\s+(?:-m))*)$),
143
+ complete: [ 'model', %w[ change options options\ copy options\ delete options\ from\ session options\ to\ session ] ],
144
144
  help: <<~EOT
145
145
  🤖 Manage AI models & profiles:
146
146
  - change: Switch active model
147
147
  - options: Edit saved profile config
148
148
  - options copy: Copy profile from another model
149
+ - options delete: Delete a saved profile
149
150
  - options from session: Save live → Saved
150
151
  - options to session: Apply Saved → Live
151
152
  -m interactively choose a model
@@ -170,6 +171,8 @@ module OllamaChat::Commands
170
171
  edit_model_options(model, profile:)
171
172
  when 'options copy'
172
173
  copy_model_options_profile(model)
174
+ when 'options delete'
175
+ delete_model_options_profile(model)
173
176
  when 'options from session'
174
177
  profile = choose_profile_for_model(model) || 'default'
175
178
  copy_model_options_from_session(model, profile:)
@@ -460,14 +463,14 @@ module OllamaChat::Commands
460
463
 
461
464
  command(
462
465
  name: :prompt,
463
- regexp: %r(^/prompt(?:\s+(edit|info|add|delete|list|duplicate|import|export|reset|-e))?(\s+(?:-[ef]|-c\s+(?:\w+|\?)))?(?:\s+([^-].*))?$),
464
- complete: [ 'prompt', %w[ edit info add delete list duplicate import export reset ] ],
466
+ regexp: %r(^/prompt(?:\s+(edit|info|add|delete|list|duplicate|import|export|reset|rename|-e))?(\s+(?:-[ef]|-c\s+(?:\w+|\?)))?(?:\s+([^-].*))?$),
467
+ complete: [ 'prompt', %w[ edit info add delete list duplicate import export reset rename ] ],
465
468
  optional: true,
466
469
  options: '[-c CONTEXT|-e|-f]',
467
470
  help: <<~EOT,
468
471
  📝 Manage prompt templates:
469
472
  Subcommands: edit, info, add, delete, list,
470
- duplicate, import, export, reset.
473
+ duplicate, import, export, reset, rename.
471
474
  Options: -c [context]
472
475
  (? for interactive in /prompt),
473
476
  -e (edit next)
@@ -499,6 +502,8 @@ module OllamaChat::Commands
499
502
  list_prompts(context:)
500
503
  when 'duplicate'
501
504
  duplicate_prompt(context:)
505
+ when 'rename'
506
+ rename_prompt(context:)
502
507
  when 'import'
503
508
  import_prompt(filename, context:)
504
509
  when 'export'
@@ -565,12 +570,13 @@ module OllamaChat::Commands
565
570
 
566
571
  command(
567
572
  name: :conversation,
568
- regexp: %r(^/conversation\s+(clean|save|load)(\s+-c)?(?:\s+([^-].*))?$),
573
+ regexp: %r(^/conversation\s+(clean|save|load)(\s+-c)?(?:\s+([^-].*\.jsonl?))?$),
569
574
  complete: [ 'conversation', %w[ save load clean ] ],
570
- options: '[-c]',
575
+ options: '[-c] [FILENAME]',
571
576
  help: <<~EOT
572
577
  💾 Save/Load conversation state
573
578
  (-c to clean before saving)
579
+ FILENAME ends with .json or .jsonl
574
580
  Or clean inplace (removes tool content, images, and thinking)
575
581
  EOT
576
582
  ) do |subcommand,opts,path|
@@ -826,7 +832,7 @@ module OllamaChat::Commands
826
832
  if opts[?p]
827
833
  words = opts.fetch(?w, 100)
828
834
  all = opts.fetch(?a, false)
829
- arg and patterns = arg.scan(/(\S+)/).flatten
835
+ patterns = extract_patterns(arg)
830
836
  next provide_file_set_content(patterns, all:, skip_blank: true) { summarize(_1, words:) } || :next
831
837
  elsif arg
832
838
  words = opts.fetch(?w, 100)
@@ -840,7 +846,7 @@ module OllamaChat::Commands
840
846
  opts = go_command('pa', opts)
841
847
  if opts[?p]
842
848
  all = opts.fetch(?a, false)
843
- patterns = arg&.scan(/(\S+)/)&.flatten.full? || [ '**/*' ]
849
+ patterns = extract_patterns(arg).full? || [ '**/*' ]
844
850
  next context_spook(patterns, all:) || :next
845
851
  elsif arg
846
852
  next context_spook(Array(arg.to_s), all: true) || :next
@@ -862,7 +868,7 @@ module OllamaChat::Commands
862
868
  tags = opts[?t].full?(:split, ?,)
863
869
  if opts[?p]
864
870
  all = opts.fetch(?a, false)
865
- arg and patterns = arg.scan(/(\S+)/).flatten
871
+ patterns = extract_patterns(arg)
866
872
  next provide_file_set_content(patterns, all:, skip_blank: true) { embed(_1, tags:) } || :next
867
873
  elsif arg
868
874
  next embed(arg, tags:) || :next
@@ -875,7 +881,7 @@ module OllamaChat::Commands
875
881
  opts = go_command('pae', opts)
876
882
  if opts[?p]
877
883
  all = opts.fetch(?a, false)
878
- arg and patterns = arg.scan(/(\S+)/).flatten
884
+ patterns = extract_patterns(arg)
879
885
  read = -> pathname {
880
886
  STDOUT.puts "Reading #{pathname.to_s.inspect}."
881
887
  pathname.read
@@ -895,7 +901,7 @@ module OllamaChat::Commands
895
901
  opts = go_command('pae', opts)
896
902
  if opts[?p]
897
903
  all = opts.fetch(?a, false)
898
- arg and patterns = arg.scan(/(\S+)/).flatten
904
+ patterns = extract_patterns(arg)
899
905
  next provide_file_set_content(patterns, all:, skip_blank: true) { import(_1) } || :next
900
906
  elsif arg
901
907
  source = arg
@@ -52,7 +52,7 @@ class OllamaChat::Database::Models::Prompt < Sequel::Model(OllamaChat::DB)
52
52
  # database when the underlying prompt is removed.
53
53
  def after_destroy
54
54
  super
55
- models::Favourite.where(context:, name:).destroy
55
+ OllamaChat::Database::Models::Favourite.where(context:, name:).destroy
56
56
  end
57
57
 
58
58
  # Seeds the prompt table from the provided chat configuration.
@@ -102,7 +102,7 @@ module OllamaChat::InputContent
102
102
  file filename.to_path
103
103
  end
104
104
  end
105
- end.to_json
105
+ end.send("to_#{format.downcase}")
106
106
  else
107
107
  if context_filename = choose_filename('.contexts/*.rb')
108
108
  ContextSpook.generate_context(context_filename, verbose: true, format:).
@@ -156,7 +156,8 @@ module OllamaChat::ModelHandling
156
156
  model_name ||= @model
157
157
  model_options = get_session_model_options
158
158
  store_model_options(model_name, model_options, profile:)
159
- STDOUT.puts "Model options #{italic{profile}} for #{bold{model_name}} were copied from session model options."
159
+ STDOUT.puts "Model options #{italic{profile}} for #{bold{model_name}} "\
160
+ "were copied from session model options."
160
161
  end
161
162
 
162
163
  # Resets the session's model options to match the stored defaults for the
@@ -169,7 +170,8 @@ module OllamaChat::ModelHandling
169
170
  model_name ||= @model
170
171
  stored_model_options = get_stored_model_options(model_name, profile:)
171
172
  session.update(model_options: stored_model_options)
172
- STDOUT.puts "Model options #{italic{profile}} of #{bold{model_name}} were copied to session model options."
173
+ STDOUT.puts "Model options #{italic{profile}} of #{bold{model_name}} "\
174
+ "were copied to session model options."
173
175
  end
174
176
 
175
177
  # Interactively copies model options from a source model/profile to the
@@ -181,7 +183,7 @@ module OllamaChat::ModelHandling
181
183
  src_model = choose_model('', model)
182
184
  src_profile = choose_profile_for_model(src_model) || 'default'
183
185
  dst_model = model
184
- dst_profile = choose_profile_for_model(dst_model, allow_new: true) || src_profile
186
+ dst_profile = choose_profile_for_model(dst_model, allow_new: true, suggest: src_profile)
185
187
 
186
188
  src_opts = get_stored_model_options(src_model, profile: src_profile).full? or return
187
189
 
@@ -200,7 +202,31 @@ module OllamaChat::ModelHandling
200
202
  end
201
203
 
202
204
  store_model_options(dst_model, src_opts, profile: dst_profile)
203
- STDOUT.puts "✅ Copied options from #{italic{src_model}}/#{italic{src_profile}} to #{bold{dst_model}}/#{italic{dst_profile}}."
205
+ STDOUT.puts "✅ Copied options from #{italic{src_model}}/#{italic{src_profile}} "\
206
+ "to #{bold{dst_model}}/#{italic{dst_profile}}."
207
+ end
208
+
209
+ # Interactively deletes a stored model options profile.
210
+ #
211
+ # If the deleted profile is the default one, it is replaced with an empty
212
+ # options hash `{}` to ensure the model always has a valid default profile.
213
+ #
214
+ # @param model [String] the model whose profile should be deleted
215
+ def delete_model_options_profile(model)
216
+ profile = choose_profile_for_model(model) or return
217
+
218
+ if confirm?(prompt: "🔔 Really delete profile #{bold{profile}} for #{bold{model}}? (y/n) ", yes: /\Ay/i)
219
+ if profile == 'default'
220
+ store_model_options(model, {}, profile:)
221
+ STDOUT.puts "Default profile #{italic{profile}} for #{bold{model}} has been reset to empty options."
222
+ else
223
+ models::ModelOptions.where(model_name: model, profile:).destroy
224
+ STDOUT.puts "Profile #{italic{profile}} for #{bold{model}} deleted."
225
+ end
226
+ log(:info, "Model options profile deleted", data: { model:, profile: })
227
+ else
228
+ STDOUT.puts "Cancelled."
229
+ end
204
230
  end
205
231
 
206
232
  # Presents an interactive list of stored configuration profiles for the
@@ -212,12 +238,14 @@ module OllamaChat::ModelHandling
212
238
  #
213
239
  # @param model_name [String] the name of the model whose profiles are to be listed
214
240
  # @return [String, nil] the selected profile name, or `nil` if none was chosen
215
- def choose_profile_for_model(model_name, allow_new: false)
241
+ def choose_profile_for_model(model_name, allow_new: false, suggest: nil)
216
242
  profiles = models::ModelOptions.where(model_name:).order(:profile).map(&:profile)
217
243
 
218
244
  if allow_new
245
+ profiles.unshift(suggest) if suggest && !profiles.member?(suggest)
219
246
  profiles = [ '[EXIT]', '[NEW]' ] + profiles
220
247
  else
248
+ profiles.unshift(suggest) if suggest && !profiles.member?(suggest)
221
249
  profiles.size < 2 and return profiles.first
222
250
  profiles = [ '[EXIT]' ] + profiles
223
251
  end
@@ -88,11 +88,11 @@ module OllamaChat::PromptManagement
88
88
  # Interactively prompts the user for a name and content (optionally loading
89
89
  # from a file) to create a new prompt template.
90
90
  #
91
- # @return [Boolean, nil] true if the prompt was added, nil if the process was
91
+ # @return [self, nil] self if the prompt was added, nil if the process was
92
92
  # cancelled
93
93
  def add_new_prompt(context: nil)
94
94
  context ||= 'prompt'
95
- switch_history(:add_prompt) do
95
+ switch_history(:prompt) do
96
96
  name = determine_valid_new_name_for_prompt('to add', context:) or return
97
97
 
98
98
  sources = %w[ [CLIPBOARD] [FILES] [EMPTY/MANUAL] ]
@@ -115,7 +115,7 @@ module OllamaChat::PromptManagement
115
115
  prompt_content = edit_text(content)
116
116
  store_prompt(name, prompt_content, context:)
117
117
  log(:info, "Prompt added", data: { name:, context: })
118
- true
118
+ self
119
119
  end
120
120
  end
121
121
 
@@ -165,31 +165,75 @@ module OllamaChat::PromptManagement
165
165
  # cancelled the operation or no prompt was selected.
166
166
  def duplicate_prompt(context: nil)
167
167
  context ||= 'prompt'
168
- selected_prompt = choose_prompt(context:, prompt: 'Which prompt shall be the basis for a new one? %s') or return
169
- STDOUT.puts kramdown_ansi_parse(
170
- selected_prompt.to_s + "\n---"
171
- )
172
- name = nil
173
- loop do
174
- name = ask?(
175
- prompt: "❓ Enter new prompt name to duplicate as, C-c ⇒ cancel: "
168
+ switch_history(:prompt) do
169
+ selected_prompt = choose_prompt(context:, prompt: 'Which prompt shall be the basis for a new one? %s') or return
170
+ STDOUT.puts kramdown_ansi_parse(
171
+ selected_prompt.to_s + "\n---"
176
172
  )
177
- if name.nil?
178
- STDOUT.puts "Cancelled."
179
- return nil
173
+ name = nil
174
+ loop do
175
+ name = ask?(
176
+ prompt: "❓ Enter new prompt name to duplicate as, C-c ⇒ cancel: "
177
+ )
178
+ if name.nil?
179
+ STDOUT.puts "Cancelled."
180
+ return nil
181
+ end
182
+ if prompt(name, context:)
183
+ STDOUT.puts "Prompt named #{bold{name}} already exists."
184
+ else
185
+ break
186
+ end
180
187
  end
181
- if prompt(name, context:)
182
- STDOUT.puts "Prompt named #{bold{name}} already exists."
183
- else
184
- break
188
+ duplicated_prompt = selected_prompt.duplicate
189
+ duplicated_prompt.name = name
190
+ duplicated_prompt.metadata['default'] = false
191
+ duplicated_prompt.save
192
+ log(:info, "Prompt duplicated", data: { name:, old_name: selected_prompt.name, context: })
193
+ self
194
+ end
195
+ end
196
+
197
+ # Interactively selects an existing prompt and renames it.
198
+ #
199
+ # @return [self, nil] the current context on success, or nil if the user
200
+ # cancelled the operation or no prompt was selected.
201
+ def rename_prompt(context: nil)
202
+ context ||= 'prompt'
203
+ switch_history(:prompt) do
204
+ selected_prompt = choose_prompt(
205
+ prompt: 'Which prompt would you like to rename? %s',
206
+ context:
207
+ ) or return
208
+
209
+ STDOUT.puts kramdown_ansi_parse(
210
+ selected_prompt.to_s + "\n---"
211
+ )
212
+
213
+ name = nil
214
+ loop do
215
+ name = ask?(
216
+ prompt: "❓ Enter new prompt name, C-c ⇒ cancel: "
217
+ )
218
+ if name.nil?
219
+ STDOUT.puts "Cancelled."
220
+ return nil
221
+ end
222
+ if name == selected_prompt.name
223
+ STDOUT.puts "That is the current name."
224
+ elsif prompt(name, context:)
225
+ STDOUT.puts "Prompt named #{bold{name}} already exists."
226
+ else
227
+ break
228
+ end
185
229
  end
230
+
231
+ old_name = selected_prompt.name
232
+ selected_prompt.name = name
233
+ selected_prompt.save
234
+ log(:info, "Prompt renamed", data: { old_name:, new_name: name, context: })
235
+ self
186
236
  end
187
- duplicated_prompt = selected_prompt.duplicate
188
- duplicated_prompt.name = name
189
- duplicated_prompt.metadata['default'] = false
190
- duplicated_prompt.save
191
- log(:info, "Prompt duplicated", data: { name:, old_name: selected_prompt.name, context: })
192
- self
193
237
  end
194
238
 
195
239
  # Interactively imports a prompt from a file.
@@ -366,21 +410,23 @@ module OllamaChat::PromptManagement
366
410
  # the operation was cancelled.
367
411
  def determine_valid_new_name_for_prompt(action, context: nil)
368
412
  context ||= 'prompt'
369
- prompt_name = nil
370
- loop do
371
- prompt_name = ask?(
372
- prompt: "❓ Enter new prompt name #{action}, C-c ⇒ cancel: "
373
- )
374
- if prompt_name.nil?
375
- STDOUT.puts "Cancelled."
376
- return nil
377
- end
378
- if prompt(prompt_name, context:)
379
- STDOUT.puts "Prompt named #{bold{prompt_name}} already exists."
380
- else
381
- break
413
+ switch_history(:prompt) do
414
+ prompt_name = nil
415
+ loop do
416
+ prompt_name = ask?(
417
+ prompt: "❓ Enter new prompt name #{action}, C-c ⇒ cancel: "
418
+ )
419
+ if prompt_name.nil?
420
+ STDOUT.puts "Cancelled."
421
+ return nil
422
+ end
423
+ if prompt(prompt_name, context:)
424
+ STDOUT.puts "Prompt named #{bold{prompt_name}} already exists."
425
+ else
426
+ break
427
+ end
382
428
  end
429
+ prompt_name
383
430
  end
384
- prompt_name
385
431
  end
386
432
  end
@@ -219,15 +219,16 @@ module OllamaChat::RAGHandling
219
219
  end
220
220
  end
221
221
 
222
- # Extracts and normalizes file patterns from a space-separated string.
222
+ # Extracts and normalizes file patterns from a shell-style string.
223
223
  #
224
- # Splits the input string by whitespace, strips each pattern, and expands
225
- # them to absolute paths. Returns an empty array if the input is blank.
224
+ # Splits the input string using shell semantics, correctly handling
225
+ # spaces enclosed in single/double quotes or escaped with backslashes.
226
+ # Each resulting pattern is expanded to an absolute path.
226
227
  #
227
- # @param patterns_str [String, nil] the space-separated glob patterns
228
+ # @param patterns_str [String, nil] the shell-style glob patterns
228
229
  # @return [Array<String>] an array of expanded absolute path patterns
229
230
  def extract_patterns(patterns_str)
230
- patterns = patterns_str.full? ? patterns_str.split(/\s+/).map(&:strip) : []
231
+ patterns = Shellwords.split(patterns_str.to_s)
231
232
  patterns.map { File.expand_path(_1) }
232
233
  end
233
234
 
@@ -1,6 +1,6 @@
1
1
  module OllamaChat
2
2
  # OllamaChat version
3
- VERSION = '0.0.105'
3
+ VERSION = '0.0.106'
4
4
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
5
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
6
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
data/ollama_chat.gemspec CHANGED
@@ -1,9 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: ollama_chat 0.0.105 ruby lib
2
+ # stub: ollama_chat 0.0.106 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "ollama_chat".freeze
6
- s.version = "0.0.105".freeze
6
+ s.version = "0.0.106".freeze
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ollama_chat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.105
4
+ version: 0.0.106
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Frank