openclacky 0.8.9 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fbd7535e2cc98aadf0d9bbc3ff8dcc3db04669ba5c972ec00b41c338f1c3ae5
4
- data.tar.gz: 345be84b2b010db47ccdd0454b0214463117519fe5d029440290c0187006ff4a
3
+ metadata.gz: 8a13cced61727911f2587013fa7f701863f8644ed62caaf235217be4dc306574
4
+ data.tar.gz: 9f9f1d42f482bf159099d2a49c8435f8bff176dc0126321cd980079297ff7078
5
5
  SHA512:
6
- metadata.gz: 317f05f87355eb3c58da51dde828b6fafd3abbc7cc8bfa874c19445b5a783f7687ee84c7e8ddcf59573b7a554687ee8b2c9e5a8ed5d14993b6167fe5a656a881
7
- data.tar.gz: ce669491b51cbff8f3fbdea2262d51d206377617d69ad981ac5025b824b86638d78d248ea6dcd89f4b9ecca92d042efa91b783dff220bec5c4ac348c8535b6d6
6
+ metadata.gz: 7b3841fdddb699185b6a0854a4413b2a604b5187235bc3a5cd408525a8bfd21746e8dab0a5e1b085fbbd928acad3f3bda845cd208d077b0420c6419815036168
7
+ data.tar.gz: 70feae263b9d3a43d3b199ffd26e40f3a79d61d8e56e27a77b9ab615a62cb9a483dcbbdde770cbe4117aee66db1aa93fcbfae6350aab7e744f8b7a8c80a9ab8d
data/CHANGELOG.md CHANGED
@@ -7,9 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.8.9] - 2026-03-13
10
+ ## [0.9.1] - 2026-03-15
11
11
 
12
12
  ### Added
13
+ - **Session context auto-injection**: the agent now automatically injects the current date and active model name into each conversation turn, so it always knows what day it is and which model it's running — helpful for time-sensitive tasks and multi-model setups
14
+ - **Kimi/Moonshot extended thinking support**: reasoning content is now preserved and echoed back correctly in message history, fixing HTTP 400 errors when using Kimi's extended thinking API
15
+
16
+ ### Improved
17
+ - **Browser tool install UX**: the `agent-browser` setup flow has been redesigned with a dedicated install script and clearer guidance, making first-time setup smoother
18
+
19
+ ## [0.9.0] - 2026-03-14
20
+
21
+ ### Added
22
+ - **Version check and one-click upgrade in WebUI**: a version badge in the sidebar shows when a newer gem is available; clicking it opens an upgrade popover with a live install log and a restart button — no terminal needed
23
+ - **Upgrade badge state machine**: the badge cycles through four visual states — amber pulsing dot (update available), spinning ring (installing), orange bouncing dot (restart needed), green check (restarted successfully)
13
24
  - **Markdown rendering in WebUI chat**: assistant responses are now rendered as rich markdown — headings, bold, code blocks, lists, and inline code are all formatted properly instead of displayed as raw text
14
25
  - **Session naming with auto-name and inline rename**: sessions are automatically named after the first exchange; users can double-click any session in the sidebar to rename it inline
15
26
  - **Session info bar with live status animation**: a slim bar below the chat header shows the session name, working directory, and a pulsing animation while the agent is thinking or executing tools
@@ -18,10 +29,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
18
29
  - **Idle compression in WebUI**: the agent now compresses long conversation history automatically when the session has been idle, keeping context efficient without manual intervention
19
30
 
20
31
  ### Improved
32
+ - **Glob tool recursive search**: bare pattern names like `controller` are now automatically expanded to `**/*controller*` so searches always return results across all subdirectories
21
33
  - **Onboard flow**: soul setup is now non-blocking; the confirmation page is skipped for a faster first-run experience; onboard now asks the user to name the AI first, then collects the user profile
22
34
  - **Token usage display ordering**: the token usage line in WebUI now always appears below the assistant message bubble, not above it
35
+ - **i18n coverage**: settings panel dynamically-rendered fields are now translated correctly at render time
23
36
 
24
37
  ### Fixed
38
+ - **Upgrade popover stays open during install and reconnect**: the popover is now locked while a gem install or server restart is in progress, preventing accidental dismissal that would leave the badge stuck in a spinning state
39
+ - **Session auto-name respects default placeholders**: session names are now assigned based on message history only, not the agent's internal name field, so placeholder names like "Session 1" no longer block the auto-naming logic
25
40
  - **Token usage line disappears after page refresh**: token usage data is now persisted in session history and correctly re-rendered when the page is reloaded
26
41
  - **Shell tool hangs on background commands**: commands containing `&` (background operator) no longer cause the shell tool to block indefinitely
27
42
  - **White flash on page load**: the page is now hidden until boot completes, preventing a flash of unstyled content or the wrong view on startup
data/lib/clacky/agent.rb CHANGED
@@ -182,6 +182,9 @@ module Clacky
182
182
  @messages << system_message
183
183
  end
184
184
 
185
+ # Inject session context (date + model) if not yet present or date has changed
186
+ inject_session_context_if_needed
187
+
185
188
  # Format user message with images and files if provided
186
189
  user_content = format_user_content(user_input, images, files)
187
190
  @messages << { role: "user", content: user_content, task_id: task_id, created_at: Time.now.to_f }
@@ -423,6 +426,9 @@ module Clacky
423
426
  end
424
427
  # Store token_usage in the message so replay_history can re-emit it
425
428
  msg[:token_usage] = response[:token_usage] if response[:token_usage]
429
+ # Preserve reasoning_content so it is echoed back to APIs that require it
430
+ # (e.g. Kimi/Moonshot extended thinking — omitting it causes HTTP 400)
431
+ msg[:reasoning_content] = response[:reasoning_content] if response[:reasoning_content]
426
432
  @messages << msg
427
433
 
428
434
  response
@@ -871,6 +877,33 @@ module Clacky
871
877
  content
872
878
  end
873
879
 
880
+ # Inject a session context message (date + model) into the conversation.
881
+ # Only injects when:
882
+ # 1. No context message exists yet in this session, OR
883
+ # 2. The existing context is from a previous day (cross-day session)
884
+ # Marked with system_injected: true so existing filters (replay_history,
885
+ # get_recent_user_messages, etc.) automatically skip it.
886
+ # Cache-safe: always inserted just before the current user message,
887
+ # so no historical cache entries are ever invalidated.
888
+ private def inject_session_context_if_needed
889
+ today = Time.now.strftime("%Y-%m-%d")
890
+
891
+ # Find the last injected context message
892
+ last_ctx = @messages.reverse.find { |m| m[:session_context] }
893
+
894
+ # Skip if we already have a context for today
895
+ return if last_ctx && last_ctx[:context_date] == today
896
+
897
+ content = "[Session context: Today is #{Time.now.strftime('%Y-%m-%d, %A')}. Current model: #{current_model}]"
898
+ @messages << {
899
+ role: "user",
900
+ content: content,
901
+ system_injected: true,
902
+ session_context: true,
903
+ context_date: today
904
+ }
905
+ end
906
+
874
907
  # Track modified files for Time Machine snapshots
875
908
  # @param tool_name [String] Name of the tool that was executed
876
909
  # @param args [Hash] Arguments passed to the tool
data/lib/clacky/client.rb CHANGED
@@ -659,13 +659,20 @@ module Clacky
659
659
  end
660
660
  end
661
661
 
662
- {
662
+ result = {
663
663
  content: message["content"],
664
664
  tool_calls: parse_tool_calls(message["tool_calls"]),
665
665
  finish_reason: data["choices"].first["finish_reason"],
666
666
  usage: usage_data,
667
667
  raw_api_usage: raw_api_usage
668
668
  }
669
+
670
+ # Preserve reasoning_content if present (e.g. Kimi/Moonshot extended thinking).
671
+ # The API requires this field to be echoed back in the message history on
672
+ # subsequent requests, otherwise it returns HTTP 400.
673
+ result[:reasoning_content] = message["reasoning_content"] if message["reasoning_content"]
674
+
675
+ result
669
676
  else
670
677
  raise_error(response)
671
678
  end
@@ -131,10 +131,20 @@ module Clacky
131
131
  @agent_config = agent_config
132
132
  @client_factory = client_factory # callable: -> { Clacky::Client.new(...) }
133
133
  @brand_test = brand_test # when true, skip remote API calls for license activation
134
+ # Capture the absolute path of the entry script and original ARGV at startup,
135
+ # so api_restart can re-exec the correct binary even if cwd changes later.
136
+ @restart_script = File.expand_path($0)
137
+ @restart_argv = ARGV.dup
134
138
  @registry = SessionRegistry.new
135
139
  @session_manager = Clacky::SessionManager.new
136
140
  @ws_clients = {} # session_id => [WebSocketConnection, ...]
137
141
  @ws_mutex = Mutex.new
142
+ # Version cache: { latest: "x.y.z", checked_at: Time }
143
+ @version_cache = nil
144
+ @version_mutex = Mutex.new
145
+ # Version cache: { latest: "x.y.z", checked_at: Time }
146
+ @version_cache = nil
147
+ @version_mutex = Mutex.new
138
148
  @scheduler = Scheduler.new(
139
149
  session_registry: @registry,
140
150
  session_builder: method(:build_session)
@@ -264,6 +274,9 @@ module Clacky
264
274
  when ["GET", "/api/brand"] then api_brand_info(res)
265
275
  when ["GET", "/api/channels"] then api_list_channels(res)
266
276
  when ["POST", "/api/upload"] then api_upload_file(req, res)
277
+ when ["GET", "/api/version"] then api_get_version(res)
278
+ when ["POST", "/api/version/upgrade"] then api_upgrade_version(req, res)
279
+ when ["POST", "/api/restart"] then api_restart(req, res)
267
280
  else
268
281
  if method == "POST" && path.match?(%r{^/api/channels/[^/]+/test$})
269
282
  platform = path.sub("/api/channels/", "").sub("/test", "")
@@ -591,6 +604,111 @@ module Clacky
591
604
  json_response(res, 200, brand.to_h)
592
605
  end
593
606
 
607
+ # ── Version API ───────────────────────────────────────────────────────────
608
+
609
+ # GET /api/version
610
+ # Returns current version and latest version from RubyGems (cached for 1 hour).
611
+ def api_get_version(res)
612
+ current = Clacky::VERSION
613
+ latest = fetch_latest_version_cached
614
+ json_response(res, 200, {
615
+ current: current,
616
+ latest: latest,
617
+ needs_update: latest ? version_older?(current, latest) : false
618
+ })
619
+ end
620
+
621
+ # POST /api/version/upgrade
622
+ # Runs `gem update openclacky --no-document` via Clacky::Tools::Shell (login shell)
623
+ # in a background thread, streaming output via WebSocket broadcast.
624
+ # On success, re-execs the process so the new gem version is loaded.
625
+ def api_upgrade_version(req, res)
626
+ json_response(res, 202, { ok: true, message: "Upgrade started" })
627
+
628
+ Thread.new do
629
+ begin
630
+ broadcast_all(type: "upgrade_log", line: "Starting upgrade: gem update openclacky --no-document\n")
631
+
632
+ shell = Clacky::Tools::Shell.new
633
+ result = shell.execute(command: "gem update openclacky --no-document",
634
+ soft_timeout: 300, hard_timeout: 600)
635
+ output = [result[:stdout], result[:stderr]].join
636
+ success = result[:exit_code] == 0
637
+
638
+ broadcast_all(type: "upgrade_log", line: output)
639
+
640
+ if success
641
+ broadcast_all(type: "upgrade_log", line: "\n✓ Upgrade successful! Please restart the server to apply the new version.\n")
642
+ broadcast_all(type: "upgrade_complete", success: true)
643
+ else
644
+ broadcast_all(type: "upgrade_log", line: "\n✗ Upgrade failed. Please try manually: gem update openclacky\n")
645
+ broadcast_all(type: "upgrade_complete", success: false)
646
+ end
647
+ rescue StandardError => e
648
+ broadcast_all(type: "upgrade_log", line: "\n✗ Error during upgrade: #{e.message}\n")
649
+ broadcast_all(type: "upgrade_complete", success: false)
650
+ end
651
+ end
652
+ end
653
+
654
+ # POST /api/restart
655
+ # Re-execs the current process so the newly installed gem version is loaded.
656
+ # Uses the absolute script path captured at startup to avoid relative-path issues.
657
+ # Responds 200 first, then waits briefly for WEBrick to flush the response before exec.
658
+ def api_restart(req, res)
659
+ json_response(res, 200, { ok: true, message: "Restarting…" })
660
+
661
+ script = @restart_script
662
+ argv = @restart_argv
663
+ Thread.new do
664
+ sleep 0.5 # Let WEBrick flush the HTTP response
665
+ Clacky::Logger.info("[Restart] exec: #{RbConfig.ruby} #{script} #{argv.join(' ')}")
666
+ exec(RbConfig.ruby, script, *argv)
667
+ end
668
+ end
669
+
670
+ # Fetch the latest gem version using `gem list -r`, with a 1-hour in-memory cache.
671
+ # Uses Clacky::Tools::Shell (login shell) so rbenv/mise shims and gem mirrors work correctly.
672
+ private def fetch_latest_version_cached
673
+ @version_mutex.synchronize do
674
+ now = Time.now
675
+ if @version_cache && (now - @version_cache[:checked_at]) < 3600
676
+ return @version_cache[:latest]
677
+ end
678
+ end
679
+
680
+ # Fetch outside the mutex to avoid blocking other requests
681
+ latest = fetch_latest_version_from_gem
682
+
683
+ @version_mutex.synchronize do
684
+ @version_cache = { latest: latest, checked_at: Time.now }
685
+ end
686
+
687
+ latest
688
+ end
689
+
690
+ # Query the latest openclacky version via `gem list -r openclacky`.
691
+ # Runs through login shell so gem source mirrors configured via rbenv/mise work correctly.
692
+ # Output format: "openclacky (0.9.0)"
693
+ private def fetch_latest_version_from_gem
694
+ shell = Clacky::Tools::Shell.new
695
+ result = shell.execute(command: "gem list -r openclacky", soft_timeout: 15, hard_timeout: 30)
696
+ return nil unless result[:exit_code] == 0
697
+
698
+ out = result[:stdout].to_s
699
+ match = out.match(/^openclacky\s+\(([^)]+)\)/)
700
+ match ? match[1].strip : nil
701
+ rescue StandardError
702
+ nil
703
+ end
704
+
705
+ # Returns true if version string `a` is strictly older than `b`.
706
+ private def version_older?(a, b)
707
+ Gem::Version.new(a) < Gem::Version.new(b)
708
+ rescue ArgumentError
709
+ false
710
+ end
711
+
594
712
  # ── Channel API ───────────────────────────────────────────────────────────
595
713
 
596
714
  # GET /api/channels
@@ -1354,8 +1472,10 @@ module Clacky
1354
1472
  end
1355
1473
  content = [content, *file_refs].join("\n") unless file_refs.empty?
1356
1474
 
1357
- # Auto-name the session from the first user message (before agent starts running)
1358
- if agent.name.empty? && agent.messages.empty?
1475
+ # Auto-name the session from the first user message (before agent starts running).
1476
+ # Check messages.empty? only agent.name may already hold a default placeholder
1477
+ # like "Session 1" assigned at creation time, so it's not a reliable signal.
1478
+ if agent.messages.empty?
1359
1479
  auto_name = content.gsub(/\s+/, " ").strip[0, 30]
1360
1480
  auto_name += "…" if content.strip.length > 30
1361
1481
  agent.rename(auto_name)
@@ -1635,15 +1755,23 @@ module Clacky
1635
1755
 
1636
1756
  pid = File.read(pid_file).strip.to_i
1637
1757
  return if pid <= 0
1758
+ # After exec-restart, the new process inherits the same PID as the old one.
1759
+ # Skip sending TERM to ourselves — we are already the new server.
1760
+ if pid == Process.pid
1761
+ Clacky::Logger.info("[Server] exec-restart detected (PID=#{pid}), skipping self-kill.")
1762
+ return
1763
+ end
1638
1764
 
1639
1765
  begin
1640
1766
  Process.kill("TERM", pid)
1767
+ Clacky::Logger.info("[Server] Stopped existing server (PID=#{pid}) on port #{port}.")
1641
1768
  puts "Stopped existing server (PID: #{pid}) on port #{port}."
1642
1769
  # Give it a moment to release the port
1643
1770
  sleep 0.5
1644
1771
  rescue Errno::ESRCH
1645
- # Process already gone — nothing to do
1772
+ Clacky::Logger.info("[Server] Existing server PID=#{pid} already gone.")
1646
1773
  rescue Errno::EPERM
1774
+ Clacky::Logger.warn("[Server] Could not stop existing server (PID=#{pid}) — permission denied.")
1647
1775
  puts "Could not stop existing server (PID: #{pid}) — permission denied."
1648
1776
  ensure
1649
1777
  File.delete(pid_file) if File.exist?(pid_file)
@@ -13,14 +13,24 @@ module Clacky
13
13
  self.tool_description = <<~DESC
14
14
  Browser automation for login-related operations (sign-in, OAuth, form submission requiring session). For simple page fetch or search, prefer web_fetch or web_search instead.
15
15
 
16
- isolated param: true = built-in browser (default, works immediately, login persists). false = user's Chrome (keeps cookies/login, needs one-time debug setup; opens URLs in new tab).
16
+ isolated: true = built-in browser (default, no setup, login persists). false = user's Chrome (keeps cookies/login, needs one-time debug setup; opens URLs in new tab).
17
17
 
18
- WORKFLOW:
19
- - Default: use isolated=true (built-in browser). No setup, works immediately.
20
- - If the user explicitly wants their Chrome (e.g. "use my Chrome"), set isolated=false. We auto-open chrome://inspect/#remote-debugging when needed guide user to enable the toggle. When opening URLs, we use a new tab to avoid replacing the user's current page.
18
+ SNAPSHOT — always run before interacting with a page. Refs (@e1, @e2...) expire after page changes, always re-snapshot before acting on a changed page:
19
+ - 'snapshot -i -C' interactive + cursor-clickable elements (recommended default)
20
+ - 'snapshot -i'interactive elements only (faster, for simple forms)
21
+ - 'snapshot' — full accessibility tree (when above miss elements)
21
22
 
22
- Commands: 'open <url>', 'snapshot -i', 'click @e1', 'fill @e2 "text"', etc.
23
- IMPORTANT: Always use 'snapshot -i' to inspect page state. NEVER use 'screenshot' without explicit user permission — it costs significantly more tokens. If snapshot is insufficient, tell the user and ask if they want a screenshot.
23
+ ELEMENT SELECTION prefer in this order:
24
+ 1. Refs: 'click @e1', 'fill @e2 "text"'
25
+ 2. Semantic find: 'find text "Submit" click', 'find role button "Login" click', 'find label "Email" fill "user@example.com"'
26
+ 3. CSS: 'click "#submit-btn"'
27
+
28
+ OTHER COMMANDS:
29
+ - 'open <url>', 'back', 'reload', 'press Enter', 'key Control+a'
30
+ - 'scroll down/up', 'scrollintoview @e1', 'wait @e1', 'wait --text "..."', 'wait --load networkidle'
31
+ - 'dialog accept/dismiss', 'tab new <url>', 'tab <n>'
32
+
33
+ SCREENSHOT: NEVER call on your own — costs far more tokens than snapshot. Last resort only. Ask user first: "Screenshots cost more tokens. Approve?" When approved: 'screenshot --screenshot-format jpeg --screenshot-quality 50'.
24
34
  DESC
25
35
  self.tool_category = "web"
26
36
  self.tool_parameters = {
@@ -47,11 +57,29 @@ module Clacky
47
57
  CHROME_DEBUG_PORT = 9222
48
58
  BROWSER_COMMAND_TIMEOUT = 30
49
59
  CHROME_DEBUG_PAGE = "chrome://inspect/#remote-debugging"
60
+ MIN_AGENT_BROWSER_VERSION = "0.20.0"
50
61
 
51
62
  def execute(command:, session: nil, isolated: nil, working_dir: nil)
52
- unless agent_browser_installed?
53
- install_result = auto_install_agent_browser
54
- return install_result if install_result[:error]
63
+ # Handle explicit install command
64
+ if command.strip == "install"
65
+ return do_install_agent_browser
66
+ end
67
+
68
+ if !agent_browser_installed?
69
+ return {
70
+ error: "agent-browser not installed",
71
+ message: "agent-browser is required for browser automation but is not installed.",
72
+ instructions: "Tell the user: 'agent-browser is not installed. It's required for browser automation. Run `browser(command: \"install\")` to install it — this may take a minute. Would you like me to install it now?' Wait for user confirmation before calling install."
73
+ }
74
+ end
75
+
76
+ if agent_browser_outdated?
77
+ current = `agent-browser --version 2>/dev/null`.strip.split.last
78
+ return {
79
+ error: "agent-browser version too old",
80
+ message: "agent-browser #{current} is installed but version >= #{MIN_AGENT_BROWSER_VERSION} is required.",
81
+ instructions: "Tell the user: 'agent-browser needs to be upgraded from #{current} to #{MIN_AGENT_BROWSER_VERSION}+. Run `browser(command: \"install\")` to upgrade — this may take a minute. Would you like me to upgrade it now?' Wait for user confirmation before calling install."
82
+ }
55
83
  end
56
84
 
57
85
  # Default to built-in browser (isolated=true). Only use user's Chrome when explicitly isolated=false.
@@ -94,12 +122,6 @@ module Clacky
94
122
  result = Shell.new.execute(command: full_command, hard_timeout: BROWSER_COMMAND_TIMEOUT, working_dir: working_dir)
95
123
  end
96
124
 
97
- if playwright_missing?(result)
98
- pw_result = install_playwright_chromium
99
- return pw_result if pw_result[:error]
100
- result = Shell.new.execute(command: full_command, hard_timeout: BROWSER_COMMAND_TIMEOUT, working_dir: working_dir)
101
- end
102
-
103
125
  if use_auto_connect && !result[:success] && connection_error?(result)
104
126
  if we_launched_chrome
105
127
  result = Shell.new.execute(command: full_command, hard_timeout: BROWSER_COMMAND_TIMEOUT, working_dir: working_dir)
@@ -363,52 +385,23 @@ module Clacky
363
385
  !!find_in_path(AGENT_BROWSER_BIN)
364
386
  end
365
387
 
366
- def auto_install_agent_browser
367
- npm = find_or_install_npm
368
- unless npm
369
- return {
370
- error: "agent-browser not installed",
371
- message: "agent-browser is required for browser automation but is not installed. " \
372
- "Node.js not found; tried to install via mise but failed.\n\n" \
373
- "Please run: mise install node@22 && mise use -g node@22"
374
- }
375
- end
376
-
377
- result = Shell.new.execute(command: "#{npm} install -g agent-browser", hard_timeout: 120)
378
- unless result[:success]
379
- return {
380
- error: "Failed to auto-install agent-browser",
381
- message: "npm install -g agent-browser failed: #{result[:stderr]}\n\nPlease run it manually."
382
- }
383
- end
384
-
385
- {}
386
- end
387
-
388
- def find_or_install_npm
389
- npm = find_in_path("npm")
390
- return npm if npm
391
-
392
- mise = find_mise_bin
393
- return nil unless mise
394
-
395
- path = `#{Shellwords.escape(mise)} which npm 2>/dev/null`.strip
396
- return path if path && !path.empty? && File.executable?(path)
397
- system(mise, "install", "node@22", out: File::NULL, err: File::NULL)
398
- system(mise, "use", "-g", "node@22", out: File::NULL, err: File::NULL)
399
-
400
- path = `#{Shellwords.escape(mise)} which npm 2>/dev/null`.strip
401
- return path if path && !path.empty? && File.executable?(path)
402
-
403
- nil
388
+ def agent_browser_outdated?
389
+ version = `agent-browser --version 2>/dev/null`.strip.split.last
390
+ return false if version.nil? || version.empty?
391
+ Gem::Version.new(version) < Gem::Version.new(MIN_AGENT_BROWSER_VERSION)
392
+ rescue StandardError
393
+ false
404
394
  end
405
395
 
406
- def find_mise_bin
407
- mise = find_in_path("mise")
408
- return mise if mise
409
-
410
- candidate = "#{Dir.home}/.local/bin/mise"
411
- File.executable?(candidate) ? candidate : nil
396
+ def do_install_agent_browser
397
+ script = File.expand_path("../../../../scripts/install_agent_browser.sh", __FILE__)
398
+ result = Shell.new.execute(command: "bash #{Shellwords.escape(script)}", hard_timeout: 180)
399
+ if result[:success]
400
+ version = `agent-browser --version 2>/dev/null`.strip.split.last
401
+ { success: true, message: "agent-browser #{version} installed successfully. You can now use browser commands." }
402
+ else
403
+ { error: "Failed to install agent-browser", message: result[:stdout].to_s.strip }
404
+ end
412
405
  end
413
406
 
414
407
  def command_name_for_temp(command)
@@ -445,25 +438,6 @@ module Clacky
445
438
  { content: first_part.join + notice, temp_file: temp_file }
446
439
  end
447
440
 
448
- def playwright_missing?(result)
449
- output = "#{result[:stdout]}#{result[:stderr]}"
450
- output.include?("Executable doesn't exist") ||
451
- output.include?("Please run the following command to download new browsers")
452
- end
453
-
454
- def install_playwright_chromium
455
- playwright = find_in_path("playwright")
456
- cmd = playwright ? "#{playwright} install chromium" : "npx playwright install chromium"
457
-
458
- result = Shell.new.execute(command: cmd, hard_timeout: 300)
459
- unless result[:success]
460
- return {
461
- error: "Failed to install Playwright Chromium",
462
- message: "Automatic browser installation failed. Please run manually:\n npx playwright install chromium"
463
- }
464
- end
465
- {}
466
- end
467
441
  end
468
442
  end
469
443
  end
@@ -63,11 +63,22 @@ module Clacky
63
63
  ignored: 0
64
64
  }
65
65
 
66
+ # Auto-expand bare patterns (no slash, no **) to recursive search.
67
+ # e.g. "*install*" -> "**/*install*", "*.rb" -> "**/*.rb"
68
+ # This avoids surprising empty results when files are in subdirectories.
69
+ effective_pattern = if !File.absolute_path?(pattern) &&
70
+ !pattern.include?("/") &&
71
+ !pattern.start_with?("**")
72
+ "**/#{pattern}"
73
+ else
74
+ pattern
75
+ end
76
+
66
77
  # Build full pattern - handle absolute paths correctly
67
- full_pattern = if File.absolute_path?(pattern)
68
- pattern
78
+ full_pattern = if File.absolute_path?(effective_pattern)
79
+ effective_pattern
69
80
  else
70
- File.join(base_path, pattern)
81
+ File.join(base_path, effective_pattern)
71
82
  end
72
83
  # Always-ignored directory names that should never appear in results
73
84
  always_ignored_dirs = Clacky::Utils::FileIgnoreHelper::ALWAYS_IGNORED_DIRS
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "0.8.9"
4
+ VERSION = "0.9.1"
5
5
  end