openclacky 0.9.21 → 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 +10 -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 +1 -1
- data/lib/clacky/default_skills/product-help/SKILL.md +1 -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_browser.sh +189 -0
- metadata +4 -1
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,16 @@ 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
|
+
|
|
10
20
|
## [0.9.21] - 2026-03-30
|
|
11
21
|
|
|
12
22
|
### Fixed
|
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
|
|
|
@@ -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
|
|
|
@@ -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".
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module Utils
|
|
7
|
+
# Detects a running browser (Chrome/Edge) that has remote debugging enabled.
|
|
8
|
+
#
|
|
9
|
+
# Detection strategy (in priority order):
|
|
10
|
+
#
|
|
11
|
+
# 1. Scan known UserData directories for DevToolsActivePort file.
|
|
12
|
+
# This file contains the exact port + WS path — most reliable.
|
|
13
|
+
# Returns { mode: :ws_endpoint, value: "ws://127.0.0.1:PORT/PATH" }
|
|
14
|
+
#
|
|
15
|
+
# 2. TCP port scan on common remote debugging ports (9222–9224).
|
|
16
|
+
# Chrome 146+ dropped HTTP /json/version, so we just probe TCP connectivity
|
|
17
|
+
# and hand off to chrome-devtools-mcp via --browserUrl.
|
|
18
|
+
# Returns { mode: :browser_url, value: "http://127.0.0.1:PORT" }
|
|
19
|
+
#
|
|
20
|
+
# 3. Nothing found → returns nil (caller should show guidance to user).
|
|
21
|
+
#
|
|
22
|
+
# Supported environments: WSL, Linux, macOS.
|
|
23
|
+
module BrowserDetector
|
|
24
|
+
# Ports to probe when DevToolsActivePort file is not found.
|
|
25
|
+
TCP_PROBE_PORTS = [9222, 9223, 9224].freeze
|
|
26
|
+
TCP_PROBE_TIMEOUT = 0.5 # seconds
|
|
27
|
+
|
|
28
|
+
# Detect a running debuggable browser.
|
|
29
|
+
# @return [Hash, nil] { mode: :ws_endpoint|:browser_url, value: String } or nil
|
|
30
|
+
def self.detect
|
|
31
|
+
result = detect_via_active_port_file
|
|
32
|
+
result ||= detect_via_tcp_probe
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# -----------------------------------------------------------------------
|
|
37
|
+
# Strategy 1: DevToolsActivePort file scan
|
|
38
|
+
# -----------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
# @return [Hash, nil]
|
|
41
|
+
def self.detect_via_active_port_file
|
|
42
|
+
user_data_dirs.each do |dir|
|
|
43
|
+
port_file = File.join(dir, "DevToolsActivePort")
|
|
44
|
+
next unless File.exist?(port_file)
|
|
45
|
+
|
|
46
|
+
ws = parse_active_port_file(port_file)
|
|
47
|
+
return { mode: :ws_endpoint, value: ws } if ws
|
|
48
|
+
end
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Hash, nil]
|
|
53
|
+
def self.detect_via_tcp_probe
|
|
54
|
+
TCP_PROBE_PORTS.each do |port|
|
|
55
|
+
return { mode: :browser_url, value: "http://127.0.0.1:#{port}" } if tcp_open?("127.0.0.1", port)
|
|
56
|
+
end
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# -----------------------------------------------------------------------
|
|
61
|
+
# UserData directory candidates per OS
|
|
62
|
+
# -----------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
# Returns ordered list of candidate UserData dirs to check.
|
|
65
|
+
# @return [Array<String>]
|
|
66
|
+
def self.user_data_dirs
|
|
67
|
+
case EnvironmentDetector.os_type
|
|
68
|
+
when :wsl then wsl_user_data_dirs
|
|
69
|
+
when :linux then linux_user_data_dirs
|
|
70
|
+
when :macos then macos_user_data_dirs
|
|
71
|
+
else []
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# WSL: Chrome/Edge run on Windows side — resolve via LOCALAPPDATA.
|
|
76
|
+
private_class_method def self.wsl_user_data_dirs
|
|
77
|
+
appdata = Utils::Encoding.cmd_to_utf8(
|
|
78
|
+
`powershell.exe -NoProfile -Command '$env:LOCALAPPDATA' 2>/dev/null`
|
|
79
|
+
).strip.tr("\r\n", "")
|
|
80
|
+
return [] if appdata.empty?
|
|
81
|
+
|
|
82
|
+
win_paths = [
|
|
83
|
+
"#{appdata}\\Microsoft\\Edge\\User Data",
|
|
84
|
+
"#{appdata}\\Google\\Chrome\\User Data",
|
|
85
|
+
"#{appdata}\\Google\\Chrome Beta\\User Data",
|
|
86
|
+
"#{appdata}\\Google\\Chrome SxS\\User Data",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
win_paths.filter_map do |win_path|
|
|
90
|
+
linux_path = Utils::Encoding.cmd_to_utf8(
|
|
91
|
+
`wslpath '#{win_path}' 2>/dev/null`, source_encoding: "UTF-8"
|
|
92
|
+
).strip
|
|
93
|
+
linux_path.empty? ? nil : linux_path
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Linux: standard XDG config paths for Chrome and Edge.
|
|
98
|
+
private_class_method def self.linux_user_data_dirs
|
|
99
|
+
config_home = ENV["XDG_CONFIG_HOME"] || File.join(Dir.home, ".config")
|
|
100
|
+
[
|
|
101
|
+
File.join(config_home, "microsoft-edge"),
|
|
102
|
+
File.join(config_home, "google-chrome"),
|
|
103
|
+
File.join(config_home, "google-chrome-beta"),
|
|
104
|
+
File.join(config_home, "google-chrome-unstable"),
|
|
105
|
+
]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# macOS: Application Support paths for Chrome and Edge.
|
|
109
|
+
private_class_method def self.macos_user_data_dirs
|
|
110
|
+
base = File.join(Dir.home, "Library", "Application Support")
|
|
111
|
+
[
|
|
112
|
+
File.join(base, "Microsoft Edge"),
|
|
113
|
+
File.join(base, "Google", "Chrome"),
|
|
114
|
+
File.join(base, "Google", "Chrome Beta"),
|
|
115
|
+
File.join(base, "Google", "Chrome Canary"),
|
|
116
|
+
]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# -----------------------------------------------------------------------
|
|
120
|
+
# Helpers
|
|
121
|
+
# -----------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
# Parse DevToolsActivePort file.
|
|
124
|
+
# Format: first line = port number, second line = WS path
|
|
125
|
+
# @return [String, nil] ws://127.0.0.1:PORT/PATH or nil on parse error
|
|
126
|
+
private_class_method def self.parse_active_port_file(path)
|
|
127
|
+
lines = File.read(path, encoding: "utf-8").split("\n").map(&:strip).reject(&:empty?)
|
|
128
|
+
return nil unless lines.size >= 2
|
|
129
|
+
|
|
130
|
+
port = lines[0].to_i
|
|
131
|
+
ws_path = lines[1]
|
|
132
|
+
return nil if port <= 0 || port > 65_535 || ws_path.empty?
|
|
133
|
+
|
|
134
|
+
"ws://127.0.0.1:#{port}#{ws_path}"
|
|
135
|
+
rescue StandardError
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Probe TCP port with a short timeout.
|
|
140
|
+
# Chrome 146+ dropped HTTP /json/version — TCP reachability is sufficient.
|
|
141
|
+
# @return [Boolean]
|
|
142
|
+
private_class_method def self.tcp_open?(host, port)
|
|
143
|
+
Socket.tcp(host, port, connect_timeout: TCP_PROBE_TIMEOUT) { true }
|
|
144
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError, Errno::EHOSTUNREACH
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module Utils
|
|
7
|
+
# Manages user-space shell scripts in ~/.clacky/scripts/.
|
|
8
|
+
#
|
|
9
|
+
# On first use, bundled scripts are copied from the gem's scripts/
|
|
10
|
+
# directory into ~/.clacky/scripts/. The user-space copy is always
|
|
11
|
+
# used so users can customise scripts without modifying the gem.
|
|
12
|
+
#
|
|
13
|
+
# Bundled scripts are re-copied when the gem is upgraded (detected
|
|
14
|
+
# via gem version stamp in ~/.clacky/scripts/.version).
|
|
15
|
+
module ScriptsManager
|
|
16
|
+
SCRIPTS_DIR = File.expand_path("~/.clacky/scripts").freeze
|
|
17
|
+
DEFAULT_SCRIPTS_DIR = File.expand_path("../../../../scripts", __dir__).freeze
|
|
18
|
+
VERSION_FILE = File.join(SCRIPTS_DIR, ".version").freeze
|
|
19
|
+
|
|
20
|
+
SCRIPTS = %w[
|
|
21
|
+
install_browser.sh
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
# Copy bundled scripts to ~/.clacky/scripts/ if missing or outdated.
|
|
25
|
+
# Called once at agent startup — fast (no-op after first run).
|
|
26
|
+
def self.setup!
|
|
27
|
+
FileUtils.mkdir_p(SCRIPTS_DIR)
|
|
28
|
+
|
|
29
|
+
current_version = Clacky::VERSION
|
|
30
|
+
stored_version = File.exist?(VERSION_FILE) ? File.read(VERSION_FILE).strip : nil
|
|
31
|
+
|
|
32
|
+
SCRIPTS.each do |script|
|
|
33
|
+
dest = File.join(SCRIPTS_DIR, script)
|
|
34
|
+
src = File.join(DEFAULT_SCRIPTS_DIR, script)
|
|
35
|
+
next unless File.exist?(src)
|
|
36
|
+
|
|
37
|
+
# Copy if missing or gem was upgraded
|
|
38
|
+
if !File.exist?(dest) || stored_version != current_version
|
|
39
|
+
FileUtils.cp(src, dest)
|
|
40
|
+
FileUtils.chmod(0o755, dest)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Write version stamp after successful copy
|
|
45
|
+
File.write(VERSION_FILE, current_version)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns the full path to a managed script.
|
|
49
|
+
# @param name [String] script filename, e.g. "install_browser.sh"
|
|
50
|
+
# @return [String, nil]
|
|
51
|
+
def self.path_for(name)
|
|
52
|
+
dest = File.join(SCRIPTS_DIR, name)
|
|
53
|
+
File.exist?(dest) ? dest : nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/tasks.js
CHANGED
|
@@ -107,8 +107,7 @@ const Tasks = (() => {
|
|
|
107
107
|
|
|
108
108
|
/** Called by Router when the tasks panel becomes active. */
|
|
109
109
|
onPanelShow() {
|
|
110
|
-
Tasks.
|
|
111
|
-
Tasks.renderSection();
|
|
110
|
+
Tasks.load();
|
|
112
111
|
const btn = $("btn-create-task");
|
|
113
112
|
if (btn) btn.onclick = () => Tasks.createInSession();
|
|
114
113
|
},
|
data/lib/clacky.rb
CHANGED
|
@@ -89,6 +89,9 @@ require_relative "clacky/ui2/progress_indicator"
|
|
|
89
89
|
# Utils
|
|
90
90
|
require_relative "clacky/utils/logger"
|
|
91
91
|
require_relative "clacky/utils/encoding"
|
|
92
|
+
require_relative "clacky/utils/environment_detector"
|
|
93
|
+
require_relative "clacky/utils/browser_detector"
|
|
94
|
+
require_relative "clacky/utils/scripts_manager"
|
|
92
95
|
require_relative "clacky/utils/model_pricing"
|
|
93
96
|
require_relative "clacky/utils/gitignore_parser"
|
|
94
97
|
require_relative "clacky/utils/limit_stack"
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Install Node.js (via mise) and chrome-devtools-mcp for browser automation.
|
|
3
|
+
# This script is copied to ~/.clacky/scripts/ on first run and invoked by
|
|
4
|
+
# the browser-setup skill when chrome-devtools-mcp is not yet installed.
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
RED='\033[0;31m'
|
|
9
|
+
GREEN='\033[0;32m'
|
|
10
|
+
YELLOW='\033[1;33m'
|
|
11
|
+
BLUE='\033[0;34m'
|
|
12
|
+
NC='\033[0m'
|
|
13
|
+
|
|
14
|
+
print_info() { echo -e "${BLUE}ℹ${NC} $1"; }
|
|
15
|
+
print_success() { echo -e "${GREEN}✓${NC} $1"; }
|
|
16
|
+
print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
|
17
|
+
print_error() { echo -e "${RED}✗${NC} $1"; }
|
|
18
|
+
print_step() { echo -e "\n${BLUE}==>${NC} $1"; }
|
|
19
|
+
|
|
20
|
+
command_exists() { command -v "$1" >/dev/null 2>&1; }
|
|
21
|
+
|
|
22
|
+
# --------------------------------------------------------------------------
|
|
23
|
+
# Network region detection (quick — only probes google + baidu)
|
|
24
|
+
# --------------------------------------------------------------------------
|
|
25
|
+
SLOW_THRESHOLD_MS=5000
|
|
26
|
+
USE_CN_MIRRORS=false
|
|
27
|
+
DEFAULT_NPM_REGISTRY="https://registry.npmjs.org"
|
|
28
|
+
CN_NPM_REGISTRY="https://registry.npmmirror.com"
|
|
29
|
+
CN_NODE_MIRROR_URL="https://cdn.npmmirror.com/binaries/node/"
|
|
30
|
+
DEFAULT_MISE_INSTALL_URL="https://mise.run"
|
|
31
|
+
CN_MISE_INSTALL_URL="https://oss.1024code.com/mise.sh"
|
|
32
|
+
MISE_INSTALL_URL="$DEFAULT_MISE_INSTALL_URL"
|
|
33
|
+
NPM_REGISTRY_URL="$DEFAULT_NPM_REGISTRY"
|
|
34
|
+
NODE_MIRROR_URL=""
|
|
35
|
+
|
|
36
|
+
_probe_url() {
|
|
37
|
+
local url="$1"
|
|
38
|
+
local out
|
|
39
|
+
out=$(curl -s -o /dev/null -w "%{http_code} %{time_total}" \
|
|
40
|
+
--connect-timeout 5 --max-time 5 "$url" 2>/dev/null) || true
|
|
41
|
+
local http_code="${out%% *}"
|
|
42
|
+
local total_time="${out#* }"
|
|
43
|
+
if [ -z "$http_code" ] || [ "$http_code" = "000" ] || [ "$http_code" = "$out" ]; then
|
|
44
|
+
echo "timeout"; return
|
|
45
|
+
fi
|
|
46
|
+
awk -v s="$total_time" 'BEGIN { printf "%d", s * 1000 }'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_is_slow() {
|
|
50
|
+
local r="$1"
|
|
51
|
+
[ "$r" = "timeout" ] && return 0
|
|
52
|
+
[ "$r" -ge "$SLOW_THRESHOLD_MS" ] 2>/dev/null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
detect_network_region() {
|
|
56
|
+
print_step "Detecting network region..."
|
|
57
|
+
local google baidu
|
|
58
|
+
google=$(_probe_url "https://www.google.com")
|
|
59
|
+
baidu=$(_probe_url "https://www.baidu.com")
|
|
60
|
+
|
|
61
|
+
if ! _is_slow "$google"; then
|
|
62
|
+
print_info "Region: global"
|
|
63
|
+
elif ! _is_slow "$baidu"; then
|
|
64
|
+
print_info "Region: china — switching to CN mirrors"
|
|
65
|
+
USE_CN_MIRRORS=true
|
|
66
|
+
MISE_INSTALL_URL="$CN_MISE_INSTALL_URL"
|
|
67
|
+
NPM_REGISTRY_URL="$CN_NPM_REGISTRY"
|
|
68
|
+
NODE_MIRROR_URL="$CN_NODE_MIRROR_URL"
|
|
69
|
+
else
|
|
70
|
+
print_warning "Region: unknown — using global defaults"
|
|
71
|
+
fi
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# --------------------------------------------------------------------------
|
|
75
|
+
# Ensure mise is available
|
|
76
|
+
# --------------------------------------------------------------------------
|
|
77
|
+
_mise_bin() {
|
|
78
|
+
if command_exists mise; then echo "mise"
|
|
79
|
+
elif [ -x "$HOME/.local/bin/mise" ]; then echo "$HOME/.local/bin/mise"
|
|
80
|
+
else echo ""
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ensure_mise() {
|
|
85
|
+
local mise
|
|
86
|
+
mise=$(_mise_bin)
|
|
87
|
+
if [ -n "$mise" ]; then
|
|
88
|
+
print_success "mise already installed"
|
|
89
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
90
|
+
eval "$("$mise" activate bash 2>/dev/null)" 2>/dev/null || true
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
print_info "Installing mise..."
|
|
95
|
+
if curl -fsSL "$MISE_INSTALL_URL" | sh; then
|
|
96
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
97
|
+
eval "$(~/.local/bin/mise activate bash 2>/dev/null)" 2>/dev/null || true
|
|
98
|
+
print_success "mise installed"
|
|
99
|
+
else
|
|
100
|
+
print_warning "mise install failed — will rely on system Node if available"
|
|
101
|
+
return 1
|
|
102
|
+
fi
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# --------------------------------------------------------------------------
|
|
106
|
+
# Ensure Node.js >= 20 via mise
|
|
107
|
+
# --------------------------------------------------------------------------
|
|
108
|
+
ensure_node() {
|
|
109
|
+
print_step "Checking Node.js..."
|
|
110
|
+
|
|
111
|
+
# Already have a good node?
|
|
112
|
+
if command_exists node; then
|
|
113
|
+
local ver
|
|
114
|
+
ver=$(node --version 2>/dev/null | sed 's/v//')
|
|
115
|
+
local major="${ver%%.*}"
|
|
116
|
+
if [ "${major:-0}" -ge 20 ] 2>/dev/null; then
|
|
117
|
+
print_success "Node.js v${ver} — OK"
|
|
118
|
+
return 0
|
|
119
|
+
else
|
|
120
|
+
print_warning "Node.js v${ver} is too old (need >=20), will install via mise"
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Install via mise
|
|
125
|
+
if ! ensure_mise; then
|
|
126
|
+
print_error "Cannot install Node.js: mise unavailable and no suitable node found"
|
|
127
|
+
return 1
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
local mise
|
|
131
|
+
mise=$(_mise_bin)
|
|
132
|
+
|
|
133
|
+
if [ "$USE_CN_MIRRORS" = true ] && [ -n "$NODE_MIRROR_URL" ]; then
|
|
134
|
+
"$mise" settings node.mirror_url="$NODE_MIRROR_URL" 2>/dev/null || true
|
|
135
|
+
print_info "Node mirror → ${NODE_MIRROR_URL}"
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
print_info "Installing Node.js 22 via mise..."
|
|
139
|
+
"$mise" install node@22 >/dev/null 2>&1 || true
|
|
140
|
+
"$mise" use -g node@22 >/dev/null 2>&1 || true
|
|
141
|
+
eval "$("$mise" activate bash 2>/dev/null)" 2>/dev/null || true
|
|
142
|
+
|
|
143
|
+
if command_exists node; then
|
|
144
|
+
print_success "Node.js $(node --version) installed"
|
|
145
|
+
else
|
|
146
|
+
print_error "Node.js installation failed"
|
|
147
|
+
return 1
|
|
148
|
+
fi
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# --------------------------------------------------------------------------
|
|
152
|
+
# Install / update chrome-devtools-mcp
|
|
153
|
+
# --------------------------------------------------------------------------
|
|
154
|
+
install_chrome_devtools_mcp() {
|
|
155
|
+
print_step "Installing chrome-devtools-mcp..."
|
|
156
|
+
|
|
157
|
+
# Set npm registry
|
|
158
|
+
if [ "$USE_CN_MIRRORS" = true ]; then
|
|
159
|
+
npm config set registry "$NPM_REGISTRY_URL" 2>/dev/null || true
|
|
160
|
+
print_info "npm registry → ${NPM_REGISTRY_URL}"
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
if npm install -g chrome-devtools-mcp@latest 2>/dev/null; then
|
|
164
|
+
print_success "chrome-devtools-mcp $(chrome-devtools-mcp --version 2>/dev/null) installed"
|
|
165
|
+
else
|
|
166
|
+
print_error "chrome-devtools-mcp installation failed"
|
|
167
|
+
print_info "Try manually: npm install -g chrome-devtools-mcp@latest"
|
|
168
|
+
return 1
|
|
169
|
+
fi
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# --------------------------------------------------------------------------
|
|
173
|
+
# Main
|
|
174
|
+
# --------------------------------------------------------------------------
|
|
175
|
+
main() {
|
|
176
|
+
echo ""
|
|
177
|
+
echo "Browser Automation Setup"
|
|
178
|
+
echo "========================"
|
|
179
|
+
|
|
180
|
+
detect_network_region
|
|
181
|
+
ensure_node || exit 1
|
|
182
|
+
install_chrome_devtools_mcp || exit 1
|
|
183
|
+
|
|
184
|
+
echo ""
|
|
185
|
+
print_success "Done. Browser automation is ready."
|
|
186
|
+
echo ""
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
main "$@"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openclacky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.22
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
@@ -448,6 +448,7 @@ files:
|
|
|
448
448
|
- lib/clacky/ui2/view_renderer.rb
|
|
449
449
|
- lib/clacky/ui_interface.rb
|
|
450
450
|
- lib/clacky/utils/arguments_parser.rb
|
|
451
|
+
- lib/clacky/utils/browser_detector.rb
|
|
451
452
|
- lib/clacky/utils/encoding.rb
|
|
452
453
|
- lib/clacky/utils/environment_detector.rb
|
|
453
454
|
- lib/clacky/utils/file_ignore_helper.rb
|
|
@@ -458,6 +459,7 @@ files:
|
|
|
458
459
|
- lib/clacky/utils/model_pricing.rb
|
|
459
460
|
- lib/clacky/utils/parser_manager.rb
|
|
460
461
|
- lib/clacky/utils/path_helper.rb
|
|
462
|
+
- lib/clacky/utils/scripts_manager.rb
|
|
461
463
|
- lib/clacky/utils/string_matcher.rb
|
|
462
464
|
- lib/clacky/utils/trash_directory.rb
|
|
463
465
|
- lib/clacky/utils/workspace_rules.rb
|
|
@@ -482,6 +484,7 @@ files:
|
|
|
482
484
|
- lib/clacky/web/ws.js
|
|
483
485
|
- scripts/install.ps1
|
|
484
486
|
- scripts/install.sh
|
|
487
|
+
- scripts/install_browser.sh
|
|
485
488
|
- scripts/install_full.sh
|
|
486
489
|
- scripts/uninstall.sh
|
|
487
490
|
- sig/clacky.rbs
|