openclacky 1.2.1 → 1.2.3
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 +17 -0
- data/lib/clacky/agent/cost_tracker.rb +2 -0
- data/lib/clacky/agent.rb +12 -2
- data/lib/clacky/client.rb +2 -0
- data/lib/clacky/default_skills/browser-setup/SKILL.md +90 -0
- data/lib/clacky/providers.rb +4 -9
- data/lib/clacky/telemetry.rb +29 -3
- data/lib/clacky/tools/glob.rb +19 -4
- data/lib/clacky/tools/grep.rb +13 -1
- data/lib/clacky/tools/security.rb +1 -2
- data/lib/clacky/tools/terminal.rb +42 -1
- data/lib/clacky/utils/file_ignore_helper.rb +78 -5
- data/lib/clacky/utils/model_pricing.rb +28 -3
- data/lib/clacky/utils/scripts_manager.rb +1 -0
- data/lib/clacky/version.rb +1 -1
- data/scripts/build/lib/apt.sh +71 -1
- data/scripts/build/src/install.sh.cc +1 -1
- data/scripts/build/src/install_rails_deps.sh.cc +4 -4
- data/scripts/build/src/install_system_deps.sh.cc +1 -1
- data/scripts/install.sh +72 -2
- data/scripts/install_rails_deps.sh +75 -5
- data/scripts/install_system_deps.sh +72 -2
- data/scripts/wsl_network_doctor.ps1 +196 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 39490977253b82c6bf0e0e7c89e3763b86dbe56a46e8c6e716774c06387bc7a7
|
|
4
|
+
data.tar.gz: 25224b037f948e7252467337a4e04051340d4bd7ff75456d7a8653acfc43e94c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ba3260b11c8b7f37c075ebc392567f3b049246932e65e1fc12df99363f86921af28c05e5fd4d46ddb1cd796092fc11315b80ef67937c13b5e6cbb9a276cdc47
|
|
7
|
+
data.tar.gz: 8de209627490544c48f3d07f8b010cc40fe6ef51e597734b61adb7c6285d65d3a309aebebe9f9f563a681db60596b797e5a79d9eaa46c85805b37b75cca9369c
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.3] - 2026-05-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Qwen 3.7 model support
|
|
12
|
+
- WSL network doctor PowerShell script
|
|
13
|
+
- Limit on glob/grep file walker to prevent excessive traversal
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Terminal single-line mode issue
|
|
17
|
+
- Terminal execution hang
|
|
18
|
+
- apt try logic
|
|
19
|
+
|
|
20
|
+
## [1.2.2] - 2026-05-25
|
|
21
|
+
|
|
22
|
+
### More
|
|
23
|
+
- Add telemetry for task cost and model tracking
|
|
24
|
+
|
|
8
25
|
## [1.2.1] - 2026-05-25
|
|
9
26
|
|
|
10
27
|
### Fixed
|
|
@@ -86,6 +86,8 @@ module Clacky
|
|
|
86
86
|
# Track cache usage for current task
|
|
87
87
|
if @task_cache_stats
|
|
88
88
|
@task_cache_stats[:total_requests] += 1
|
|
89
|
+
@task_cache_stats[:prompt_tokens] += usage[:prompt_tokens].to_i
|
|
90
|
+
@task_cache_stats[:completion_tokens] += usage[:completion_tokens].to_i
|
|
89
91
|
|
|
90
92
|
if usage[:cache_creation_input_tokens]
|
|
91
93
|
@task_cache_stats[:cache_creation_input_tokens] += usage[:cache_creation_input_tokens]
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -218,6 +218,11 @@ module Clacky
|
|
|
218
218
|
@config.effective_model_name
|
|
219
219
|
end
|
|
220
220
|
|
|
221
|
+
private def current_provider
|
|
222
|
+
return nil unless @client.respond_to?(:provider_id)
|
|
223
|
+
@client.provider_id
|
|
224
|
+
end
|
|
225
|
+
|
|
221
226
|
# Rename this session. Called by auto-naming (first message) or user explicit rename.
|
|
222
227
|
def rename(new_name)
|
|
223
228
|
@name = new_name.to_s.strip
|
|
@@ -248,6 +253,8 @@ module Clacky
|
|
|
248
253
|
@task_cache_stats = {
|
|
249
254
|
cache_creation_input_tokens: 0,
|
|
250
255
|
cache_read_input_tokens: 0,
|
|
256
|
+
prompt_tokens: 0,
|
|
257
|
+
completion_tokens: 0,
|
|
251
258
|
total_requests: 0,
|
|
252
259
|
cache_hit_requests: 0
|
|
253
260
|
}
|
|
@@ -381,6 +388,7 @@ module Clacky
|
|
|
381
388
|
|
|
382
389
|
@hooks.trigger(:on_start, user_input)
|
|
383
390
|
|
|
391
|
+
result = nil
|
|
384
392
|
begin
|
|
385
393
|
# Track if request_user_feedback was called
|
|
386
394
|
awaiting_user_feedback = false
|
|
@@ -583,7 +591,7 @@ module Clacky
|
|
|
583
591
|
@pending_error_rollback = true if e.is_a?(Clacky::BadRequestError)
|
|
584
592
|
|
|
585
593
|
# Build error result for session data, but let CLI handle error display
|
|
586
|
-
result = build_result(:error, error: e.message)
|
|
594
|
+
result = build_result(:error, error: e.message)
|
|
587
595
|
raise
|
|
588
596
|
ensure
|
|
589
597
|
# Safety net: ensure any lingering progress spinner is stopped.
|
|
@@ -598,7 +606,7 @@ module Clacky
|
|
|
598
606
|
|
|
599
607
|
# Fire-and-forget telemetry after every agent run.
|
|
600
608
|
# Tracks daily active users (distinct devices per day) and task volume.
|
|
601
|
-
Clacky::Telemetry.task!
|
|
609
|
+
Clacky::Telemetry.task!(result: result)
|
|
602
610
|
end
|
|
603
611
|
end
|
|
604
612
|
|
|
@@ -1117,6 +1125,8 @@ module Clacky
|
|
|
1117
1125
|
{
|
|
1118
1126
|
status: status,
|
|
1119
1127
|
session_id: @session_id,
|
|
1128
|
+
model: current_model,
|
|
1129
|
+
provider: current_provider,
|
|
1120
1130
|
iterations: task_iterations,
|
|
1121
1131
|
duration_seconds: Time.now - @start_time,
|
|
1122
1132
|
total_cost_usd: task_cost.round(4),
|
data/lib/clacky/client.rb
CHANGED
|
@@ -111,6 +111,96 @@ 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
|
+
|
|
114
204
|
### Step 3 — Verify Chrome/Edge is running with remote debugging
|
|
115
205
|
|
|
116
206
|
**CRITICAL**: Do NOT attempt `browser()` calls yet. First check if the browser is reachable using the API:
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -335,28 +335,23 @@ module Clacky
|
|
|
335
335
|
"name" => "Qwen (Alibaba)",
|
|
336
336
|
"base_url" => "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
337
337
|
"api" => "openai-completions",
|
|
338
|
-
"default_model" => "qwen3.
|
|
338
|
+
"default_model" => "qwen3.7-max",
|
|
339
339
|
"models" => [
|
|
340
|
+
"qwen3.7-max",
|
|
340
341
|
"qwen3.6-plus",
|
|
341
342
|
"qwen3.6-max",
|
|
342
343
|
"qwen3.6-27b",
|
|
343
344
|
"qwen3.6-flash",
|
|
344
345
|
"qwen-plus-latest",
|
|
345
|
-
"qwen-vl-plus",
|
|
346
|
-
"qwen-vl-max"
|
|
347
346
|
],
|
|
348
347
|
"endpoint_variants" => [
|
|
349
348
|
{ "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://dashscope.aliyuncs.com/compatible-mode/v1", "region" => "cn" }.freeze,
|
|
350
349
|
{ "label" => "Singapore", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "region" => "intl" }.freeze,
|
|
351
350
|
{ "label" => "US (Virginia)", "label_key" => "settings.models.baseurl.variant.us", "base_url" => "https://dashscope-us.aliyuncs.com/compatible-mode/v1", "region" => "us" }.freeze
|
|
352
351
|
].freeze,
|
|
353
|
-
"capabilities" => { "vision" =>
|
|
354
|
-
"model_capabilities" => {
|
|
355
|
-
"qwen3.6-27b" => { "vision" => true }.freeze,
|
|
356
|
-
"qwen-vl-plus" => { "vision" => true }.freeze,
|
|
357
|
-
"qwen-vl-max" => { "vision" => true }.freeze
|
|
358
|
-
}.freeze,
|
|
352
|
+
"capabilities" => { "vision" => true }.freeze,
|
|
359
353
|
"lite_models" => {
|
|
354
|
+
"qwen3.7-max" => "qwen3.6-flash",
|
|
360
355
|
"qwen3.6-plus" => "qwen3.6-flash",
|
|
361
356
|
"qwen3.6-max" => "qwen3.6-flash",
|
|
362
357
|
"qwen3.6-27b" => "qwen3.6-flash",
|
data/lib/clacky/telemetry.rb
CHANGED
|
@@ -46,7 +46,10 @@ module Clacky
|
|
|
46
46
|
# Tracks usage activity and daily task volume.
|
|
47
47
|
# No client-side dedup — the server keeps every event for task counting,
|
|
48
48
|
# and derives DAU from distinct devices per day.
|
|
49
|
-
|
|
49
|
+
#
|
|
50
|
+
# @param result [Hash, nil] optional build_result hash from Agent#run.
|
|
51
|
+
# When present, enriches the payload with model/provider/tokens/cost/duration/status.
|
|
52
|
+
def task!(result: nil)
|
|
50
53
|
return unless enabled?
|
|
51
54
|
|
|
52
55
|
brand = Clacky::BrandConfig.load
|
|
@@ -54,9 +57,10 @@ module Clacky
|
|
|
54
57
|
device_id: resolve_device_id(brand),
|
|
55
58
|
version: Clacky::VERSION,
|
|
56
59
|
brand: brand.branded? ? brand.package_name : nil
|
|
57
|
-
}
|
|
60
|
+
}
|
|
61
|
+
payload.merge!(extract_task_metrics(result)) if result.is_a?(Hash)
|
|
58
62
|
|
|
59
|
-
fire_and_forget("/api/v1/telemetry/task", payload)
|
|
63
|
+
fire_and_forget("/api/v1/telemetry/task", payload.compact)
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
# ── private helpers ────────────────────────────────────────────────
|
|
@@ -70,6 +74,28 @@ module Clacky
|
|
|
70
74
|
brand.device_id
|
|
71
75
|
end
|
|
72
76
|
|
|
77
|
+
private def extract_task_metrics(result)
|
|
78
|
+
cache = result[:cache_stats] || {}
|
|
79
|
+
duration = result[:duration_seconds]
|
|
80
|
+
error = result[:error]
|
|
81
|
+
{
|
|
82
|
+
model: result[:model],
|
|
83
|
+
provider: result[:provider],
|
|
84
|
+
status: result[:status],
|
|
85
|
+
iterations: result[:iterations],
|
|
86
|
+
duration_ms: duration ? (duration * 1000).round : nil,
|
|
87
|
+
cost_usd: result[:total_cost_usd],
|
|
88
|
+
cost_source: result[:cost_source],
|
|
89
|
+
prompt_tokens: cache[:prompt_tokens],
|
|
90
|
+
completion_tokens: cache[:completion_tokens],
|
|
91
|
+
cache_creation_tokens: cache[:cache_creation_input_tokens],
|
|
92
|
+
cache_read_tokens: cache[:cache_read_input_tokens],
|
|
93
|
+
cache_total_requests: cache[:total_requests],
|
|
94
|
+
cache_hit_requests: cache[:cache_hit_requests],
|
|
95
|
+
error_kind: error.is_a?(String) ? error[0, 64] : error&.class&.name
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
73
99
|
# Send a POST to the telemetry endpoint in a background thread.
|
|
74
100
|
# Fire-and-forget: no retry, no error surfacing, no blocking.
|
|
75
101
|
#
|
data/lib/clacky/tools/glob.rb
CHANGED
|
@@ -49,6 +49,14 @@ module Clacky
|
|
|
49
49
|
return { error: "Base path does not exist: #{base_path}" }
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
if Clacky::Utils::FileIgnoreHelper.dangerous_root?(base_path)
|
|
53
|
+
return {
|
|
54
|
+
error: "Refusing to recursively glob from broad path '#{base_path}'. " \
|
|
55
|
+
"Narrow base_path to a specific subdirectory, " \
|
|
56
|
+
"or use '.' to search the working directory."
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
52
60
|
begin
|
|
53
61
|
expanded_path = base_path
|
|
54
62
|
|
|
@@ -70,7 +78,8 @@ module Clacky
|
|
|
70
78
|
fnmatch_flags = File::FNM_PATHNAME | File::FNM_DOTMATCH
|
|
71
79
|
|
|
72
80
|
matches = []
|
|
73
|
-
|
|
81
|
+
walk_status = {}
|
|
82
|
+
Clacky::Utils::FileIgnoreHelper.walk_files(expanded_path, skipped: skipped, status: walk_status) do |file|
|
|
74
83
|
relative = file[(expanded_path.length + 1)..]
|
|
75
84
|
|
|
76
85
|
unless File.fnmatch(effective_pattern, relative, fnmatch_flags)
|
|
@@ -101,11 +110,13 @@ module Clacky
|
|
|
101
110
|
# Convert to absolute paths
|
|
102
111
|
matches = matches.map { |path| File.expand_path(path) }
|
|
103
112
|
|
|
113
|
+
walk_truncated = walk_status[:truncated] == true
|
|
104
114
|
{
|
|
105
115
|
matches: matches,
|
|
106
116
|
total_matches: total_matches,
|
|
107
117
|
returned: matches.length,
|
|
108
|
-
truncated: total_matches > limit,
|
|
118
|
+
truncated: total_matches > limit || walk_truncated,
|
|
119
|
+
truncation_reason: walk_status[:truncation_reason],
|
|
109
120
|
skipped_files: skipped,
|
|
110
121
|
error: nil
|
|
111
122
|
}
|
|
@@ -129,9 +140,13 @@ module Clacky
|
|
|
129
140
|
count = result[:returned] || 0
|
|
130
141
|
total = result[:total_matches] || 0
|
|
131
142
|
truncated = result[:truncated] ? " (truncated)" : ""
|
|
132
|
-
|
|
143
|
+
|
|
133
144
|
msg = "[OK] Found #{count}/#{total} files#{truncated}"
|
|
134
|
-
|
|
145
|
+
|
|
146
|
+
if result[:truncation_reason]
|
|
147
|
+
msg += " [walk #{result[:truncation_reason]}]"
|
|
148
|
+
end
|
|
149
|
+
|
|
135
150
|
# Add skipped files info if present
|
|
136
151
|
if result[:skipped_files]
|
|
137
152
|
skipped = result[:skipped_files]
|
data/lib/clacky/tools/grep.rb
CHANGED
|
@@ -92,6 +92,14 @@ module Clacky
|
|
|
92
92
|
return { error: "Path does not exist: #{path}" }
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
if File.directory?(expanded_path) && Clacky::Utils::FileIgnoreHelper.dangerous_root?(expanded_path)
|
|
96
|
+
return {
|
|
97
|
+
error: "Refusing to recursively grep from broad path '#{path}'. " \
|
|
98
|
+
"Narrow the path to a specific subdirectory, " \
|
|
99
|
+
"or use '.' to search the working directory."
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
95
103
|
# Limit context_lines
|
|
96
104
|
context_lines = [[context_lines, 0].max, 10].min
|
|
97
105
|
|
|
@@ -115,10 +123,14 @@ module Clacky
|
|
|
115
123
|
else
|
|
116
124
|
fnmatch_flags = File::FNM_PATHNAME | File::FNM_DOTMATCH
|
|
117
125
|
collected = []
|
|
118
|
-
|
|
126
|
+
walk_status = {}
|
|
127
|
+
Clacky::Utils::FileIgnoreHelper.walk_files(expanded_path, skipped: skipped, status: walk_status) do |f|
|
|
119
128
|
relative = f[(expanded_path.length + 1)..]
|
|
120
129
|
collected << f if File.fnmatch(file_pattern, relative, fnmatch_flags)
|
|
121
130
|
end
|
|
131
|
+
if walk_status[:truncated]
|
|
132
|
+
truncation_reason ||= "walk #{walk_status[:truncation_reason]}"
|
|
133
|
+
end
|
|
122
134
|
collected
|
|
123
135
|
end
|
|
124
136
|
|
|
@@ -20,7 +20,7 @@ module Clacky
|
|
|
20
20
|
# to a shell / PTY for execution):
|
|
21
21
|
#
|
|
22
22
|
# 1. Block hard-dangerous commands: sudo, pkill clacky, eval, exec,
|
|
23
|
-
# `...`,
|
|
23
|
+
# `...`, | sh, | bash,
|
|
24
24
|
# redirect to /etc /usr /bin.
|
|
25
25
|
# 2. Rewrite `curl ... | bash` → save script to a file for manual
|
|
26
26
|
# review instead of exec.
|
|
@@ -246,7 +246,6 @@ module Clacky
|
|
|
246
246
|
/exec\s*\(/,
|
|
247
247
|
/system\s*\(/,
|
|
248
248
|
/`[^`]+`/,
|
|
249
|
-
/\$\([^)]+\)/,
|
|
250
249
|
/\|\s*sh\s*$/,
|
|
251
250
|
/\|\s*bash\s*$/,
|
|
252
251
|
/>\s*\/etc\//,
|
|
@@ -72,6 +72,9 @@ module Clacky
|
|
|
72
72
|
{session_id, input:"pw\n"} reply to prompt / poll (input:"")
|
|
73
73
|
{session_id, kill:true} stop
|
|
74
74
|
|
|
75
|
+
Single-line only. For multi-line scripts (heredoc, loops, multi-statement blocks)
|
|
76
|
+
write a file first, then run it: write(path:"/tmp/run.sh", content:...) → terminal(command:"bash /tmp/run.sh").
|
|
77
|
+
|
|
75
78
|
Response: exit_code = done; session_id = running (state: waiting/background/timeout).
|
|
76
79
|
If output exceeds the limit, `output` is truncated and `full_output_file` points
|
|
77
80
|
at a file on disk — use terminal(command: "grep ... <path>") to search it.
|
|
@@ -228,6 +231,17 @@ module Clacky
|
|
|
228
231
|
|
|
229
232
|
# Start a new command
|
|
230
233
|
if command && !command.to_s.strip.empty?
|
|
234
|
+
if multiline_command?(command)
|
|
235
|
+
return {
|
|
236
|
+
error: "Multi-line commands are unreliable in our PTY shell " \
|
|
237
|
+
"(heredocs / unclosed quotes / multi-line blocks can hang the session).",
|
|
238
|
+
hint: "Write the script to a file first, then execute it. " \
|
|
239
|
+
"Example: 1) write(path: \"/tmp/run.sh\", content: \"...\") " \
|
|
240
|
+
"2) terminal(command: \"bash /tmp/run.sh\")",
|
|
241
|
+
multiline_blocked: true
|
|
242
|
+
}
|
|
243
|
+
end
|
|
244
|
+
|
|
231
245
|
return do_start(command.to_s, cwd: cwd, env: env, timeout: timeout,
|
|
232
246
|
idle_ms: idle_ms, background: background ? true : false)
|
|
233
247
|
end
|
|
@@ -325,6 +339,12 @@ module Clacky
|
|
|
325
339
|
project_root: cwd || Dir.pwd
|
|
326
340
|
)
|
|
327
341
|
|
|
342
|
+
# WSL interop fix: Windows .exe processes inherit the PTY's stdin fd
|
|
343
|
+
# and attempt to use it as a Windows Console, causing them to hang
|
|
344
|
+
# indefinitely. Redirect stdin from /dev/null for any .exe invocation
|
|
345
|
+
# that doesn't already have an explicit stdin redirect.
|
|
346
|
+
safe_command = redirect_exe_stdin(safe_command)
|
|
347
|
+
|
|
328
348
|
# Background / dedicated path — never reuse the persistent shell,
|
|
329
349
|
# because these commands stay running and would occupy the slot.
|
|
330
350
|
if background
|
|
@@ -768,7 +788,7 @@ module Clacky
|
|
|
768
788
|
|
|
769
789
|
spawn_env = {
|
|
770
790
|
"TERM" => "xterm-256color",
|
|
771
|
-
"PS1" => "",
|
|
791
|
+
"PS1" => " ",
|
|
772
792
|
# Prevent our sub-shell from polluting the user's ~/.zsh_history
|
|
773
793
|
# (or ~/.bash_history). We fork a full interactive login shell to
|
|
774
794
|
# get rbenv/nvm/brew-shellenv/mise loaded, but every command we
|
|
@@ -1179,6 +1199,17 @@ module Clacky
|
|
|
1179
1199
|
SLOW_COMMAND_PATTERNS.any? { |pat| s.include?(pat) }
|
|
1180
1200
|
end
|
|
1181
1201
|
|
|
1202
|
+
# True when `command` spans multiple lines. Trailing newlines are
|
|
1203
|
+
# ignored — a single-line command terminated with "\n" is still
|
|
1204
|
+
# single-line. Multi-line commands frequently hang the persistent
|
|
1205
|
+
# PTY shell (incomplete heredoc, unclosed quote, multi-line block
|
|
1206
|
+
# without closer) — the agent should write a script file and
|
|
1207
|
+
# invoke it instead.
|
|
1208
|
+
private def multiline_command?(command)
|
|
1209
|
+
return false if command.nil?
|
|
1210
|
+
command.to_s.sub(/\n+\z/, "").include?("\n")
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1182
1213
|
# Apply per-line truncation to a cleaned (post-OutputCleaner) string.
|
|
1183
1214
|
# If any single line exceeds MAX_LINE_CHARS, we chop it at that length
|
|
1184
1215
|
# and append `…[line truncated: <original> chars]` so the LLM knows
|
|
@@ -1358,6 +1389,16 @@ module Clacky
|
|
|
1358
1389
|
return "" if lines.empty?
|
|
1359
1390
|
lines.last(DISPLAY_TAIL_LINES).join("\n")
|
|
1360
1391
|
end
|
|
1392
|
+
|
|
1393
|
+
# WSL interop fix: Windows .exe processes inherit the PTY stdin fd
|
|
1394
|
+
# and try to use it as a Windows Console, which hangs indefinitely.
|
|
1395
|
+
# Detect .exe invocations and redirect stdin from /dev/null unless
|
|
1396
|
+
# the command already has an explicit stdin redirect.
|
|
1397
|
+
private def redirect_exe_stdin(command)
|
|
1398
|
+
return command unless command =~ /\.exe\b/i
|
|
1399
|
+
return command if command =~ /<\s*[^\s|&;]/
|
|
1400
|
+
"#{command} </dev/null"
|
|
1401
|
+
end
|
|
1361
1402
|
end
|
|
1362
1403
|
end
|
|
1363
1404
|
end
|
|
@@ -116,21 +116,94 @@ module Clacky
|
|
|
116
116
|
CONFIG_FILE_PATTERNS.any? { |pattern| file.match?(pattern) }
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
# Paths considered too broad to recursively walk by default. Searching from
|
|
120
|
+
# these would commonly traverse millions of files (system roots, $HOME with
|
|
121
|
+
# many workspaces, WSL Windows mounts). Tools should refuse such requests
|
|
122
|
+
# and ask for a narrower base_path.
|
|
123
|
+
def self.dangerous_root?(path)
|
|
124
|
+
return false if path.nil? || path.empty?
|
|
125
|
+
|
|
126
|
+
expanded = File.expand_path(path)
|
|
127
|
+
return true if expanded == "/"
|
|
128
|
+
|
|
129
|
+
system_roots = ["/root", "/home", "/Users", "/mnt", "/media", "/var", "/etc", "/usr", "/opt"]
|
|
130
|
+
return true if system_roots.include?(expanded)
|
|
131
|
+
|
|
132
|
+
["/Users/", "/home/"].each do |prefix|
|
|
133
|
+
next unless expanded.start_with?(prefix)
|
|
134
|
+
tail = expanded[prefix.length..]
|
|
135
|
+
return true if tail && !tail.empty? && !tail.include?("/")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
return true if expanded =~ %r{\A/mnt/[a-zA-Z]\z}
|
|
139
|
+
|
|
140
|
+
home = ENV["HOME"]
|
|
141
|
+
return true if home && !home.empty? && expanded == File.expand_path(home)
|
|
142
|
+
|
|
143
|
+
false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Hard ceiling on directories visited in a single walk. Prevents indefinite
|
|
147
|
+
# traversal across huge trees (e.g. /root, $HOME, /mnt/c on WSL).
|
|
148
|
+
MAX_DIRS_VISITED = 20_000
|
|
149
|
+
|
|
150
|
+
# Wall-clock budget for a single walk, in seconds.
|
|
151
|
+
WALK_TIMEOUT_SECONDS = 15
|
|
152
|
+
|
|
153
|
+
# Raised internally to abort a walk when a budget is exhausted.
|
|
154
|
+
class WalkBudgetExceeded < StandardError
|
|
155
|
+
attr_reader :reason
|
|
156
|
+
def initialize(reason)
|
|
157
|
+
@reason = reason
|
|
158
|
+
super(reason.to_s)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
119
162
|
# Walk a directory tree, pruning ignored directories early.
|
|
120
163
|
# Yields each non-ignored file path. Supports nested .gitignore files.
|
|
121
164
|
# @param skipped [Hash, nil] If provided, increments :ignored for each gitignore-skipped entry.
|
|
122
|
-
|
|
123
|
-
|
|
165
|
+
# @param status [Hash, nil] If provided, populated with :truncated and :truncation_reason
|
|
166
|
+
# when the walk is aborted due to dir-count or wall-clock budget.
|
|
167
|
+
def self.walk_files(base_path, gitignore: nil, skipped: nil, status: nil,
|
|
168
|
+
max_dirs_visited: MAX_DIRS_VISITED,
|
|
169
|
+
timeout_seconds: WALK_TIMEOUT_SECONDS,
|
|
170
|
+
&block)
|
|
171
|
+
unless block_given?
|
|
172
|
+
return enum_for(:walk_files, base_path,
|
|
173
|
+
gitignore: gitignore, skipped: skipped, status: status,
|
|
174
|
+
max_dirs_visited: max_dirs_visited, timeout_seconds: timeout_seconds)
|
|
175
|
+
end
|
|
124
176
|
|
|
125
177
|
root_gitignore = gitignore || begin
|
|
126
178
|
gi_path = find_gitignore(base_path)
|
|
127
179
|
gi_path ? Clacky::GitignoreParser.new(gi_path) : nil
|
|
128
180
|
end
|
|
129
181
|
|
|
130
|
-
|
|
182
|
+
budget = {
|
|
183
|
+
dirs_visited: 0,
|
|
184
|
+
max_dirs: max_dirs_visited,
|
|
185
|
+
deadline: timeout_seconds ? (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_seconds) : nil
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
begin
|
|
189
|
+
_walk_recursive(base_path, base_path, root_gitignore, skipped, budget, &block)
|
|
190
|
+
rescue WalkBudgetExceeded => e
|
|
191
|
+
if status
|
|
192
|
+
status[:truncated] = true
|
|
193
|
+
status[:truncation_reason] = e.reason.to_s
|
|
194
|
+
end
|
|
195
|
+
end
|
|
131
196
|
end
|
|
132
197
|
|
|
133
|
-
def self._walk_recursive(dir, base_path, gitignore, skipped, &block)
|
|
198
|
+
def self._walk_recursive(dir, base_path, gitignore, skipped, budget, &block)
|
|
199
|
+
budget[:dirs_visited] += 1
|
|
200
|
+
if budget[:dirs_visited] > budget[:max_dirs]
|
|
201
|
+
raise WalkBudgetExceeded.new(:max_dirs_visited)
|
|
202
|
+
end
|
|
203
|
+
if budget[:deadline] && Process.clock_gettime(Process::CLOCK_MONOTONIC) > budget[:deadline]
|
|
204
|
+
raise WalkBudgetExceeded.new(:timeout)
|
|
205
|
+
end
|
|
206
|
+
|
|
134
207
|
child_gitignore_path = File.join(dir, ".gitignore")
|
|
135
208
|
if dir != base_path && File.exist?(child_gitignore_path)
|
|
136
209
|
gitignore ||= Clacky::GitignoreParser.new(nil)
|
|
@@ -153,7 +226,7 @@ module Clacky
|
|
|
153
226
|
if gitignore&.ignored?("#{relative}/") || should_ignore_file?(full, base_path, gitignore)
|
|
154
227
|
next
|
|
155
228
|
end
|
|
156
|
-
_walk_recursive(full, base_path, gitignore, skipped, &block)
|
|
229
|
+
_walk_recursive(full, base_path, gitignore, skipped, budget, &block)
|
|
157
230
|
else
|
|
158
231
|
if !is_config_file?(full) && should_ignore_file?(full, base_path, gitignore)
|
|
159
232
|
skipped[:ignored] += 1 if skipped
|
|
@@ -366,6 +366,24 @@ module Clacky
|
|
|
366
366
|
# surprising users with the explicit 25% surcharge).
|
|
367
367
|
# - We bill reads at 20% (implicit rate) — the conservative side; users on
|
|
368
368
|
# explicit caching will see real bills slightly *lower* than displayed.
|
|
369
|
+
"qwen3.7-max" => {
|
|
370
|
+
input: { default: 1.20, over_200k: 1.20 },
|
|
371
|
+
output: { default: 6.00, over_200k: 6.00 },
|
|
372
|
+
cache: { write: 1.20, read: 0.24 }
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
"qwen3.7-plus" => {
|
|
376
|
+
input: { default: 0.40, over_200k: 0.40 },
|
|
377
|
+
output: { default: 2.40, over_200k: 2.40 },
|
|
378
|
+
cache: { write: 0.40, read: 0.08 }
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
"qwen3.7-flash" => {
|
|
382
|
+
input: { default: 0.15, over_200k: 0.15 },
|
|
383
|
+
output: { default: 0.90, over_200k: 0.90 },
|
|
384
|
+
cache: { write: 0.15, read: 0.03 }
|
|
385
|
+
},
|
|
386
|
+
|
|
369
387
|
"qwen3.6-plus" => {
|
|
370
388
|
input: { default: 0.40, over_200k: 0.40 },
|
|
371
389
|
output: { default: 2.40, over_200k: 2.40 },
|
|
@@ -571,9 +589,16 @@ module Clacky
|
|
|
571
589
|
"minimax-m2.7"
|
|
572
590
|
|
|
573
591
|
# Qwen (Alibaba DashScope) — strict anchored match per registered
|
|
574
|
-
# model id in providers.rb. qwen3.
|
|
575
|
-
# qwen-plus-latest is the
|
|
576
|
-
# release; qwen-vl-* are
|
|
592
|
+
# model id in providers.rb. qwen3.7-* is the latest flagship line;
|
|
593
|
+
# qwen3.6-* are the previous generation; qwen-plus-latest is the
|
|
594
|
+
# rolling alias for the latest Qwen-Plus release; qwen-vl-* are
|
|
595
|
+
# the multimodal SKUs.
|
|
596
|
+
when /^qwen3\.7-max$/i
|
|
597
|
+
"qwen3.7-max"
|
|
598
|
+
when /^qwen3\.7-plus$/i
|
|
599
|
+
"qwen3.7-plus"
|
|
600
|
+
when /^qwen3\.7-flash$/i
|
|
601
|
+
"qwen3.7-flash"
|
|
577
602
|
when /^qwen3\.6-plus$/i
|
|
578
603
|
"qwen3.6-plus"
|
|
579
604
|
when /^qwen3\.6-max$/i
|
data/lib/clacky/version.rb
CHANGED
data/scripts/build/lib/apt.sh
CHANGED
|
@@ -4,6 +4,76 @@
|
|
|
4
4
|
# Sets-Vars: (none)
|
|
5
5
|
# Include via: @include lib/apt.sh
|
|
6
6
|
|
|
7
|
+
# Wait until apt/dpkg lock files are no longer held (e.g. by apt-daily on
|
|
8
|
+
# freshly-booted WSL/Ubuntu). Uses flock(1) — the same mechanism apt uses —
|
|
9
|
+
# rather than checking file existence (the lock files are always present;
|
|
10
|
+
# advisory locks live in the kernel, not the filesystem).
|
|
11
|
+
wait_apt_lock() {
|
|
12
|
+
[ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ] || return 0
|
|
13
|
+
|
|
14
|
+
local locks=(
|
|
15
|
+
"/var/lib/dpkg/lock-frontend"
|
|
16
|
+
"/var/lib/dpkg/lock"
|
|
17
|
+
"/var/lib/apt/lists/lock"
|
|
18
|
+
)
|
|
19
|
+
local max_wait="${1:-120}"
|
|
20
|
+
local waited=0
|
|
21
|
+
local announced=false
|
|
22
|
+
|
|
23
|
+
while :; do
|
|
24
|
+
local busy=false
|
|
25
|
+
for f in "${locks[@]}"; do
|
|
26
|
+
[ -e "$f" ] || continue
|
|
27
|
+
if ! sudo flock -n "$f" -c true 2>/dev/null; then
|
|
28
|
+
busy=true
|
|
29
|
+
break
|
|
30
|
+
fi
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
[ "$busy" = false ] && break
|
|
34
|
+
|
|
35
|
+
if [ "$announced" = false ]; then
|
|
36
|
+
print_info "Waiting for system apt/dpkg to finish (up to ${max_wait}s)..."
|
|
37
|
+
announced=true
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ "$waited" -ge "$max_wait" ]; then
|
|
41
|
+
print_error "apt is still locked after ${max_wait}s."
|
|
42
|
+
print_info "On WSL try: 'wsl --shutdown' from PowerShell, then rerun the installer."
|
|
43
|
+
return 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
sleep 3
|
|
47
|
+
waited=$((waited + 3))
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
[ "$announced" = true ] && print_success "apt lock released"
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Run an apt-get subcommand with lock-wait + transient-failure retry.
|
|
55
|
+
# Usage: apt_get_run update [-qq]
|
|
56
|
+
# apt_get_run install -y pkg1 pkg2
|
|
57
|
+
apt_get_run() {
|
|
58
|
+
local attempts=3
|
|
59
|
+
local i=1
|
|
60
|
+
while [ "$i" -le "$attempts" ]; do
|
|
61
|
+
wait_apt_lock 120 || return 1
|
|
62
|
+
if sudo apt-get "$@"; then
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
local rc=$?
|
|
66
|
+
if [ "$i" -lt "$attempts" ]; then
|
|
67
|
+
print_warning "apt-get $1 failed (exit $rc), retrying ($i/$((attempts-1)))..."
|
|
68
|
+
sleep 5
|
|
69
|
+
else
|
|
70
|
+
print_error "apt-get $1 failed after $attempts attempts."
|
|
71
|
+
return "$rc"
|
|
72
|
+
fi
|
|
73
|
+
i=$((i + 1))
|
|
74
|
+
done
|
|
75
|
+
}
|
|
76
|
+
|
|
7
77
|
# Configure apt mirror for CN region and run apt-get update.
|
|
8
78
|
# Guards: only runs on ubuntu/debian ($DISTRO).
|
|
9
79
|
# Relies on $USE_CN_MIRRORS set by detect_network_region (network.sh).
|
|
@@ -51,6 +121,6 @@ EOF
|
|
|
51
121
|
print_info "Region: global — using default apt sources"
|
|
52
122
|
fi
|
|
53
123
|
|
|
54
|
-
|
|
124
|
+
apt_get_run update -qq || return 1
|
|
55
125
|
print_success "apt updated"
|
|
56
126
|
}
|
|
@@ -34,7 +34,7 @@ ensure_ruby() {
|
|
|
34
34
|
|
|
35
35
|
if is_linux_apt; then
|
|
36
36
|
print_info "Installing Ruby via apt..."
|
|
37
|
-
|
|
37
|
+
apt_get_run install -y ruby ruby-dev 2>/dev/null && check_ruby && return 0
|
|
38
38
|
print_warning "apt Ruby install failed or version too old"
|
|
39
39
|
fi
|
|
40
40
|
|
|
@@ -31,8 +31,8 @@ install_ruby() {
|
|
|
31
31
|
if is_macos; then
|
|
32
32
|
brew install openssl@3 libyaml gmp
|
|
33
33
|
elif is_linux_apt; then
|
|
34
|
-
|
|
35
|
-
rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev
|
|
34
|
+
apt_get_run install -y \
|
|
35
|
+
rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev || return 1
|
|
36
36
|
fi
|
|
37
37
|
|
|
38
38
|
ensure_mise || return 1
|
|
@@ -75,8 +75,8 @@ install_postgres() {
|
|
|
75
75
|
|
|
76
76
|
elif is_linux_apt; then
|
|
77
77
|
setup_apt_mirror
|
|
78
|
-
|
|
79
|
-
libssl-dev libreadline-dev zlib1g-dev libyaml-dev libffi-dev
|
|
78
|
+
apt_get_run install -y postgresql libpq-dev \
|
|
79
|
+
libssl-dev libreadline-dev zlib1g-dev libyaml-dev libffi-dev || return 1
|
|
80
80
|
sudo systemctl enable --now postgresql || true
|
|
81
81
|
fi
|
|
82
82
|
|
data/scripts/install.sh
CHANGED
|
@@ -271,6 +271,76 @@ detect_network_region() {
|
|
|
271
271
|
|
|
272
272
|
# ---[ @include lib/apt.sh ]---
|
|
273
273
|
|
|
274
|
+
# Wait until apt/dpkg lock files are no longer held (e.g. by apt-daily on
|
|
275
|
+
# freshly-booted WSL/Ubuntu). Uses flock(1) — the same mechanism apt uses —
|
|
276
|
+
# rather than checking file existence (the lock files are always present;
|
|
277
|
+
# advisory locks live in the kernel, not the filesystem).
|
|
278
|
+
wait_apt_lock() {
|
|
279
|
+
[ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ] || return 0
|
|
280
|
+
|
|
281
|
+
local locks=(
|
|
282
|
+
"/var/lib/dpkg/lock-frontend"
|
|
283
|
+
"/var/lib/dpkg/lock"
|
|
284
|
+
"/var/lib/apt/lists/lock"
|
|
285
|
+
)
|
|
286
|
+
local max_wait="${1:-120}"
|
|
287
|
+
local waited=0
|
|
288
|
+
local announced=false
|
|
289
|
+
|
|
290
|
+
while :; do
|
|
291
|
+
local busy=false
|
|
292
|
+
for f in "${locks[@]}"; do
|
|
293
|
+
[ -e "$f" ] || continue
|
|
294
|
+
if ! sudo flock -n "$f" -c true 2>/dev/null; then
|
|
295
|
+
busy=true
|
|
296
|
+
break
|
|
297
|
+
fi
|
|
298
|
+
done
|
|
299
|
+
|
|
300
|
+
[ "$busy" = false ] && break
|
|
301
|
+
|
|
302
|
+
if [ "$announced" = false ]; then
|
|
303
|
+
print_info "Waiting for system apt/dpkg to finish (up to ${max_wait}s)..."
|
|
304
|
+
announced=true
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
if [ "$waited" -ge "$max_wait" ]; then
|
|
308
|
+
print_error "apt is still locked after ${max_wait}s."
|
|
309
|
+
print_info "On WSL try: 'wsl --shutdown' from PowerShell, then rerun the installer."
|
|
310
|
+
return 1
|
|
311
|
+
fi
|
|
312
|
+
|
|
313
|
+
sleep 3
|
|
314
|
+
waited=$((waited + 3))
|
|
315
|
+
done
|
|
316
|
+
|
|
317
|
+
[ "$announced" = true ] && print_success "apt lock released"
|
|
318
|
+
return 0
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Run an apt-get subcommand with lock-wait + transient-failure retry.
|
|
322
|
+
# Usage: apt_get_run update [-qq]
|
|
323
|
+
# apt_get_run install -y pkg1 pkg2
|
|
324
|
+
apt_get_run() {
|
|
325
|
+
local attempts=3
|
|
326
|
+
local i=1
|
|
327
|
+
while [ "$i" -le "$attempts" ]; do
|
|
328
|
+
wait_apt_lock 120 || return 1
|
|
329
|
+
if sudo apt-get "$@"; then
|
|
330
|
+
return 0
|
|
331
|
+
fi
|
|
332
|
+
local rc=$?
|
|
333
|
+
if [ "$i" -lt "$attempts" ]; then
|
|
334
|
+
print_warning "apt-get $1 failed (exit $rc), retrying ($i/$((attempts-1)))..."
|
|
335
|
+
sleep 5
|
|
336
|
+
else
|
|
337
|
+
print_error "apt-get $1 failed after $attempts attempts."
|
|
338
|
+
return "$rc"
|
|
339
|
+
fi
|
|
340
|
+
i=$((i + 1))
|
|
341
|
+
done
|
|
342
|
+
}
|
|
343
|
+
|
|
274
344
|
# Configure apt mirror for CN region and run apt-get update.
|
|
275
345
|
# Guards: only runs on ubuntu/debian ($DISTRO).
|
|
276
346
|
# Relies on $USE_CN_MIRRORS set by detect_network_region (network.sh).
|
|
@@ -318,7 +388,7 @@ EOF
|
|
|
318
388
|
print_info "Region: global — using default apt sources"
|
|
319
389
|
fi
|
|
320
390
|
|
|
321
|
-
|
|
391
|
+
apt_get_run update -qq || return 1
|
|
322
392
|
print_success "apt updated"
|
|
323
393
|
}
|
|
324
394
|
|
|
@@ -427,7 +497,7 @@ ensure_ruby() {
|
|
|
427
497
|
|
|
428
498
|
if is_linux_apt; then
|
|
429
499
|
print_info "Installing Ruby via apt..."
|
|
430
|
-
|
|
500
|
+
apt_get_run install -y ruby ruby-dev 2>/dev/null && check_ruby && return 0
|
|
431
501
|
print_warning "apt Ruby install failed or version too old"
|
|
432
502
|
fi
|
|
433
503
|
|
|
@@ -275,6 +275,76 @@ detect_network_region() {
|
|
|
275
275
|
|
|
276
276
|
# ---[ @include lib/apt.sh ]---
|
|
277
277
|
|
|
278
|
+
# Wait until apt/dpkg lock files are no longer held (e.g. by apt-daily on
|
|
279
|
+
# freshly-booted WSL/Ubuntu). Uses flock(1) — the same mechanism apt uses —
|
|
280
|
+
# rather than checking file existence (the lock files are always present;
|
|
281
|
+
# advisory locks live in the kernel, not the filesystem).
|
|
282
|
+
wait_apt_lock() {
|
|
283
|
+
[ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ] || return 0
|
|
284
|
+
|
|
285
|
+
local locks=(
|
|
286
|
+
"/var/lib/dpkg/lock-frontend"
|
|
287
|
+
"/var/lib/dpkg/lock"
|
|
288
|
+
"/var/lib/apt/lists/lock"
|
|
289
|
+
)
|
|
290
|
+
local max_wait="${1:-120}"
|
|
291
|
+
local waited=0
|
|
292
|
+
local announced=false
|
|
293
|
+
|
|
294
|
+
while :; do
|
|
295
|
+
local busy=false
|
|
296
|
+
for f in "${locks[@]}"; do
|
|
297
|
+
[ -e "$f" ] || continue
|
|
298
|
+
if ! sudo flock -n "$f" -c true 2>/dev/null; then
|
|
299
|
+
busy=true
|
|
300
|
+
break
|
|
301
|
+
fi
|
|
302
|
+
done
|
|
303
|
+
|
|
304
|
+
[ "$busy" = false ] && break
|
|
305
|
+
|
|
306
|
+
if [ "$announced" = false ]; then
|
|
307
|
+
print_info "Waiting for system apt/dpkg to finish (up to ${max_wait}s)..."
|
|
308
|
+
announced=true
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
if [ "$waited" -ge "$max_wait" ]; then
|
|
312
|
+
print_error "apt is still locked after ${max_wait}s."
|
|
313
|
+
print_info "On WSL try: 'wsl --shutdown' from PowerShell, then rerun the installer."
|
|
314
|
+
return 1
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
sleep 3
|
|
318
|
+
waited=$((waited + 3))
|
|
319
|
+
done
|
|
320
|
+
|
|
321
|
+
[ "$announced" = true ] && print_success "apt lock released"
|
|
322
|
+
return 0
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Run an apt-get subcommand with lock-wait + transient-failure retry.
|
|
326
|
+
# Usage: apt_get_run update [-qq]
|
|
327
|
+
# apt_get_run install -y pkg1 pkg2
|
|
328
|
+
apt_get_run() {
|
|
329
|
+
local attempts=3
|
|
330
|
+
local i=1
|
|
331
|
+
while [ "$i" -le "$attempts" ]; do
|
|
332
|
+
wait_apt_lock 120 || return 1
|
|
333
|
+
if sudo apt-get "$@"; then
|
|
334
|
+
return 0
|
|
335
|
+
fi
|
|
336
|
+
local rc=$?
|
|
337
|
+
if [ "$i" -lt "$attempts" ]; then
|
|
338
|
+
print_warning "apt-get $1 failed (exit $rc), retrying ($i/$((attempts-1)))..."
|
|
339
|
+
sleep 5
|
|
340
|
+
else
|
|
341
|
+
print_error "apt-get $1 failed after $attempts attempts."
|
|
342
|
+
return "$rc"
|
|
343
|
+
fi
|
|
344
|
+
i=$((i + 1))
|
|
345
|
+
done
|
|
346
|
+
}
|
|
347
|
+
|
|
278
348
|
# Configure apt mirror for CN region and run apt-get update.
|
|
279
349
|
# Guards: only runs on ubuntu/debian ($DISTRO).
|
|
280
350
|
# Relies on $USE_CN_MIRRORS set by detect_network_region (network.sh).
|
|
@@ -322,7 +392,7 @@ EOF
|
|
|
322
392
|
print_info "Region: global — using default apt sources"
|
|
323
393
|
fi
|
|
324
394
|
|
|
325
|
-
|
|
395
|
+
apt_get_run update -qq || return 1
|
|
326
396
|
print_success "apt updated"
|
|
327
397
|
}
|
|
328
398
|
|
|
@@ -632,8 +702,8 @@ install_ruby() {
|
|
|
632
702
|
if is_macos; then
|
|
633
703
|
brew install openssl@3 libyaml gmp
|
|
634
704
|
elif is_linux_apt; then
|
|
635
|
-
|
|
636
|
-
rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev
|
|
705
|
+
apt_get_run install -y \
|
|
706
|
+
rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev || return 1
|
|
637
707
|
fi
|
|
638
708
|
|
|
639
709
|
ensure_mise || return 1
|
|
@@ -676,8 +746,8 @@ install_postgres() {
|
|
|
676
746
|
|
|
677
747
|
elif is_linux_apt; then
|
|
678
748
|
setup_apt_mirror
|
|
679
|
-
|
|
680
|
-
libssl-dev libreadline-dev zlib1g-dev libyaml-dev libffi-dev
|
|
749
|
+
apt_get_run install -y postgresql libpq-dev \
|
|
750
|
+
libssl-dev libreadline-dev zlib1g-dev libyaml-dev libffi-dev || return 1
|
|
681
751
|
sudo systemctl enable --now postgresql || true
|
|
682
752
|
fi
|
|
683
753
|
|
|
@@ -270,6 +270,76 @@ detect_network_region() {
|
|
|
270
270
|
|
|
271
271
|
# ---[ @include lib/apt.sh ]---
|
|
272
272
|
|
|
273
|
+
# Wait until apt/dpkg lock files are no longer held (e.g. by apt-daily on
|
|
274
|
+
# freshly-booted WSL/Ubuntu). Uses flock(1) — the same mechanism apt uses —
|
|
275
|
+
# rather than checking file existence (the lock files are always present;
|
|
276
|
+
# advisory locks live in the kernel, not the filesystem).
|
|
277
|
+
wait_apt_lock() {
|
|
278
|
+
[ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ] || return 0
|
|
279
|
+
|
|
280
|
+
local locks=(
|
|
281
|
+
"/var/lib/dpkg/lock-frontend"
|
|
282
|
+
"/var/lib/dpkg/lock"
|
|
283
|
+
"/var/lib/apt/lists/lock"
|
|
284
|
+
)
|
|
285
|
+
local max_wait="${1:-120}"
|
|
286
|
+
local waited=0
|
|
287
|
+
local announced=false
|
|
288
|
+
|
|
289
|
+
while :; do
|
|
290
|
+
local busy=false
|
|
291
|
+
for f in "${locks[@]}"; do
|
|
292
|
+
[ -e "$f" ] || continue
|
|
293
|
+
if ! sudo flock -n "$f" -c true 2>/dev/null; then
|
|
294
|
+
busy=true
|
|
295
|
+
break
|
|
296
|
+
fi
|
|
297
|
+
done
|
|
298
|
+
|
|
299
|
+
[ "$busy" = false ] && break
|
|
300
|
+
|
|
301
|
+
if [ "$announced" = false ]; then
|
|
302
|
+
print_info "Waiting for system apt/dpkg to finish (up to ${max_wait}s)..."
|
|
303
|
+
announced=true
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
if [ "$waited" -ge "$max_wait" ]; then
|
|
307
|
+
print_error "apt is still locked after ${max_wait}s."
|
|
308
|
+
print_info "On WSL try: 'wsl --shutdown' from PowerShell, then rerun the installer."
|
|
309
|
+
return 1
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
sleep 3
|
|
313
|
+
waited=$((waited + 3))
|
|
314
|
+
done
|
|
315
|
+
|
|
316
|
+
[ "$announced" = true ] && print_success "apt lock released"
|
|
317
|
+
return 0
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# Run an apt-get subcommand with lock-wait + transient-failure retry.
|
|
321
|
+
# Usage: apt_get_run update [-qq]
|
|
322
|
+
# apt_get_run install -y pkg1 pkg2
|
|
323
|
+
apt_get_run() {
|
|
324
|
+
local attempts=3
|
|
325
|
+
local i=1
|
|
326
|
+
while [ "$i" -le "$attempts" ]; do
|
|
327
|
+
wait_apt_lock 120 || return 1
|
|
328
|
+
if sudo apt-get "$@"; then
|
|
329
|
+
return 0
|
|
330
|
+
fi
|
|
331
|
+
local rc=$?
|
|
332
|
+
if [ "$i" -lt "$attempts" ]; then
|
|
333
|
+
print_warning "apt-get $1 failed (exit $rc), retrying ($i/$((attempts-1)))..."
|
|
334
|
+
sleep 5
|
|
335
|
+
else
|
|
336
|
+
print_error "apt-get $1 failed after $attempts attempts."
|
|
337
|
+
return "$rc"
|
|
338
|
+
fi
|
|
339
|
+
i=$((i + 1))
|
|
340
|
+
done
|
|
341
|
+
}
|
|
342
|
+
|
|
273
343
|
# Configure apt mirror for CN region and run apt-get update.
|
|
274
344
|
# Guards: only runs on ubuntu/debian ($DISTRO).
|
|
275
345
|
# Relies on $USE_CN_MIRRORS set by detect_network_region (network.sh).
|
|
@@ -317,7 +387,7 @@ EOF
|
|
|
317
387
|
print_info "Region: global — using default apt sources"
|
|
318
388
|
fi
|
|
319
389
|
|
|
320
|
-
|
|
390
|
+
apt_get_run update -qq || return 1
|
|
321
391
|
print_success "apt updated"
|
|
322
392
|
}
|
|
323
393
|
|
|
@@ -462,7 +532,7 @@ ensure_linux_deps() {
|
|
|
462
532
|
|
|
463
533
|
detect_network_region
|
|
464
534
|
setup_apt_mirror
|
|
465
|
-
|
|
535
|
+
apt_get_run install -y build-essential git curl python3 || return 1
|
|
466
536
|
print_success "Dependencies installed"
|
|
467
537
|
}
|
|
468
538
|
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# wsl_network_doctor.ps1 — diagnose & repair WSL2 mirrored networking for the browser tool.
|
|
2
|
+
#
|
|
3
|
+
# Designed to be invoked from inside WSL via:
|
|
4
|
+
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File <win-path-to-this-script> <subcommand>
|
|
5
|
+
#
|
|
6
|
+
# Subcommands:
|
|
7
|
+
# status Check whether mirrored networking is configured.
|
|
8
|
+
# enable Write networkingMode=mirrored to %USERPROFILE%\.wslconfig.
|
|
9
|
+
# repair Restart Windows Host Network Service (HNS) via UAC elevation.
|
|
10
|
+
#
|
|
11
|
+
# Exit codes (status only):
|
|
12
|
+
# 0 OK — mirrored configured, OR running on WSL1 (no config needed)
|
|
13
|
+
# 10 NEED_ENABLE — mirrored not configured, run `enable`
|
|
14
|
+
# 20 NEED_REPAIR — configured but suspected broken, run `repair`
|
|
15
|
+
# 1 unexpected error
|
|
16
|
+
#
|
|
17
|
+
# `enable` and `repair` exit 0 on success, 1 on failure.
|
|
18
|
+
|
|
19
|
+
param(
|
|
20
|
+
[Parameter(Position = 0)]
|
|
21
|
+
[ValidateSet('status', 'enable', 'repair')]
|
|
22
|
+
[string]$Command
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
$ErrorActionPreference = 'Stop'
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Helpers
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function Get-WslConfigPath {
|
|
32
|
+
return (Join-Path $env:USERPROFILE '.wslconfig')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Test-MirroredConfigured {
|
|
36
|
+
$cfg = Get-WslConfigPath
|
|
37
|
+
if (-not (Test-Path $cfg)) { return $false }
|
|
38
|
+
$content = Get-Content $cfg -Raw -ErrorAction SilentlyContinue
|
|
39
|
+
if ($null -eq $content) { return $false }
|
|
40
|
+
return ($content -match '(?im)^\s*networkingMode\s*=\s*mirrored\s*$')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Returns 1 or 2 if Ubuntu is registered, $null otherwise.
|
|
44
|
+
# Parses `wsl.exe -l -v` output (UTF-16, may contain a star marker on default distro).
|
|
45
|
+
function Get-UbuntuWslVersion {
|
|
46
|
+
try {
|
|
47
|
+
$raw = & wsl.exe -l -v 2>$null
|
|
48
|
+
} catch {
|
|
49
|
+
return $null
|
|
50
|
+
}
|
|
51
|
+
if (-not $raw) { return $null }
|
|
52
|
+
|
|
53
|
+
foreach ($line in $raw) {
|
|
54
|
+
$clean = ($line -replace '\s+', ' ').Trim().TrimStart('*').Trim()
|
|
55
|
+
if ($clean -match '^Ubuntu(?:-[\w\.]+)?\s+\S+\s+(\d+)\s*$') {
|
|
56
|
+
return [int]$matches[1]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return $null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Subcommand: status
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
function Invoke-Status {
|
|
67
|
+
$wslVer = Get-UbuntuWslVersion
|
|
68
|
+
if ($wslVer -eq 1) {
|
|
69
|
+
Write-Host "OK: Ubuntu is running on WSL1 — shares the Windows network stack directly."
|
|
70
|
+
Write-Host "No mirrored configuration needed. The browser tool can connect to 127.0.0.1 as-is."
|
|
71
|
+
exit 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Test-MirroredConfigured) {
|
|
75
|
+
Write-Host "OK: mirrored networking is configured in .wslconfig."
|
|
76
|
+
Write-Host "If the browser tool still cannot connect, run: wsl_network_doctor.ps1 repair"
|
|
77
|
+
exit 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Write-Host "NEED_ENABLE: mirrored networking is not configured."
|
|
81
|
+
Write-Host "Run: wsl_network_doctor.ps1 enable"
|
|
82
|
+
exit 10
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Subcommand: enable
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
function Invoke-Enable {
|
|
90
|
+
if (Test-MirroredConfigured) {
|
|
91
|
+
Write-Host "OK: already enabled. No changes needed."
|
|
92
|
+
Write-Host "If the browser tool still cannot connect, run: wsl_network_doctor.ps1 repair"
|
|
93
|
+
exit 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
$cfg = Get-WslConfigPath
|
|
97
|
+
Write-Host "Writing networkingMode=mirrored to $cfg ..."
|
|
98
|
+
|
|
99
|
+
if (-not (Test-Path $cfg)) {
|
|
100
|
+
New-Item -ItemType File -Path $cfg -Force | Out-Null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
$content = Get-Content $cfg -Raw -ErrorAction SilentlyContinue
|
|
104
|
+
if ($null -eq $content) { $content = '' }
|
|
105
|
+
|
|
106
|
+
if ($content -match '(?im)^\s*networkingMode\s*=') {
|
|
107
|
+
$new = [regex]::Replace($content, '(?im)^\s*networkingMode\s*=.*$', 'networkingMode=mirrored')
|
|
108
|
+
Set-Content -Path $cfg -Value $new -NoNewline
|
|
109
|
+
} else {
|
|
110
|
+
if ($content -notmatch '(?im)^\[wsl2\]') {
|
|
111
|
+
if ($content.Length -gt 0 -and -not $content.EndsWith([char]10)) {
|
|
112
|
+
Add-Content -Path $cfg -Value ''
|
|
113
|
+
}
|
|
114
|
+
Add-Content -Path $cfg -Value '[wsl2]'
|
|
115
|
+
}
|
|
116
|
+
Add-Content -Path $cfg -Value 'networkingMode=mirrored'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Write-Host "WROTE: .wslconfig updated."
|
|
120
|
+
Write-Host ""
|
|
121
|
+
Write-Host "Next step (cannot be done from inside WSL):"
|
|
122
|
+
Write-Host " 1. Open Windows PowerShell"
|
|
123
|
+
Write-Host " 2. Run: wsl --shutdown"
|
|
124
|
+
Write-Host " 3. Reopen Clacky and run /browser-setup again"
|
|
125
|
+
exit 0
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Subcommand: repair
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Restart Windows Host Network Service (HNS). Requires admin → triggers UAC.
|
|
132
|
+
# Does NOT call `wsl --shutdown` here — the user must run it manually after
|
|
133
|
+
# the elevated window finishes, otherwise our own WSL session would be killed.
|
|
134
|
+
|
|
135
|
+
function Invoke-Repair {
|
|
136
|
+
Write-Host "Repairing Windows Host Network Service (HNS) ..."
|
|
137
|
+
Write-Host ""
|
|
138
|
+
Write-Host "A Windows User Account Control (UAC) dialog will appear."
|
|
139
|
+
Write-Host "Please click 'Yes' to allow the repair script to run."
|
|
140
|
+
Write-Host ""
|
|
141
|
+
|
|
142
|
+
$inner = @'
|
|
143
|
+
try {
|
|
144
|
+
Stop-Service hns -Force -ErrorAction SilentlyContinue
|
|
145
|
+
Start-Service hns -ErrorAction Stop
|
|
146
|
+
Write-Host "HNS restarted successfully."
|
|
147
|
+
} catch {
|
|
148
|
+
Write-Host "Repair failed: $_"
|
|
149
|
+
Start-Sleep 5
|
|
150
|
+
exit 1
|
|
151
|
+
}
|
|
152
|
+
Write-Host ""
|
|
153
|
+
Write-Host "Repair complete. Please run 'wsl --shutdown' in PowerShell, then reopen Clacky."
|
|
154
|
+
Start-Sleep 4
|
|
155
|
+
'@
|
|
156
|
+
|
|
157
|
+
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($inner))
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile', '-EncodedCommand', $encoded
|
|
161
|
+
} catch {
|
|
162
|
+
Write-Host "FAILED: could not trigger UAC prompt: $_"
|
|
163
|
+
Write-Host ""
|
|
164
|
+
Write-Host "You can run the repair manually:"
|
|
165
|
+
Write-Host " 1. Open PowerShell as Administrator"
|
|
166
|
+
Write-Host " 2. Run: net stop hns; net start hns"
|
|
167
|
+
Write-Host " 3. Run: wsl --shutdown"
|
|
168
|
+
Write-Host " 4. Reopen Clacky"
|
|
169
|
+
exit 1
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
Write-Host "Repair script launched in an elevated PowerShell window."
|
|
173
|
+
Write-Host ""
|
|
174
|
+
Write-Host "After the elevated window finishes:"
|
|
175
|
+
Write-Host " 1. Run in regular PowerShell: wsl --shutdown"
|
|
176
|
+
Write-Host " 2. Reopen Clacky and run /browser-setup again"
|
|
177
|
+
exit 0
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# Dispatch
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
switch ($Command) {
|
|
185
|
+
'status' { Invoke-Status }
|
|
186
|
+
'enable' { Invoke-Enable }
|
|
187
|
+
'repair' { Invoke-Repair }
|
|
188
|
+
default {
|
|
189
|
+
Write-Host "Usage: wsl_network_doctor.ps1 {status|enable|repair}"
|
|
190
|
+
Write-Host ""
|
|
191
|
+
Write-Host " status Check whether WSL2 mirrored networking is configured."
|
|
192
|
+
Write-Host " enable Write networkingMode=mirrored to %USERPROFILE%\.wslconfig."
|
|
193
|
+
Write-Host " repair Restart Windows Host Network Service (HNS) via UAC."
|
|
194
|
+
exit 2
|
|
195
|
+
}
|
|
196
|
+
}
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openclacky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -596,6 +596,7 @@ files:
|
|
|
596
596
|
- scripts/install_rails_deps.sh
|
|
597
597
|
- scripts/install_system_deps.sh
|
|
598
598
|
- scripts/uninstall.sh
|
|
599
|
+
- scripts/wsl_network_doctor.ps1
|
|
599
600
|
- sig/clacky.rbs
|
|
600
601
|
homepage: https://github.com/clacky-ai/openclacky
|
|
601
602
|
licenses:
|