openclacky 1.2.5 → 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 +23 -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/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 +2 -2
- data/lib/clacky/utils/file_processor.rb +3 -0
- data/lib/clacky/utils/parser_manager.rb +3 -0
- 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 -5
- 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
|
@@ -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
|
|
@@ -1513,6 +1513,7 @@ module Clacky
|
|
|
1513
1513
|
# Detect .exe invocations and redirect stdin from /dev/null unless
|
|
1514
1514
|
# the command already has an explicit stdin redirect.
|
|
1515
1515
|
private def redirect_exe_stdin(command)
|
|
1516
|
+
return command unless Clacky::Utils::EnvironmentDetector.wsl?
|
|
1516
1517
|
return command unless command =~ /\.exe\b/i
|
|
1517
1518
|
return command if command =~ /<\s*[^\s|&;]/
|
|
1518
1519
|
|
|
@@ -1531,8 +1532,7 @@ module Clacky
|
|
|
1531
1532
|
# `???`. Inject UTF-8 setup into the user's PowerShell command so the
|
|
1532
1533
|
# shell emits UTF-8 bytes regardless of host locale.
|
|
1533
1534
|
POWERSHELL_PREAMBLE =
|
|
1534
|
-
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;"
|
|
1535
|
-
"$OutputEncoding=[Text.Encoding]::UTF8;"
|
|
1535
|
+
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;"
|
|
1536
1536
|
|
|
1537
1537
|
# Only rewrites simple `powershell[.exe]` / `pwsh[.exe]` invocations.
|
|
1538
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