openclacky 1.5.4 → 1.5.5

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/lib/clacky/agent/message_compressor_helper.rb +4 -1
  4. data/lib/clacky/agent/session_serializer.rb +1 -0
  5. data/lib/clacky/agent.rb +1 -1
  6. data/lib/clacky/agent_config.rb +4 -2
  7. data/lib/clacky/agent_profile.rb +25 -3
  8. data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -2
  9. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +42 -33
  10. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +4 -2
  11. data/lib/clacky/default_extensions/git/ext.yml +3 -0
  12. data/lib/clacky/default_extensions/meeting/ext.yml +3 -0
  13. data/lib/clacky/default_extensions/time_machine/ext.yml +4 -0
  14. data/lib/clacky/default_skills/new/SKILL.md +17 -12
  15. data/lib/clacky/extension/api_extension.rb +2 -0
  16. data/lib/clacky/extension/loader.rb +1 -0
  17. data/lib/clacky/extension/packager.rb +60 -15
  18. data/lib/clacky/extension/verifier.rb +1 -1
  19. data/lib/clacky/message_format/anthropic.rb +1 -1
  20. data/lib/clacky/message_format/bedrock.rb +1 -1
  21. data/lib/clacky/message_format/open_ai.rb +8 -0
  22. data/lib/clacky/providers.rb +61 -0
  23. data/lib/clacky/server/http_server.rb +154 -15
  24. data/lib/clacky/server/web_ui_controller.rb +12 -2
  25. data/lib/clacky/session_manager.rb +1 -0
  26. data/lib/clacky/tools/terminal.rb +30 -3
  27. data/lib/clacky/version.rb +1 -1
  28. data/lib/clacky/web/app.css +301 -171
  29. data/lib/clacky/web/app.js +16 -2
  30. data/lib/clacky/web/features/extensions/store.js +214 -9
  31. data/lib/clacky/web/features/extensions/view.js +156 -11
  32. data/lib/clacky/web/features/new-session/store.js +25 -3
  33. data/lib/clacky/web/features/new-session/view.js +337 -140
  34. data/lib/clacky/web/features/skills/view.js +1 -1
  35. data/lib/clacky/web/i18n.js +29 -11
  36. data/lib/clacky/web/index.html +36 -64
  37. data/lib/clacky/web/sessions.js +2 -1
  38. data/lib/clacky/web/utils.js +1 -0
  39. data/lib/clacky/web/ws-dispatcher.js +39 -3
  40. metadata +1 -1
@@ -477,6 +477,67 @@ module Clacky
477
477
  "website_url" => "https://console.volcengine.com/ark/region:cn-beijing/overview"
478
478
  }.freeze,
479
479
 
480
+ "ollama" => {
481
+ "name" => "Ollama (Cloud)",
482
+ # Ollama Cloud API — OpenAI-compatible endpoint serving both local and
483
+ # cloud models. Cloud models are available immediately without pulling.
484
+ # Authentication: API key via Authorization: Bearer <key> header.
485
+ # Create API keys at: https://ollama.com/settings/keys
486
+ "base_url" => "https://ollama.com",
487
+ "api" => "openai-completions",
488
+ "default_model" => "deepseek-v4-flash",
489
+ # Curated list of cloud-enabled models from the Ollama library.
490
+ # Users can type any Ollama model name manually; this list seeds the picker.
491
+ "models" => [
492
+ "glm-5.2",
493
+ "kimi-k3",
494
+ "gemma4",
495
+ "qwen3.5",
496
+ "glm-5.1",
497
+ "minimax-m2.7",
498
+ "nemotron-3-super",
499
+ "minimax-m3",
500
+ "kimi-k2.7-code",
501
+ "kimi-k2.6",
502
+ "deepseek-v4-flash",
503
+ "deepseek-v4-pro",
504
+ "nemotron-3-ultra",
505
+ "gemini-3-flash-preview",
506
+ "nemotron-3-nano",
507
+ "mistral-large-3",
508
+ "gpt-oss"
509
+ ],
510
+ # Provider-level default: many Ollama cloud models are vision-capable.
511
+ "capabilities" => { "vision" => true }.freeze,
512
+ # Model-level overrides: text-only models that don't accept image input.
513
+ "model_capabilities" => {
514
+ "glm-5.2" => { "vision" => false }.freeze,
515
+ "glm-5.1" => { "vision" => false }.freeze,
516
+ "minimax-m2.7" => { "vision" => false }.freeze,
517
+ "deepseek-v4-pro" => { "vision" => false }.freeze,
518
+ "deepseek-v4-flash" => { "vision" => false }.freeze,
519
+ "nemotron-3-super" => { "vision" => false }.freeze,
520
+ "nemotron-3-ultra" => { "vision" => false }.freeze,
521
+ "nemotron-3-nano" => { "vision" => false }.freeze,
522
+ "gpt-oss" => { "vision" => false }.freeze
523
+ }.freeze,
524
+ # Per-primary lite pairing: subagents use smaller models for cheap/fast work.
525
+ "lite_models" => {
526
+ "glm-5.2" => "glm-5.1",
527
+ "kimi-k3" => "kimi-k2.6",
528
+ "gemma4" => "nemotron-3-nano",
529
+ "qwen3.5" => "gemini-3-flash-preview",
530
+ "minimax-m3" => "minimax-m2.7",
531
+ "kimi-k2.7-code" => "kimi-k2.6",
532
+ "deepseek-v4-pro" => "deepseek-v4-flash",
533
+ "mistral-large-3" => "nemotron-3-nano",
534
+ "gpt-oss" => "nemotron-3-nano",
535
+ "gemini-3-flash-preview" => "nemotron-3-nano"
536
+ },
537
+ "default_ocr_model" => "kimi-k2.7-code",
538
+ "website_url" => "https://ollama.com/settings/keys"
539
+ }.freeze,
540
+
480
541
  "openai" => {
481
542
  "name" => "OpenAI (GPT)",
482
543
  "base_url" => "https://api.openai.com/v1",
@@ -217,6 +217,9 @@ module Clacky
217
217
  # Lazy: process-wide MCP registry. Created on first /api/mcp/:name access
218
218
  # so test setups that override Dir.home in before-hooks still work.
219
219
  @mcp_registry_mutex = Mutex.new
220
+ # Install jobs: job_id => { stage:, progress:, error:, done: }
221
+ @install_jobs = {}
222
+ @install_jobs_mutex = Mutex.new
220
223
  # Access key authentication:
221
224
  # - localhost (127.0.0.1 / ::1) is always trusted; auth is skipped entirely.
222
225
  # - Any other bind address requires CLACKY_ACCESS_KEY env var.
@@ -370,10 +373,13 @@ module Clacky
370
373
 
371
374
  server.mount_proc("/") do |req, res|
372
375
  if req.path == "/" || req.path == "/index.html"
373
- product_name = Clacky::BrandConfig.load.product_name || "OpenClacky"
376
+ brand = Clacky::BrandConfig.load
377
+ product_name = brand.product_name || "OpenClacky"
378
+ logo_url = brand.logo_url || "/logo_nav_dark.png"
374
379
  pure = req.query["pure"] == "true"
375
380
  html = File.read(index_html_path)
376
381
  .gsub("{{BRAND_NAME}}", product_name)
382
+ .gsub("{{BRAND_LOGO_URL}}", logo_url)
377
383
  .gsub("{{EXT_SCRIPTS}}", pure ? "" : self.send(:webui_ext_script_tags))
378
384
  res.status = 200
379
385
  res["Content-Type"] = "text/html; charset=utf-8"
@@ -493,7 +499,9 @@ module Clacky
493
499
  # Building/extracting a tar.gz of ~/.clacky can take a while.
494
500
  120
495
501
  elsif path == "/api/store/extension/install"
496
- 300
502
+ 10 # now async — just spawns a thread and returns job_id immediately
503
+ elsif path == "/api/store/extension/install/status"
504
+ 10
497
505
  else
498
506
  30
499
507
  end
@@ -560,9 +568,11 @@ module Clacky
560
568
  when ["GET", "/api/store/skills"] then api_store_skills(res)
561
569
  when ["GET", "/api/store/extensions"] then api_store_extensions(req, res)
562
570
  when ["GET", "/api/store/extensions/brand"] then api_store_extensions_brand(res)
571
+ when ["GET", "/api/store/extensions/system"] then api_store_extensions_system(res)
563
572
  when ["GET", "/api/store/extensions/installed"] then api_store_extensions_installed(res)
564
573
  when ["GET", "/api/store/extension"] then api_store_extension_detail(req, res)
565
574
  when ["POST", "/api/store/extension/install"] then api_store_extension_install(req, res)
575
+ when ["GET", "/api/store/extension/install/status"] then api_store_extension_install_status(req, res)
566
576
  when ["POST", "/api/store/extension/disable"] then api_store_extension_disable(req, res)
567
577
  when ["POST", "/api/store/extension/enable"] then api_store_extension_enable(req, res)
568
578
  when ["DELETE", "/api/store/extension"] then api_store_extension_uninstall(req, res)
@@ -2527,6 +2537,47 @@ module Clacky
2527
2537
  end
2528
2538
  end
2529
2539
 
2540
+ # GET /api/store/extensions/system
2541
+ #
2542
+ # Returns only the builtin (system) extensions shipped with the gem.
2543
+ def api_store_extensions_system(res)
2544
+ result = Clacky::ExtensionLoader.load_all
2545
+ disabled = Clacky::ExtensionLoader.disabled_ids
2546
+
2547
+ hidden = %w[coding general ext-studio].to_set
2548
+
2549
+ extensions = Array(result&.containers).filter_map do |ext_id, container|
2550
+ next unless container[:layer] == :builtin
2551
+ next if hidden.include?(ext_id)
2552
+
2553
+ local_name = container[:name].to_s.then { |n| n.empty? ? ext_id : n }
2554
+ local_desc = container.dig(:raw, "description").to_s
2555
+ local_desc_zh = container.dig(:raw, "description_zh").to_s
2556
+ local_author = container[:author].to_s
2557
+ local_units = units_from_container(container)
2558
+ {
2559
+ "id" => ext_id,
2560
+ "name" => local_name,
2561
+ "display_name" => container.dig(:raw, "display_name") || local_name,
2562
+ "display_name_zh" => container.dig(:raw, "display_name_zh"),
2563
+ "slug" => ext_id,
2564
+ "version" => container[:version],
2565
+ "description" => local_desc,
2566
+ "description_zh" => local_desc_zh,
2567
+ "author" => local_author,
2568
+ "units" => local_units,
2569
+ "layer" => "builtin",
2570
+ "installed" => true,
2571
+ "removable" => false,
2572
+ "disabled" => disabled.include?(ext_id),
2573
+ }
2574
+ end
2575
+
2576
+ json_response(res, 200, { ok: true, extensions: extensions })
2577
+ rescue StandardError => e
2578
+ json_response(res, 500, { ok: false, error: e.message })
2579
+ end
2580
+
2530
2581
  # GET /api/store/extensions/installed
2531
2582
  #
2532
2583
  # Returns all locally installed extensions (all layers: builtin, installed,
@@ -2656,6 +2707,7 @@ module Clacky
2656
2707
  slug = ext["name"] || ext[:name] || ext["slug"] || ext[:slug]
2657
2708
  container = extension_container(slug)
2658
2709
  ext = ext.merge(
2710
+ "slug" => slug,
2659
2711
  "installed" => !container.nil?,
2660
2712
  "installed_version" => container&.dig(:version),
2661
2713
  "removable" => container && container[:layer] == :installed,
@@ -2732,7 +2784,9 @@ module Clacky
2732
2784
  json_response(res, 500, { ok: false, error: e.message })
2733
2785
  end
2734
2786
 
2735
- # DELETE /api/store/extension body: { id: <slug> }
2787
+ # POST /api/store/extension/install body: { download_url:, name: }
2788
+ # Starts an async install job and returns immediately with a job_id.
2789
+ # Poll GET /api/store/extension/install/status?job_id=xxx for progress.
2736
2790
  def api_store_extension_install(req, res)
2737
2791
  body = parse_json_body(req)
2738
2792
  download_url = body["download_url"].to_s.strip
@@ -2743,14 +2797,71 @@ module Clacky
2743
2797
  return
2744
2798
  end
2745
2799
 
2746
- Clacky::ExtensionPackager.install(download_url, force: true)
2747
- Clacky::ExtensionLoader.invalidate_cache!
2748
- Clacky::Telemetry.extension_install!(name) unless name.empty?
2749
- json_response(res, 200, { ok: true, name: name })
2750
- rescue Clacky::ExtensionPackager::Error => e
2751
- json_response(res, 422, { ok: false, error: e.message })
2752
- rescue StandardError => e
2753
- json_response(res, 500, { ok: false, error: e.message })
2800
+ job_id = SecureRandom.hex(8)
2801
+ @install_jobs_mutex.synchronize do
2802
+ sweep_stale_install_jobs!
2803
+ @install_jobs[job_id] = { stage: "downloading", progress: 0, error: nil, done: false, updated_at: Time.now }
2804
+ end
2805
+
2806
+ Thread.new do
2807
+ begin
2808
+ on_progress = lambda do |info|
2809
+ @install_jobs_mutex.synchronize do
2810
+ @install_jobs[job_id] = info.merge(error: nil, done: false, updated_at: Time.now)
2811
+ end
2812
+ end
2813
+ Clacky::ExtensionPackager.install(download_url, force: true, on_progress: on_progress)
2814
+ Clacky::ExtensionLoader.invalidate_cache!
2815
+ Clacky::Telemetry.extension_install!(name) unless name.empty?
2816
+ @install_jobs_mutex.synchronize do
2817
+ @install_jobs[job_id] = { stage: "done", progress: 100, error: nil, done: true, updated_at: Time.now }
2818
+ end
2819
+ rescue StandardError => e
2820
+ @install_jobs_mutex.synchronize do
2821
+ @install_jobs[job_id] = { stage: "error", progress: 0, error: e.message, done: true, updated_at: Time.now }
2822
+ end
2823
+ end
2824
+ end
2825
+
2826
+ json_response(res, 200, { ok: true, job_id: job_id })
2827
+ end
2828
+
2829
+ # GET /api/store/extension/install/status?job_id=xxx
2830
+ def api_store_extension_install_status(req, res)
2831
+ job_id = req.query["job_id"].to_s.strip
2832
+ if job_id.empty?
2833
+ json_response(res, 400, { ok: false, error: "Missing job_id." })
2834
+ return
2835
+ end
2836
+ job = @install_jobs_mutex.synchronize do
2837
+ sweep_stale_install_jobs!
2838
+ @install_jobs[job_id]
2839
+ end
2840
+ if job.nil?
2841
+ json_response(res, 404, { ok: false, error: "Job not found." })
2842
+ return
2843
+ end
2844
+ # Clean up finished jobs after they are read as done
2845
+ if job[:done]
2846
+ @install_jobs_mutex.synchronize { @install_jobs.delete(job_id) }
2847
+ end
2848
+ json_response(res, 200, { ok: true }.merge(job))
2849
+ end
2850
+
2851
+ # Install jobs can go stale in two ways:
2852
+ # - the client abandons the poll (tab closed, network drop) so a
2853
+ # finished job never gets picked up and deleted by the status endpoint
2854
+ # - the worker thread dies without ever updating the job hash (should
2855
+ # not happen given the rescue in api_store_extension_install, but a
2856
+ # hung Thread with no timeout upstream would otherwise pin memory
2857
+ # forever)
2858
+ # Sweep anything past its TTL. Must be called from inside
2859
+ # @install_jobs_mutex.synchronize.
2860
+ INSTALL_JOB_TTL = 30 * 60 # seconds
2861
+
2862
+ private def sweep_stale_install_jobs!
2863
+ cutoff = Time.now - INSTALL_JOB_TTL
2864
+ @install_jobs.delete_if { |_id, job| (job[:updated_at] || Time.at(0)) < cutoff }
2754
2865
  end
2755
2866
 
2756
2867
  def api_store_extension_uninstall(req, res)
@@ -4354,12 +4465,30 @@ module Clacky
4354
4465
  # Each entry carries { id, title, title_zh, description, description_zh,
4355
4466
  # source, order, layer, author }. Extensions can supply title_zh /
4356
4467
  # description_zh / author in their ext.yml so the New Session cards read
4357
- # naturally and credit their author.
4468
+ # naturally and credit their author. Third-party extension agents are
4469
+ # ordered by most-recently-used (derived from session history) so the
4470
+ # ones the user actually uses float to the front of that group.
4358
4471
  def api_list_agents(res)
4359
- agents = Clacky::AgentProfile.all
4472
+ recency = agent_last_used_recency
4473
+ agents = Clacky::AgentProfile.all(recency: recency)
4360
4474
  json_response(res, 200, { agents: agents })
4361
4475
  end
4362
4476
 
4477
+ # Map agent_profile id => most recent session's updated_at (epoch seconds),
4478
+ # scanning every session on disk. Used to rank extension agent cards by
4479
+ # recent usage instead of a static declared `order`.
4480
+ private def agent_last_used_recency
4481
+ recency = {}
4482
+ @session_manager.all_sessions.each do |s|
4483
+ id = (s[:agent_profile] || "general").to_s
4484
+ ts = s[:updated_at] || s[:created_at]
4485
+ next unless ts
4486
+ epoch = Time.parse(ts.to_s).to_i rescue next
4487
+ recency[id] = epoch if epoch > (recency[id] || 0)
4488
+ end
4489
+ recency
4490
+ end
4491
+
4363
4492
  # GET /api/sessions/:id/skills — list user-invocable skills for a session,
4364
4493
  # filtered by the session's agent profile. Used by the frontend slash-command
4365
4494
  # autocomplete so only skills valid for the current profile are suggested.
@@ -5667,6 +5796,7 @@ module Clacky
5667
5796
  json_response(res, 200, {
5668
5797
  ok: true,
5669
5798
  enable_compression: @agent_config.enable_compression,
5799
+ enable_idle_compression: @agent_config.enable_idle_compression,
5670
5800
  enable_prompt_caching: @agent_config.enable_prompt_caching,
5671
5801
  memory_update_enabled: @agent_config.memory_update_enabled,
5672
5802
  proxy_url: @agent_config.proxy_url.to_s,
@@ -5692,6 +5822,9 @@ module Clacky
5692
5822
  if body.key?("enable_compression")
5693
5823
  @agent_config.enable_compression = !!body["enable_compression"]
5694
5824
  end
5825
+ if body.key?("enable_idle_compression")
5826
+ @agent_config.enable_idle_compression = !!body["enable_idle_compression"]
5827
+ end
5695
5828
  if body.key?("enable_prompt_caching")
5696
5829
  @agent_config.enable_prompt_caching = !!body["enable_prompt_caching"]
5697
5830
  end
@@ -6264,7 +6397,7 @@ module Clacky
6264
6397
  end
6265
6398
 
6266
6399
  # PATCH /api/sessions/:id/reasoning_effort
6267
- # Body: { "reasoning_effort": "off" | "low" | "medium" | "high" | "xhigh" }
6400
+ # Body: { "reasoning_effort": "off" | "low" | "medium" | "high" | "xhigh" | "max" }
6268
6401
  def api_switch_session_reasoning_effort(session_id, req, res)
6269
6402
  body = parse_json_body(req)
6270
6403
  raw = body["reasoning_effort"]
@@ -6766,7 +6899,13 @@ module Clacky
6766
6899
  msg_created_at = Time.now.to_f
6767
6900
  web_ui = nil
6768
6901
  @registry.with_session(session_id) { |s| web_ui = s[:ui] }
6769
- web_ui&.show_user_message(content, created_at: msg_created_at, source: :web)
6902
+ # Pass the uploaded files through to the realtime broadcast so images and
6903
+ # attachments render in the bubble immediately. agent.run later appends the
6904
+ # authoritative display_files to history — but only after vision/OCR
6905
+ # processing finishes, which can take seconds. Without the preview here, a
6906
+ # page refresh inside that window shows the message without its image (the
6907
+ # "need to refresh several times before the image appears" bug).
6908
+ web_ui&.show_user_message(content, created_at: msg_created_at, source: :web, files: Array(files))
6770
6909
 
6771
6910
  # File references are now handled inside agent.run — injected as a system_injected
6772
6911
  # message after the user message, so replay_history skips them automatically.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cgi"
3
4
  require "json"
4
5
  require "securerandom"
5
6
  require_relative "../ui_interface"
@@ -70,11 +71,20 @@ module Clacky
70
71
  data[:created_at] = created_at if created_at
71
72
  # Build ev.images for the frontend renderer (history_user_message):
72
73
  # - Images with data_url → pass the data_url directly (<img> thumbnail)
73
- # - Disk files (PDF, doc, etc., no data_url) → "pdf:name" sentinel (renders a badge)
74
+ # - Disk image files (type=="image", has path) → /api/local-image proxy URL
75
+ # - Disk files (PDF, doc, etc., no data_url, no image path) → "pdf:name" sentinel
74
76
  rendered = Array(files).filter_map do |f|
75
77
  url = f[:data_url] || f["data_url"]
76
78
  name = f[:name] || f["name"]
77
- url || (name ? "pdf:#{name}" : nil)
79
+ type = f[:type] || f["type"]
80
+ path = f[:path] || f["path"]
81
+ if url
82
+ url
83
+ elsif type.to_s == "image" && path && File.exist?(path.to_s)
84
+ "/api/local-image?path=#{CGI.escape(path.to_s)}&v=#{File.mtime(path.to_s).to_i}"
85
+ elsif name
86
+ "pdf:#{name}"
87
+ end
78
88
  end
79
89
  data[:images] = rendered unless rendered.empty?
80
90
  emit("history_user_message", **data)
@@ -36,6 +36,7 @@ module Clacky
36
36
  # Keep only the most recent 200 sessions (best-effort, never block save)
37
37
  begin
38
38
  cleanup_by_count(keep: 200, keep_cron: 200)
39
+ cleanup_trash(days: 8)
39
40
  rescue Exception # rubocop:disable Lint/RescueException
40
41
  # Cleanup is non-critical; swallow all errors (including AgentInterrupted)
41
42
  end
@@ -480,9 +480,7 @@ module Clacky
480
480
  # leaves the result as invalid UTF-8. Re-scrub after truncation so
481
481
  # everything downstream (JSON.generate, format_result, UI) gets a
482
482
  # guaranteed-valid UTF-8 string.
483
- preview = cleaned.byteslice(0, OVERFLOW_PREVIEW_CHARS)
484
- preview.force_encoding(Encoding::UTF_8)
485
- preview = preview.scrub("?") unless preview.valid_encoding?
483
+ preview = smart_truncate(cleaned, OVERFLOW_PREVIEW_CHARS)
486
484
 
487
485
  notice = if overflow_file
488
486
  "\n\n...[Output truncated for LLM: showing first #{OVERFLOW_PREVIEW_CHARS} " \
@@ -1429,6 +1427,35 @@ module Clacky
1429
1427
  nil
1430
1428
  end
1431
1429
 
1430
+ # Smart truncation: keep head + tail within char budget.
1431
+ # Ensures error messages at end of output remain visible.
1432
+ private def smart_truncate(text, max_chars)
1433
+ total_chars = text.bytesize
1434
+ return text if total_chars <= max_chars
1435
+
1436
+ total_lines = text.lines.count
1437
+
1438
+ # 60% head, 40% tail
1439
+ head_budget = (max_chars * 0.6).to_i
1440
+ tail_budget = max_chars - head_budget
1441
+
1442
+ head = text.byteslice(0, head_budget)
1443
+ head.force_encoding(Encoding::UTF_8)
1444
+ head = head.scrub("?") unless head.valid_encoding?
1445
+
1446
+ tail = text.byteslice(-tail_budget, tail_budget)
1447
+ tail.force_encoding(Encoding::UTF_8)
1448
+ tail = tail.scrub("?") unless tail.valid_encoding?
1449
+
1450
+ # Align tail to line boundary if cut mid-line
1451
+ if tail.include?("\n") && !tail.start_with?("\n")
1452
+ first_nl = tail.index("\n")
1453
+ tail = tail[(first_nl + 1)..-1] || ""
1454
+ end
1455
+
1456
+ "#{head}\n\n...[#{total_lines} lines, #{total_chars} chars total, truncated]...\n\n#{tail}"
1457
+ end
1458
+
1432
1459
 
1433
1460
 
1434
1461
  # Max visible length of a command inside the tool-call summary line.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.5.4"
4
+ VERSION = "1.5.5"
5
5
  end