ollama_chat 0.0.113 → 0.0.115

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +120 -0
  3. data/README.md +7 -0
  4. data/bin/ollama_chat_log +1 -1
  5. data/bin/ollama_chat_send +3 -1
  6. data/lib/ollama_chat/chat.rb +30 -14
  7. data/lib/ollama_chat/commands.rb +36 -18
  8. data/lib/ollama_chat/compaction.rb +289 -0
  9. data/lib/ollama_chat/config_handling.rb +32 -0
  10. data/lib/ollama_chat/database/migrations/010_add_enabled_to_collections.rb +7 -0
  11. data/lib/ollama_chat/database/models/app_state.rb +48 -16
  12. data/lib/ollama_chat/database/models/session.rb +1 -2
  13. data/lib/ollama_chat/database.rb +4 -2
  14. data/lib/ollama_chat/information.rb +53 -6
  15. data/lib/ollama_chat/logging.rb +3 -2
  16. data/lib/ollama_chat/message.rb +22 -1
  17. data/lib/ollama_chat/message_format.rb +1 -2
  18. data/lib/ollama_chat/message_list.rb +223 -9
  19. data/lib/ollama_chat/model_handling.rb +1 -1
  20. data/lib/ollama_chat/oc.rb +17 -7
  21. data/lib/ollama_chat/ollama_chat_config/default_config.yml +72 -3
  22. data/lib/ollama_chat/parsing.rb +2 -1
  23. data/lib/ollama_chat/prompt_handling.rb +1 -1
  24. data/lib/ollama_chat/prompt_management.rb +1 -0
  25. data/lib/ollama_chat/rag_handling.rb +19 -5
  26. data/lib/ollama_chat/session_management.rb +5 -3
  27. data/lib/ollama_chat/token_estimator/crude.rb +1 -1
  28. data/lib/ollama_chat/token_estimator.rb +1 -1
  29. data/lib/ollama_chat/tools/concern.rb +17 -0
  30. data/lib/ollama_chat/tools/directory_structure.rb +11 -7
  31. data/lib/ollama_chat/tools/eval_ruby.rb +16 -6
  32. data/lib/ollama_chat/tools/execute_shell.rb +158 -0
  33. data/lib/ollama_chat/tools/get_rfc.rb +2 -0
  34. data/lib/ollama_chat/tools/lookup_group.rb +100 -0
  35. data/lib/ollama_chat/tools/paste_from_clipboard.rb +1 -1
  36. data/lib/ollama_chat/tools/patch_file.rb +23 -5
  37. data/lib/ollama_chat/tools/read_file.rb +1 -1
  38. data/lib/ollama_chat/tools/resolve_tag.rb +1 -1
  39. data/lib/ollama_chat/tools/search_knowledge.rb +7 -1
  40. data/lib/ollama_chat/tools/write_file.rb +25 -6
  41. data/lib/ollama_chat/tools.rb +2 -0
  42. data/lib/ollama_chat/utils/analyze_directory.rb +5 -4
  43. data/lib/ollama_chat/utils/fetcher.rb +1 -1
  44. data/lib/ollama_chat/utils/log_rotation.rb +29 -0
  45. data/lib/ollama_chat/utils/strip_ansi.rb +22 -0
  46. data/lib/ollama_chat/utils/tag_resolver.rb +3 -3
  47. data/lib/ollama_chat/utils.rb +2 -0
  48. data/lib/ollama_chat/version.rb +1 -1
  49. data/lib/ollama_chat.rb +7 -0
  50. data/ollama_chat.gemspec +5 -5
  51. data/spec/ollama_chat/commands_spec.rb +25 -0
  52. data/spec/ollama_chat/compaction_spec.rb +381 -0
  53. data/spec/ollama_chat/config_handling_spec.rb +60 -0
  54. data/spec/ollama_chat/message_list_spec.rb +229 -0
  55. data/spec/ollama_chat/message_spec.rb +143 -0
  56. data/spec/ollama_chat/rag_handling_spec.rb +3 -0
  57. data/spec/ollama_chat/system_prompt_management_spec.rb +0 -2
  58. data/spec/ollama_chat/tools/browse_spec.rb +7 -0
  59. data/spec/ollama_chat/tools/compute_bmi_spec.rb +15 -0
  60. data/spec/ollama_chat/tools/concern_spec.rb +77 -0
  61. data/spec/ollama_chat/tools/copy_to_clipboard_spec.rb +8 -0
  62. data/spec/ollama_chat/tools/delete_file_spec.rb +8 -0
  63. data/spec/ollama_chat/tools/directory_structure_spec.rb +4 -0
  64. data/spec/ollama_chat/tools/execute_grep_spec.rb +13 -0
  65. data/spec/ollama_chat/tools/execute_ri_spec.rb +8 -0
  66. data/spec/ollama_chat/tools/execute_shell_spec.rb +232 -0
  67. data/spec/ollama_chat/tools/file_context_spec.rb +5 -0
  68. data/spec/ollama_chat/tools/gem_path_lookup_spec.rb +2 -0
  69. data/spec/ollama_chat/tools/generate_image_spec.rb +12 -0
  70. data/spec/ollama_chat/tools/generate_password_spec.rb +5 -0
  71. data/spec/ollama_chat/tools/get_cve_spec.rb +4 -0
  72. data/spec/ollama_chat/tools/get_endoflife_spec.rb +4 -0
  73. data/spec/ollama_chat/tools/get_ghr_spec.rb +6 -0
  74. data/spec/ollama_chat/tools/get_location_spec.rb +4 -0
  75. data/spec/ollama_chat/tools/get_rfc_spec.rb +2 -0
  76. data/spec/ollama_chat/tools/get_time_spec.rb +2 -0
  77. data/spec/ollama_chat/tools/get_url_spec.rb +7 -0
  78. data/spec/ollama_chat/tools/lookup_group_spec.rb +111 -0
  79. data/spec/ollama_chat/tools/move_file_spec.rb +10 -0
  80. data/spec/ollama_chat/tools/open_file_in_editor_spec.rb +6 -0
  81. data/spec/ollama_chat/tools/paste_from_clipboard_spec.rb +5 -0
  82. data/spec/ollama_chat/tools/paste_into_editor_spec.rb +6 -0
  83. data/spec/ollama_chat/tools/patch_file_spec.rb +62 -0
  84. data/spec/ollama_chat/tools/read_file_spec.rb +10 -3
  85. data/spec/ollama_chat/tools/resolve_tag_spec.rb +46 -0
  86. data/spec/ollama_chat/tools/roll_dice_spec.rb +11 -0
  87. data/spec/ollama_chat/tools/run_tests_spec.rb +8 -0
  88. data/spec/ollama_chat/tools/search_knowledge_spec.rb +2 -0
  89. data/spec/ollama_chat/tools/write_file_spec.rb +98 -2
  90. data/spec/ollama_chat/utils/analyze_directory_spec.rb +46 -7
  91. data/spec/ollama_chat/utils/log_rotation_spec.rb +71 -0
  92. data/spec/spec_helper.rb +2 -1
  93. metadata +25 -1
@@ -0,0 +1,158 @@
1
+ require 'open3'
2
+
3
+ # Shell command executor with mandatory human review.
4
+ #
5
+ # The LLM proposes a command; the user reviews it in their configured
6
+ # editor (modify or cancel), then answers a y/n/i confirm prompt that
7
+ # cannot be disabled via configuration. Output is displayed via pager.
8
+ class OllamaChat::Tools::ExecuteShell
9
+ include OllamaChat::Tools::Concern
10
+ include Term::ANSIColor
11
+ include OllamaChat::Utils::StripANSI
12
+
13
+ # @return [String] the registered name for this tool
14
+ def self.register_name = 'execute_shell'
15
+
16
+ # Compaction summary: one-liner for lookup_group output.
17
+ #
18
+ # @param result [String] the raw JSON result string
19
+ # @return [String] a short human-readable summary
20
+ def self.summary_template(result:)
21
+ data = JSON.parse(result)
22
+ if data['error']
23
+ "❌ #{data['message']}"
24
+ else
25
+ "✅ #{data['command']} (exit #{data['exit_code']})"
26
+ end
27
+ end
28
+
29
+ # @return [Ollama::Tool] a tool definition for shell execution
30
+ def tool
31
+ Tool.new(
32
+ type: 'function',
33
+ function: Tool::Function.new(
34
+ name:,
35
+ description: <<~EOT,
36
+ Shell executor – Runs a shell command via `sh -c`. The
37
+ command is opened in your editor for review/modification,
38
+ then a mandatory y/n/i confirm prompt gates execution.
39
+ Last resort: use ONLY when no dedicated tool covers
40
+ the task (e.g. `git commit`, `bundle install`, `make`).
41
+ Prefer `execute_grep`, `directory_structure`, `read_file`,
42
+ `patch_file` over shell equivalents. Only invoke when
43
+ the user explicitly requests it or you have proposed
44
+ the command to the user beforehand. Runs in the current
45
+ working directory.
46
+ EOT
47
+ parameters: Tool::Function::Parameters.new(
48
+ type: 'object',
49
+ properties: {
50
+ command: Tool::Function::Parameters::Property.new(
51
+ type: 'string',
52
+ description: 'The shell command to execute'
53
+ ),
54
+ monochrome: Tool::Function::Parameters::Property.new(
55
+ type: 'boolean',
56
+ description: 'Strip ANSI codes from captured output' \
57
+ ' (default: true)'
58
+ ),
59
+ },
60
+ required: %w[command]
61
+ )
62
+ )
63
+ )
64
+ end
65
+
66
+ # Execute the shell command after editor review and confirmation.
67
+ #
68
+ # @param tool_call [OllamaChat::Tool::Call] the tool call with args
69
+ # @param opts [Hash] additional options
70
+ # @option opts [OllamaChat::Chat] :chat the chat instance
71
+ # @return [String] a JSON result string
72
+ def execute(tool_call, **opts)
73
+ chat = opts[:chat]
74
+ args = tool_call.function.arguments
75
+ command = args.command.to_s.strip
76
+ raise OllamaChat::ToolFunctionArgumentError,
77
+ 'command required' if command.empty?
78
+
79
+ monochrome = args.monochrome.nil? ? true : args.monochrome
80
+
81
+ unless OllamaChat.test_mode?
82
+ reviewed = review(command, chat:)
83
+ raise OllamaChat::ToolFunctionArgumentError,
84
+ "Shell command cancelled by user: #{command}" unless reviewed
85
+ command = reviewed
86
+
87
+ STDOUT.puts "\n$ #{command}\n"
88
+ answer = chat.confirm?(
89
+ prompt: '❓ Allow ✅[y]es / ⛔️[n]o / 📝[i]nstruct? '
90
+ )&.to_s&.downcase
91
+
92
+ case answer
93
+ when 'y'
94
+ # proceed to execution
95
+ when 'n'
96
+ raise OllamaChat::ToolFunctionArgumentError,
97
+ "Shell command cancelled by user: #{command}"
98
+ else
99
+ instr = chat.ask?(prompt: '📝 Instructions: ')&.strip
100
+ resolve = instr.full? || 'You **MUST** ask the user for instructions!'
101
+ raise OllamaChat::ToolFunctionArgumentError,
102
+ "User instruct: #{resolve} (command: #{command})"
103
+ end
104
+ end
105
+
106
+ stdout, stderr, status = Open3.capture3('sh', '-c', command)
107
+ exit_code = status.exitstatus
108
+ if monochrome
109
+ stdout = strip_ansi(stdout)
110
+ stderr = strip_ansi(stderr)
111
+ end
112
+
113
+ chat.log(:info, 'Shell executed', data: {
114
+ tool: name, command:, exit_code:,
115
+ stdout_bytes: stdout.bytesize,
116
+ stderr_bytes: stderr.bytesize,
117
+ })
118
+
119
+ display(chat, stdout, stderr, exit_code)
120
+
121
+ {
122
+ stdout:, stderr:, exit_code:, command:,
123
+ message: "Shell command finished (exit #{exit_code}): #{command}",
124
+ }.to_json
125
+ rescue => e
126
+ cmd = args&.command&.to_s
127
+ chat.log(:error, e, data: { tool: name, command: cmd })
128
+ { error: e.class.name, message: e.message, command: cmd }.to_json
129
+ end
130
+
131
+ private
132
+
133
+ # Open the command in the user's editor for review.
134
+ #
135
+ # @return [String, nil] the (possibly modified) command, or nil
136
+ # if the user abandoned or cleared the editor.
137
+ def review(command, chat:)
138
+ edited = chat.edit_text(command, basename: %w[cmd .sh])
139
+ edited.full?(:chomp)
140
+ end
141
+
142
+ # Display command output through the pager.
143
+ def display(chat, stdout, stderr, exit_code)
144
+ chat.use_pager do |io|
145
+ if stdout.present?
146
+ io.puts bold { "stdout:" }, ""
147
+ io.puts stdout
148
+ end
149
+ if stderr.present?
150
+ io.puts bold { "stderr:" }, ""
151
+ io.puts stderr
152
+ end
153
+ io.puts bold{ "🎌 exit: #{exit_code}" }
154
+ end
155
+ end
156
+
157
+ self
158
+ end.register
@@ -60,7 +60,9 @@ class OllamaChat::Tools::GetRFC
60
60
  },
61
61
  &:read
62
62
  )
63
+ message = "RFC #{rfc_id} was fetched."
63
64
  {
65
+ message:,
64
66
  rfc_id: ,
65
67
  content:,
66
68
  }.to_json
@@ -0,0 +1,100 @@
1
+ # A tool for retrieving full message content from a previous conversation
2
+ # group by its short UUID suffix.
3
+ #
4
+ # When compaction summarizes older messages, the generated summary contains
5
+ # short group UUIDs (last 8 hex chars) that reference the original
6
+ # conversation turns. This tool lets the model retrieve the full content of
7
+ # any referenced group, enabling it to "zoom back in" on specific details
8
+ # that were compressed into the narrative.
9
+ #
10
+ # The search matches on the *suffix* of the `group_uuid` because the summary
11
+ # only stores the last 8 hex characters (the random portion of the UUIDv7),
12
+ # not the full 36-character string.
13
+ #
14
+ # @note The tool is read-only; it never mutates the chat or message list.
15
+ class OllamaChat::Tools::LookupGroup
16
+ include OllamaChat::Tools::Concern
17
+
18
+ # @return [String] the registered name for this tool
19
+ def self.register_name = 'lookup_group'
20
+
21
+ # Returns the tool definition for use with the Ollama API.
22
+ #
23
+ # @return [Ollama::Tool] a tool definition for group lookup
24
+ def tool
25
+ Tool.new(
26
+ type: 'function',
27
+ function: Tool::Function.new(
28
+ name:,
29
+ description: <<~EOT,
30
+ Retrieve the full messages of a previous conversation group.
31
+
32
+ **IMPORTANT:** Provide the **last 8 hex characters** of the
33
+ group_uuid (the random suffix), e.g. "3b874564" — NOT the
34
+ full UUID, NOT the first 8 characters.
35
+
36
+ The summary's tool-summaries section and narrative contain
37
+ these short suffixes in parentheses after group references.
38
+ EOT
39
+ parameters: Tool::Function::Parameters.new(
40
+ type: 'object',
41
+ properties: {
42
+ group_uuid: Tool::Function::Parameters::Property.new(
43
+ type: 'string',
44
+ description: <<~EOT,
45
+ The **last 8 hex characters** (suffix) of the group UUID,
46
+ exactly as it appears in the compaction summary. e.g.
47
+ "3b874564".
48
+ EOT
49
+ )
50
+ },
51
+ required: %w[group_uuid]
52
+ )
53
+ )
54
+ )
55
+ end
56
+
57
+ # Retrieves the messages belonging to the group identified by the short
58
+ # UUID suffix and returns them as formatted JSON.
59
+ #
60
+ # @param tool_call [OllamaChat::Tool::Call] the tool call with arguments
61
+ # @param opts [Hash] additional options
62
+ # @option opts [OllamaChat::Chat] :chat the chat instance
63
+ #
64
+ # @return [String] a JSON string containing the group messages
65
+ # @return [String] an error message as a JSON string if the group is
66
+ # not found or the argument is invalid
67
+ def execute(tool_call, **opts)
68
+ chat = opts[:chat]
69
+ args = tool_call.function.arguments
70
+
71
+ uuid = args.group_uuid.to_s.strip
72
+ raise OllamaChat::ToolFunctionArgumentError, 'group_uuid required' if uuid.empty?
73
+
74
+ messages = chat.messages.each_group(role: %w[user assistant tool], tool: true).find do |group|
75
+ group.first.group_uuid.to_s.end_with?(uuid)
76
+ end
77
+
78
+ raise OllamaChat::OllamaChatError, "Group not found: #{uuid}" if messages.nil?
79
+
80
+ chat.log(:info, 'Group retrieved', data: {
81
+ tool: name, group_uuid: messages.first.group_uuid, messages: messages.size
82
+ })
83
+
84
+ formatted = messages.map do |msg|
85
+ tool_tag = msg.tool_name ? " (#{msg.tool_name})" : ''
86
+ "[#{msg.role}#{tool_tag}] #{msg.content.to_s.strip}"
87
+ end
88
+
89
+ {
90
+ group_uuid: messages.first.group_uuid,
91
+ message: "Retrieved #{messages.size} message(s) for group #{uuid}.",
92
+ messages: formatted,
93
+ }.to_json
94
+ rescue => e
95
+ chat.log(:error, e, data: { tool: name, group_uuid: args.group_uuid })
96
+ { error: e.class.name, message: e.message }.to_json
97
+ end
98
+
99
+ self
100
+ end.register
@@ -50,7 +50,7 @@ class OllamaChat::Tools::PasteFromClipboard
50
50
 
51
51
  # Use the chat instance's clipboard paste functionality
52
52
  content = chat.perform_paste_from_clipboard(edit:)
53
- message = "Pasted #{format_bytes(content.to_s.size)} of content."
53
+ message = "Pasted #{format_bytes(content.to_s.bytesize)} of content."
54
54
 
55
55
  {
56
56
  success: true,
@@ -164,16 +164,34 @@ class OllamaChat::Tools::PatchFile
164
164
  "Failed to apply patch to file #{path.to_s.inspect}."
165
165
  end
166
166
 
167
+ syntax_check = nil
168
+ if result[:success] && (checker = chat.syntax_checker_for(path))
169
+ check = chat.run_syntax_check(checker, path)
170
+ syntax_check = check if check &&
171
+ (check[:status] == 'fail' || check[:output] != '')
172
+ end
173
+ if syntax_check
174
+ case syntax_check[:status]
175
+ when 'fail'
176
+ message = "#{message} — ❌ Syntax error detected"
177
+ when 'pass'
178
+ if warning = syntax_check[:output].lines.first&.chomp
179
+ message = "#{message} — ⚠️ #{warning}"
180
+ end
181
+ end
182
+ end
183
+
167
184
  mtime, checksum = File.open(path, 'rb') {
168
185
  [ _1.mtime.iso8601(0), '%08x' % Zlib.crc32(_1) ]
169
186
  }
170
187
 
171
188
  (result | {
172
- path: path.to_s,
173
- message: ,
174
- mtime: ,
175
- checksum: ,
176
- }).to_json
189
+ path: path.to_s,
190
+ message: ,
191
+ mtime: ,
192
+ checksum: ,
193
+ syntax_check: syntax_check,
194
+ }).compact.to_json
177
195
 
178
196
  rescue => e
179
197
  chat.log(:error, e, data: { tool: name, path: path.to_s })
@@ -91,7 +91,7 @@ class OllamaChat::Tools::ReadFile
91
91
  content, line_count = extract_range(full_content, start_line, end_line, line_numbers:)
92
92
  checksum = '%08x' % Zlib.crc32(full_content) if line_numbers && !start_line && !end_line
93
93
  es = OllamaChat::TokenEstimator.estimate(content)
94
- message = "Read #{es.bytes_formatted} (#{es.tokens_formatted}) from #{path.to_s.inspect}."
94
+ message = "Read #{es.tokens_formatted} (#{es.bytes_formatted}) from #{path.to_s.inspect}."
95
95
  chat.log(:info, "File read", data: {
96
96
  tool: name, path: path.to_s, bytes: es.bytes_formatted, tokens: es.tokens_formatted
97
97
  })
@@ -66,7 +66,7 @@ class OllamaChat::Tools::ResolveTag
66
66
 
67
67
  tags = OC::OLLAMA::CHAT::TOOLS::TAGS_FILE
68
68
  results = OllamaChat::Utils::TagResolver.new(tags).
69
- resolve(symbol:, kind:, directory:)
69
+ resolve(chat:, symbol:, kind:, directory:)
70
70
 
71
71
  message = "Found %{results_count} results of symbol \"%{symbol}\"." % {
72
72
  results_count: results.size, symbol:
@@ -114,7 +114,13 @@ class OllamaChat::Tools::SearchKnowledge
114
114
 
115
115
  if collection = args.collection.full?
116
116
  unless collection.to_s.match?(/\A#{OllamaChat::COLLECTION_NAME_REGEXP.source}\z/)
117
- raise OllamaChat::ToolFunctionArgumentError, "Invalid collection name: #{collection}"
117
+ raise OllamaChat::ToolFunctionArgumentError,
118
+ "Invalid collection name: #{collection}"
119
+ end
120
+ col = chat.database_collection?(collection)
121
+ if col&.enabled == false
122
+ raise OllamaChat::ToolFunctionArgumentError,
123
+ "Collection #{collection} is disabled."
118
124
  end
119
125
  old_collection = chat.documents.collection
120
126
  chat.documents.collection = collection
@@ -112,17 +112,36 @@ class OllamaChat::Tools::WriteFile
112
112
  raise ArgumentError, 'Invalid mode %s' % args.mode.inspect
113
113
  end
114
114
 
115
- message = "Wrote #{es.bytes_formatted} (#{es.tokens_formatted}) to file #{path.to_s.inspect}."
115
+ syntax_check = nil
116
+ if checker = chat.syntax_checker_for(path)
117
+ check = chat.run_syntax_check(checker, path)
118
+ syntax_check = check if check &&
119
+ (check[:status] == 'fail' || check[:output] != '')
120
+ end
121
+
122
+ message = "Wrote #{es.tokens_formatted} (#{es.bytes_formatted}) to file #{path.to_s.inspect}."
123
+ if syntax_check
124
+ case syntax_check[:status]
125
+ when 'fail'
126
+ message << " — ❌ Syntax error detected"
127
+ when 'pass'
128
+ if warning = syntax_check[:output].lines.first&.chomp
129
+ message << " — ⚠️ #{warning}"
130
+ end
131
+ end
132
+ end
133
+
116
134
  chat.log(:info, "File written", data: {
117
135
  tool: name, path: path.to_s, mode: args.mode || 'overwrite', bytes: es.bytes_formatted
118
136
  })
119
137
 
120
138
  {
121
- success: true,
122
- path: path.to_s,
123
- backup: backup_path.to_s,
124
- message: ,
125
- }.to_json
139
+ success: true,
140
+ path: path.to_s,
141
+ backup: backup_path.to_s,
142
+ message: ,
143
+ syntax_check: syntax_check,
144
+ }.compact.to_json
126
145
  rescue => e
127
146
  chat.log(:error, e, data: { tool: name, path: args.path })
128
147
  {
@@ -48,6 +48,7 @@ require 'ollama_chat/tools/directory_structure'
48
48
  require 'ollama_chat/tools/eval_ruby'
49
49
  require 'ollama_chat/tools/execute_grep'
50
50
  require 'ollama_chat/tools/execute_ri'
51
+ require 'ollama_chat/tools/execute_shell'
51
52
  require 'ollama_chat/tools/execute_jira_twg'
52
53
  require 'ollama_chat/tools/file_context'
53
54
  require 'ollama_chat/tools/gem_path_lookup'
@@ -62,6 +63,7 @@ require 'ollama_chat/tools/get_location'
62
63
  require 'ollama_chat/tools/get_rfc'
63
64
  require 'ollama_chat/tools/get_time'
64
65
  require 'ollama_chat/tools/get_url'
66
+ require 'ollama_chat/tools/lookup_group'
65
67
  require 'ollama_chat/tools/move_file'
66
68
  require 'ollama_chat/tools/open_file_in_editor'
67
69
  require 'ollama_chat/tools/paste_from_clipboard'
@@ -37,8 +37,9 @@ module OllamaChat::Utils::AnalyzeDirectory
37
37
  # only files with this suffix are included in the result. Pass `nil` or an empty string
38
38
  # to include all files.
39
39
  # @param max_depth [Integer, nil] Optional depth limit. If `nil`,
40
- # the entire tree is returned. When an integer is supplied, all
41
- # entries deeper than that depth are pruned.
40
+ # the entire tree is returned. When an integer is supplied, entries
41
+ # at that level and deeper are pruned. Use **1** to show only
42
+ # immediate children, **2** for children + grandchildren, etc.
42
43
  #
43
44
  # @return [Array<Hash>] An array of entry hashes. Each hash contains:
44
45
  # * `:type` – "file" or "directory"
@@ -70,9 +71,9 @@ module OllamaChat::Utils::AnalyzeDirectory
70
71
 
71
72
  structure_each_entry(entries) { |e| e[:height] = height }
72
73
 
73
- if max_depth && max_depth < height
74
+ if max_depth && max_depth <= height
74
75
  structure_each_entry(entries) do |e|
75
- e[:children]&.reject! { |c| c[:depth] > max_depth }
76
+ e[:children]&.reject! { |c| c[:depth] >= max_depth }
76
77
  end
77
78
  end
78
79
 
@@ -312,7 +312,7 @@ class OllamaChat::Utils::Fetcher
312
312
  end
313
313
  body = response.body
314
314
  tmp.print body
315
- infobar.update(message: message(body.size, body.size), force: true)
315
+ infobar.update(message: message(body.bytesize, body.bytesize), force: true)
316
316
  decorate_io(tmp, response)
317
317
  infobar.finish
318
318
  block.(tmp)
@@ -0,0 +1,29 @@
1
+ require 'file/tail'
2
+
3
+ # Truncates a log file in-place, keeping only the last N lines.
4
+ #
5
+ # Uses `File::Tail#backward` to seek to the start of the tail portion
6
+ # without loading the entire file into memory. The file is rewritten
7
+ # in-place (same inode) so that tailing processes (e.g.
8
+ # `ollama_chat_log`) can continue following the file.
9
+ module OllamaChat::Utils::LogRotation
10
+ # Truncates a log file in-place, keeping only the last +keep_lines+.
11
+ #
12
+ # If the file has fewer lines than +keep_lines+, it is left untouched.
13
+ # The operation is a no-op if the file does not exist.
14
+ #
15
+ # @param path [String, Pathname] the log file to truncate
16
+ # @param keep_lines [Integer] the number of lines to retain
17
+ def self.truncate_tail(path, keep_lines:)
18
+ return unless File.exist?(path)
19
+ File.open(path, 'r+') do |f|
20
+ f.extend(File::Tail)
21
+ f.backward(keep_lines)
22
+ return if f.pos.zero?
23
+ tail = f.read
24
+ f.rewind
25
+ f.write(tail)
26
+ f.truncate(f.pos)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ # Strips ANSI escape sequences from a string.
2
+ #
3
+ # Handles both SGR (Select Graphic Rendition) sequences like
4
+ # `\e[38;2;255;128;0m` and OSC (Operating System Command) sequences
5
+ # like `\e]0;title\a` or `\e]8;;url\e\\`.
6
+ #
7
+ # @example
8
+ # OllamaChat::Utils::StripANSI.strip_ansi("\e[31mred\e[0m")
9
+ # # => "red"
10
+ module OllamaChat::Utils::StripANSI
11
+ module_function
12
+
13
+ ANSI_RE = /\e\[[0-9;?]*[A-Za-z]|\e\].*?(\e\\|\a)/m
14
+
15
+ # Removes all ANSI escape sequences from the given string.
16
+ #
17
+ # @param line [String] the string potentially containing ANSI escapes
18
+ # @return [String] the string with all ANSI escape sequences removed
19
+ def strip_ansi(line)
20
+ line.to_s.gsub(ANSI_RE, '')
21
+ end
22
+ end
@@ -35,7 +35,7 @@ class OllamaChat::Utils::TagResolver
35
35
  # A lightweight struct representing a single tag entry. It extends the base
36
36
  # `Struct` with convenience methods for generating human‑readable messages and
37
37
  # JSON representations.
38
- class TagResult < Struct.new(*(HEADERS + %i[ linenumber ]))
38
+ class TagResult < Data.define(*(HEADERS + %i[ linenumber ]))
39
39
  # @return [String] Human readable description of this tag result.
40
40
  def message
41
41
  "#{symbol} of kind #{kind} (#{kind_type}) at #{filename}:#{linenumber}"
@@ -86,7 +86,7 @@ class OllamaChat::Utils::TagResolver
86
86
  # with this string.
87
87
  # @return [Array<TagResult>] All matching results sorted by the order they
88
88
  # appear in the tags file.
89
- def resolve(symbol:, kind: nil, directory: nil)
89
+ def resolve(chat:, symbol:, kind: nil, directory: nil)
90
90
  directory and directory = Pathname.new(directory).expand_path.to_path
91
91
  @tags_file.rewind
92
92
  results = []
@@ -117,7 +117,7 @@ class OllamaChat::Utils::TagResolver
117
117
  obj.linenumber = linenumber
118
118
  results << obj
119
119
  rescue => e
120
- log(:error, e, data: { context: 'tag_resolver' }, warn: true)
120
+ chat.log(:error, e, data: { context: 'tag_resolver' }, warn: true)
121
121
  end
122
122
  results
123
123
  end
@@ -20,5 +20,7 @@ require 'ollama_chat/utils/path_completer'
20
20
  require 'ollama_chat/utils/path_validator'
21
21
  require 'ollama_chat/utils/png_metadata_extractor'
22
22
  require 'ollama_chat/utils/tag_resolver'
23
+ require 'ollama_chat/utils/strip_ansi'
23
24
  require 'ollama_chat/utils/value_formatter'
24
25
  require 'ollama_chat/utils/log_viewer'
26
+ require 'ollama_chat/utils/log_rotation'
@@ -1,6 +1,6 @@
1
1
  module OllamaChat
2
2
  # OllamaChat version
3
- VERSION = '0.0.113'
3
+ VERSION = '0.0.115'
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/lib/ollama_chat.rb CHANGED
@@ -66,6 +66,12 @@ module OllamaChat
66
66
  class OllamaChatQuitError < OllamaChatError
67
67
  end
68
68
 
69
+ # Raised when a compaction operation fails, e.g. the LLM returns an
70
+ # empty or unusable summary, or the context window is too small to
71
+ # accommodate the summarization prompt.
72
+ class CompactionError < OllamaChatError
73
+ end
74
+
69
75
  # Regular expression defining the allowed characters for collection names.
70
76
  #
71
77
  # Matches alphanumeric characters, underscores, hyphens, and dots.
@@ -146,6 +152,7 @@ require 'ollama_chat/favourites_management'
146
152
  require 'ollama_chat/prompt_handling'
147
153
  require 'ollama_chat/system_prompt_management'
148
154
  require 'ollama_chat/prompt_management'
155
+ require 'ollama_chat/compaction'
149
156
  require 'ollama_chat/token_estimator'
150
157
  require 'ollama_chat/say'
151
158
  require 'ollama_chat/chat'