openclacky 0.9.20 → 0.9.22
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 +21 -0
- data/lib/clacky/agent.rb +3 -0
- data/lib/clacky/client.rb +3 -3
- data/lib/clacky/default_skills/browser-setup/SKILL.md +32 -30
- data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +5 -5
- data/lib/clacky/default_skills/product-help/SKILL.md +1 -0
- data/lib/clacky/default_skills/skill-creator/SKILL.md +6 -0
- data/lib/clacky/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/clacky/message_format/bedrock.rb +57 -2
- data/lib/clacky/providers.rb +4 -4
- data/lib/clacky/server/scheduler.rb +2 -1
- data/lib/clacky/tools/browser.rb +27 -3
- data/lib/clacky/utils/browser_detector.rb +149 -0
- data/lib/clacky/utils/scripts_manager.rb +57 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/tasks.js +1 -2
- data/lib/clacky.rb +3 -0
- data/scripts/install.ps1 +4 -4
- data/scripts/install.sh +37 -19
- data/scripts/install_browser.sh +189 -0
- metadata +5 -2
- data/scripts/install_simple.sh +0 -630
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 636621284dfcd7f7329f793bc80f2364f16a2777dedacd2fdf310b2a7f3be58a
|
|
4
|
+
data.tar.gz: 5f3a12e563e36445d3aa32ae050f5b759b204325323cac119b8692bea7d3657b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23e59f8ae883ded129b4fee900c0ffdf6cec3ada4bd019e9a0243a3942abc70f5bbc8b9cc4830a34d7e66e22e4c663988295eed972ad1df63247c6f0bdbf1a68
|
|
7
|
+
data.tar.gz: 218bd984151af6f087c6ac44f3aa213e19ee30981a1a69484344cdbd00774611465fc3847ecfc0933243e2ae6277792116d96d0aed04b9148a9cfed14f09e14e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.22] - 2026-03-31
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **ClackyAI provider (Bedrock with prompt caching)**: added `clackyai` as a first-class provider — uses AWS Bedrock under the hood with prompt caching enabled, normalising token usage to Anthropic semantics so cost calculation works correctly
|
|
14
|
+
- **Browser auto-install script**: `browser-setup` skill can now detect the Chrome/Edge version and automatically download and run the install script, reducing manual setup steps
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Feishu setup timeout**: `navigate` method was using `open` (new tab) instead of `navigate` (current tab), causing intermittent timeouts on macOS when opening feishu.cn
|
|
18
|
+
- **Cron task schedule YAML format**: fixed a YAML serialisation bug in the scheduler that produced invalid schedule files
|
|
19
|
+
|
|
20
|
+
## [0.9.21] - 2026-03-30
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **Feishu channel setup compatibility with v2.6**: fixed Ruby 3.1 syntax incompatibility in the Feishu setup script that caused failures on newer Feishu API versions
|
|
24
|
+
|
|
25
|
+
### Improved
|
|
26
|
+
- **skill-creator YAML validation**: added frontmatter schema validation for skill files, catching malformed skill definitions before they cause runtime errors
|
|
27
|
+
|
|
28
|
+
### More
|
|
29
|
+
- Removed `install_simple.sh` (consolidated into `install.sh`)
|
|
30
|
+
|
|
10
31
|
## [0.9.20] - 2026-03-30
|
|
11
32
|
|
|
12
33
|
### Added
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -102,6 +102,9 @@ module Clacky
|
|
|
102
102
|
|
|
103
103
|
# Ensure user-space parsers are in place (~/.clacky/parsers/)
|
|
104
104
|
Utils::ParserManager.setup!
|
|
105
|
+
|
|
106
|
+
# Ensure bundled shell scripts are in place (~/.clacky/scripts/)
|
|
107
|
+
Utils::ScriptsManager.setup!
|
|
105
108
|
end
|
|
106
109
|
|
|
107
110
|
# Restore from a saved session
|
data/lib/clacky/client.rb
CHANGED
|
@@ -88,7 +88,7 @@ module Clacky
|
|
|
88
88
|
cloned = deep_clone(messages)
|
|
89
89
|
|
|
90
90
|
if bedrock?
|
|
91
|
-
send_bedrock_request(cloned, model, tools, max_tokens)
|
|
91
|
+
send_bedrock_request(cloned, model, tools, max_tokens, caching_enabled)
|
|
92
92
|
elsif anthropic_format?
|
|
93
93
|
send_anthropic_request(cloned, model, tools, max_tokens, caching_enabled)
|
|
94
94
|
else
|
|
@@ -124,8 +124,8 @@ module Clacky
|
|
|
124
124
|
|
|
125
125
|
# ── Bedrock Converse request / response ───────────────────────────────────
|
|
126
126
|
|
|
127
|
-
def send_bedrock_request(messages, model, tools, max_tokens)
|
|
128
|
-
body = MessageFormat::Bedrock.build_request_body(messages, model, tools, max_tokens)
|
|
127
|
+
def send_bedrock_request(messages, model, tools, max_tokens, caching_enabled)
|
|
128
|
+
body = MessageFormat::Bedrock.build_request_body(messages, model, tools, max_tokens, caching_enabled)
|
|
129
129
|
response = bedrock_connection.post(bedrock_endpoint(model)) { |r| r.body = body.to_json }
|
|
130
130
|
|
|
131
131
|
raise_error(response) unless response.status == 200
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: browser-setup
|
|
3
3
|
description: |
|
|
4
|
-
Configure the browser tool for Clacky. Guides the user through Chrome setup,
|
|
4
|
+
Configure the browser tool for Clacky. Guides the user through Chrome or Edge setup,
|
|
5
5
|
verifies the connection, and writes ~/.clacky/browser.yml.
|
|
6
|
+
Supports macOS, Linux, and WSL (Windows Chrome/Edge via remote debugging).
|
|
6
7
|
Trigger on: "browser setup", "setup browser", "配置浏览器", "browser config",
|
|
7
8
|
"browser doctor".
|
|
8
9
|
Subcommands: setup, doctor.
|
|
@@ -31,45 +32,43 @@ If no subcommand is clear, default to `setup`.
|
|
|
31
32
|
|
|
32
33
|
## `setup`
|
|
33
34
|
|
|
34
|
-
### Step 1 —
|
|
35
|
+
### Step 1 — Ensure chrome-devtools-mcp is installed
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
Check if already installed:
|
|
37
38
|
```bash
|
|
38
|
-
|
|
39
|
+
chrome-devtools-mcp --version 2>/dev/null
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
If
|
|
42
|
+
If found and exits 0 → skip to Step 2.
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
> Please install it first: https://nodejs.org/
|
|
45
|
-
> Let me know when done and I'll retry.
|
|
46
|
-
|
|
47
|
-
Then install/update `chrome-devtools-mcp`:
|
|
44
|
+
If missing, run the bundled installer (handles Node.js + chrome-devtools-mcp + CN/global mirrors automatically):
|
|
48
45
|
```bash
|
|
49
|
-
|
|
46
|
+
bash ~/.clacky/scripts/install_browser.sh
|
|
50
47
|
```
|
|
51
48
|
|
|
52
|
-
If
|
|
49
|
+
If the script exits non-zero or `~/.clacky/scripts/install_browser.sh` doesn't exist, stop and tell the user:
|
|
53
50
|
|
|
54
|
-
> ❌ Failed to install chrome-devtools-mcp.
|
|
51
|
+
> ❌ Failed to install chrome-devtools-mcp automatically.
|
|
52
|
+
> Please run manually:
|
|
55
53
|
> ```
|
|
56
54
|
> npm install -g chrome-devtools-mcp@latest
|
|
57
55
|
> ```
|
|
56
|
+
> (Requires Node.js 20+. If Node.js is missing, install it first, then retry.)
|
|
58
57
|
> Let me know when done.
|
|
59
58
|
|
|
60
|
-
### Step 2 — Try to connect to
|
|
59
|
+
### Step 2 — Try to connect to the browser
|
|
61
60
|
|
|
62
61
|
Immediately attempt to connect — do **not** ask the user anything first:
|
|
63
62
|
|
|
64
63
|
```
|
|
65
|
-
browser(action="act", kind="evaluate", js="navigator.userAgentData?.brands?.find(b => b.brand === 'Google Chrome')?.version || navigator.userAgent.match(/Chrome
|
|
64
|
+
browser(action="act", kind="evaluate", js="navigator.userAgentData?.brands?.find(b => b.brand === 'Google Chrome' || b.brand === 'Microsoft Edge')?.version || navigator.userAgent.match(/Chrome\/([\d]+)/)?.[1] || 'unknown'")
|
|
66
65
|
```
|
|
67
66
|
|
|
68
|
-
**If connection succeeds** → parse the
|
|
67
|
+
**If connection succeeds** → parse the browser version and jump to Step 3.
|
|
69
68
|
|
|
70
69
|
**If connection fails** → inspect the error message from the evaluate result to diagnose:
|
|
71
70
|
|
|
72
|
-
**Case A — error contains `"timed out"`**: The MCP daemon failed to start — Chrome is not running or remote debugging is not enabled. Try to open the page for the user:
|
|
71
|
+
**Case A — error contains `"timed out"`**: The MCP daemon failed to start — Chrome or Edge is not running or remote debugging is not enabled. Try to open the page for the user:
|
|
73
72
|
|
|
74
73
|
```bash
|
|
75
74
|
open "chrome://inspect/#remote-debugging"
|
|
@@ -77,35 +76,38 @@ open "chrome://inspect/#remote-debugging"
|
|
|
77
76
|
|
|
78
77
|
Tell the user:
|
|
79
78
|
|
|
80
|
-
> I've opened `chrome://inspect/#remote-debugging` in
|
|
79
|
+
> I've opened `chrome://inspect/#remote-debugging` in your browser.
|
|
81
80
|
> Please click **"Allow remote debugging for this browser instance"** and let me know when done.
|
|
82
81
|
|
|
83
|
-
If `open` fails, fall back to:
|
|
82
|
+
If `open` fails (e.g. on WSL or Linux), fall back to:
|
|
84
83
|
|
|
85
|
-
> Please open this URL in Chrome:
|
|
84
|
+
> Please open this URL in Chrome or Edge:
|
|
86
85
|
> `chrome://inspect/#remote-debugging`
|
|
87
86
|
> Then click **"Allow remote debugging for this browser instance"** and let me know when done.
|
|
87
|
+
>
|
|
88
|
+
> On WSL: launch your browser from PowerShell with remote debugging enabled:
|
|
89
|
+
> - Edge: `Start-Process msedge --ArgumentList "--remote-debugging-port=9222"`
|
|
90
|
+
> - Chrome: `Start-Process chrome --ArgumentList "--remote-debugging-port=9222"`
|
|
88
91
|
|
|
89
92
|
Wait for the user to confirm, then retry the connection once. If still failing, stop:
|
|
90
93
|
|
|
91
|
-
> ❌ Could not connect to
|
|
94
|
+
> ❌ Could not connect to the browser. Please make sure Chrome or Edge is open and remote debugging is enabled, then run `/browser-setup` again.
|
|
92
95
|
|
|
93
|
-
**Case B — error contains `"Chrome MCP error:"`**: The MCP daemon is alive but
|
|
96
|
+
**Case B — error contains `"Chrome MCP error:"`**: The MCP daemon is alive but the browser's CDP connection is broken — this is a known issue after long sessions. Tell the user:
|
|
94
97
|
|
|
95
|
-
>
|
|
96
|
-
> Please restart
|
|
98
|
+
> The browser's remote debugging connection is unstable.
|
|
99
|
+
> Please restart your browser and let me know when done.
|
|
97
100
|
|
|
98
101
|
Wait for the user to confirm, then retry once. If still failing, stop with the same error message as Case A.
|
|
99
102
|
|
|
100
|
-
### Step 3 — Check
|
|
103
|
+
### Step 3 — Check browser version
|
|
101
104
|
|
|
102
|
-
Parse the version number from Step 2:
|
|
105
|
+
Parse the version number from Step 2 (works for both Chrome and Edge, both are Chromium-based):
|
|
103
106
|
- version >= 146 → proceed
|
|
104
107
|
- version 144–145 → warn but proceed:
|
|
105
|
-
> ⚠️ Your
|
|
108
|
+
> ⚠️ Your browser version is vXXX. Version 146+ is recommended. Continuing anyway...
|
|
106
109
|
- version < 144 or unknown → stop:
|
|
107
|
-
> ❌
|
|
108
|
-
> Let me know when you've upgraded and I'll retry.
|
|
110
|
+
> ❌ Browser vXXX is too old. Please upgrade Chrome or Edge to v146+, then let me know and I'll retry.
|
|
109
111
|
|
|
110
112
|
### Step 4 — Save config and start daemon
|
|
111
113
|
|
|
@@ -122,7 +124,7 @@ If this fails (server not running), skip silently — the daemon will start lazi
|
|
|
122
124
|
|
|
123
125
|
> ✅ Browser configured.
|
|
124
126
|
>
|
|
125
|
-
> Chrome v<VERSION> is connected and ready to use.
|
|
127
|
+
> Chrome/Edge v<VERSION> is connected and ready to use.
|
|
126
128
|
|
|
127
129
|
---
|
|
128
130
|
|
|
@@ -71,9 +71,9 @@ BOT_PERMISSIONS = %w[
|
|
|
71
71
|
# Logging helpers
|
|
72
72
|
# ---------------------------------------------------------------------------
|
|
73
73
|
|
|
74
|
-
def step(msg)
|
|
75
|
-
def ok(msg)
|
|
76
|
-
def warn(msg)
|
|
74
|
+
def step(msg); puts("[feishu-setup] #{msg}"); end
|
|
75
|
+
def ok(msg); puts("[feishu-setup] ✅ #{msg}"); end
|
|
76
|
+
def warn(msg); puts("[feishu-setup] ⚠️ #{msg}"); end
|
|
77
77
|
def fail!(msg)
|
|
78
78
|
puts("[feishu-setup] ❌ #{msg}")
|
|
79
79
|
exit 1
|
|
@@ -125,7 +125,7 @@ class BrowserSession
|
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
def navigate(url)
|
|
128
|
-
@client.call("
|
|
128
|
+
@client.call("navigate", url: url)
|
|
129
129
|
sleep 2
|
|
130
130
|
snapshot
|
|
131
131
|
end
|
|
@@ -501,7 +501,7 @@ def run_setup(browser, api)
|
|
|
501
501
|
id = s["id"].to_s
|
|
502
502
|
name_to_id[name] = id if name && !id.empty?
|
|
503
503
|
end
|
|
504
|
-
ids = BOT_PERMISSIONS.
|
|
504
|
+
ids = BOT_PERMISSIONS.map { |n| name_to_id[n] }.compact
|
|
505
505
|
missing = BOT_PERMISSIONS.reject { |n| name_to_id.key?(n) }
|
|
506
506
|
warn "#{missing.size} permissions not matched: #{missing.join(", ")}" unless missing.empty?
|
|
507
507
|
fail! "No permission IDs matched. API response keys: #{name_to_id.keys.first(5).inspect}" if ids.empty?
|
|
@@ -53,6 +53,7 @@ Answer the user's question using the official documentation below. Always fetch
|
|
|
53
53
|
| Built-in skills, default skills, what skills ship with OpenClacky | https://www.openclacky.com/docs/built-in-skills |
|
|
54
54
|
| Memory system, long-term memory, ~/.clacky/memories | https://www.openclacky.com/docs/memory-system |
|
|
55
55
|
| Session management, conversation history, context window | https://www.openclacky.com/docs/session-management |
|
|
56
|
+
| Browser automation, browser tool, Chrome, Edge, CDP, remote debugging, WSL browser, browser-setup skill | https://www.openclacky.com/docs/browser-tool |
|
|
56
57
|
|
|
57
58
|
## Workflow
|
|
58
59
|
|
|
@@ -86,6 +86,12 @@ Components to fill in:
|
|
|
86
86
|
>
|
|
87
87
|
> **YAML description gotcha**: If the description contains `word: value` patterns (colons followed by space), YAML treats them as key-value pairs and the frontmatter parse fails silently. Always wrap description values in single quotes. Avoid embedded double-quotes inside single-quoted strings (use rephrasing instead).
|
|
88
88
|
|
|
89
|
+
> **After writing SKILL.md — always validate and auto-fix**: Run this immediately after creating or updating any skill file:
|
|
90
|
+
> ```bash
|
|
91
|
+
> ruby SKILL_DIR/scripts/validate_skill_frontmatter.rb /path/to/new-skill/SKILL.md
|
|
92
|
+
> ```
|
|
93
|
+
> The script validates the YAML frontmatter and auto-fixes common issues (unquoted descriptions, multi-line block scalars with colons). If it prints `OK:` — you're done. If it prints `Auto-fixed and saved` — it repaired the file automatically. If it prints `ERROR` — manual fix required.
|
|
94
|
+
|
|
89
95
|
### Skill Writing Guide
|
|
90
96
|
|
|
91
97
|
#### Anatomy of a Skill
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# validate_skill_frontmatter.rb
|
|
5
|
+
#
|
|
6
|
+
# Validates and auto-fixes the YAML frontmatter of a SKILL.md file.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ruby validate_skill_frontmatter.rb <path/to/SKILL.md>
|
|
10
|
+
#
|
|
11
|
+
# What it does:
|
|
12
|
+
# 1. Parses the frontmatter between --- delimiters
|
|
13
|
+
# 2. If YAML is invalid OR description is not a plain String:
|
|
14
|
+
# - Extracts name/description via regex fallback
|
|
15
|
+
# - Re-wraps description in single quotes (collapsed to one line)
|
|
16
|
+
# - Rewrites the frontmatter in the file
|
|
17
|
+
# 3. Exits 0 on success (with or without auto-fix), 1 on unrecoverable error
|
|
18
|
+
|
|
19
|
+
require "yaml"
|
|
20
|
+
|
|
21
|
+
path = ARGV[0]
|
|
22
|
+
|
|
23
|
+
if path.nil? || path.strip.empty?
|
|
24
|
+
warn "Usage: ruby validate_skill_frontmatter.rb <path/to/SKILL.md>"
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
unless File.exist?(path)
|
|
29
|
+
warn "File not found: #{path}"
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
content = File.read(path)
|
|
34
|
+
|
|
35
|
+
# Extract frontmatter block
|
|
36
|
+
fm_match = content.match(/\A(---\n)(.*?)(\n---[ \t]*\n?)/m)
|
|
37
|
+
unless fm_match
|
|
38
|
+
warn "ERROR: No frontmatter block found in #{path}"
|
|
39
|
+
exit 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
prefix = fm_match[1] # "---\n"
|
|
43
|
+
yaml_raw = fm_match[2] # raw YAML text
|
|
44
|
+
suffix = fm_match[3] # "\n---\n"
|
|
45
|
+
body = content[fm_match.end(0)..] # rest of file after frontmatter
|
|
46
|
+
|
|
47
|
+
# Attempt normal YAML parse
|
|
48
|
+
parse_ok = false
|
|
49
|
+
data = nil
|
|
50
|
+
begin
|
|
51
|
+
data = YAML.safe_load(yaml_raw) || {}
|
|
52
|
+
parse_ok = data["description"].is_a?(String)
|
|
53
|
+
rescue Psych::Exception => e
|
|
54
|
+
warn "YAML parse error: #{e.message}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if parse_ok
|
|
58
|
+
puts "OK: name=#{data['name'].inspect} description_length=#{data['description'].length}"
|
|
59
|
+
exit 0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- Auto-fix ---
|
|
63
|
+
puts "Frontmatter invalid or description broken — attempting auto-fix..."
|
|
64
|
+
|
|
65
|
+
# Regex fallback: extract name and description lines
|
|
66
|
+
name_match = yaml_raw.match(/^name:\s*(.+)$/)
|
|
67
|
+
unless name_match
|
|
68
|
+
warn "ERROR: Cannot extract 'name' field from frontmatter. Manual fix required."
|
|
69
|
+
exit 1
|
|
70
|
+
end
|
|
71
|
+
name_value = name_match[1].strip.gsub(/\A['"]|['"]\z/, "")
|
|
72
|
+
|
|
73
|
+
# description may be:
|
|
74
|
+
# description: some text (unquoted)
|
|
75
|
+
# description: 'some text' (single-quoted)
|
|
76
|
+
# description: "some text" (double-quoted)
|
|
77
|
+
# description: first line\n continuation (multi-line block scalar)
|
|
78
|
+
desc_match = yaml_raw.match(/^description:\s*(.+?)(?=\n[a-z]|\z)/m)
|
|
79
|
+
unless desc_match
|
|
80
|
+
warn "ERROR: Cannot extract 'description' field from frontmatter. Manual fix required."
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
raw_desc = desc_match[1].strip
|
|
85
|
+
|
|
86
|
+
# Strip existing outer quotes if present (simple single-line quoted values)
|
|
87
|
+
if raw_desc.start_with?("'") && raw_desc.end_with?("'")
|
|
88
|
+
raw_desc = raw_desc[1..-2]
|
|
89
|
+
elsif raw_desc.start_with?('"') && raw_desc.end_with?('"')
|
|
90
|
+
raw_desc = raw_desc[1..-2]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Collapse multi-line: strip leading whitespace from continuation lines
|
|
94
|
+
description_value = raw_desc.gsub(/\n\s+/, " ").strip
|
|
95
|
+
|
|
96
|
+
# Escape any single quotes inside the description value
|
|
97
|
+
description_value_escaped = description_value.gsub("'", "''")
|
|
98
|
+
|
|
99
|
+
# Extract all other frontmatter lines (everything except name: and description:)
|
|
100
|
+
other_lines = yaml_raw.each_line.reject do |line|
|
|
101
|
+
line.match?(/^(name|description):/) || line.match?(/^\s+\S/) && yaml_raw.match?(/^description:.*\n(\s+.+\n)*/m)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# More precise: collect lines that are not part of the name/description block
|
|
105
|
+
remaining = []
|
|
106
|
+
skip_continuation = false
|
|
107
|
+
yaml_raw.each_line do |line|
|
|
108
|
+
if line.match?(/^(name|description):/)
|
|
109
|
+
skip_continuation = true
|
|
110
|
+
next
|
|
111
|
+
end
|
|
112
|
+
if skip_continuation && line.match?(/^\s+\S/)
|
|
113
|
+
next # continuation of a multi-line block value
|
|
114
|
+
end
|
|
115
|
+
skip_continuation = false
|
|
116
|
+
remaining << line unless line.strip.empty? && remaining.empty?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Rebuild frontmatter
|
|
120
|
+
fixed_fm_lines = []
|
|
121
|
+
fixed_fm_lines << "name: #{name_value}"
|
|
122
|
+
fixed_fm_lines << "description: '#{description_value_escaped}'"
|
|
123
|
+
remaining.each { |l| fixed_fm_lines << l.chomp }
|
|
124
|
+
|
|
125
|
+
# Remove trailing blank lines from remaining
|
|
126
|
+
fixed_fm = fixed_fm_lines.join("\n").strip
|
|
127
|
+
|
|
128
|
+
new_content = "#{prefix}#{fixed_fm}#{suffix}#{body}"
|
|
129
|
+
|
|
130
|
+
File.write(path, new_content)
|
|
131
|
+
puts "Auto-fixed and saved: #{path}"
|
|
132
|
+
|
|
133
|
+
# Final verification
|
|
134
|
+
begin
|
|
135
|
+
verify_content = File.read(path)
|
|
136
|
+
verify_match = verify_content.match(/\A---\n(.*?)\n---/m)
|
|
137
|
+
verify_data = YAML.safe_load(verify_match[1])
|
|
138
|
+
raise "description not a String" unless verify_data["description"].is_a?(String)
|
|
139
|
+
puts "OK: name=#{verify_data['name'].inspect} description_length=#{verify_data['description'].length}"
|
|
140
|
+
rescue => e
|
|
141
|
+
warn "ERROR: Auto-fix failed, manual intervention required: #{e.message}"
|
|
142
|
+
exit 1
|
|
143
|
+
end
|
|
@@ -32,13 +32,21 @@ module Clacky
|
|
|
32
32
|
# @param max_tokens [Integer]
|
|
33
33
|
# @param caching_enabled [Boolean] (currently unused for Bedrock)
|
|
34
34
|
# @return [Hash] ready to serialize as JSON body
|
|
35
|
-
def build_request_body(messages, model, tools, max_tokens,
|
|
35
|
+
def build_request_body(messages, model, tools, max_tokens, caching_enabled = false)
|
|
36
36
|
system_messages = messages.select { |m| m[:role] == "system" }
|
|
37
37
|
regular_messages = messages.reject { |m| m[:role] == "system" }
|
|
38
38
|
|
|
39
39
|
# Merge consecutive same-role messages (Bedrock requires alternating roles)
|
|
40
40
|
api_messages = merge_consecutive_tool_results(regular_messages.map { |msg| to_api_message(msg) })
|
|
41
41
|
|
|
42
|
+
# Inject cachePoint blocks AFTER conversion to Bedrock API format.
|
|
43
|
+
# Doing this on canonical messages (before to_api_message) is incorrect because
|
|
44
|
+
# tool-result messages (role: "tool") are converted to toolResult blocks, and
|
|
45
|
+
# Bedrock does not support cachePoint inside toolResult.content.
|
|
46
|
+
# Operating on the final Bedrock format ensures cachePoint is always a top-level
|
|
47
|
+
# sibling block in the message's content array, which is what Bedrock expects.
|
|
48
|
+
api_messages = apply_api_caching(api_messages) if caching_enabled
|
|
49
|
+
|
|
42
50
|
body = { messages: api_messages }
|
|
43
51
|
|
|
44
52
|
# Add system prompt if present
|
|
@@ -86,11 +94,24 @@ module Clacky
|
|
|
86
94
|
else data["stopReason"]
|
|
87
95
|
end
|
|
88
96
|
|
|
97
|
+
cache_read = usage["cacheReadInputTokens"].to_i
|
|
98
|
+
cache_write = usage["cacheWriteInputTokens"].to_i
|
|
99
|
+
|
|
100
|
+
# Bedrock `inputTokens` = non-cached input only.
|
|
101
|
+
# Anthropic direct `input_tokens` = all input including cache_read.
|
|
102
|
+
# Normalise to Anthropic semantics so ModelPricing.calculate_cost works correctly:
|
|
103
|
+
# prompt_tokens = inputTokens + cacheReadInputTokens
|
|
104
|
+
# (calculate_cost subtracts cache_read_tokens from prompt_tokens to get
|
|
105
|
+
# the billable non-cached portion; that arithmetic requires the Anthropic convention.)
|
|
106
|
+
prompt_tokens = usage["inputTokens"].to_i + cache_read
|
|
107
|
+
|
|
89
108
|
usage_data = {
|
|
90
|
-
prompt_tokens:
|
|
109
|
+
prompt_tokens: prompt_tokens,
|
|
91
110
|
completion_tokens: usage["outputTokens"].to_i,
|
|
92
111
|
total_tokens: usage["totalTokens"].to_i
|
|
93
112
|
}
|
|
113
|
+
usage_data[:cache_read_input_tokens] = cache_read if cache_read > 0
|
|
114
|
+
usage_data[:cache_creation_input_tokens] = cache_write if cache_write > 0
|
|
94
115
|
|
|
95
116
|
{ content: content, tool_calls: tool_calls, finish_reason: finish_reason,
|
|
96
117
|
usage: usage_data, raw_api_usage: usage }
|
|
@@ -185,6 +206,8 @@ module Clacky
|
|
|
185
206
|
when "image"
|
|
186
207
|
block # already Bedrock format
|
|
187
208
|
else
|
|
209
|
+
# Pass through Bedrock-native blocks (e.g. cachePoint) unchanged
|
|
210
|
+
return block if block[:cachePoint]
|
|
188
211
|
# Fallback: try to extract text
|
|
189
212
|
{ text: (block[:text] || block.to_s) }
|
|
190
213
|
end
|
|
@@ -252,6 +275,38 @@ module Clacky
|
|
|
252
275
|
end
|
|
253
276
|
merged
|
|
254
277
|
end
|
|
278
|
+
|
|
279
|
+
# Inject cachePoint blocks into already-converted Bedrock API format messages.
|
|
280
|
+
# Marks the last 2 messages (from the tail) so Bedrock can cache the conversation
|
|
281
|
+
# prefix up to those points.
|
|
282
|
+
#
|
|
283
|
+
# Why operate on Bedrock API format (not canonical):
|
|
284
|
+
# - tool-result canonical messages (role: "tool") become toolResult blocks inside
|
|
285
|
+
# a user message. Bedrock does NOT allow cachePoint inside toolResult.content.
|
|
286
|
+
# - After merge_consecutive_tool_results, message boundaries may differ from canonical.
|
|
287
|
+
# - Operating here guarantees cachePoint is always a top-level sibling block.
|
|
288
|
+
private_class_method def self.apply_api_caching(api_messages)
|
|
289
|
+
return api_messages if api_messages.empty?
|
|
290
|
+
|
|
291
|
+
candidate_indices = []
|
|
292
|
+
(api_messages.length - 1).downto(0) do |i|
|
|
293
|
+
break if candidate_indices.length >= 2
|
|
294
|
+
candidate_indices << i
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
api_messages.map.with_index do |msg, idx|
|
|
298
|
+
next msg unless candidate_indices.include?(idx)
|
|
299
|
+
|
|
300
|
+
content = msg[:content]
|
|
301
|
+
next msg unless content.is_a?(Array)
|
|
302
|
+
|
|
303
|
+
# Don't double-add cachePoint if already present
|
|
304
|
+
already_marked = content.last.is_a?(Hash) && content.last[:cachePoint]
|
|
305
|
+
next msg if already_marked
|
|
306
|
+
|
|
307
|
+
msg.merge(content: content + [{ cachePoint: { type: "default" } }])
|
|
308
|
+
end
|
|
309
|
+
end
|
|
255
310
|
end
|
|
256
311
|
end
|
|
257
312
|
end
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -48,16 +48,16 @@ module Clacky
|
|
|
48
48
|
"website_url" => "https://console.anthropic.com/settings/keys"
|
|
49
49
|
}.freeze,
|
|
50
50
|
|
|
51
|
-
"
|
|
52
|
-
"name" => "
|
|
53
|
-
"base_url" => "https://
|
|
51
|
+
"clackyai" => {
|
|
52
|
+
"name" => "Clacky AI",
|
|
53
|
+
"base_url" => "https://api.clacky.ai",
|
|
54
54
|
"api" => "bedrock",
|
|
55
55
|
"default_model" => "jp.anthropic.claude-sonnet-4-6",
|
|
56
56
|
"models" => [
|
|
57
57
|
"jp.anthropic.claude-sonnet-4-6",
|
|
58
58
|
"jp.anthropic.claude-haiku-4-6"
|
|
59
59
|
],
|
|
60
|
-
"website_url" => "https://
|
|
60
|
+
"website_url" => "https://clacky.ai"
|
|
61
61
|
}.freeze
|
|
62
62
|
|
|
63
63
|
}.freeze
|
|
@@ -300,7 +300,8 @@ module Clacky
|
|
|
300
300
|
return [] unless File.exist?(SCHEDULES_FILE)
|
|
301
301
|
|
|
302
302
|
data = YAMLCompat.load_file(SCHEDULES_FILE, permitted_classes: [Symbol])
|
|
303
|
-
|
|
303
|
+
raw = data.is_a?(Hash) ? data["schedules"] : data
|
|
304
|
+
Array(raw).select { |s| s.is_a?(Hash) }
|
|
304
305
|
rescue => e
|
|
305
306
|
Clacky::Logger.error("scheduler_load_schedules_error", error: e)
|
|
306
307
|
[]
|
data/lib/clacky/tools/browser.rb
CHANGED
|
@@ -542,9 +542,33 @@ module Clacky
|
|
|
542
542
|
end
|
|
543
543
|
|
|
544
544
|
def self.build_mcp_command(user_data_dir: nil)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
545
|
+
# If an explicit user_data_dir is given, use it directly (e.g. from browser.yml).
|
|
546
|
+
if user_data_dir && !user_data_dir.to_s.empty?
|
|
547
|
+
return ["chrome-devtools-mcp", *CHROME_MCP_BASE_ARGS, "--userDataDir", user_data_dir.to_s]
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# On non-macOS/Linux platforms (especially WSL), chrome-devtools-mcp's built-in
|
|
551
|
+
# --autoConnect only scans Linux-side paths and misses Windows-side Chrome/Edge.
|
|
552
|
+
# Use BrowserDetector to find any running debuggable browser across all platforms.
|
|
553
|
+
detected = Clacky::Utils::BrowserDetector.detect
|
|
554
|
+
|
|
555
|
+
args = base_args_without_autoconnect
|
|
556
|
+
case detected&.fetch(:mode)
|
|
557
|
+
when :ws_endpoint
|
|
558
|
+
# DevToolsActivePort found — use exact WS endpoint (most reliable)
|
|
559
|
+
["chrome-devtools-mcp", *args, "--wsEndpoint", detected[:value]]
|
|
560
|
+
when :browser_url
|
|
561
|
+
# TCP port reachable — let chrome-devtools-mcp handle WS negotiation
|
|
562
|
+
["chrome-devtools-mcp", *args, "--browserUrl", detected[:value]]
|
|
563
|
+
else
|
|
564
|
+
# Nothing detected — fall back to --autoConnect (works on plain macOS/Linux)
|
|
565
|
+
["chrome-devtools-mcp", *CHROME_MCP_BASE_ARGS]
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# Base args without --autoConnect, used when we supply explicit connection info.
|
|
570
|
+
def self.base_args_without_autoconnect
|
|
571
|
+
CHROME_MCP_BASE_ARGS.reject { |a| a == "--autoConnect" }
|
|
548
572
|
end
|
|
549
573
|
|
|
550
574
|
# Delegate to BrowserManager. Auto-retries once on "selected page has been closed".
|