openclacky 0.9.16 → 0.9.18
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 +20 -0
- data/lib/clacky/agent/cost_tracker.rb +0 -1
- data/lib/clacky/agent/hook_manager.rb +0 -1
- data/lib/clacky/agent/message_compressor.rb +0 -1
- data/lib/clacky/agent/message_compressor_helper.rb +0 -1
- data/lib/clacky/agent/session_serializer.rb +0 -1
- data/lib/clacky/agent/skill_manager.rb +0 -1
- data/lib/clacky/brand_config.rb +0 -1
- data/lib/clacky/client.rb +0 -1
- data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +0 -2
- data/lib/clacky/plain_ui_controller.rb +0 -1
- data/lib/clacky/providers.rb +10 -5
- data/lib/clacky/server/browser_manager.rb +0 -1
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +0 -1
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +0 -1
- data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +0 -1
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +27 -6
- data/lib/clacky/server/channel/adapters/wecom/adapter.rb +0 -1
- data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +31 -6
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +0 -1
- data/lib/clacky/server/channel/adapters/weixin/api_client.rb +0 -1
- data/lib/clacky/server/channel/channel_manager.rb +0 -1
- data/lib/clacky/server/channel/channel_ui_controller.rb +0 -1
- data/lib/clacky/server/http_server.rb +2 -2
- data/lib/clacky/server/server_master.rb +0 -1
- data/lib/clacky/server/session_registry.rb +0 -1
- data/lib/clacky/server/web_ui_controller.rb +0 -1
- data/lib/clacky/session_manager.rb +0 -1
- data/lib/clacky/skill.rb +0 -1
- data/lib/clacky/skill_loader.rb +4 -1
- data/lib/clacky/tools/browser.rb +44 -6
- data/lib/clacky/tools/file_reader.rb +0 -1
- data/lib/clacky/tools/grep.rb +0 -1
- data/lib/clacky/tools/run_project.rb +0 -1
- data/lib/clacky/tools/todo_manager.rb +0 -1
- data/lib/clacky/ui2/components/command_suggestions.rb +0 -1
- data/lib/clacky/ui2/components/inline_input.rb +0 -1
- data/lib/clacky/ui2/components/input_area.rb +0 -1
- data/lib/clacky/ui2/components/message_component.rb +0 -1
- data/lib/clacky/ui2/components/modal_component.rb +0 -1
- data/lib/clacky/ui2/components/todo_area.rb +0 -1
- data/lib/clacky/ui2/components/tool_component.rb +0 -1
- data/lib/clacky/ui2/components/welcome_banner.rb +0 -1
- data/lib/clacky/ui2/markdown_renderer.rb +0 -1
- data/lib/clacky/ui2/progress_indicator.rb +0 -1
- data/lib/clacky/ui2/screen_buffer.rb +0 -1
- data/lib/clacky/ui2/theme_manager.rb +0 -1
- data/lib/clacky/ui2/themes/base_theme.rb +0 -1
- data/lib/clacky/ui2/ui_controller.rb +0 -1
- data/lib/clacky/utils/arguments_parser.rb +0 -1
- data/lib/clacky/utils/gitignore_parser.rb +0 -1
- data/lib/clacky/utils/limit_stack.rb +0 -1
- data/lib/clacky/utils/model_pricing.rb +0 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/i18n.js +4 -0
- data/lib/clacky/web/index.html +4 -1
- data/lib/clacky/web/onboard.js +11 -0
- data/lib/clacky/web/settings.js +14 -1
- data/scripts/install.ps1 +81 -34
- data/scripts/install.sh +215 -494
- data/scripts/install_full.sh +891 -0
- data/scripts/install_simple.sh +3 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 538f6014a8386fcddf69dbf69efd162a00ad594d776b812eba5ac04f98953995
|
|
4
|
+
data.tar.gz: 4d9952dbbf2e20d1a598b136a0f01e821a1e53391240c197afe60e3570d4bfce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ed52fb94f805bc83c8ab996c9a1f01635665b054750900623002edc29a0197c9ab99be08d192e2f1f1040c8665451e155d9878fec5a71c213d7c3130fbbb956
|
|
7
|
+
data.tar.gz: 248569c522a59c37c3bda8342141be750609ca4fca996a8d563fcb953e6dcb1446fbc379082691b5503679d26d9168aa8e2503f7b169b3b4daeafa9428b7cf5b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.18] - 2026-03-28
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Brand skill config now reloads from disk on every `load_all`**: brand skills installed or activated after the initial startup were previously invisible until restart — the skill loader now refreshes `BrandConfig` each time it loads skills, so newly installed brand skills take effect immediately
|
|
14
|
+
|
|
15
|
+
### More
|
|
16
|
+
- Remove `private` keyword from all internal classes to improve Ruby 2.6 compatibility
|
|
17
|
+
- Rename `install.sh` → `install_full.sh`; promote `install_simple.sh` → `install.sh` as the default entry point
|
|
18
|
+
|
|
19
|
+
## [0.9.17] - 2026-03-27
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **Browser screenshots now saved to disk**: every screenshot action automatically saves both the original full-resolution PNG and the compressed (800px) version to disk — the agent reports both file paths so you can reference, open, or pass the screenshots to other tools
|
|
23
|
+
- **Provider "Get API Key" links in onboarding**: the setup wizard now shows a direct link to the provider's website when you select a provider that has a `website_url` — making it easier to sign up and get your API key without leaving the flow
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **WebSocket auto-reconnect for Feishu and WeCom channels**: the WebSocket clients for Feishu and WeCom now automatically retry the connection after failures — channels stay online without manual intervention after a network hiccup
|
|
27
|
+
- **Brand command in simple install script**: the `clacky` brand command was incorrectly invoked in `install_simple.sh` — now fixed so the post-install branding step runs correctly
|
|
28
|
+
- **Windows WSL2 and Hyper-V detection in PowerShell installer**: improved detection logic for WSL2 and Hyper-V environments in `install.ps1`, reducing false negatives on Windows machines with non-standard configurations
|
|
29
|
+
|
|
10
30
|
## [0.9.16] - 2026-03-27
|
|
11
31
|
|
|
12
32
|
### Fixed
|
data/lib/clacky/brand_config.rb
CHANGED
data/lib/clacky/client.rb
CHANGED
|
@@ -104,7 +104,6 @@ class ToolClient
|
|
|
104
104
|
raise "ToolClient connection failed: #{e.message}"
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
private
|
|
108
107
|
|
|
109
108
|
def http
|
|
110
109
|
return @http if @http
|
|
@@ -286,7 +285,6 @@ class FeishuApiClient
|
|
|
286
285
|
get_json("#{FEISHU_API_BASE}/app/#{app_id}")
|
|
287
286
|
end
|
|
288
287
|
|
|
289
|
-
private
|
|
290
288
|
|
|
291
289
|
# Execute a GET fetch in the browser page context.
|
|
292
290
|
# Uses window.csrfToken — required by all /developers/v1/ endpoints.
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -17,7 +17,8 @@ module Clacky
|
|
|
17
17
|
"base_url" => "https://openrouter.ai/api/v1",
|
|
18
18
|
"api" => "openai-responses",
|
|
19
19
|
"default_model" => "anthropic/claude-sonnet-4-6",
|
|
20
|
-
"models" => [] # Dynamic - fetched from API
|
|
20
|
+
"models" => [], # Dynamic - fetched from API
|
|
21
|
+
"website_url" => "https://openrouter.ai/keys"
|
|
21
22
|
}.freeze,
|
|
22
23
|
|
|
23
24
|
"minimax" => {
|
|
@@ -25,7 +26,8 @@ module Clacky
|
|
|
25
26
|
"base_url" => "https://api.minimaxi.com/v1",
|
|
26
27
|
"api" => "openai-completions",
|
|
27
28
|
"default_model" => "MiniMax-M2.7",
|
|
28
|
-
"models" => ["MiniMax-M2.5", "MiniMax-M2.7"]
|
|
29
|
+
"models" => ["MiniMax-M2.5", "MiniMax-M2.7"],
|
|
30
|
+
"website_url" => "https://www.minimaxi.com/user-center/basic-information/interface-key"
|
|
29
31
|
}.freeze,
|
|
30
32
|
|
|
31
33
|
"kimi" => {
|
|
@@ -33,7 +35,8 @@ module Clacky
|
|
|
33
35
|
"base_url" => "https://api.moonshot.cn/v1",
|
|
34
36
|
"api" => "openai-completions",
|
|
35
37
|
"default_model" => "kimi-k2.5",
|
|
36
|
-
"models" => ["kimi-k2.5"]
|
|
38
|
+
"models" => ["kimi-k2.5"],
|
|
39
|
+
"website_url" => "https://platform.moonshot.cn/console/api-keys"
|
|
37
40
|
}.freeze,
|
|
38
41
|
|
|
39
42
|
"anthropic" => {
|
|
@@ -41,7 +44,8 @@ module Clacky
|
|
|
41
44
|
"base_url" => "https://api.anthropic.com",
|
|
42
45
|
"api" => "anthropic-messages",
|
|
43
46
|
"default_model" => "claude-sonnet-4.6",
|
|
44
|
-
"models" => ["claude-opus-4-6", "claude-sonnet-4.6", "claude-haiku-4.5"]
|
|
47
|
+
"models" => ["claude-opus-4-6", "claude-sonnet-4.6", "claude-haiku-4.5"],
|
|
48
|
+
"website_url" => "https://console.anthropic.com/settings/keys"
|
|
45
49
|
}.freeze,
|
|
46
50
|
|
|
47
51
|
"bedrock-jp" => {
|
|
@@ -52,7 +56,8 @@ module Clacky
|
|
|
52
56
|
"models" => [
|
|
53
57
|
"jp.anthropic.claude-sonnet-4-6",
|
|
54
58
|
"jp.anthropic.claude-haiku-4-6"
|
|
55
|
-
]
|
|
59
|
+
],
|
|
60
|
+
"website_url" => "https://console.aws.amazon.com/iam/home#/security_credentials"
|
|
56
61
|
}.freeze
|
|
57
62
|
|
|
58
63
|
}.freeze
|
|
@@ -190,7 +190,6 @@ module Clacky
|
|
|
190
190
|
# ---------------------------------------------------------------------------
|
|
191
191
|
# Private
|
|
192
192
|
# ---------------------------------------------------------------------------
|
|
193
|
-
private
|
|
194
193
|
|
|
195
194
|
def load_config
|
|
196
195
|
return {} unless File.exist?(BROWSER_CONFIG_PATH)
|
|
@@ -51,7 +51,11 @@ module Clacky
|
|
|
51
51
|
@ws_socket&.close rescue nil
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
|
|
55
|
+
# Timeout for IO.select on the read loop. Feishu server sends pings every
|
|
56
|
+
# @ping_interval seconds (default 90s). Allow two missed pings before
|
|
57
|
+
# treating the connection as dead.
|
|
58
|
+
READ_TIMEOUT_MULTIPLIER = 2.5
|
|
55
59
|
|
|
56
60
|
def connect_and_listen
|
|
57
61
|
Clacky::Logger.info("[feishu-ws] Fetching WebSocket endpoint...")
|
|
@@ -92,9 +96,22 @@ module Clacky
|
|
|
92
96
|
|
|
93
97
|
start_ping_thread
|
|
94
98
|
|
|
99
|
+
# read_timeout is based on the server-provided ping interval so it
|
|
100
|
+
# automatically adapts if Feishu changes the cadence.
|
|
101
|
+
read_timeout = (@ping_interval * READ_TIMEOUT_MULTIPLIER).ceil
|
|
102
|
+
|
|
95
103
|
loop do
|
|
96
104
|
break unless @running
|
|
97
|
-
|
|
105
|
+
|
|
106
|
+
# Use IO.select with a timeout to detect silent connection drops
|
|
107
|
+
# (NAT expiry, firewall idle-kill) that never send a TCP FIN/RST.
|
|
108
|
+
ready = IO.select([socket], nil, nil, read_timeout)
|
|
109
|
+
unless ready
|
|
110
|
+
Clacky::Logger.warn("[feishu-ws] read timeout (#{read_timeout}s), reconnecting...")
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
data = socket.read_nonblock(4096)
|
|
98
115
|
@incoming << data
|
|
99
116
|
while (frame = @incoming.next)
|
|
100
117
|
case frame.type
|
|
@@ -106,13 +123,14 @@ module Clacky
|
|
|
106
123
|
when :ping
|
|
107
124
|
send_raw_frame(:pong, frame.data)
|
|
108
125
|
when :close
|
|
109
|
-
Clacky::Logger.info("[feishu-ws] WebSocket closed, will reconnect")
|
|
126
|
+
Clacky::Logger.info("[feishu-ws] WebSocket closed by server, will reconnect")
|
|
110
127
|
return
|
|
111
128
|
end
|
|
112
129
|
end
|
|
113
130
|
end
|
|
114
|
-
rescue EOFError, Errno::ECONNRESET
|
|
115
|
-
|
|
131
|
+
rescue EOFError, IOError, Errno::ECONNRESET, Errno::EPIPE,
|
|
132
|
+
Errno::ETIMEDOUT, OpenSSL::SSL::SSLError => e
|
|
133
|
+
Clacky::Logger.warn("[feishu-ws] Connection lost (#{e.class}: #{e.message}), reconnecting in #{RECONNECT_DELAY}s...")
|
|
116
134
|
ensure
|
|
117
135
|
@ws_open = false
|
|
118
136
|
@ws_socket = nil
|
|
@@ -254,7 +272,10 @@ module Clacky
|
|
|
254
272
|
headers: { "type" => "ping" }
|
|
255
273
|
)
|
|
256
274
|
rescue => e
|
|
257
|
-
warn
|
|
275
|
+
Clacky::Logger.warn("[feishu-ws] ping failed (#{e.class}: #{e.message}), forcing reconnect")
|
|
276
|
+
# Close the socket so IO.select in the read loop immediately
|
|
277
|
+
# returns nil / read_nonblock raises IOError, triggering reconnect.
|
|
278
|
+
@ws_socket&.close rescue nil
|
|
258
279
|
break
|
|
259
280
|
end
|
|
260
281
|
end
|
|
@@ -115,7 +115,13 @@ module Clacky
|
|
|
115
115
|
raise
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
|
|
119
|
+
# Timeout for IO.select on the read loop. If no data arrives within this
|
|
120
|
+
# window we treat the connection as dead and reconnect. This catches the
|
|
121
|
+
# silent-drop case where the TCP stack never delivers a FIN/RST (e.g.
|
|
122
|
+
# NAT timeout, firewall idle-kill). The WeCom server sends pings every
|
|
123
|
+
# ~30 s, so 75 s gives two missed pings before we give up.
|
|
124
|
+
READ_TIMEOUT_S = 75
|
|
119
125
|
|
|
120
126
|
def connect_and_listen
|
|
121
127
|
uri = URI.parse(@ws_url)
|
|
@@ -151,7 +157,17 @@ module Clacky
|
|
|
151
157
|
|
|
152
158
|
loop do
|
|
153
159
|
break unless @running
|
|
154
|
-
|
|
160
|
+
|
|
161
|
+
# Use IO.select with a timeout so we detect silent connection drops
|
|
162
|
+
# (e.g. NAT expiry) that never deliver a TCP FIN/RST. Without this,
|
|
163
|
+
# readpartial blocks forever and the thread hangs permanently.
|
|
164
|
+
ready = IO.select([ssl], nil, nil, READ_TIMEOUT_S)
|
|
165
|
+
unless ready
|
|
166
|
+
Clacky::Logger.warn("[WecomWSClient] read timeout (#{READ_TIMEOUT_S}s), reconnecting...")
|
|
167
|
+
return
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
data = ssl.read_nonblock(4096)
|
|
155
171
|
@incoming << data
|
|
156
172
|
while (frame = @incoming.next)
|
|
157
173
|
case frame.type
|
|
@@ -160,13 +176,14 @@ module Clacky
|
|
|
160
176
|
when :ping
|
|
161
177
|
send_raw_frame(:pong, frame.data)
|
|
162
178
|
when :close
|
|
163
|
-
Clacky::Logger.info("[WecomWSClient] connection closed")
|
|
179
|
+
Clacky::Logger.info("[WecomWSClient] connection closed by server")
|
|
164
180
|
return
|
|
165
181
|
end
|
|
166
182
|
end
|
|
167
183
|
end
|
|
168
|
-
rescue EOFError, Errno::ECONNRESET
|
|
169
|
-
|
|
184
|
+
rescue EOFError, IOError, Errno::ECONNRESET, Errno::EPIPE,
|
|
185
|
+
Errno::ETIMEDOUT, OpenSSL::SSL::SSLError => e
|
|
186
|
+
Clacky::Logger.info("[WecomWSClient] connection lost (#{e.class}: #{e.message}), reconnecting...")
|
|
170
187
|
ensure
|
|
171
188
|
@ws_open = false
|
|
172
189
|
@ws_socket = nil
|
|
@@ -257,7 +274,15 @@ module Clacky
|
|
|
257
274
|
loop do
|
|
258
275
|
sleep HEARTBEAT_INTERVAL
|
|
259
276
|
break unless @running
|
|
260
|
-
|
|
277
|
+
begin
|
|
278
|
+
send_frame(cmd: "ping", req_id: generate_req_id("ping"))
|
|
279
|
+
rescue => e
|
|
280
|
+
Clacky::Logger.warn("[WecomWSClient] ping failed (#{e.class}: #{e.message}), forcing reconnect")
|
|
281
|
+
# Close the socket so IO.select in the read loop immediately
|
|
282
|
+
# returns nil / read_nonblock raises IOError, triggering reconnect.
|
|
283
|
+
@ws_socket&.close rescue nil
|
|
284
|
+
break
|
|
285
|
+
end
|
|
261
286
|
end
|
|
262
287
|
end
|
|
263
288
|
end
|
|
@@ -299,7 +299,6 @@ module Clacky
|
|
|
299
299
|
server.start
|
|
300
300
|
end
|
|
301
301
|
|
|
302
|
-
private
|
|
303
302
|
|
|
304
303
|
# ── Router ────────────────────────────────────────────────────────────────
|
|
305
304
|
|
|
@@ -1528,7 +1527,8 @@ module Clacky
|
|
|
1528
1527
|
name: preset["name"],
|
|
1529
1528
|
base_url: preset["base_url"],
|
|
1530
1529
|
default_model: preset["default_model"],
|
|
1531
|
-
models: preset["models"] || []
|
|
1530
|
+
models: preset["models"] || [],
|
|
1531
|
+
website_url: preset["website_url"]
|
|
1532
1532
|
}
|
|
1533
1533
|
end
|
|
1534
1534
|
json_response(res, 200, { providers: providers })
|
data/lib/clacky/skill.rb
CHANGED
data/lib/clacky/skill_loader.rb
CHANGED
|
@@ -42,6 +42,10 @@ module Clacky
|
|
|
42
42
|
# Clears previously loaded skills before loading to ensure idempotency
|
|
43
43
|
# @return [Array<Skill>] Loaded skills
|
|
44
44
|
def load_all
|
|
45
|
+
# Always refresh brand_config from disk so newly installed/activated brand
|
|
46
|
+
# skills are visible even if this SkillLoader was created before the change.
|
|
47
|
+
@brand_config = Clacky::BrandConfig.load
|
|
48
|
+
|
|
45
49
|
# Clear existing skills to ensure idempotent reloading
|
|
46
50
|
clear
|
|
47
51
|
|
|
@@ -297,7 +301,6 @@ module Clacky
|
|
|
297
301
|
true
|
|
298
302
|
end
|
|
299
303
|
|
|
300
|
-
private
|
|
301
304
|
|
|
302
305
|
def load_skills_from_directory(dir, source_type)
|
|
303
306
|
return [] unless dir.exist?
|
data/lib/clacky/tools/browser.rb
CHANGED
|
@@ -6,6 +6,9 @@ require "timeout"
|
|
|
6
6
|
require "tmpdir"
|
|
7
7
|
require "shellwords"
|
|
8
8
|
require "yaml"
|
|
9
|
+
require "base64"
|
|
10
|
+
require "fileutils"
|
|
11
|
+
require "securerandom"
|
|
9
12
|
require_relative "base"
|
|
10
13
|
|
|
11
14
|
module Clacky
|
|
@@ -159,11 +162,20 @@ module Clacky
|
|
|
159
162
|
action = result[:action].to_s
|
|
160
163
|
|
|
161
164
|
if action == "screenshot" && result[:image_data]
|
|
162
|
-
mime_type
|
|
163
|
-
image_data
|
|
164
|
-
data_url
|
|
165
|
+
mime_type = result[:mime_type] || "image/png"
|
|
166
|
+
image_data = result[:image_data]
|
|
167
|
+
data_url = "data:#{mime_type};base64,#{image_data}"
|
|
168
|
+
original_path = result[:original_path]
|
|
169
|
+
compressed_path = result[:compressed_path]
|
|
170
|
+
|
|
171
|
+
text = "Screenshot captured."
|
|
172
|
+
if original_path || compressed_path
|
|
173
|
+
text += "\n- Original (full resolution): #{original_path || 'unavailable'}" \
|
|
174
|
+
"\n- Compressed (800px, sent to AI): #{compressed_path || 'unavailable'}"
|
|
175
|
+
end
|
|
176
|
+
|
|
165
177
|
return [
|
|
166
|
-
{ type: "text", text:
|
|
178
|
+
{ type: "text", text: text },
|
|
167
179
|
{ type: "image_url", image_url: { url: data_url } }
|
|
168
180
|
]
|
|
169
181
|
end
|
|
@@ -180,7 +192,6 @@ module Clacky
|
|
|
180
192
|
}.compact
|
|
181
193
|
end
|
|
182
194
|
|
|
183
|
-
private
|
|
184
195
|
|
|
185
196
|
BROWSER_CONFIG_PATH = File.expand_path("~/.clacky/browser.yml").freeze
|
|
186
197
|
|
|
@@ -416,6 +427,9 @@ module Clacky
|
|
|
416
427
|
output: text.empty? ? "Screenshot captured." : text }
|
|
417
428
|
end
|
|
418
429
|
|
|
430
|
+
# Save original (full-resolution) PNG to disk before any downscaling
|
|
431
|
+
original_path = save_screenshot_to_disk(image_block["data"], suffix: "original")
|
|
432
|
+
|
|
419
433
|
image_data = png_downscale_base64(image_block["data"], SCREENSHOT_MAX_WIDTH)
|
|
420
434
|
|
|
421
435
|
if image_data.bytesize > SCREENSHOT_MAX_BASE64_BYTES
|
|
@@ -424,8 +438,13 @@ module Clacky
|
|
|
424
438
|
output: "Screenshot too large after resize (#{size_kb}KB). Use action=snapshot instead." }
|
|
425
439
|
end
|
|
426
440
|
|
|
441
|
+
# Save compressed (800px) PNG for AI reference
|
|
442
|
+
compressed_path = save_screenshot_to_disk(image_data, suffix: "compressed")
|
|
443
|
+
|
|
427
444
|
{ action: "screenshot", success: true, profile: "user",
|
|
428
|
-
image_data: image_data, mime_type: "image/png",
|
|
445
|
+
image_data: image_data, mime_type: "image/png",
|
|
446
|
+
original_path: original_path, compressed_path: compressed_path,
|
|
447
|
+
output: "Screenshot captured." }
|
|
429
448
|
end
|
|
430
449
|
|
|
431
450
|
private def png_downscale_base64(b64, max_width)
|
|
@@ -443,6 +462,25 @@ module Clacky
|
|
|
443
462
|
result
|
|
444
463
|
end
|
|
445
464
|
|
|
465
|
+
# Save a base64-encoded PNG screenshot to disk and return the file path.
|
|
466
|
+
# suffix: "original" or "compressed" — embedded in filename for clarity.
|
|
467
|
+
# Uses the same upload directory as other image files so the agent can
|
|
468
|
+
# reference, read, or pass the path to other tools.
|
|
469
|
+
private def save_screenshot_to_disk(base64_data, suffix: nil)
|
|
470
|
+
upload_dir = File.join(Dir.tmpdir, "clacky-uploads")
|
|
471
|
+
FileUtils.mkdir_p(upload_dir)
|
|
472
|
+
ts = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
473
|
+
hex = SecureRandom.hex(4)
|
|
474
|
+
label = suffix ? "_#{suffix}" : ""
|
|
475
|
+
filename = "screenshot_#{ts}_#{hex}#{label}.png"
|
|
476
|
+
path = File.join(upload_dir, filename)
|
|
477
|
+
File.binwrite(path, Base64.strict_decode64(base64_data))
|
|
478
|
+
path
|
|
479
|
+
rescue => e
|
|
480
|
+
Clacky::Logger.error("screenshot_save_failed", error: e.message)
|
|
481
|
+
nil
|
|
482
|
+
end
|
|
483
|
+
|
|
446
484
|
# -----------------------------------------------------------------------
|
|
447
485
|
# Chrome MCP
|
|
448
486
|
# -----------------------------------------------------------------------
|
data/lib/clacky/tools/grep.rb
CHANGED