openclacky 1.2.6 → 1.2.7
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 +20 -0
- data/README.md +34 -0
- data/README_CN.md +34 -0
- data/lib/clacky/agent/cost_tracker.rb +7 -1
- data/lib/clacky/agent/message_compressor.rb +2 -1
- data/lib/clacky/agent/message_compressor_helper.rb +6 -2
- data/lib/clacky/agent/session_serializer.rb +23 -4
- data/lib/clacky/agent.rb +43 -2
- data/lib/clacky/agent_config.rb +54 -6
- data/lib/clacky/brand_config.rb +0 -6
- data/lib/clacky/cli.rb +2 -1
- data/lib/clacky/client.rb +24 -3
- data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +5 -2
- data/lib/clacky/plain_ui_controller.rb +1 -1
- data/lib/clacky/providers.rb +11 -2
- data/lib/clacky/server/channel/channel_manager.rb +148 -12
- data/lib/clacky/server/channel/channel_ui_controller.rb +4 -2
- data/lib/clacky/server/http_server.rb +109 -9
- data/lib/clacky/server/session_registry.rb +30 -4
- data/lib/clacky/server/web_ui_controller.rb +6 -3
- data/lib/clacky/tools/terminal.rb +22 -26
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +1 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +237 -1
- data/lib/clacky/web/app.js +0 -1
- data/lib/clacky/web/i18n.js +24 -0
- data/lib/clacky/web/index.html +33 -0
- data/lib/clacky/web/sessions.js +203 -14
- data/lib/clacky/web/settings.js +59 -17
- data/lib/clacky/web/workspace.js +204 -0
- data/lib/clacky/web/ws-dispatcher.js +19 -3
- data/lib/clacky.rb +9 -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: 8057fdabcb077c8ca378fcbbc0efa442abc6b9664464d2d8d1b737e0206d2ed9
|
|
4
|
+
data.tar.gz: 657f282a20664ef793d7ff663e963739f5fbdb478b8e174df9c5e459ec09231b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fad3e045271032a1150745f1d8531aeed24ea376efdcd99346aeaeec4eb5f1edec187e9dbe38ee8d19e5a9cf599bfb7e5431ad55e5993f1dd243bb4ebf5faa2d
|
|
7
|
+
data.tar.gz: 95b1a7ec783b459f5c70b99d3535f5a0054d4a8c72d855d9c98b9771cde9d59cb33dcf609be844fade932e1487c82c525cf4354438c5ad4b271494a4166dc729
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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.7] - 2026-06-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Session workspace file explorer — browse and download files in Web UI sidebar
|
|
12
|
+
- Channel manager: new, clear, and skill commands
|
|
13
|
+
- Top-up link in Web UI under billing section
|
|
14
|
+
- Sub-model switching within active sessions
|
|
15
|
+
- Claude 4.8 model support
|
|
16
|
+
- Connection test improvements for custom Claude models
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Nil error in compress top match parsing
|
|
20
|
+
- Empty 0-token responses causing agent stalls (close #218)
|
|
21
|
+
- Brand config built-in skills loaded without MANIFEST.enc.json (C-5627)
|
|
22
|
+
- Terminal .exe stdin isolation at command-wrap on WSL (close #221)
|
|
23
|
+
- Overly verbose builtin skills installed message during onboarding
|
|
24
|
+
|
|
25
|
+
### More
|
|
26
|
+
- Add Docker installation section to README
|
|
27
|
+
|
|
8
28
|
## [1.2.6] - 2026-05-29
|
|
9
29
|
|
|
10
30
|
### Added
|
data/README.md
CHANGED
|
@@ -108,6 +108,40 @@ gem install openclacky
|
|
|
108
108
|
|
|
109
109
|
see more: https://www.openclacky.com/docs/installation
|
|
110
110
|
|
|
111
|
+
### Docker
|
|
112
|
+
|
|
113
|
+
Build:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/clacky-ai/openclacky.git
|
|
117
|
+
cd openclacky
|
|
118
|
+
docker build -t openclacky .
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Linux:**
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
docker run -d --network=host -e CLACKY_ACCESS_KEY="" openclacky
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`--network=host` is required so the agent inside the container can reach Chrome's remote debugging port running on the host.
|
|
128
|
+
|
|
129
|
+
**macOS / Windows:**
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
docker run -d -p 7070:7070 -e CLACKY_ACCESS_KEY="" openclacky
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
> **Note:** On macOS/Windows, `--network=host` is not supported — browser automation may be limited.
|
|
136
|
+
|
|
137
|
+
Open **http://localhost:7070** after starting.
|
|
138
|
+
|
|
139
|
+
Environment variables:
|
|
140
|
+
|
|
141
|
+
| Variable | Description |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `CLACKY_ACCESS_KEY` | Protect the Web UI with an access key (empty = public mode) |
|
|
144
|
+
|
|
111
145
|
|
|
112
146
|
## Quick Start
|
|
113
147
|
|
data/README_CN.md
CHANGED
|
@@ -104,6 +104,40 @@ gem install openclacky
|
|
|
104
104
|
|
|
105
105
|
详见:https://www.openclacky.com/docs/installation
|
|
106
106
|
|
|
107
|
+
### Docker
|
|
108
|
+
|
|
109
|
+
构建:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
git clone https://github.com/clacky-ai/openclacky.git
|
|
113
|
+
cd openclacky
|
|
114
|
+
docker build -t openclacky .
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Linux:**
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
docker run -d --network=host -e CLACKY_ACCESS_KEY="" openclacky
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`--network=host` 使容器与宿主机共享网络栈,Agent 可直接访问宿主机上运行的 Chrome 远程调试端口。
|
|
124
|
+
|
|
125
|
+
**macOS / Windows:**
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
docker run -d -p 7070:7070 -e CLACKY_ACCESS_KEY="" openclacky
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
> **注意:** macOS/Windows 不支持 `--network=host`,浏览器自动化功能可能受限。
|
|
132
|
+
|
|
133
|
+
启动后访问 **http://localhost:7070**。
|
|
134
|
+
|
|
135
|
+
环境变量:
|
|
136
|
+
|
|
137
|
+
| 变量 | 说明 |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `CLACKY_ACCESS_KEY` | 设置访问密钥保护 Web UI(留空 = 公开模式) |
|
|
140
|
+
|
|
107
141
|
## 快速开始
|
|
108
142
|
|
|
109
143
|
### 终端(CLI)
|
|
@@ -177,7 +177,13 @@ module Clacky
|
|
|
177
177
|
else
|
|
178
178
|
total_tokens - @previous_total_tokens
|
|
179
179
|
end
|
|
180
|
-
|
|
180
|
+
|
|
181
|
+
# Guard: do NOT overwrite @previous_total_tokens with 0 when the upstream
|
|
182
|
+
# returns missing/zero usage (observed when history overflows the model
|
|
183
|
+
# context: response comes back as content="" + finish_reason="stop" +
|
|
184
|
+
# zero usage). Resetting to 0 would disable the compression trigger on
|
|
185
|
+
# subsequent turns and poison the session permanently.
|
|
186
|
+
@previous_total_tokens = total_tokens if total_tokens > 0
|
|
181
187
|
|
|
182
188
|
{
|
|
183
189
|
delta_tokens: delta_tokens,
|
|
@@ -137,7 +137,8 @@ module Clacky
|
|
|
137
137
|
# Returns the topics string if found, nil otherwise.
|
|
138
138
|
# e.g. "<topics>Rails setup, database config</topics>" → "Rails setup, database config"
|
|
139
139
|
def parse_topics(content)
|
|
140
|
-
|
|
140
|
+
return nil if content.nil? || content.to_s.empty?
|
|
141
|
+
m = content.to_s.match(/<topics>(.*?)<\/topics>/m)
|
|
141
142
|
m ? m[1].strip : nil
|
|
142
143
|
end
|
|
143
144
|
|
|
@@ -124,8 +124,12 @@ module Clacky
|
|
|
124
124
|
# Check if compression is enabled
|
|
125
125
|
return nil unless @config.enable_compression
|
|
126
126
|
|
|
127
|
-
# Use
|
|
128
|
-
|
|
127
|
+
# Use the larger of: API-reported tokens from last response, or current
|
|
128
|
+
# estimated history size. The estimate guards the case where a single
|
|
129
|
+
# huge tool result was just appended after the last API call — without
|
|
130
|
+
# this, the next request can ship the bloated history before any
|
|
131
|
+
# compression triggers (issue #218).
|
|
132
|
+
total_tokens = [@previous_total_tokens, @history.estimate_tokens].max
|
|
129
133
|
message_count = @history.size
|
|
130
134
|
|
|
131
135
|
# Force compression (for idle compression) - use lower threshold
|
|
@@ -80,12 +80,28 @@ module Clacky
|
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# Re-apply the per-session sub-model pin (if any). Done AFTER
|
|
84
|
+
# switch_model_by_id so the overlay isn't cleared by the card switch
|
|
85
|
+
# invariant. Validation happens at write-time (the WebUI/API enforces
|
|
86
|
+
# the name belongs to the card's provider) — at restore-time we trust
|
|
87
|
+
# what we previously wrote.
|
|
88
|
+
saved_sub_model = session_data.dig(:config, :sub_model)
|
|
89
|
+
if saved_sub_model && !saved_sub_model.to_s.empty?
|
|
90
|
+
set_session_sub_model(saved_sub_model)
|
|
91
|
+
end
|
|
92
|
+
|
|
83
93
|
# Rebuild and refresh the system prompt so any newly installed skills
|
|
84
94
|
# (or other configuration changes since the session was saved) are
|
|
85
95
|
# reflected immediately — without requiring the user to create a new session.
|
|
86
96
|
refresh_system_prompt
|
|
87
97
|
end
|
|
88
98
|
|
|
99
|
+
private def persisted_card_field(key)
|
|
100
|
+
card_id = @config.current_model_id
|
|
101
|
+
return nil unless card_id
|
|
102
|
+
@config.models.find { |m| m["id"] == card_id }&.dig(key)
|
|
103
|
+
end
|
|
104
|
+
|
|
89
105
|
# Generate session data for saving
|
|
90
106
|
# @param status [Symbol] Status of the last task: :success, :error, or :interrupted
|
|
91
107
|
# @param error_message [String] Error message if status is :error
|
|
@@ -134,10 +150,13 @@ module Clacky
|
|
|
134
150
|
reasoning_effort: @reasoning_effort,
|
|
135
151
|
# Persist the current model identity so the session can restore its
|
|
136
152
|
# original model on restart. model_name + model_base_url form a
|
|
137
|
-
# composite key
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
|
|
153
|
+
# composite key that points at the underlying card (NOT the
|
|
154
|
+
# sub-model overlay) — overlays are layered on top via :sub_model
|
|
155
|
+
# below so card lookup stays stable when the user toggles
|
|
156
|
+
# sub-models.
|
|
157
|
+
model_name: persisted_card_field("model"),
|
|
158
|
+
model_base_url: persisted_card_field("base_url"),
|
|
159
|
+
sub_model: @config.session_model_overlay_name
|
|
141
160
|
},
|
|
142
161
|
stats: stats_data,
|
|
143
162
|
messages: @history.to_a
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -172,6 +172,18 @@ module Clacky
|
|
|
172
172
|
true
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
+
# Pin this session to a sub-model name without changing its underlying
|
|
176
|
+
# card (credentials / base_url stay put). Pass nil or "" to clear and
|
|
177
|
+
# fall back to the card's default model. Validation that the name is
|
|
178
|
+
# listed under the current provider is the caller's job.
|
|
179
|
+
# @param model_name [String, nil]
|
|
180
|
+
# @return [Boolean]
|
|
181
|
+
def set_session_sub_model(model_name)
|
|
182
|
+
@config.session_model_overlay = model_name
|
|
183
|
+
rebuild_client_for_current_model!
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
|
|
175
187
|
# Rebuild the underlying Client (and dependent components) to pick up
|
|
176
188
|
# credentials/model name from the currently-selected model in @config.
|
|
177
189
|
private def rebuild_client_for_current_model!
|
|
@@ -206,10 +218,16 @@ module Clacky
|
|
|
206
218
|
model = @config.current_model
|
|
207
219
|
return nil unless model
|
|
208
220
|
|
|
221
|
+
card_id = @config.current_model_id
|
|
222
|
+
base_entry = card_id ? @config.models.find { |m| m["id"] == card_id } : nil
|
|
223
|
+
sub_model = @config.session_model_overlay_name
|
|
224
|
+
|
|
209
225
|
{
|
|
210
226
|
id: model["id"],
|
|
211
227
|
model: model["model"],
|
|
212
|
-
base_url: model["base_url"]
|
|
228
|
+
base_url: model["base_url"],
|
|
229
|
+
card_model: base_entry&.dig("model"),
|
|
230
|
+
sub_model: sub_model
|
|
213
231
|
}
|
|
214
232
|
end
|
|
215
233
|
|
|
@@ -1019,7 +1037,10 @@ module Clacky
|
|
|
1019
1037
|
check_stale!
|
|
1020
1038
|
|
|
1021
1039
|
formatted_messages = @client.format_tool_results(response, tool_results, model: current_model)
|
|
1022
|
-
formatted_messages.each
|
|
1040
|
+
formatted_messages.each do |msg|
|
|
1041
|
+
truncated = truncate_oversized_tool_content(msg)
|
|
1042
|
+
@history.append(truncated.merge(task_id: @current_task_id))
|
|
1043
|
+
end
|
|
1023
1044
|
|
|
1024
1045
|
# Append a follow-up `role:"user"` message for any image payloads that
|
|
1025
1046
|
# could not be delivered inside the tool message.
|
|
@@ -1055,6 +1076,26 @@ module Clacky
|
|
|
1055
1076
|
end
|
|
1056
1077
|
end
|
|
1057
1078
|
|
|
1079
|
+
# Cap oversized tool result content to keep a single tool message from
|
|
1080
|
+
# blowing up the prompt budget (issue #218: a 7350-path glob produced a
|
|
1081
|
+
# ~890k-char result that pushed history past the model context window
|
|
1082
|
+
# and poisoned the session). Only string content is truncated — Array
|
|
1083
|
+
# content (multipart/image blocks) is left alone since image payloads
|
|
1084
|
+
# are handled by the image_inject path above.
|
|
1085
|
+
MAX_TOOL_RESULT_CHARS = 80_000
|
|
1086
|
+
|
|
1087
|
+
private def truncate_oversized_tool_content(msg)
|
|
1088
|
+
content = msg[:content]
|
|
1089
|
+
return msg unless content.is_a?(String) && content.length > MAX_TOOL_RESULT_CHARS
|
|
1090
|
+
|
|
1091
|
+
original_len = content.length
|
|
1092
|
+
head = content[0, MAX_TOOL_RESULT_CHARS]
|
|
1093
|
+
truncated = head + "\n\n[Tool result truncated: #{original_len} chars total, " \
|
|
1094
|
+
"showing first #{MAX_TOOL_RESULT_CHARS}. Use a more specific query/limit, " \
|
|
1095
|
+
"or read the raw output via file_reader/grep on the underlying source.]"
|
|
1096
|
+
msg.merge(content: truncated)
|
|
1097
|
+
end
|
|
1098
|
+
|
|
1058
1099
|
# Enqueue an inline skill injection to be flushed after observe().
|
|
1059
1100
|
# Called by InvokeSkill#execute to avoid injecting during tool execution,
|
|
1060
1101
|
# which would break Bedrock's toolUse/toolResult pairing requirement.
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -211,6 +211,14 @@ module Clacky
|
|
|
211
211
|
# Keys honored: "api_key", "base_url", "model", "anthropic_format".
|
|
212
212
|
# @return [Hash, nil]
|
|
213
213
|
@virtual_model_overlay = options[:virtual_model_overlay]
|
|
214
|
+
|
|
215
|
+
# Per-session sub-model override. Persists across restarts via the
|
|
216
|
+
# session file. Independent of @virtual_model_overlay (which is for
|
|
217
|
+
# short-lived subagent forks). Used by the WebUI sub-model switcher
|
|
218
|
+
# to pin a session to e.g. "dsk-deepseek-v4-pro" while the underlying
|
|
219
|
+
# card still says "abs-claude-sonnet-4-6". Only the "model" key is
|
|
220
|
+
# honored — sub-model switching never changes credentials.
|
|
221
|
+
@session_model_overlay = options[:session_model_overlay]
|
|
214
222
|
end
|
|
215
223
|
|
|
216
224
|
# Load configuration from file
|
|
@@ -354,6 +362,9 @@ module Clacky
|
|
|
354
362
|
if @virtual_model_overlay
|
|
355
363
|
copy.instance_variable_set(:@virtual_model_overlay, @virtual_model_overlay.dup)
|
|
356
364
|
end
|
|
365
|
+
if @session_model_overlay
|
|
366
|
+
copy.instance_variable_set(:@session_model_overlay, @session_model_overlay.dup)
|
|
367
|
+
end
|
|
357
368
|
copy
|
|
358
369
|
end
|
|
359
370
|
|
|
@@ -431,9 +442,12 @@ module Clacky
|
|
|
431
442
|
index = @models.find_index { |m| m["id"] == id }
|
|
432
443
|
return false if index.nil?
|
|
433
444
|
|
|
445
|
+
previous_id = @current_model_id
|
|
434
446
|
@current_model_id = id
|
|
435
447
|
@current_model_index = index
|
|
436
448
|
|
|
449
|
+
@session_model_overlay = nil if previous_id != id
|
|
450
|
+
|
|
437
451
|
true
|
|
438
452
|
end
|
|
439
453
|
|
|
@@ -760,14 +774,18 @@ module Clacky
|
|
|
760
774
|
resolved = resolve_current_model_entry
|
|
761
775
|
return nil unless resolved
|
|
762
776
|
|
|
763
|
-
#
|
|
764
|
-
#
|
|
765
|
-
#
|
|
777
|
+
# Merge order (low → high): base entry, session-level sub-model override,
|
|
778
|
+
# then short-lived subagent overlay. Both layers are kept separate so
|
|
779
|
+
# a subagent fork can stack its own credentials on top of an active
|
|
780
|
+
# sub-model pin without erasing it.
|
|
781
|
+
merged = resolved
|
|
782
|
+
if @session_model_overlay && !@session_model_overlay.empty?
|
|
783
|
+
merged = merged.merge(@session_model_overlay)
|
|
784
|
+
end
|
|
766
785
|
if @virtual_model_overlay && !@virtual_model_overlay.empty?
|
|
767
|
-
|
|
768
|
-
else
|
|
769
|
-
resolved
|
|
786
|
+
merged = merged.merge(@virtual_model_overlay)
|
|
770
787
|
end
|
|
788
|
+
merged.equal?(resolved) ? resolved : merged
|
|
771
789
|
end
|
|
772
790
|
|
|
773
791
|
# Internal: resolve the current model entry from @models (no overlay).
|
|
@@ -822,6 +840,36 @@ module Clacky
|
|
|
822
840
|
@virtual_model_overlay
|
|
823
841
|
end
|
|
824
842
|
|
|
843
|
+
# Apply a session-level sub-model override. Lives on this AgentConfig
|
|
844
|
+
# only (each session deep_copy's its own scalar ivar) and survives a
|
|
845
|
+
# restart through the session file. Pass nil or "" to clear.
|
|
846
|
+
#
|
|
847
|
+
# The override only rewrites the resolved current_model's "model" field —
|
|
848
|
+
# api_key / base_url / anthropic_format come from the underlying card,
|
|
849
|
+
# so the user's credentials and provider identity are untouched.
|
|
850
|
+
#
|
|
851
|
+
# @param model_name [String, nil] sub-model name, e.g. "dsk-deepseek-v4-pro"
|
|
852
|
+
def session_model_overlay=(model_name)
|
|
853
|
+
if model_name.nil? || model_name.to_s.strip.empty?
|
|
854
|
+
@session_model_overlay = nil
|
|
855
|
+
else
|
|
856
|
+
@session_model_overlay = { "model" => model_name.to_s.strip }
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
# @return [Hash, nil] the active session sub-model overlay
|
|
861
|
+
def session_model_overlay
|
|
862
|
+
@session_model_overlay
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
# Convenience accessor: the sub-model name currently pinned on this
|
|
866
|
+
# session, or nil when no override is active. Used by serializers and
|
|
867
|
+
# the WebUI to surface "card · sub-model" two-line displays.
|
|
868
|
+
# @return [String, nil]
|
|
869
|
+
def session_model_overlay_name
|
|
870
|
+
@session_model_overlay && @session_model_overlay["model"]
|
|
871
|
+
end
|
|
872
|
+
|
|
825
873
|
# Query whether the *current* model supports a given capability.
|
|
826
874
|
#
|
|
827
875
|
# This is the single entry-point callers (Agent, downgrade pipeline, UI)
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -783,12 +783,6 @@ module Clacky
|
|
|
783
783
|
|
|
784
784
|
FileUtils.rm_f(tmp_zip)
|
|
785
785
|
|
|
786
|
-
if encrypted
|
|
787
|
-
manifest_path = File.join(dest_dir, "MANIFEST.enc.json")
|
|
788
|
-
raise "MANIFEST.enc.json missing after extraction" unless File.exist?(manifest_path)
|
|
789
|
-
JSON.parse(File.read(manifest_path))
|
|
790
|
-
end
|
|
791
|
-
|
|
792
786
|
record_installed_skill(slug, version, skill_info["description"],
|
|
793
787
|
encrypted: encrypted,
|
|
794
788
|
description_zh: skill_info["description_zh"],
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -559,7 +559,8 @@ module Clacky
|
|
|
559
559
|
else
|
|
560
560
|
error_message = format_error(exception)
|
|
561
561
|
session_manager&.save(agent.to_session_data(status: :error, error_message: error_message))
|
|
562
|
-
|
|
562
|
+
code = exception.is_a?(Clacky::InsufficientCreditError) ? exception.error_code : nil
|
|
563
|
+
ui_controller.show_error("Error: #{exception.message}", code: code)
|
|
563
564
|
end
|
|
564
565
|
end
|
|
565
566
|
|
data/lib/clacky/client.rb
CHANGED
|
@@ -301,7 +301,12 @@ module Clacky
|
|
|
301
301
|
end
|
|
302
302
|
end
|
|
303
303
|
|
|
304
|
-
|
|
304
|
+
unless response.status == 200
|
|
305
|
+
recovered_body = response.body.to_s
|
|
306
|
+
recovered_body = sse_buf.to_s if recovered_body.empty?
|
|
307
|
+
recovered = Struct.new(:status, :body).new(response.status, recovered_body)
|
|
308
|
+
raise_error(recovered)
|
|
309
|
+
end
|
|
305
310
|
MessageFormat::Anthropic.parse_response(aggregator.to_h)
|
|
306
311
|
end
|
|
307
312
|
|
|
@@ -536,13 +541,23 @@ module Clacky
|
|
|
536
541
|
def raise_error(response)
|
|
537
542
|
error_body = JSON.parse(response.body) rescue nil
|
|
538
543
|
error_message = extract_error_message(error_body, response.body)
|
|
544
|
+
error_code = extract_error_code(error_body)
|
|
539
545
|
|
|
540
546
|
Clacky::Logger.warn("client.raise_error",
|
|
541
547
|
status: response.status,
|
|
542
548
|
body: response.body.to_s[0, 2000],
|
|
543
|
-
error_message: error_message.to_s[0, 500]
|
|
549
|
+
error_message: error_message.to_s[0, 500],
|
|
550
|
+
error_code: error_code
|
|
544
551
|
)
|
|
545
552
|
|
|
553
|
+
if error_code == "insufficient_credit" || response.status == 402
|
|
554
|
+
raise InsufficientCreditError.new(
|
|
555
|
+
"[LLM] Insufficient credit: #{error_message}",
|
|
556
|
+
error_code: "insufficient_credit",
|
|
557
|
+
provider_id: @provider_id
|
|
558
|
+
)
|
|
559
|
+
end
|
|
560
|
+
|
|
546
561
|
case response.status
|
|
547
562
|
when 400
|
|
548
563
|
# Well-behaved APIs (Anthropic, OpenAI) never put quota/availability issues in 400.
|
|
@@ -557,7 +572,6 @@ module Clacky
|
|
|
557
572
|
# broken message is not replayed on the next user turn.
|
|
558
573
|
raise BadRequestError, "[LLM] Client request error: #{error_message}"
|
|
559
574
|
when 401 then raise AgentError, "[LLM] Invalid API key"
|
|
560
|
-
when 402 then raise AgentError, "[LLM] Billing or payment issue (possibly out of credits): #{error_message}"
|
|
561
575
|
when 403 then raise AgentError, "[LLM] Access denied: #{error_message}"
|
|
562
576
|
when 404 then raise AgentError, "[LLM] API endpoint not found: #{error_message}"
|
|
563
577
|
when 429 then raise RetryableError, "[LLM] Rate limit exceeded, please wait a moment"
|
|
@@ -574,6 +588,13 @@ module Clacky
|
|
|
574
588
|
end
|
|
575
589
|
end
|
|
576
590
|
|
|
591
|
+
private def extract_error_code(error_body)
|
|
592
|
+
return nil unless error_body.is_a?(Hash)
|
|
593
|
+
err = error_body["error"]
|
|
594
|
+
return err["code"] if err.is_a?(Hash) && err["code"].is_a?(String)
|
|
595
|
+
nil
|
|
596
|
+
end
|
|
597
|
+
|
|
577
598
|
def extract_error_message(error_body, raw_body)
|
|
578
599
|
if raw_body.is_a?(String) && raw_body.strip.start_with?("<!DOCTYPE", "<html")
|
|
579
600
|
return "Invalid API endpoint or server error (received HTML instead of JSON)"
|
|
@@ -216,8 +216,8 @@ Silently run `ruby "SKILL_DIR/scripts/install_builtin_skills.rb"`,
|
|
|
216
216
|
then parse the last stdout line as JSON and read `installed` as N.
|
|
217
217
|
|
|
218
218
|
- If N > 0, show one line:
|
|
219
|
-
- zh: `✅ 已为你内置 N
|
|
220
|
-
- en: `✅ Installed N builtin skills
|
|
219
|
+
- zh: `✅ 已为你内置 N 个技能。`
|
|
220
|
+
- en: `✅ Installed N builtin skills.`
|
|
221
221
|
|
|
222
222
|
### A.10. Import external skills (optional)
|
|
223
223
|
|
|
@@ -101,8 +101,11 @@ module Clacky
|
|
|
101
101
|
emit("warning", message: message)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
def show_error(message)
|
|
105
|
-
|
|
104
|
+
def show_error(message, code: nil, top_up_url: nil)
|
|
105
|
+
payload = { message: message }
|
|
106
|
+
payload[:code] = code if code
|
|
107
|
+
payload[:top_up_url] = top_up_url if top_up_url
|
|
108
|
+
emit("error", **payload)
|
|
106
109
|
end
|
|
107
110
|
|
|
108
111
|
def show_success(message)
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -31,6 +31,7 @@ module Clacky
|
|
|
31
31
|
"api" => "bedrock",
|
|
32
32
|
"default_model" => "abs-claude-sonnet-4-6",
|
|
33
33
|
"models" => [
|
|
34
|
+
"abs-claude-opus-4-8",
|
|
34
35
|
"abs-claude-opus-4-7",
|
|
35
36
|
"abs-claude-opus-4-6",
|
|
36
37
|
"abs-claude-sonnet-4-6",
|
|
@@ -61,6 +62,7 @@ module Clacky
|
|
|
61
62
|
# sibling wired up (yet) on this provider; subagents using the
|
|
62
63
|
# Gemini default will just reuse it for lite work until we add one.
|
|
63
64
|
"lite_models" => {
|
|
65
|
+
"abs-claude-opus-4-8" => "abs-claude-haiku-4-5",
|
|
64
66
|
"abs-claude-opus-4-7" => "abs-claude-haiku-4-5",
|
|
65
67
|
"abs-claude-opus-4-6" => "abs-claude-haiku-4-5",
|
|
66
68
|
"abs-claude-sonnet-4-6" => "abs-claude-haiku-4-5",
|
|
@@ -88,6 +90,7 @@ module Clacky
|
|
|
88
90
|
# ID manually; this list only seeds the picker.
|
|
89
91
|
"models" => [
|
|
90
92
|
"anthropic/claude-sonnet-4-6",
|
|
93
|
+
"anthropic/claude-opus-4-8",
|
|
91
94
|
"anthropic/claude-opus-4-7",
|
|
92
95
|
"anthropic/claude-opus-4-6",
|
|
93
96
|
"anthropic/claude-haiku-4-5",
|
|
@@ -101,6 +104,7 @@ module Clacky
|
|
|
101
104
|
# cheap/fast sidekick automatically.
|
|
102
105
|
"lite_models" => {
|
|
103
106
|
"anthropic/claude-sonnet-4-6" => "anthropic/claude-haiku-4-5",
|
|
107
|
+
"anthropic/claude-opus-4-8" => "anthropic/claude-haiku-4-5",
|
|
104
108
|
"anthropic/claude-opus-4-7" => "anthropic/claude-haiku-4-5",
|
|
105
109
|
"anthropic/claude-opus-4-6" => "anthropic/claude-haiku-4-5",
|
|
106
110
|
"openai/gpt-5.5" => "openai/gpt-5.4-mini",
|
|
@@ -232,8 +236,8 @@ module Clacky
|
|
|
232
236
|
"name" => "Anthropic (Claude)",
|
|
233
237
|
"base_url" => "https://api.anthropic.com",
|
|
234
238
|
"api" => "anthropic-messages",
|
|
235
|
-
"default_model" => "claude-sonnet-4
|
|
236
|
-
"models" => ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4
|
|
239
|
+
"default_model" => "claude-sonnet-4-6",
|
|
240
|
+
"models" => ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"],
|
|
237
241
|
"website_url" => "https://console.anthropic.com/settings/keys"
|
|
238
242
|
}.freeze,
|
|
239
243
|
|
|
@@ -545,6 +549,11 @@ module Clacky
|
|
|
545
549
|
return "openclacky"
|
|
546
550
|
end
|
|
547
551
|
|
|
552
|
+
if base_url.is_a?(String) &&
|
|
553
|
+
base_url.match?(%r{\Ahttps?://(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|/|\z)}i)
|
|
554
|
+
return "openclacky"
|
|
555
|
+
end
|
|
556
|
+
|
|
548
557
|
nil
|
|
549
558
|
end
|
|
550
559
|
|