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 +4 -4
- data/CHANGELOG.md +16 -1
- data/lib/clacky/agent.rb +33 -0
- data/lib/clacky/client.rb +8 -1
- data/lib/clacky/server/http_server.rb +131 -3
- data/lib/clacky/tools/browser.rb +52 -78
- data/lib/clacky/tools/glob.rb +14 -3
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +313 -0
- data/lib/clacky/web/i18n.js +32 -0
- data/lib/clacky/web/index.html +11 -0
- data/lib/clacky/web/version.js +362 -0
- data/scripts/install.sh +3 -34
- data/scripts/install_agent_browser.sh +67 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a13cced61727911f2587013fa7f701863f8644ed62caaf235217be4dc306574
|
|
4
|
+
data.tar.gz: 9f9f1d42f482bf159099d2a49c8435f8bff176dc0126321cd980079297ff7078
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
data/lib/clacky/tools/browser.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
return
|
|
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
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
data/lib/clacky/tools/glob.rb
CHANGED
|
@@ -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?(
|
|
68
|
-
|
|
78
|
+
full_pattern = if File.absolute_path?(effective_pattern)
|
|
79
|
+
effective_pattern
|
|
69
80
|
else
|
|
70
|
-
File.join(base_path,
|
|
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
|
data/lib/clacky/version.rb
CHANGED