openclacky 1.2.4 → 1.2.6
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 +32 -0
- data/lib/clacky/agent/cost_tracker.rb +17 -9
- data/lib/clacky/agent/llm_caller.rb +25 -3
- data/lib/clacky/agent/tool_executor.rb +14 -0
- data/lib/clacky/agent/tool_registry.rb +0 -7
- data/lib/clacky/agent.rb +0 -8
- data/lib/clacky/billing/billing_store.rb +62 -4
- data/lib/clacky/brand_config.rb +11 -0
- data/lib/clacky/cli.rb +74 -23
- data/lib/clacky/client.rb +36 -2
- data/lib/clacky/default_parsers/wps_parser.rb +82 -0
- data/lib/clacky/default_skills/browser-setup/SKILL.md +16 -90
- data/lib/clacky/message_format/anthropic.rb +13 -3
- data/lib/clacky/message_format/bedrock.rb +2 -2
- data/lib/clacky/platform_http_client.rb +28 -1
- data/lib/clacky/providers.rb +0 -27
- data/lib/clacky/server/http_server.rb +24 -4
- data/lib/clacky/tools/browser.rb +4 -13
- data/lib/clacky/tools/terminal.rb +18 -7
- data/lib/clacky/utils/file_processor.rb +3 -0
- data/lib/clacky/utils/parser_manager.rb +3 -0
- data/lib/clacky/utils/scripts_manager.rb +0 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +422 -74
- data/lib/clacky/web/billing.js +371 -99
- data/lib/clacky/web/i18n.js +24 -2
- data/lib/clacky/web/index.html +1 -1
- data/lib/clacky/web/sessions.js +10 -68
- data/lib/clacky.rb +0 -3
- metadata +3 -6
- data/lib/clacky/tools/list_tasks.rb +0 -54
- data/lib/clacky/tools/redo_task.rb +0 -41
- data/lib/clacky/tools/undo_task.rb +0 -35
- data/scripts/wsl_network_doctor.ps1 +0 -196
|
@@ -111,96 +111,6 @@ chrome-devtools-mcp --version 2>/dev/null
|
|
|
111
111
|
|
|
112
112
|
If still missing after user confirms, stop with error message.
|
|
113
113
|
|
|
114
|
-
### Step 2.5 — WSL networking setup (only when session context shows `OS: WSL/Windows`)
|
|
115
|
-
|
|
116
|
-
**Skip this entire step on macOS / Linux.** Look at the session context line that begins with `[Session context: ...]` — only run this step if it includes `OS: WSL/Windows`.
|
|
117
|
-
|
|
118
|
-
#### Background (read this so you know what to do)
|
|
119
|
-
|
|
120
|
-
The browser tool runs inside WSL but Chrome/Edge runs on Windows. By default WSL2 uses NAT networking, which means `127.0.0.1` inside WSL **cannot** reach Windows' Chrome debug port. The fix is to enable WSL2 **mirrored networking** (`networkingMode=mirrored` in `%USERPROFILE%\.wslconfig`), which makes WSL share Windows' network stack so `127.0.0.1` works directly.
|
|
121
|
-
|
|
122
|
-
We have a helper script that handles all the Windows-side details:
|
|
123
|
-
|
|
124
|
-
```
|
|
125
|
-
~/.clacky/scripts/wsl_network_doctor.ps1
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
It exposes three subcommands:
|
|
129
|
-
|
|
130
|
-
| Subcommand | What it does | Exit code |
|
|
131
|
-
|---|---|---|
|
|
132
|
-
| `status` | Check whether mirrored is configured (auto-passes on WSL1) | `0` OK / `10` NEED_ENABLE |
|
|
133
|
-
| `enable` | Write `networkingMode=mirrored` to `.wslconfig` (does NOT shut down WSL) | `0` success / `1` fail |
|
|
134
|
-
| `repair` | Restart Windows Host Network Service (HNS) via UAC prompt | `0` launched / `1` fail |
|
|
135
|
-
|
|
136
|
-
Invoke it from WSL like this:
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(wslpath -w ~/.clacky/scripts/wsl_network_doctor.ps1)" <subcommand>
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
#### Step 2.5.1 — Check status
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(wslpath -w ~/.clacky/scripts/wsl_network_doctor.ps1)" status
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
- Exit `0` (output starts with `OK:`) → either mirrored is configured (WSL2) or
|
|
149
|
-
Ubuntu is running on WSL1 (which shares the Windows network stack and needs no
|
|
150
|
-
config). Either way, proceed to Step 3.
|
|
151
|
-
- Exit `10` (output starts with `NEED_ENABLE:`) → continue to Step 2.5.2.
|
|
152
|
-
- Any other failure → show the output to the user and ask them to retry. Stop here.
|
|
153
|
-
|
|
154
|
-
#### Step 2.5.2 — Enable mirrored (only when NEED_ENABLE)
|
|
155
|
-
|
|
156
|
-
Tell the user what's about to happen (in their language):
|
|
157
|
-
|
|
158
|
-
> WSL doesn't have mirrored networking enabled yet — the browser tool needs it to reach Chrome on Windows.
|
|
159
|
-
> I'll add one line to `%USERPROFILE%\.wslconfig`. Your current WSL session will NOT be restarted.
|
|
160
|
-
|
|
161
|
-
Run:
|
|
162
|
-
|
|
163
|
-
```bash
|
|
164
|
-
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(wslpath -w ~/.clacky/scripts/wsl_network_doctor.ps1)" enable
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
If the script exits `0`:
|
|
168
|
-
|
|
169
|
-
> ✅ `.wslconfig` updated. Tell the user (in their language):
|
|
170
|
-
>
|
|
171
|
-
> The config takes effect only after WSL restarts, but we can't restart WSL from inside WSL.
|
|
172
|
-
> Please:
|
|
173
|
-
>
|
|
174
|
-
> 1. Open **PowerShell** on Windows
|
|
175
|
-
> 2. Run: `wsl --shutdown`
|
|
176
|
-
> 3. Reopen the Clacky terminal
|
|
177
|
-
> 4. Run `/browser-setup` again
|
|
178
|
-
>
|
|
179
|
-
> Stop here. Wait for the user to come back in a new session.
|
|
180
|
-
|
|
181
|
-
If the script exits non-zero, show the output to the user and stop. Do NOT proceed to Step 3 — without mirrored networking the browser tool will not work.
|
|
182
|
-
|
|
183
|
-
#### Step 2.5.3 — When to run repair
|
|
184
|
-
|
|
185
|
-
Do NOT run `repair` proactively. Only run it later if **all** of the following are true:
|
|
186
|
-
|
|
187
|
-
- `status` returned `OK` (mirrored is configured)
|
|
188
|
-
- The user has restarted WSL since the config was written
|
|
189
|
-
- Step 3's `browser(action="status")` still fails with a "Chrome/Edge is not running or remote debugging is not enabled" error
|
|
190
|
-
|
|
191
|
-
In that situation, tell the user (in their language):
|
|
192
|
-
|
|
193
|
-
> The config looks correct but the browser still can't connect. Windows Host Network Service may be stuck — I'll restart it.
|
|
194
|
-
> **A Windows User Account Control (UAC) prompt will appear shortly. Please click "Yes".**
|
|
195
|
-
|
|
196
|
-
Then run:
|
|
197
|
-
|
|
198
|
-
```bash
|
|
199
|
-
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(wslpath -w ~/.clacky/scripts/wsl_network_doctor.ps1)" repair
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
After it returns, tell the user to run `wsl --shutdown` in PowerShell and reopen Clacky. Stop and wait.
|
|
203
|
-
|
|
204
114
|
### Step 3 — Verify Chrome/Edge is running with remote debugging
|
|
205
115
|
|
|
206
116
|
**CRITICAL**: Do NOT attempt `browser()` calls yet. First check if the browser is reachable using the API:
|
|
@@ -278,6 +188,12 @@ If still failing:
|
|
|
278
188
|
>
|
|
279
189
|
> Run `/browser-setup doctor` to diagnose the issue in detail.
|
|
280
190
|
|
|
191
|
+
Fetch the online troubleshooting guide for additional help:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
web_fetch(url="https://www.openclacky.com/docs/browser-troubleshooting")
|
|
195
|
+
```
|
|
196
|
+
|
|
281
197
|
Stop here and suggest running doctor.
|
|
282
198
|
|
|
283
199
|
#### Case B: Other errors (MCP handshake timeout, daemon crash, etc.)
|
|
@@ -514,3 +430,13 @@ After fixing these issues, run `/browser-setup` again to verify.
|
|
|
514
430
|
|
|
515
431
|
The browser tool is ready to use.
|
|
516
432
|
```
|
|
433
|
+
|
|
434
|
+
### Online Troubleshooting Guide
|
|
435
|
+
|
|
436
|
+
If any ❌ items are found, or the user reports a problem you cannot resolve with the above scenarios, fetch the latest troubleshooting guide:
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
web_fetch(url="https://www.openclacky.com/docs/browser-troubleshooting")
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Use the content from that page to provide up-to-date diagnosis and resolution steps. This online document is maintained separately and may contain newer solutions not listed here.
|
|
@@ -39,6 +39,17 @@ module Clacky
|
|
|
39
39
|
msg[:content].select { |b| b[:type] == "tool_result" }.map { |b| b[:tool_use_id] }
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Anthropic requires tool_use.id to match ^[a-zA-Z0-9_-]+$ (max 128 chars).
|
|
43
|
+
# Some OpenAI-compatible upstreams (e.g. kimi-k2.6) return ids like "tool_name:0"
|
|
44
|
+
# — fine for OpenAI, rejected by Anthropic. We replace illegal chars with "_"
|
|
45
|
+
# at the format boundary so ids stay self-consistent across use/result pairs
|
|
46
|
+
# (pure function → same input maps to same output in both directions).
|
|
47
|
+
def sanitize_tool_use_id(id)
|
|
48
|
+
s = id.to_s
|
|
49
|
+
s = s.gsub(/[^a-zA-Z0-9_-]/, "_")
|
|
50
|
+
s.length > 128 ? s[0, 128] : s
|
|
51
|
+
end
|
|
52
|
+
|
|
42
53
|
# ── Request building ──────────────────────────────────────────────────────
|
|
43
54
|
|
|
44
55
|
# Convert canonical @messages + tools into an Anthropic API request body.
|
|
@@ -156,7 +167,6 @@ module Clacky
|
|
|
156
167
|
end
|
|
157
168
|
|
|
158
169
|
# ── Tool result formatting ────────────────────────────────────────────────
|
|
159
|
-
|
|
160
170
|
# Format tool results into canonical messages to append to @messages.
|
|
161
171
|
# Input: response (canonical, has :tool_calls), tool_results array
|
|
162
172
|
# Output: canonical messages: [{ role: "tool", tool_call_id:, content: }]
|
|
@@ -211,7 +221,7 @@ module Clacky
|
|
|
211
221
|
else
|
|
212
222
|
raw_args
|
|
213
223
|
end
|
|
214
|
-
blocks << { type: "tool_use", id: tc[:id], name: name, input: input || {} }
|
|
224
|
+
blocks << { type: "tool_use", id: sanitize_tool_use_id(tc[:id]), name: name, input: input || {} }
|
|
215
225
|
end
|
|
216
226
|
|
|
217
227
|
return { role: "assistant", content: blocks }
|
|
@@ -250,7 +260,7 @@ module Clacky
|
|
|
250
260
|
else
|
|
251
261
|
raw_content
|
|
252
262
|
end
|
|
253
|
-
block = { type: "tool_result", tool_use_id: msg[:tool_call_id], content: tool_content }
|
|
263
|
+
block = { type: "tool_result", tool_use_id: sanitize_tool_use_id(msg[:tool_call_id]), content: tool_content }
|
|
254
264
|
block[:cache_control] = hoisted_cache_control if hoisted_cache_control
|
|
255
265
|
return { role: "user", content: [block] }
|
|
256
266
|
end
|
|
@@ -187,7 +187,7 @@ module Clacky
|
|
|
187
187
|
name = func[:name] || tc[:name]
|
|
188
188
|
raw_args = func[:arguments] || tc[:arguments]
|
|
189
189
|
input = raw_args.is_a?(String) ? (JSON.parse(raw_args) rescue {}) : (raw_args || {})
|
|
190
|
-
blocks << { toolUse: { toolUseId: tc[:id], name: name, input: input } }
|
|
190
|
+
blocks << { toolUse: { toolUseId: Anthropic.sanitize_tool_use_id(tc[:id]), name: name, input: input } }
|
|
191
191
|
end
|
|
192
192
|
|
|
193
193
|
return { role: "assistant", content: blocks }
|
|
@@ -208,7 +208,7 @@ module Clacky
|
|
|
208
208
|
end
|
|
209
209
|
return {
|
|
210
210
|
role: "user",
|
|
211
|
-
content: [{ toolResult: { toolUseId: msg[:tool_call_id], content: result_blocks } }]
|
|
211
|
+
content: [{ toolResult: { toolUseId: Anthropic.sanitize_tool_use_id(msg[:tool_call_id]), content: result_blocks } }]
|
|
212
212
|
}
|
|
213
213
|
end
|
|
214
214
|
|
|
@@ -228,12 +228,17 @@ module Clacky
|
|
|
228
228
|
h.request(req) do |resp|
|
|
229
229
|
case resp.code.to_i
|
|
230
230
|
when 200
|
|
231
|
+
expected_len = resp["content-length"]&.to_i
|
|
231
232
|
File.open(dest, "wb") do |f|
|
|
232
233
|
resp.read_body do |chunk|
|
|
233
234
|
f.write(chunk)
|
|
234
235
|
written += chunk.bytesize
|
|
235
236
|
end
|
|
236
237
|
end
|
|
238
|
+
if expected_len && expected_len > 0 && written != expected_len
|
|
239
|
+
raise RetryableNetworkError,
|
|
240
|
+
"Truncated download: got #{written} bytes, expected #{expected_len}"
|
|
241
|
+
end
|
|
237
242
|
when 301, 302, 303, 307, 308
|
|
238
243
|
location = resp["location"]
|
|
239
244
|
raise RetryableNetworkError, "Redirect with no Location header" if location.nil? || location.empty?
|
|
@@ -354,13 +359,35 @@ module Clacky
|
|
|
354
359
|
{ success: true, data: body["data"] || body }
|
|
355
360
|
else
|
|
356
361
|
error_code = body["code"]
|
|
362
|
+
server_msg = extract_server_error_message(body)
|
|
357
363
|
error_msg = API_ERROR_MESSAGES[error_code] ||
|
|
358
|
-
|
|
364
|
+
server_msg ||
|
|
359
365
|
"Request failed (HTTP #{code}#{error_code ? ", code: #{error_code}" : ""}). Please contact support."
|
|
360
366
|
{ success: false, error: error_msg, data: body }
|
|
361
367
|
end
|
|
362
368
|
end
|
|
363
369
|
|
|
370
|
+
# Server error messages can come back under different keys / shapes:
|
|
371
|
+
# { "error": "msg" } — single string
|
|
372
|
+
# { "errors": ["msg1", "msg2"] } — array of strings (Rails .errors.full_messages)
|
|
373
|
+
# { "errors": "msg" } — string (less common)
|
|
374
|
+
# { "message": "msg" } — alternative key
|
|
375
|
+
# Returns the first non-blank human-readable string, or nil if none.
|
|
376
|
+
private def extract_server_error_message(body)
|
|
377
|
+
return nil unless body.is_a?(Hash)
|
|
378
|
+
|
|
379
|
+
[body["error"], body["errors"], body["message"]].each do |val|
|
|
380
|
+
case val
|
|
381
|
+
when String
|
|
382
|
+
return val unless val.strip.empty?
|
|
383
|
+
when Array
|
|
384
|
+
joined = val.compact.map(&:to_s).reject(&:empty?).join("; ")
|
|
385
|
+
return joined unless joined.empty?
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
nil
|
|
389
|
+
end
|
|
390
|
+
|
|
364
391
|
# Raised for transient failures that should be retried (timeouts, conn resets, SSL errors).
|
|
365
392
|
class RetryableNetworkError < StandardError; end
|
|
366
393
|
end
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -237,33 +237,6 @@ module Clacky
|
|
|
237
237
|
"website_url" => "https://console.anthropic.com/settings/keys"
|
|
238
238
|
}.freeze,
|
|
239
239
|
|
|
240
|
-
"clackyai-sea" => {
|
|
241
|
-
"name" => "ClackyAI(Sea)",
|
|
242
|
-
"base_url" => "https://api.clacky.ai",
|
|
243
|
-
"api" => "bedrock",
|
|
244
|
-
"default_model" => "abs-claude-sonnet-4-5",
|
|
245
|
-
"models" => [
|
|
246
|
-
"abs-claude-opus-4-6",
|
|
247
|
-
"abs-claude-sonnet-4-6",
|
|
248
|
-
"abs-claude-sonnet-4-5",
|
|
249
|
-
"abs-claude-haiku-4-5"
|
|
250
|
-
],
|
|
251
|
-
# Claude family — all vision-capable.
|
|
252
|
-
"capabilities" => { "vision" => true }.freeze,
|
|
253
|
-
# Per-primary lite pairing — see openclacky preset for rationale.
|
|
254
|
-
"lite_models" => {
|
|
255
|
-
"abs-claude-opus-4-6" => "abs-claude-haiku-4-5",
|
|
256
|
-
"abs-claude-sonnet-4-6" => "abs-claude-haiku-4-5",
|
|
257
|
-
"abs-claude-sonnet-4-5" => "abs-claude-haiku-4-5"
|
|
258
|
-
},
|
|
259
|
-
# Fallback chain: if a model is unavailable, try the next one in order.
|
|
260
|
-
# Keys are primary model names; values are the fallback model to use instead.
|
|
261
|
-
"fallback_models" => {
|
|
262
|
-
"abs-claude-sonnet-4-6" => "abs-claude-sonnet-4-5"
|
|
263
|
-
},
|
|
264
|
-
"website_url" => "https://clacky.ai"
|
|
265
|
-
}.freeze,
|
|
266
|
-
|
|
267
240
|
"mimo" => {
|
|
268
241
|
"name" => "MiMo (Xiaomi)",
|
|
269
242
|
"base_url" => "https://api.xiaomimimo.com/v1",
|
|
@@ -436,6 +436,7 @@ module Clacky
|
|
|
436
436
|
when ["GET", "/api/billing/summary"] then api_billing_summary(req, res)
|
|
437
437
|
when ["GET", "/api/billing/daily"] then api_billing_daily(req, res)
|
|
438
438
|
when ["GET", "/api/billing/records"] then api_billing_records(req, res)
|
|
439
|
+
when ["DELETE", "/api/billing/clear"] then api_billing_clear(req, res)
|
|
439
440
|
when ["PATCH", "/api/sessions/:id/model"] then api_switch_session_model(req, res)
|
|
440
441
|
when ["PATCH", "/api/sessions/:id/working_dir"] then api_change_session_working_dir(req, res)
|
|
441
442
|
else
|
|
@@ -1113,30 +1114,32 @@ module Clacky
|
|
|
1113
1114
|
|
|
1114
1115
|
# GET /api/billing/summary
|
|
1115
1116
|
# Returns billing summary for a time period
|
|
1116
|
-
# Query params: period (day|week|month|year|all, default: month)
|
|
1117
|
+
# Query params: period (day|week|month|year|all, default: month), model (optional)
|
|
1117
1118
|
def api_billing_summary(req, res)
|
|
1118
1119
|
require_relative "../billing/billing_store"
|
|
1119
1120
|
|
|
1120
1121
|
query = URI.decode_www_form(req.query_string.to_s).to_h
|
|
1121
1122
|
period = (query["period"] || "month").to_sym
|
|
1123
|
+
model = query["model"]
|
|
1122
1124
|
|
|
1123
1125
|
store = Clacky::Billing::BillingStore.new
|
|
1124
|
-
summary = store.summary(period: period)
|
|
1126
|
+
summary = store.summary(period: period, model: model)
|
|
1125
1127
|
|
|
1126
1128
|
json_response(res, 200, summary)
|
|
1127
1129
|
end
|
|
1128
1130
|
|
|
1129
1131
|
# GET /api/billing/daily
|
|
1130
1132
|
# Returns daily cost breakdown
|
|
1131
|
-
# Query params: days (default: 30)
|
|
1133
|
+
# Query params: days (default: 30), model (optional)
|
|
1132
1134
|
def api_billing_daily(req, res)
|
|
1133
1135
|
require_relative "../billing/billing_store"
|
|
1134
1136
|
|
|
1135
1137
|
query = URI.decode_www_form(req.query_string.to_s).to_h
|
|
1136
1138
|
days = [(query["days"] || "30").to_i, 90].min
|
|
1139
|
+
model = query["model"]
|
|
1137
1140
|
|
|
1138
1141
|
store = Clacky::Billing::BillingStore.new
|
|
1139
|
-
daily = store.daily_breakdown(days: days)
|
|
1142
|
+
daily = store.daily_breakdown(days: days, model: model)
|
|
1140
1143
|
|
|
1141
1144
|
json_response(res, 200, { days: daily })
|
|
1142
1145
|
end
|
|
@@ -1161,6 +1164,23 @@ module Clacky
|
|
|
1161
1164
|
})
|
|
1162
1165
|
end
|
|
1163
1166
|
|
|
1167
|
+
# DELETE /api/billing/clear
|
|
1168
|
+
# Clears billing records
|
|
1169
|
+
# Query params: scope (today|all, default: today)
|
|
1170
|
+
def api_billing_clear(req, res)
|
|
1171
|
+
require_relative "../billing/billing_store"
|
|
1172
|
+
|
|
1173
|
+
query = URI.decode_www_form(req.query_string.to_s).to_h
|
|
1174
|
+
scope = query["scope"] || "today"
|
|
1175
|
+
|
|
1176
|
+
store = Clacky::Billing::BillingStore.new
|
|
1177
|
+
deleted = store.clear(scope: scope.to_sym)
|
|
1178
|
+
|
|
1179
|
+
json_response(res, 200, { ok: true, deleted: deleted, scope: scope })
|
|
1180
|
+
rescue => e
|
|
1181
|
+
json_response(res, 500, { error: e.message })
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1164
1184
|
# GET /api/version
|
|
1165
1185
|
# Returns current version and latest version from RubyGems (cached for 1 hour).
|
|
1166
1186
|
def api_get_version(res)
|
data/lib/clacky/tools/browser.rb
CHANGED
|
@@ -35,7 +35,7 @@ module Clacky
|
|
|
35
35
|
Workflow: open → snapshot(interactive:true) → act(ref=...). New tab from `open` is auto-selected; only use `focus` to switch back to a previously-opened tab.
|
|
36
36
|
snapshot: returns hierarchical a11y tree truncated to ~8KB. Use query="text" to seek, or offset=N to page.
|
|
37
37
|
act kinds: click | dblclick | type | fill | press | hover | scroll | drag | select | wait | evaluate | click_at
|
|
38
|
-
evaluate: `js` is a
|
|
38
|
+
evaluate: `js` is a JS expression, e.g. "document.title". For multi-line / async logic use an IIFE: "(async () => { const r = await fetch(...); return r.status })()". Result is JSON-encoded.
|
|
39
39
|
screenshot: expensive — pass `ref` to capture one element instead of the whole page.
|
|
40
40
|
DESC
|
|
41
41
|
self.tool_category = "web"
|
|
@@ -58,7 +58,7 @@ module Clacky
|
|
|
58
58
|
amount: { type: "integer", description: "act scroll pixels (default 300)" },
|
|
59
59
|
ms: { type: "integer", description: "act wait ms" },
|
|
60
60
|
selector: { type: "string", description: "act wait: text or CSS selector" },
|
|
61
|
-
js: { type: "string", description: "act evaluate: JS
|
|
61
|
+
js: { type: "string", description: "act evaluate: JS expression (use IIFE for multi-line/async)" },
|
|
62
62
|
target_ref: { type: "string", description: "act drag destination ref" },
|
|
63
63
|
values: { type: "array", items: { type: "string" }, description: "act select options" },
|
|
64
64
|
x: { type: "number", description: "click_at x px" },
|
|
@@ -264,7 +264,7 @@ module Clacky
|
|
|
264
264
|
url = require_url(opts)
|
|
265
265
|
return url if url.is_a?(Hash)
|
|
266
266
|
invalidate_page_cache!
|
|
267
|
-
mcp_call("navigate_page", { type: "url", url: url })
|
|
267
|
+
mcp_call("navigate_page", with_page({ type: "url", url: url }))
|
|
268
268
|
wait_for_page_ready
|
|
269
269
|
{ action: "navigate", success: true, profile: "user", url: url, output: "Navigated to: #{url}" }
|
|
270
270
|
|
|
@@ -385,19 +385,10 @@ module Clacky
|
|
|
385
385
|
pid ? args.merge(pageId: pid) : args
|
|
386
386
|
end
|
|
387
387
|
|
|
388
|
-
# Wrap user-supplied JS as a Chrome-DevTools-MCP `function` argument.
|
|
389
|
-
# We treat `js` as a function body so users can write `const x = ...; return x;`
|
|
390
|
-
# naturally. For pure expressions ("document.title"), we auto-prepend `return`
|
|
391
|
-
# so the result still flows back. Detection is conservative — the presence of
|
|
392
|
-
# `return` or any top-level statement keyword skips the auto-return.
|
|
393
388
|
private def build_evaluate_function(js)
|
|
394
389
|
body = js.to_s.strip
|
|
395
390
|
return "() => {}" if body.empty?
|
|
396
|
-
|
|
397
|
-
looks_like_statement = body.match?(/(^|[\s;{])(return|const|let|var|if|for|while|throw|try|switch|function|class|do|await|async)\b/) ||
|
|
398
|
-
body.include?(";")
|
|
399
|
-
body = "return (#{body})" unless looks_like_statement
|
|
400
|
-
"() => { #{body} }"
|
|
391
|
+
"() => (#{body})"
|
|
401
392
|
end
|
|
402
393
|
|
|
403
394
|
SCREENSHOT_MAX_WIDTH = 800
|
|
@@ -1166,20 +1166,26 @@ module Clacky
|
|
|
1166
1166
|
# the literal printf tail `"$__clacky_ec"`. Until we see that, we
|
|
1167
1167
|
# accumulate; once we do, we strip the whole wrapper out and only
|
|
1168
1168
|
# emit whatever real output came after it.
|
|
1169
|
+
#
|
|
1170
|
+
# However, when stty -echo is active (the normal case for our
|
|
1171
|
+
# persistent sessions), the wrapper is never echoed — so the tail
|
|
1172
|
+
# marker never appears. We detect this by checking: if we have a
|
|
1173
|
+
# complete line (\n present) and it does NOT contain the wrapper
|
|
1174
|
+
# fingerprint, echo was suppressed and we can start streaming
|
|
1175
|
+
# immediately.
|
|
1169
1176
|
unless wrapper_swallowed
|
|
1170
1177
|
tail_marker = '"$__clacky_ec"'
|
|
1171
1178
|
tail_idx = stream_pending.index(tail_marker)
|
|
1172
1179
|
if tail_idx
|
|
1173
|
-
# Strip from start through end-of-line of the printf tail.
|
|
1174
1180
|
eol_after = stream_pending.index("\n", tail_idx) || (stream_pending.bytesize - 1)
|
|
1175
1181
|
stream_pending.replace(stream_pending.byteslice(eol_after + 1, stream_pending.bytesize - eol_after - 1).to_s)
|
|
1176
1182
|
wrapper_swallowed = true
|
|
1177
1183
|
elsif force_partial
|
|
1178
|
-
|
|
1179
|
-
|
|
1184
|
+
wrapper_swallowed = true
|
|
1185
|
+
elsif stream_pending.include?("\n") && !stream_pending.include?("__clacky_ec")
|
|
1186
|
+
# stty -echo suppressed the wrapper echo; real output is arriving.
|
|
1180
1187
|
wrapper_swallowed = true
|
|
1181
1188
|
else
|
|
1182
|
-
# Still hunting; keep buffering. Emit nothing yet.
|
|
1183
1189
|
return
|
|
1184
1190
|
end
|
|
1185
1191
|
end
|
|
@@ -1206,7 +1212,12 @@ module Clacky
|
|
|
1206
1212
|
ln.include?("__clacky_pc") ||
|
|
1207
1213
|
ln.match?(/\A\s*\}\s*>\s*\/dev\/null\s+2>&1;?\s*\z/)
|
|
1208
1214
|
end.join
|
|
1209
|
-
|
|
1215
|
+
# Collapse runs of 3+ blank lines into a single blank line so
|
|
1216
|
+
# PTY noise (cursor-positioning codes cleaned to empty lines)
|
|
1217
|
+
# doesn't produce a wall of whitespace in the streaming UI.
|
|
1218
|
+
cleaned = cleaned.gsub(/\n{3,}/, "\n\n")
|
|
1219
|
+
cleaned = cleaned.lstrip if cleaned.match?(/\A\n+\z/)
|
|
1220
|
+
on_output.call(cleaned) unless cleaned.empty? || cleaned.match?(/\A\s*\z/)
|
|
1210
1221
|
rescue StandardError
|
|
1211
1222
|
# Streaming is best-effort — never let a UI bug abort the command.
|
|
1212
1223
|
end
|
|
@@ -1502,6 +1513,7 @@ module Clacky
|
|
|
1502
1513
|
# Detect .exe invocations and redirect stdin from /dev/null unless
|
|
1503
1514
|
# the command already has an explicit stdin redirect.
|
|
1504
1515
|
private def redirect_exe_stdin(command)
|
|
1516
|
+
return command unless Clacky::Utils::EnvironmentDetector.wsl?
|
|
1505
1517
|
return command unless command =~ /\.exe\b/i
|
|
1506
1518
|
return command if command =~ /<\s*[^\s|&;]/
|
|
1507
1519
|
|
|
@@ -1520,8 +1532,7 @@ module Clacky
|
|
|
1520
1532
|
# `???`. Inject UTF-8 setup into the user's PowerShell command so the
|
|
1521
1533
|
# shell emits UTF-8 bytes regardless of host locale.
|
|
1522
1534
|
POWERSHELL_PREAMBLE =
|
|
1523
|
-
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;"
|
|
1524
|
-
"$OutputEncoding=[Text.Encoding]::UTF8;"
|
|
1535
|
+
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;"
|
|
1525
1536
|
|
|
1526
1537
|
# Only rewrites simple `powershell[.exe]` / `pwsh[.exe]` invocations.
|
|
1527
1538
|
# Skips -File / -EncodedCommand / commands already handling encoding /
|
|
@@ -43,10 +43,12 @@ module Clacky
|
|
|
43
43
|
.mp3 .mp4 .avi .mov .mkv .wav .flac
|
|
44
44
|
.ttf .otf .woff .woff2
|
|
45
45
|
.db .sqlite .bin .dat
|
|
46
|
+
.wps .et .dps
|
|
46
47
|
].freeze
|
|
47
48
|
|
|
48
49
|
GLOB_ALLOWED_BINARY_EXTENSIONS = %w[
|
|
49
50
|
.pdf .doc .docx .ppt .pptx .xls .xlsx .odt .odp .ods
|
|
51
|
+
.wps .et .dps
|
|
50
52
|
].freeze
|
|
51
53
|
|
|
52
54
|
LLM_BINARY_EXTENSIONS = %w[.png .jpg .jpeg .gif .webp .pdf].freeze
|
|
@@ -64,6 +66,7 @@ module Clacky
|
|
|
64
66
|
".docx" => :document, ".doc" => :document,
|
|
65
67
|
".xlsx" => :spreadsheet, ".xls" => :spreadsheet,
|
|
66
68
|
".pptx" => :presentation, ".ppt" => :presentation,
|
|
69
|
+
".wps" => :document, ".et" => :spreadsheet, ".dps" => :presentation,
|
|
67
70
|
".pdf" => :pdf,
|
|
68
71
|
".zip" => :zip, ".gz" => :zip, ".tgz" => :zip, ".tar" => :zip, ".rar" => :zip, ".7z" => :zip,
|
|
69
72
|
".png" => :image, ".jpg" => :image, ".jpeg" => :image,
|
|
@@ -30,6 +30,9 @@ module Clacky
|
|
|
30
30
|
".xls" => "xlsx_parser.rb",
|
|
31
31
|
".pptx" => "pptx_parser.rb",
|
|
32
32
|
".ppt" => "pptx_parser.rb",
|
|
33
|
+
".wps" => "wps_parser.rb",
|
|
34
|
+
".et" => "wps_parser.rb",
|
|
35
|
+
".dps" => "wps_parser.rb",
|
|
33
36
|
}.freeze
|
|
34
37
|
|
|
35
38
|
# Ensure ~/.clacky/parsers/ exists and all default parsers are present.
|
data/lib/clacky/version.rb
CHANGED