ollama_chat 0.0.122 → 0.0.123

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: d77fd2cddf7c921b77712a90d4d1d31b3daf0f80dc4170f71eb209e7ce46390b
4
- data.tar.gz: af9f30997ca31d67d7ba1914a971b5abb165d1913b2723c0727f07929b4512e4
3
+ metadata.gz: f2d0c5676e03433f245a21795872a9d7bd64357306ec691de3012195b5403bef
4
+ data.tar.gz: 933dca2b53647c523b9849d958cc1625106c97500caf2814bc731458a0e683f4
5
5
  SHA512:
6
- metadata.gz: ba243ffe79c5062b0feb5fc2d6389efff95216961567dbe9275f020cbdf811d1f04f238e95972d007e611109c3bcc795057c8a2c4afeeaf7d1b739301b91c0f5
7
- data.tar.gz: badb4e4ccca4ee006cdb48fd66b107367c81d7bf3ee3717482e4c61f3bc4d6fcda80d4cc54edefb9a0b5af5a6dbcf32173eeb3e0f48f545d32f5c3508d472ba1
6
+ metadata.gz: 4589b5f1cf689bb35bdf26c288e59f40b87ef926eaf7f91e187c3fa729958c9d042f784eb3bf9a4b6e64966c201312713c2e20607291c6aabc10d306cf4cf272
7
+ data.tar.gz: 19102c3d01a2ebb4bb14b8ea617b09a7e168d0175225024a490dc85e23b41ce3c7eba249d82cc671f5530da0090e945f8f6358d17ca7f6b7587a413002e27dc2
data/CHANGES.md CHANGED
@@ -1,5 +1,99 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-09-25 v0.0.123
4
+
5
+ ### Added
6
+
7
+ * README section 5 documenting `/conversation compact`: token-aware
8
+ keep-recent budget, safe & retryable behavior, visible before/after
9
+ metrics, and iterative re-compaction.
10
+ * Three-step context escalation ladder in the README: `/conversation
11
+ compact` first, then `/conversation report` + `/clear` + `/input path`
12
+ as the preferred second step, and `/conversation clean` as the emergency
13
+ exit.
14
+ * `execute_shell` tool to the Filesystem row in the README, noted as running
15
+ shell commands as a last resort.
16
+ * `execute_jira_twg` tool to the Web/External row in the README for running
17
+ Jira CLI queries via `twg`.
18
+ * `lookup_group` tool to the Knowledge row in the README for retrieving prior
19
+ conversation groups by UUID.
20
+ * Speech Services (ASR) section documenting `OLLAMA_CHAT_ASR_URL`,
21
+ `OLLAMA_CHAT_ASR_CONVERT_COMMAND`, and `OLLAMA_CHAT_ASR_MODEL`.
22
+ * Audio Playback section documenting `OLLAMA_CHAT_AUDIO_PLAYER_CONFIG`.
23
+ * Invidious (YouTube Proxy) section documenting `OLLAMA_CHAT_INVIDIOUS_URL`,
24
+ `OLLAMA_CHAT_INVIDIOUS_COMPANION_KEY`, and
25
+ `OLLAMA_CHAT_INVIDIOUS_CAPTION_LANGUAGES`.
26
+ * `markdown:` keyword argument (default `true`) to
27
+ `kramdown_markdown_remove`; when `false`, skips the `kramdown_ansi_parse`
28
+ step while still removing emoji, ANSI, and box-drawing characters.
29
+ * Explanatory comment block to `eval_template` in
30
+ `lib/ollama_chat/tools/execute_grep.rb` clarifying that the `cmd` config
31
+ value is a Ruby string template whose `#{...}` interpolations are resolved
32
+ against the method's local parameters via `eval`.
33
+ * `cleaned` string prefix in `save_conversation` so the `STDOUT`/`STDERR`
34
+ confirmation messages read "Saved cleaned conversation to…" when `clean:
35
+ true` is passed.
36
+ * "Cancelled." message when the user skips saving in conversation report and
37
+ summarize flows.
38
+
39
+ ### Changed
40
+
41
+ * Renamed `report_session` to `generate_conversation_report` to clarify it
42
+ only builds the report text.
43
+ * Reworked `report_conversation` to always display in the pager first, then
44
+ optionally save to a file; removed the `save:` keyword argument.
45
+ * Reworked `summarize_conversation` in `session_management.rb` to always
46
+ render via `use_pager` first, then optionally save via `ask_for_filename?`
47
+ and `should_overwrite?`.
48
+ * Removed the `save:` keyword argument from `summarize_conversation` and the
49
+ `-f` flag from the `/conversation` command dispatch in `commands.rb`.
50
+ * Tightened the `/conversation` regex from `(?:[sf]|c)` to `[sc]` and updated
51
+ the `options:` help text to drop `-f save`.
52
+ * Changed "Nothing to summarize!" and "Nothing to report!" output from
53
+ `STDERR` to `STDOUT`.
54
+ * `kramdown_markdown_remove` now strips `\p{Extended_Pictographic}` emoji
55
+ before any further processing, preventing TTS engines from vocalizing them.
56
+ * `kramdown_markdown_remove` in `lib/ollama_chat/kramdown_ansi.rb` now strips
57
+ Unicode box-drawing characters (`U+2500`..`U+257F`) that kramdown renders
58
+ as table borders, preventing TTS engines from stammering on them.
59
+ * Collapses resulting multi-space runs and strips leading/trailing whitespace
60
+ in `kramdown_markdown_remove`.
61
+ * Moved the `kramdown_markdown_remove` call from `flush_buffer` into
62
+ `enqueue_tts` in `lib/ollama_chat/tts.rb` so all enqueue paths are covered,
63
+ passing `markdown: @chat.markdown.on?`.
64
+ * `multipart_form_data` in `lib/ollama_chat/asr.rb` now returns a hash
65
+ containing only `headers:`, `expects:`, and `body:`, which are the actual
66
+ Excon request options, instead of merging `rest` (`model:`, `language:`)
67
+ into the returned options hash.
68
+ * Updated YARD documentation to reflect the new parameter and emoji stripping
69
+ behavior.
70
+ * Updated `spec/ollama_chat/asr_spec.rb` expectations from
71
+ `hash_including(model: ...)` / `hash_including(language: ...)` to
72
+ `hash_including(body: kind_of(String))` to match the new option shape.
73
+ * Updated `commands_spec.rb` to remove the obsolete `/conversation summarize
74
+ -f` and `/conversation report -f` specs and drop `save:` from expectations.
75
+ * Updated `session_management_spec.rb` for the new show-then-save flow,
76
+ `STDOUT` stream, and `Cancelled.` expectation.
77
+ * Updated the corresponding expectation in `conversation_spec.rb` to match
78
+ the new message text.
79
+ * Renamed former section 5 "Session Reports" to section 6 "Conversation
80
+ Reports" to match the actual command name.
81
+ * Updated Conversation Archiving section from `/session summarize` to
82
+ `/conversation summarize`, removed obsolete `-f` flag reference, and
83
+ described the show-then-save flow.
84
+
85
+ ### Fixed
86
+ * Fixed `TTS_URL` to `OLLAMA_CHAT_TTS_URL` to match the actual env var name
87
+ in `oc.rb`.
88
+ * Wrapped `@prefix` with `Regexp.quote()` in `RedisCache#unpre` to ensure the
89
+ prefix is always matched as a literal string.
90
+ * Fixed typos in height validation error message.
91
+
92
+ ### Removed
93
+ * `context_spook` from development dependencies in `Rakefile` and
94
+ `ollama_chat.gemspec`.
95
+ * The `-f` flag from the `/report` command in `commands.rb`.
96
+
3
97
  ## 2026-09-25 v0.0.122
4
98
 
5
99
  ## Features
data/README.md CHANGED
@@ -62,7 +62,7 @@ The following environment variables can be used to configure behavior:
62
62
  - `OLLAMA_URL` - Base URL for Ollama server (default: `http://localhost:11434`)
63
63
  - `OLLAMA_HOST` - Base hostname for Ollama server (default: `localhost:11434`)
64
64
  - `OLLAMA_SEARXNG_URL` - SearxNG search endpoint URL
65
- - `TTS_URL` - Base URL for the [audio.cpp](https://github.com/0xshug0/audio.cpp) TTS server (default: `http://localhost:8880`)
65
+ - `OLLAMA_CHAT_TTS_URL` - Base URL for the [audio.cpp](https://github.com/0xshug0/audio.cpp) TTS server (default: `http://localhost:8880`)
66
66
 
67
67
  #### Chat Settings
68
68
  - `OLLAMA_CHAT_MODEL` - Default model to use (e.g., `llama3.1`)
@@ -75,6 +75,19 @@ The following environment variables can be used to configure behavior:
75
75
  - `OLLAMA_CHAT_LOG_DATABASE` - Database log file path (default: `$XDG_STATE_HOME/database.log`)
76
76
  - `OLLAMA_CHAT_LOG_TAIL_LINES` - Max lines to retain in log files (default: `10000`)
77
77
 
78
+ #### Speech Services (ASR)
79
+ - `OLLAMA_CHAT_ASR_URL` - Base URL for the ASR (Speech-to-Text) service (default: `http://localhost:8880`)
80
+ - `OLLAMA_CHAT_ASR_CONVERT_COMMAND` - Shell command template to convert video to 16 kHz mono PCM WAV; use `%{input}` / `%{output}` as placeholders (default: `ffmpeg -y -i %{input} -vn -acodec pcm_s16le -ar 16000 -ac 1 %{output}`)
81
+ - `OLLAMA_CHAT_ASR_MODEL` - ASR model identifier (default: `qwen3-asr-1.7b`)
82
+
83
+ #### Audio Playback
84
+ - `OLLAMA_CHAT_AUDIO_PLAYER_CONFIG` - JSON object with `command`, `frequency`, `bits`, `pause` keys controlling the ffplay-based TTS audio player
85
+
86
+ #### Invidious (YouTube Proxy)
87
+ - `OLLAMA_CHAT_INVIDIOUS_URL` - Base URL for an Invidious instance
88
+ - `OLLAMA_CHAT_INVIDIOUS_COMPANION_KEY` - Bearer token for the Invidious companion player API (required when `URL` is set)
89
+ - `OLLAMA_CHAT_INVIDIOUS_CAPTION_LANGUAGES` - JSON array of language-code regexes for caption selection priority (default: `["\\Aen\\z","\\Aen-","\\Ade\\z","\\Ade-"]`)
90
+
78
91
  #### Tool Configuration
79
92
  - `OLLAMA_CHAT_TOOLS_TEST_RUNNER` - Configured test runner for `run_tests` (default: `rspec`)
80
93
  - `OLLAMA_CHAT_TOOLS_CTAGS_TOOL` - Path to the ctags tool
@@ -243,14 +256,57 @@ switch. The profile system allows you to manage "Live" vs "Saved" settings:
243
256
  "bake" those live settings into a permanent profile using `options from
244
257
  session`.
245
258
 
246
- #### 4. Conversation Archiving (`/session summarize`)
259
+ #### 4. Conversation Archiving (`/conversation summarize`)
247
260
 
248
261
  Turn your long-form brainstorming sessions into structured documents. The
249
- `/session summarize` command can distill an entire conversation into a concise
250
- summary.
262
+ `/conversation summarize` command distills an entire conversation into a
263
+ concise summary, displayed in the pager first, then optionally saved.
251
264
  * **Quick View**: Use `-s` for a one-sentence executive summary.
252
- * **Archiving**: Use `-f <filename>` to dump the full summary directly into a
253
- Markdown file, creating an automatic journal of your project's evolution.
265
+ * **Archiving**: After the summary is displayed, you are prompted for a
266
+ filename to save it as a Markdown file, creating an automatic journal
267
+ of your project's evolution.
268
+
269
+ #### 5. Context Compaction (`/conversation compact`)
270
+
271
+ Long sessions inevitably fill up the model's context window. The
272
+ `/conversation compact` command summarizes older messages into a single
273
+ compact summary while preserving recent exchanges, freeing up space for
274
+ continued work.
275
+
276
+ * **Token-aware**: The keep-recent budget is derived from the active
277
+ model's context length, so it adapts automatically.
278
+ * **Safe & retryable**: If the LLM summarization fails, the message list
279
+ is left untouched and you can simply retry.
280
+ * **Visible metrics**: After compaction, a report shows summarized
281
+ message count, summary size, and context usage before/after.
282
+ * **Iterative**: Repeated compactions merge into the existing summary
283
+ rather than stacking multiple summary messages.
284
+
285
+ Run it whenever context usage climbs into the red zone (visible in the
286
+ runtime info gauge) — the model stays sharp and the conversation keeps
287
+ flowing.
288
+
289
+ If compaction alone isn't enough, a practical escalation ladder:
290
+
291
+ 1. **`/conversation compact`** — summarize old messages, keep recent tail.
292
+ Try this first; it's the least destructive.
293
+ 2. **`/conversation report`** → **`/clear`** → **`/input path
294
+ standup.md`** — generate a standup-style report, save it, clear
295
+ the messages, then feed the report back as context. Do this
296
+ before `clean`, because `clean` strips the tool output and
297
+ thinking that make a good report.
298
+ 3. **`/conversation clean`** — strips tool results, images, and
299
+ thinking content from all messages. More radical, but the
300
+ conversation structure and text remain intact. The emergency
301
+ exit: if context is so stuffed that even a report can't fit,
302
+ `clean` carves out enough room for anything to work again.
303
+
304
+ #### 6. Conversation Reports (`/conversation report`)
305
+
306
+ Generate a structured report from your conversation using configurable
307
+ prompt templates. The report is displayed in the pager first, then
308
+ optionally saved to a file. Particularly useful for daily standup summaries
309
+ — run it at the end of the day and save the output for your team sync.
254
310
 
255
311
  ### Using a Persona
256
312
 
@@ -467,12 +523,12 @@ context, manipulate files, and retrieve external information.
467
523
 
468
524
  | Category | Tools | Description |
469
525
  | :--- | :--- | :--- |
470
- | **Filesystem** | `read_file`, `write_file`, `patch_file`, `delete_file`, `move_file`, `directory_structure`, `execute_grep` | Read, write, and search files within allowed directories. |
526
+ | **Filesystem** | `read_file`, `write_file`, `patch_file`, `delete_file`, `move_file`, `directory_structure`, `execute_grep`, `execute_shell` | Read, write, and search files within allowed directories; run shell commands as a last resort. |
471
527
  | **Ruby/Dev** | `resolve_tag`, `execute_ri`, `gem_path_lookup`, `run_tests`, `eval_ruby` | Introspect Ruby code, check documentation, and run test suites. |
472
- | **Web/External** | `search_web`, `get_url`, `browse`, `get_rfc`, `get_cve`, `get_endoflife`, `get_ghr`, `get_jira_issue` | Access the internet, fetch specific URLs, and look up technical standards. |
528
+ | **Web/External** | `search_web`, `get_url`, `browse`, `get_rfc`, `get_cve`, `get_endoflife`, `get_ghr`, `get_jira_issue`, `execute_jira_twg` | Access the internet, fetch specific URLs, and look up technical standards; run Jira CLI queries via `twg`. |
473
529
  | **System/Util** | `get_time`, `get_location`, `get_current_weather`, `generate_password`, `compute_bmi`, `roll_dice` | General utility functions for time, location, and simple calculations. |
474
530
  | **Editor/Clip** | `copy_to_clipboard`, `paste_from_clipboard`, `paste_into_editor`, `open_file_in_editor` | Bridge the gap between the chat and the system clipboard or editor. |
475
- | **Knowledge** | `search_knowledge`, `file_context` | Semantic search for specific snippets vs. broad retrieval of structured project context. |
531
+ | **Knowledge** | `search_knowledge`, `file_context`, `lookup_group` | Semantic search for specific snippets, broad retrieval of structured project context, or retrieving prior message groups by UUID. |
476
532
  | **Multimodal** | `generate_image` | Generate images via a local ComfyUI server. |
477
533
 
478
534
  ***Note on Knowledge Tools**: Use `search_knowledge` for precise,
data/Rakefile CHANGED
@@ -68,7 +68,6 @@ GemHadar do
68
68
  development_dependency 'webmock'
69
69
  development_dependency 'debug'
70
70
  development_dependency 'simplecov'
71
- development_dependency 'context_spook'
72
71
  development_dependency 'utils'
73
72
 
74
73
  licenses << 'MIT'
@@ -99,10 +99,10 @@ module OllamaChat
99
99
  body << Excon::CR_NL
100
100
  body << "--#{boundary}--" << Excon::CR_NL
101
101
 
102
- rest.symbolize_keys_recursive.compact | {
102
+ {
103
103
  headers: { 'Content-Type' => %{multipart/form-data; boundary="#{boundary}"} },
104
104
  expects: 200,
105
- body:,
105
+ body: ,
106
106
  }
107
107
  end
108
108
  end
@@ -374,15 +374,15 @@ module OllamaChat::Commands
374
374
 
375
375
  command(
376
376
  name: :conversation,
377
- regexp: %r(^/conversation\s+(clean|compact|save|load|summarize|report)((?:\s+-(?:[sf]|c))*)(?:\s+([^-].*\.jsonl?))?$),
377
+ regexp: %r(^/conversation\s+(clean|compact|save|load|summarize|report)((?:\s+-[sc])*)(?:\s+([^-].*\.jsonl?))?$),
378
378
  complete: [ 'conversation', %w[ save load clean compact summarize report ] ],
379
- options: '[-s|-f|-c] [FILENAME]',
379
+ options: '[-s|-c] [FILENAME]',
380
380
  help: <<~EOT
381
381
  💾 Manage conversation content:
382
382
  - save/load: Export/import as .json or .jsonl
383
383
  - clean: Remove tool content, images, thinking
384
384
  - compact: Summarize old messages, keep recent
385
- - summarize: Per-message narrative (-s sentence, -f save)
385
+ - summarize: Per-message narrative (-s sentence)
386
386
  - report: Generate a session report document
387
387
  EOT
388
388
  ) do |subcommand,opts,path|
@@ -420,11 +420,10 @@ module OllamaChat::Commands
420
420
  STDOUT.puts 'Cancelled.'
421
421
  end
422
422
  when 'summarize'
423
- opts = go_command('sf', opts)
424
- summarize_conversation(save: opts[?f], sentence: opts[?s])
423
+ opts = go_command('s', opts)
424
+ summarize_conversation(sentence: opts[?s])
425
425
  when 'report'
426
- opts = go_command('f', opts)
427
- report_conversation(save: opts[?f])
426
+ report_conversation
428
427
  end
429
428
  :next
430
429
  end
@@ -26,12 +26,13 @@ module OllamaChat::Conversation
26
26
  # @example Save conversation with explicit filename
27
27
  # chat.save_conversation('conversations/2023-10-15_my_session.json')
28
28
  def save_conversation(filename, clean: false)
29
+ cleaned = 'cleaned ' if clean
29
30
  filename = Pathname.new(filename)
30
31
  should_overwrite?(filename) or return
31
32
  if messages.save_conversation(filename, messages: clean ? messages.clean_messages : messages.messages)
32
- STDOUT.puts "Saved conversation to #{filename.to_s.inspect}."
33
+ STDOUT.puts "Saved #{cleaned}conversation to #{filename.to_s.inspect}."
33
34
  else
34
- STDERR.puts "Saving conversation to #{filename.to_s.inspect} failed."
35
+ STDERR.puts "Saving #{cleaned}conversation to #{filename.to_s.inspect} failed."
35
36
  end
36
37
  end
37
38
 
@@ -49,18 +49,29 @@ module OllamaChat::KramdownANSI
49
49
  retry
50
50
  end
51
51
 
52
- # The kramdown_markdown_remove method strips all markdown formatting from
53
- # content by first rendering it to ANSI and then removing the escape
54
- # sequences, yielding plain text suitable for speech synthesis.
52
+ # The kramdown_markdown_remove method strips markdown formatting, emoji,
53
+ # ANSI escape sequences, and box-drawing characters from content, yielding
54
+ # plain text suitable for TTS playback.
55
55
  #
56
- # @param content [ String, nil ] the markdown content to strip.
57
- # If nil, returns an empty string.
56
+ # @param content [ String, nil ] the content to strip.
57
+ # If nil or empty, returns an empty string.
58
+ # @param markdown [ Boolean] if true (default), runs the full kramdown
59
+ # ANSI parse before stripping; if false, only removes emoji, ANSI,
60
+ # and box-drawing characters without markdown rendering.
58
61
  #
59
- # @return [ String ] the content with all markdown and ANSI
60
- # formatting removed, suitable for TTS playback
61
- def kramdown_markdown_remove(content)
62
- content = kramdown_ansi_parse(content)
63
- content.empty? and return ''
64
- OllamaChat::Utils::StripANSI.strip_ansi(content)
62
+ # @return [ String ] the content with all markdown, ANSI, emoji, and
63
+ # box-drawing formatting removed, suitable for TTS playback
64
+ def kramdown_markdown_remove(content, markdown: true)
65
+ content.full? or return ''
66
+ content = content.gsub(/\p{Extended_Pictographic}/, '').full? or return ''
67
+ if markdown
68
+ content = kramdown_ansi_parse(content).full? or return ''
69
+ end
70
+ content = OllamaChat::Utils::StripANSI.strip_ansi(content)
71
+ # Remove box-drawing characters (U+2500..U+257F) that kramdown uses
72
+ # for table borders/separators — TTS engines stammer on these.
73
+ content.gsub(/[\u2500-\u257F]/, ' ')
74
+ .gsub(/\s{2,}/, ' ')
75
+ .strip
65
76
  end
66
77
  end
@@ -151,6 +151,6 @@ class OllamaChat::RedisCache
151
151
  # @param key [String] The prefixed key
152
152
  # @return [String] The key without prefix
153
153
  def unpre(key)
154
- key.sub(/\A#@prefix/, '')
154
+ key.sub(/\A#{Regexp.quote(@prefix)}/, '')
155
155
  end
156
156
  end
@@ -384,34 +384,27 @@ module OllamaChat::SessionManagement
384
384
  contents * "\n\n"
385
385
  end
386
386
 
387
- # Summarizes the conversation and displays or saves the result.
387
+ # Summarizes the conversation and displays / saves the result.
388
388
  #
389
389
  # @param sentence [Boolean] summarize each message in one sentence (default: false)
390
- # @param save [Boolean] save to file instead of pager (default: false)
391
- def summarize_conversation(sentence: false, save: false)
392
- if save
393
- filename = ask_for_filename?(action: 'for summarization') or return
394
- should_overwrite?(filename) or return
395
- summary = summarize_session(sentence:) do |content|
396
- infobar.puts kramdown_ansi_parse(content)
390
+ def summarize_conversation(sentence: false)
391
+ summary = summarize_session(sentence:) do |content|
392
+ infobar.puts kramdown_ansi_parse(content) << ?\n
393
+ end
394
+ if summary.full?
395
+ use_pager do |output|
396
+ output.puts kramdown_ansi_parse(summary)
397
397
  end
398
- if summary.full?
398
+ if filename = ask_for_filename?(action: 'for summarization') and
399
+ should_overwrite?(filename)
400
+ then
399
401
  filename.write(summary)
400
402
  STDOUT.puts "File successfully written."
401
403
  else
402
- STDERR.puts "Nothing to summarize!"
404
+ STDOUT.puts "Cancelled."
403
405
  end
404
406
  else
405
- summary = summarize_session(sentence:) do |content|
406
- infobar.puts kramdown_ansi_parse(content) << ?\n
407
- end
408
- if summary.full?
409
- use_pager do |output|
410
- output.puts kramdown_ansi_parse(summary)
411
- end
412
- else
413
- STDERR.puts "Nothing to summarize!"
414
- end
407
+ STDOUT.puts "Nothing to summarize!"
415
408
  end
416
409
  end
417
410
 
@@ -425,7 +418,7 @@ module OllamaChat::SessionManagement
425
418
  #
426
419
  # @param name [String, nil] specific template name (skips chooser)
427
420
  # @return [String, nil] the report or nil if empty
428
- def report_session(name: nil)
421
+ def generate_conversation_report(name: nil)
429
422
  content = messages.compacted_messages.inject('') do |c, message|
430
423
  message.content.present? or next c
431
424
  sender = sender_name_displayed(message)
@@ -446,7 +439,7 @@ module OllamaChat::SessionManagement
446
439
  system = prompt(:report, context: 'system').to_s
447
440
 
448
441
  Infobar.busy(
449
- label: 'Generating session report…',
442
+ label: 'Generating conversation report…',
450
443
  frames: :braille7,
451
444
  output: STDOUT,
452
445
  ) do
@@ -454,30 +447,26 @@ module OllamaChat::SessionManagement
454
447
  end
455
448
  end
456
449
 
457
- # Generates a report and displays or saves the result.
450
+ # Generates a report and displays / saves the result.
458
451
  #
459
452
  # @param name [String, nil] specific report template name (skips chooser)
460
- # @param save [Boolean] save to file instead of pager (default: false)
461
- def report_conversation(name: nil, save: false)
462
- if save
463
- filename = ask_for_filename?(action: 'for report') or return
464
- should_overwrite?(filename) or return
465
- result = report_session(name:)
466
- if result.full?
453
+ def report_conversation(name: nil)
454
+ result = generate_conversation_report(name:)
455
+ if result.full?
456
+ use_pager do |output|
457
+ output.puts kramdown_ansi_parse(result)
458
+ end
459
+ if filename = ask_for_filename?(action: 'for report') and
460
+ should_overwrite?(filename)
461
+ then
467
462
  filename.write(result)
468
463
  STDOUT.puts "File successfully written."
469
464
  else
470
- STDERR.puts "Nothing to report!"
465
+ STDOUT.puts "Cancelled."
471
466
  end
472
467
  else
473
- result = report_session(name:)
474
- if result.full?
475
- use_pager do |output|
476
- output.puts kramdown_ansi_parse(result)
477
- end
478
- else
479
- STDERR.puts "Nothing to report!"
480
- end
468
+ STDOUT.puts "Nothing to report!"
469
+ return
481
470
  end
482
471
  end
483
472
 
@@ -82,8 +82,8 @@ class OllamaChat::Tools::ComputeBMI
82
82
  units = 'SI'
83
83
  end
84
84
 
85
- raise OllamaChat::ToolFunctionArgumentError, 'Height must be greater than zero and in kg/lbs' if height <= 0
86
- raise OllamaChat::ToolFunctionArgumentError, 'Weight must be less than 3m and in meter/feet' if height > 3
85
+ raise OllamaChat::ToolFunctionArgumentError, 'Height must be greater than zero and in meter/feet' if height <= 0
86
+ raise OllamaChat::ToolFunctionArgumentError, 'Height must be less than 3m and in meter/feet' if height > 3
87
87
 
88
88
  bmi = ( weight / height**2 ).round(2)
89
89
  category = calculate_category(bmi)
@@ -125,6 +125,10 @@ class OllamaChat::Tools::ExecuteGrep
125
125
  # Evaluates a template string using the provided configuration and
126
126
  # parameters.
127
127
  #
128
+ # The `cmd` config value is a Ruby string template — its `#{...}`
129
+ # interpolations are resolved against the local parameters via `eval`,
130
+ # so all arguments below serve as the interpolation context.
131
+ #
128
132
  # @param config [Object] the configuration object containing tool settings
129
133
  # @param pattern [String] the regex pattern to search for
130
134
  # @param path [String] the file or directory path to search in
@@ -132,7 +132,6 @@ class OllamaChat::TTS
132
132
  size = match[0].size
133
133
 
134
134
  chunk = @buffer.slice!(0, size).full?(:strip) or next
135
- chunk = kramdown_markdown_remove(chunk) if @chat.markdown.on?
136
135
  enqueue_tts(chunk)
137
136
  end
138
137
  end
@@ -147,6 +146,7 @@ class OllamaChat::TTS
147
146
  #
148
147
  # @return [Thread] the spawned TTS fetch thread
149
148
  def enqueue_tts(block)
149
+ block = kramdown_markdown_remove(block, markdown: @chat.markdown.on?)
150
150
  thread_id = @id_mutex.synchronize { @next_id += 1 }
151
151
  @chat.log(:info, "TTS: Enqueueing block for synthesis", data: { thread_id:, block: })
152
152
  @id_mutex.synchronize { @enqueued_count += 1 }
@@ -1,6 +1,6 @@
1
1
  module OllamaChat
2
2
  # OllamaChat version
3
- VERSION = '0.0.122'
3
+ VERSION = '0.0.123'
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.122 ruby lib
2
+ # stub: ollama_chat 0.0.123 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "ollama_chat".freeze
6
- s.version = "0.0.122".freeze
6
+ s.version = "0.0.123".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]
@@ -31,7 +31,6 @@ Gem::Specification.new do |s|
31
31
  s.add_development_dependency(%q<webmock>.freeze, [">= 0".freeze])
32
32
  s.add_development_dependency(%q<debug>.freeze, [">= 0".freeze])
33
33
  s.add_development_dependency(%q<simplecov>.freeze, [">= 0".freeze])
34
- s.add_development_dependency(%q<context_spook>.freeze, [">= 0".freeze])
35
34
  s.add_development_dependency(%q<utils>.freeze, [">= 0".freeze])
36
35
  s.add_runtime_dependency(%q<excon>.freeze, ["~> 1.0".freeze])
37
36
  s.add_runtime_dependency(%q<ollama-ruby>.freeze, ["~> 1.23".freeze])
@@ -23,7 +23,7 @@ describe OllamaChat::ASR do
23
23
  .with(
24
24
  :post,
25
25
  'http://localhost:8880/v1/audio/transcriptions',
26
- hash_including(model: 'qwen3-asr-1.7b')
26
+ hash_including(body: kind_of(String))
27
27
  ).and_yield(double(body: '{"text": "Hello Florian"}'))
28
28
 
29
29
  result = described_class.transcribe(audio_io, chat:)
@@ -35,7 +35,7 @@ describe OllamaChat::ASR do
35
35
  .with(
36
36
  :post,
37
37
  'http://localhost:8880/v1/audio/transcriptions',
38
- hash_including(language: 'de')
38
+ hash_including(body: kind_of(String))
39
39
  ).and_yield(double(body: '{"text": "Hallo"}'))
40
40
 
41
41
  described_class.transcribe(audio_io, chat:, language: 'de')
@@ -675,33 +675,20 @@ describe OllamaChat::Commands, protect_env: true do
675
675
 
676
676
  it 'returns :next when input is "/conversation summarize"' do
677
677
  expect(chat).to receive(:summarize_conversation)
678
- .with(save: false, sentence: false)
678
+ .with(sentence: false)
679
679
  expect(chat.handle_input("/conversation summarize")).to eq :next
680
680
  end
681
681
 
682
682
  it 'returns :next when input is "/conversation summarize -s"' do
683
683
  expect(chat).to receive(:summarize_conversation)
684
- .with(save: false, sentence: 1)
684
+ .with(sentence: 1)
685
685
  expect(chat.handle_input("/conversation summarize -s")).to eq :next
686
686
  end
687
687
 
688
- it 'returns :next when input is "/conversation summarize -f"' do
689
- expect(chat).to receive(:summarize_conversation)
690
- .with(save: 1, sentence: false)
691
- expect(chat.handle_input("/conversation summarize -f")).to eq :next
692
- end
693
-
694
688
  it 'returns :next when input is "/conversation report"' do
695
689
  expect(chat).to receive(:report_conversation)
696
- .with(save: false)
697
690
  expect(chat.handle_input("/conversation report")).to eq :next
698
691
  end
699
-
700
- it 'returns :next when input is "/conversation report -f"' do
701
- expect(chat).to receive(:report_conversation).
702
- with(save: 1)
703
- expect(chat.handle_input("/conversation report -f")).to eq :next
704
- end
705
692
  end
706
693
 
707
694
  describe 'tools' do
@@ -20,7 +20,7 @@ describe OllamaChat::Conversation do
20
20
  .with(Pathname.new('./new_chat.jsonl'), messages: chat.messages.clean_messages)
21
21
  .and_return(true)
22
22
  expect(STDOUT).to receive(:puts)
23
- .with('Saved conversation to "./new_chat.jsonl".')
23
+ .with('Saved cleaned conversation to "./new_chat.jsonl".')
24
24
  chat.save_conversation('./new_chat.jsonl', clean: true)
25
25
  end
26
26
 
@@ -40,4 +40,64 @@ describe OllamaChat::KramdownANSI do
40
40
  expect(chat.kramdown_ansi_parse(nil)).to eq ''
41
41
  end
42
42
  end
43
+
44
+ describe '#kramdown_markdown_remove' do
45
+ it 'returns empty string for nil' do
46
+ expect(chat.kramdown_markdown_remove(nil)).to eq ''
47
+ end
48
+
49
+ it 'returns empty string for empty string' do
50
+ expect(chat.kramdown_markdown_remove('')).to eq ''
51
+ end
52
+
53
+ it 'removes table separator rows' do
54
+ input = "Name | Age\n------|-----\nFoo | 42"
55
+ result = chat.kramdown_markdown_remove(input)
56
+ expect(result).not_to include('-')
57
+ end
58
+
59
+ it 'replaces box-drawing characters from rendered tables' do
60
+ input = "| Name | Age |\n|------|-----|\n| Foo | 42 |"
61
+ result = chat.kramdown_markdown_remove(input)
62
+ expect(result).not_to include("│")
63
+ expect(result).not_to include("╭")
64
+ expect(result).to include('Name')
65
+ expect(result).to include('Foo')
66
+ end
67
+
68
+ it 'preserves ASCII pipes in non-table lines (code, formulas)' do
69
+ input = 'look at this shell command `ls | grep foo`'
70
+ result = chat.kramdown_markdown_remove(input)
71
+ expect(result).to include('|')
72
+ end
73
+
74
+ it 'collapses multiple spaces in table rows' do
75
+ input = '| Name | Age |'
76
+ result = chat.kramdown_markdown_remove(input)
77
+ expect(result).not_to match(/ {2,}/)
78
+ end
79
+
80
+ it 'strips bold and other markdown formatting' do
81
+ input = '**hello** world'
82
+ result = chat.kramdown_markdown_remove(input)
83
+ expect(result).to eq 'hello world'
84
+ end
85
+
86
+ it 'removes emoji before processing' do
87
+ input = 'what is this? 🍓'
88
+ result = chat.kramdown_markdown_remove(input)
89
+ expect(result).to eq 'what is this?'
90
+ end
91
+
92
+ it 'returns empty string when only emoji are present' do
93
+ expect(chat.kramdown_markdown_remove('🍓✨')).to eq ''
94
+ end
95
+
96
+ it 'skips kramdown parse when markdown: false' do
97
+ input = '**bold** text'
98
+ expect(chat).not_to receive(:kramdown_ansi_parse)
99
+ result = chat.kramdown_markdown_remove(input, markdown: false)
100
+ expect(result).to include('**bold**')
101
+ end
102
+ end
43
103
  end
@@ -278,6 +278,7 @@ describe OllamaChat::SessionManagement do
278
278
 
279
279
  it 'returns nil if the user provides an empty string (cancel)' do
280
280
  expect(chat).to receive(:ask?).and_return('')
281
+ expect(STDOUT).to receive(:puts).with('Cancelled.')
281
282
  expect(chat.determine_valid_new_name_for_session('to create')).to be_nil
282
283
  end
283
284
  end
@@ -321,27 +322,30 @@ describe OllamaChat::SessionManagement do
321
322
  end
322
323
 
323
324
  describe '#summarize_conversation' do
324
- it 'displays summary in pager when not saving' do
325
+ it 'displays summary in pager and offers save' do
325
326
  chat.messages << OllamaChat::Message.new(role: 'user', content: 'hello')
326
327
  expect(chat).to receive(:summarize_session).and_return('# Summary content')
327
328
  expect(chat).to receive(:use_pager)
329
+ expect(chat).to receive(:ask_for_filename?).and_return(nil)
330
+ expect(STDOUT).to receive(:puts).with('Cancelled.')
328
331
  chat.summarize_conversation
329
332
  end
330
333
 
331
334
  it 'reports nothing when no messages' do
332
335
  expect(chat).to receive(:summarize_session).and_return(nil)
333
- expect(STDERR).to receive(:puts).with('Nothing to summarize!')
336
+ expect(STDOUT).to receive(:puts).with('Nothing to summarize!')
334
337
  chat.summarize_conversation
335
338
  end
336
339
 
337
- it 'saves summary to file when save: true' do
340
+ it 'saves summary to file when user provides a filename' do
338
341
  chat.messages << OllamaChat::Message.new(role: 'user', content: 'hello')
339
342
  tmpfile = Pathname.new(File.join(Dir.tmpdir, "summ_#{$$}_test.md"))
343
+ expect(chat).to receive(:summarize_session).and_return('# Summary content')
344
+ expect(chat).to receive(:use_pager)
340
345
  expect(chat).to receive(:ask_for_filename?).and_return(tmpfile)
341
346
  expect(chat).to receive(:should_overwrite?).and_return(true)
342
- expect(chat).to receive(:summarize_session).and_return('# Summary content')
343
347
  expect(STDOUT).to receive(:puts).with('File successfully written.')
344
- chat.summarize_conversation(save: true)
348
+ chat.summarize_conversation
345
349
  expect(tmpfile.exist?).to be_truthy
346
350
  expect(tmpfile.read).to include('Summary content')
347
351
  ensure
@@ -349,20 +353,20 @@ describe OllamaChat::SessionManagement do
349
353
  end
350
354
  end
351
355
 
352
- describe '#report_session' do
356
+ describe '#generate_conversation_report' do
353
357
  it 'generates a report from compacted messages' do
354
358
  chat.messages << OllamaChat::Message.new(role: 'user', content: 'hello world')
355
359
  expect(chat).to receive(:sender_name_displayed).and_return('User')
356
360
  expect(chat).to receive(:choose_prompt).and_return(double(to_s: '%{content}'))
357
361
  expect(chat).to receive(:generate).and_return('Generated report')
358
362
 
359
- result = chat.report_session
363
+ result = chat.generate_conversation_report
360
364
  expect(result).to eq('Generated report')
361
365
  end
362
366
 
363
367
  it 'returns nil when no messages have content' do
364
368
  expect(chat.messages).to receive(:compacted_messages).and_return([])
365
- expect(chat.report_session).to be_nil
369
+ expect(chat.generate_conversation_report).to be_nil
366
370
  end
367
371
 
368
372
  it 'uses named prompt when name is provided' do
@@ -373,21 +377,23 @@ describe OllamaChat::SessionManagement do
373
377
  expect(chat).to receive(:prompt).with(:report, context: 'system')
374
378
  .and_return(double(to_s: '%{content}'))
375
379
  expect(chat).to receive(:generate).and_return('Brief')
376
- expect(chat.report_session(name: 'coding_brief')).to eq('Brief')
380
+ expect(chat.generate_conversation_report(name: 'coding_brief')).to eq('Brief')
377
381
  end
378
382
  end
379
383
 
380
384
  describe '#report_conversation' do
381
385
  it 'displays report in pager when not saving' do
382
386
  chat.messages << OllamaChat::Message.new(role: 'user', content: 'hello')
383
- expect(chat).to receive(:report_session).and_return('# Report content')
387
+ expect(chat).to receive(:generate_conversation_report).and_return('# Report content')
384
388
  expect(chat).to receive(:use_pager)
389
+ expect(chat).to receive(:ask_for_filename?).and_return(nil)
390
+ expect(STDOUT).to receive(:puts).with('Cancelled.')
385
391
  chat.report_conversation
386
392
  end
387
393
 
388
394
  it 'reports nothing when no content' do
389
- expect(chat).to receive(:report_session).and_return(nil)
390
- expect(STDERR).to receive(:puts).with('Nothing to report!')
395
+ expect(chat).to receive(:generate_conversation_report).and_return(nil)
396
+ expect(STDOUT).to receive(:puts).with('Nothing to report!')
391
397
  chat.report_conversation
392
398
  end
393
399
 
@@ -396,9 +402,10 @@ describe OllamaChat::SessionManagement do
396
402
  tmpfile = Pathname.new(File.join(Dir.tmpdir, "report_#{$$}_test.md"))
397
403
  expect(chat).to receive(:ask_for_filename?).and_return(tmpfile)
398
404
  expect(chat).to receive(:should_overwrite?).and_return(true)
399
- expect(chat).to receive(:report_session).and_return('# Report content')
405
+ expect(chat).to receive(:generate_conversation_report).and_return('# Report content')
406
+ expect(STDOUT).to receive(:puts).with(/Report content/)
400
407
  expect(STDOUT).to receive(:puts).with('File successfully written.')
401
- chat.report_conversation(save: true)
408
+ chat.report_conversation
402
409
  expect(tmpfile.exist?).to be_truthy
403
410
  expect(tmpfile.read).to include('Report content')
404
411
  ensure
@@ -123,7 +123,7 @@ describe OllamaChat::TTS do
123
123
 
124
124
  tts.expose(:process_pending_blocks)
125
125
 
126
- expect(enqueued).to eq ["first paragraph\n", "second paragraph\n"]
126
+ expect(enqueued).to eq ["first paragraph", "second paragraph"]
127
127
  expect(tts.instance_variable_get(:@buffer)).to eq 'incomplete'
128
128
  end
129
129
 
@@ -134,7 +134,7 @@ describe OllamaChat::TTS do
134
134
 
135
135
  tts.expose(:process_pending_blocks)
136
136
 
137
- expect(enqueued).to eq ["real text\n", "more\n"]
137
+ expect(enqueued).to eq ["real text", "more"]
138
138
  expect(tts.instance_variable_get(:@buffer)).to eq ''
139
139
  end
140
140
  end
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.122
4
+ version: 0.0.123
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Frank
@@ -107,20 +107,6 @@ dependencies:
107
107
  - - ">="
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0'
110
- - !ruby/object:Gem::Dependency
111
- name: context_spook
112
- requirement: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - ">="
115
- - !ruby/object:Gem::Version
116
- version: '0'
117
- type: :development
118
- prerelease: false
119
- version_requirements: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - ">="
122
- - !ruby/object:Gem::Version
123
- version: '0'
124
110
  - !ruby/object:Gem::Dependency
125
111
  name: utils
126
112
  requirement: !ruby/object:Gem::Requirement