openclacky 1.1.3 → 1.1.4
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 +16 -0
- data/README.md +4 -0
- data/README_CN.md +198 -0
- data/docs/engineering-article.md +1 -1
- data/lib/clacky/agent/llm_caller.rb +1 -0
- data/lib/clacky/agent/session_serializer.rb +4 -0
- data/lib/clacky/agent.rb +22 -1
- data/lib/clacky/brand_config.rb +87 -5
- data/lib/clacky/client.rb +15 -11
- data/lib/clacky/message_format/anthropic.rb +13 -1
- data/lib/clacky/message_format/bedrock.rb +13 -1
- data/lib/clacky/message_format/open_ai.rb +5 -1
- data/lib/clacky/server/http_server.rb +130 -15
- data/lib/clacky/server/session_registry.rb +9 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1278 -1116
- data/lib/clacky/web/brand.js +20 -5
- data/lib/clacky/web/i18n.js +42 -0
- data/lib/clacky/web/index.html +25 -6
- data/lib/clacky/web/sessions.js +194 -11
- data/lib/clacky/web/settings.js +34 -5
- data/lib/clacky/web/skills.js +53 -31
- data/lib/clacky/web/vendor/hljs/highlight.min.js +1244 -0
- data/lib/clacky/web/vendor/hljs/hljs-theme.css +95 -0
- data/scripts/install_browser.sh +2 -1
- data/scripts/install_full.sh +2 -1
- data/scripts/install_rails_deps.sh +30 -9
- data/scripts/install_system_deps.sh +30 -9
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4107ac9d894f9af9647f19152855f13854d4cfebd8013d2d22057ca98de87274
|
|
4
|
+
data.tar.gz: 3c86e0b865cccc27f8c785f39c49384744ad4e6cce6cdf257ca0485f1d62c260
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6d66fd62f6434c05420f5f639a412fb953e49634c3f0740f155a2d20b70d91acb12a9844a61330f0761d88ff2c284a1d071f0590b3e53553bd11d57a035669f
|
|
7
|
+
data.tar.gz: c64f138d54a5397ba0a49b6878b79576f12b6fc2dec05f7e4b24b5eaf3490060743a0e3a20da17b6cf47476a85eaa093742b616b5be9ab65859db6e8db035404
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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.1.4] - 2026-05-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Thinking level control for AI models, configurable per provider
|
|
12
|
+
- Free brand customization support
|
|
13
|
+
- Syntax highlighting and per-block copy button for code blocks in Web UI (#152)
|
|
14
|
+
- Font size setting (small/medium/large) with proportional UI scaling in Web UI (#147)
|
|
15
|
+
- Chinese README documentation (#148)
|
|
16
|
+
|
|
17
|
+
### Improved
|
|
18
|
+
- Unify POST /api/file-action with download support for remote deployments (#153)
|
|
19
|
+
- Hover interaction polish across Web UI
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Upgrade hot reload no longer leaves stale process (#143)
|
|
23
|
+
|
|
8
24
|
## [1.1.3] - 2026-05-20
|
|
9
25
|
|
|
10
26
|
### Added
|
data/README.md
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
[](https://rubygems.org/gems/openclacky)
|
|
7
7
|
[](LICENSE.txt)
|
|
8
8
|
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="README.md">English</a> · <a href="README_CN.md">简体中文</a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
9
13
|
**The most Token-efficient open-source AI Agent.**
|
|
10
14
|
|
|
11
15
|
OpenClacky matches Claude Code on capability at comparable cost, and saves significantly against other open-source agents (~50% vs OpenClaw, ~3× cheaper than Hermes). 100% open source (MIT), BYOK with any OpenAI-compatible model, built on two years of Agentic R&D and harness engineering.
|
data/README_CN.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# OpenClacky
|
|
2
|
+
|
|
3
|
+
[](https://github.com/clacky-ai/openclacky/actions)
|
|
4
|
+
[](https://rubygems.org/gems/openclacky)
|
|
5
|
+
[](https://www.ruby-lang.org)
|
|
6
|
+
[](https://rubygems.org/gems/openclacky)
|
|
7
|
+
[](LICENSE.txt)
|
|
8
|
+
|
|
9
|
+
**最省 Token 的开源 AI Agent。**
|
|
10
|
+
|
|
11
|
+
OpenClacky 在任务能力上对齐 Claude Code,成本相当,同时相比其他开源 Agent 有显著优势(约节省 50% vs OpenClaw,约便宜 3× vs Hermes)。100% 开源(MIT),支持 BYOK 接入任意 OpenAI 兼容模型,背后是两年 Agentic 研发与 Harness 工程积累。
|
|
12
|
+
|
|
13
|
+
> 官网:https://www.openclacky.com/ · 投资方:**奇绩创坛 · 真格基金 · 红杉中国 · 高瓴资本**
|
|
14
|
+
|
|
15
|
+
## 为什么选 OpenClacky?
|
|
16
|
+
|
|
17
|
+
同一个任务,你要花多少钱?在可比的 Agent 工作负载下,OpenClacky 相比主流方案节省了大量 Token 费用。
|
|
18
|
+
|
|
19
|
+
| Agent | 相对成本 | 备注 |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| **OpenClacky** | **~0.8×** | 16 个工具 · 近 100% 缓存命中 · 子 Agent 路由 |
|
|
22
|
+
| Claude Code | 1.0×(基准) | 世界级 Harness,闭源订阅制 |
|
|
23
|
+
| OpenClaw | ~1.5× | 能力对标的 Harness Agent |
|
|
24
|
+
| Hermes | ~3× | 52 个内置工具,Schema 体积膨胀 ~3–4× |
|
|
25
|
+
|
|
26
|
+
*数据为内部常见 Agent 任务均值,以 Claude Code 为基准。完整基准测试报告将在 GitHub 发布。*
|
|
27
|
+
|
|
28
|
+
## 功能对比
|
|
29
|
+
|
|
30
|
+
核心 Agent 能力各家大致对齐,真正的差异在于**成本、开放性、Skill 进化能力和集成支持**。
|
|
31
|
+
|
|
32
|
+
| 功能 | Claude Code | OpenClaw | Hermes | **OpenClacky** |
|
|
33
|
+
|---|:---:|:---:|:---:|:---:|
|
|
34
|
+
| Token 成本 | 1.0× | ~1.5× | ~3× | **~0.8×** |
|
|
35
|
+
| 开源 | ❌ 闭源 | ✅ 开源 | ✅ 开源 | ✅ MIT |
|
|
36
|
+
| BYOK / 自由选模型 | ❌ 仅限 Anthropic | ✅ | ✅ | ✅ |
|
|
37
|
+
| Skill 自我进化 | ❌ | ❌ | ✅ | ✅ |
|
|
38
|
+
| IM 集成(飞书/企微/微信/Discord/Telegram) | ❌ | ✅ | ✅ | ✅ |
|
|
39
|
+
|
|
40
|
+
## 成本是怎么降下来的
|
|
41
|
+
|
|
42
|
+
不是靠裁剪功能——而是在每一层都做了正确的取舍,效果叠加。
|
|
43
|
+
|
|
44
|
+
### 1. 极高缓存命中率
|
|
45
|
+
Session 不重启、双缓存标记、**先插入再压缩**——System Prompt 从不被修改,压缩后仍能复用缓存。**实测缓存命中率:接近 100%。**
|
|
46
|
+
|
|
47
|
+
### 2. 极简工具集
|
|
48
|
+
仅 **16 个核心工具**。扩展能力通过一个 `invoke_skill` 元工具交给 Skill 生态承载。工具数量不是指标——任务完成率才是。
|
|
49
|
+
|
|
50
|
+
| OpenClacky | Claude Code | OpenClaw | Hermes |
|
|
51
|
+
|:--:|:--:|:--:|:--:|
|
|
52
|
+
| **16** | 40+ | 23 | 52 |
|
|
53
|
+
|
|
54
|
+
### 3. 空闲时自动压缩
|
|
55
|
+
去开个会、倒杯咖啡——Agent 在后台压缩长上下文并预热缓存。你回来发第一条消息就能直接命中缓存。**冷启动首 Token 成本降低 50%+。**
|
|
56
|
+
|
|
57
|
+
### 4. BYOK——你选模型,你定成本
|
|
58
|
+
任意 OpenAI 兼容 API,即插即用。官方直连、聚合路由、兼容中转——100% 由你决定。代码用 Claude,子任务自动路由到 DeepSeek,再省一截。
|
|
59
|
+
|
|
60
|
+
背后是 **2 年 · 3 代 Agentic 架构 · 6 个核心 Harness 工程决策**的积累。
|
|
61
|
+
|
|
62
|
+
## Skills——Agent 的灵魂
|
|
63
|
+
|
|
64
|
+
- **`/` 唤起** — 即时浏览、模糊搜索、直接调用。数百个 Skill 触手可及。
|
|
65
|
+
- **用自然语言创建 Skill** — 描述你想要的,Agent 自动起草 `SKILL.md`、拆解步骤、跑验证。无需写代码。
|
|
66
|
+
- **自我进化** — 每次运行后,Agent 根据执行上下文和结果更新 Skill。下次调用更稳定、更精准。
|
|
67
|
+
- **开放兼容** — 支持 Claude Skills / Markdown Pack / 自定义格式。
|
|
68
|
+
- **可变现** — 打磨好的 Skill 可打包出售,支持加密分发、License 管理、创作者自定价。
|
|
69
|
+
|
|
70
|
+
## 安装
|
|
71
|
+
|
|
72
|
+
### 桌面安装器(推荐)
|
|
73
|
+
|
|
74
|
+
双击安装,环境、依赖、Skill 全部自动配置好。
|
|
75
|
+
|
|
76
|
+
- **macOS** — [下载 `.dmg`](https://oss.1024code.com/openclacky-installer/official/openclacky-installer.dmg)(Apple Silicon / Intel)
|
|
77
|
+
- **Windows** — [下载 `.exe`](https://oss.1024code.com/openclacky-installer/official/openclacky-installer.exe)(Windows 10 2004+ / Windows 11)
|
|
78
|
+
|
|
79
|
+
更多选项:https://www.openclacky.com/
|
|
80
|
+
|
|
81
|
+
### 命令行安装
|
|
82
|
+
|
|
83
|
+
一键安装(Mac/Ubuntu):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
/bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/clacky-ai/openclacky/main/scripts/install.sh)"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Windows:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
powershell -c "& ([scriptblock]::Create((irm 'https://raw.githubusercontent.com/clacky-ai/openclacky/main/scripts/install.ps1')))"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
或使用 Ruby(3.x/4.x):
|
|
96
|
+
|
|
97
|
+
**环境要求:** Ruby >= 3.1.0
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
gem install openclacky
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
详见:https://www.openclacky.com/docs/installation
|
|
104
|
+
|
|
105
|
+
## 快速开始
|
|
106
|
+
|
|
107
|
+
### 终端(CLI)
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
openclacky # 在当前目录启动交互式 Agent
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Web UI
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
openclacky server # 默认地址:http://localhost:7070
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
打开 **http://localhost:7070**,享受完整的聊天界面,支持多 Session 并行——同时跑编码、文案、研究等多个任务。
|
|
120
|
+
|
|
121
|
+
选项:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
openclacky server --port 8080 # 自定义端口
|
|
125
|
+
openclacky server --host 0.0.0.0 # 监听所有接口(支持远程访问)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## 配置
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
$ openclacky
|
|
132
|
+
> /config
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
设置你的 **API Key**、**模型**和 **Base URL**(任意 OpenAI 兼容提供商)。
|
|
136
|
+
|
|
137
|
+
开箱即支持:**Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter**,或任意自定义端点。
|
|
138
|
+
|
|
139
|
+
## 代码开发场景
|
|
140
|
+
|
|
141
|
+
OpenClacky 是一款通用 AI 编程助手——搭建全栈应用脚手架、添加功能,或快速探索陌生代码库:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
$ openclacky
|
|
145
|
+
> /new my-app # 创建新项目脚手架
|
|
146
|
+
> 添加邮箱密码登录功能
|
|
147
|
+
> 支付模块是怎么实现的?
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Star 历史
|
|
151
|
+
|
|
152
|
+
<a href="https://www.star-history.com/?repos=clacky-ai%2Fopenclacky&type=date&legend=top-left">
|
|
153
|
+
<picture>
|
|
154
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=clacky-ai/openclacky&type=date&theme=dark&legend=top-left" />
|
|
155
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=clacky-ai/openclacky&type=date&legend=top-left" />
|
|
156
|
+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=clacky-ai/openclacky&type=date&legend=top-left" />
|
|
157
|
+
</picture>
|
|
158
|
+
</a>
|
|
159
|
+
|
|
160
|
+
## 进阶——创作者计划
|
|
161
|
+
|
|
162
|
+
已有深度用户将自己的工作流打磨成垂直 AI 专家在 OpenClacky 上发布——支持加密分发、License 管理、自定义定价。法律、医疗、财务规划等领域均有落地。
|
|
163
|
+
|
|
164
|
+
了解更多:https://www.openclacky.com/ → Creators
|
|
165
|
+
|
|
166
|
+
## 从源码安装
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
git clone https://github.com/clacky-ai/openclacky.git
|
|
170
|
+
cd openclacky
|
|
171
|
+
bundle install
|
|
172
|
+
bin/clacky
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## 信任与背书
|
|
176
|
+
|
|
177
|
+
- **100% 开源** — MIT 协议,所有代码公开,所有决策可溯源
|
|
178
|
+
- **2 年 Agentic 研发** — 经历 3 代架构演进
|
|
179
|
+
- **16 个核心工具** — 极简设计
|
|
180
|
+
- **投资方** — 奇绩创坛 · 真格基金 · 红杉中国 · 高瓴资本
|
|
181
|
+
|
|
182
|
+
## 关注作者公众号
|
|
183
|
+
|
|
184
|
+
本项目由 **李亚飞** 创立并主导开发。如果你对 AI Agent 工程、Harness 设计、创业经历感兴趣,欢迎关注微信公众号: **技术达人李亚飞**
|
|
185
|
+
|
|
186
|
+
近期文章:
|
|
187
|
+
|
|
188
|
+
- [从 ShowMeBug 到 OpenClacky:我对 AI 时代的 4 次下注](https://mp.weixin.qq.com/s/wTW-IU5Czu-OpJTFh_mwgA)
|
|
189
|
+
- [我把 AI 账单从 30 美金打到 5 美金](https://mp.weixin.qq.com/s/BDhE0y8xbX0ea3vLlV37Ig)
|
|
190
|
+
- [100% Cache 命中的 Harness 怎么设计:一个开源 AI Agent 的 7 个工程决策](https://mp.weixin.qq.com/s/Rc1xk0Qw168D4Y07kkBiGQ)
|
|
191
|
+
|
|
192
|
+
## 参与贡献
|
|
193
|
+
|
|
194
|
+
欢迎在 GitHub 提交 Bug 报告和 Pull Request:https://github.com/clacky-ai/openclacky 。参与贡献者须遵守[行为准则](https://github.com/clacky-ai/openclacky/blob/main/CODE_OF_CONDUCT.md)。
|
|
195
|
+
|
|
196
|
+
## 许可证
|
|
197
|
+
|
|
198
|
+
基于 [MIT 协议](https://opensource.org/licenses/MIT) 开源发布。
|
data/docs/engineering-article.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
I'm Yafei Lee, founder of [OpenClacky](https://github.com/clacky-ai/openclacky), an open-source AI
|
|
7
|
+
I'm Yafei Lee, founder of [OpenClacky](https://github.com/clacky-ai/openclacky), an open-source AI Agent written in Ruby. We wanted an agent with skills, memory, sub-agents, browser automation, dynamic model switching, and long-running sessions. Each of those features made prompt caching worse in a different way.
|
|
8
8
|
|
|
9
9
|
That was the real architecture problem. Not how to call an LLM, not how to add another tool, not how to orchestrate more agents — how to keep the cache prefix stable while the product keeps changing.
|
|
10
10
|
|
|
@@ -63,6 +63,9 @@ module Clacky
|
|
|
63
63
|
@pending_error_rollback = true
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
saved_reasoning = session_data.dig(:config, :reasoning_effort)
|
|
67
|
+
self.reasoning_effort = saved_reasoning if saved_reasoning
|
|
68
|
+
|
|
66
69
|
# Restore the session's original model if it still exists in the current
|
|
67
70
|
# config. This prevents all sessions from silently switching to the new
|
|
68
71
|
# default model when the user changes it and restarts. Falls back to the
|
|
@@ -128,6 +131,7 @@ module Clacky
|
|
|
128
131
|
enable_prompt_caching: @config.enable_prompt_caching,
|
|
129
132
|
max_tokens: @config.max_tokens,
|
|
130
133
|
verbose: @config.verbose,
|
|
134
|
+
reasoning_effort: @reasoning_effort,
|
|
131
135
|
# Persist the current model identity so the session can restore its
|
|
132
136
|
# original model on restart. model_name + model_base_url form a
|
|
133
137
|
# composite key to avoid matching a different provider's model of
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -43,13 +43,30 @@ module Clacky
|
|
|
43
43
|
attr_reader :session_id, :name, :history, :iterations, :total_cost, :working_dir, :created_at, :total_tasks, :todos,
|
|
44
44
|
:cache_stats, :cost_source, :ui, :skill_loader, :agent_profile,
|
|
45
45
|
:status, :error, :updated_at, :source,
|
|
46
|
-
:latest_latency # Hash of latency metrics from the most recent LLM call (see Client#send_messages_with_tools)
|
|
46
|
+
:latest_latency, # Hash of latency metrics from the most recent LLM call (see Client#send_messages_with_tools)
|
|
47
|
+
:reasoning_effort
|
|
47
48
|
attr_accessor :pinned
|
|
48
49
|
|
|
50
|
+
REASONING_EFFORTS = %w[low medium high].freeze
|
|
51
|
+
|
|
49
52
|
def permission_mode
|
|
50
53
|
@config&.permission_mode&.to_s || ""
|
|
51
54
|
end
|
|
52
55
|
|
|
56
|
+
def reasoning_effort=(value)
|
|
57
|
+
@reasoning_effort = normalize_reasoning_effort(value)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private def normalize_reasoning_effort(value)
|
|
61
|
+
return nil if value.nil?
|
|
62
|
+
str = value.to_s.strip.downcase
|
|
63
|
+
return nil if str.empty? || str == "off" || str == "none"
|
|
64
|
+
return str if REASONING_EFFORTS.include?(str)
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
public
|
|
69
|
+
|
|
53
70
|
def initialize(client, config, working_dir:, ui:, profile:, session_id:, source:)
|
|
54
71
|
@client = client # Client for current model
|
|
55
72
|
@config = config.is_a?(AgentConfig) ? config : AgentConfig.new(config)
|
|
@@ -79,6 +96,7 @@ module Clacky
|
|
|
79
96
|
@task_cost_source = :estimated # Track cost source for current task
|
|
80
97
|
@previous_total_tokens = 0 # Track tokens from previous iteration for delta calculation
|
|
81
98
|
@latest_latency = nil # Most recent LLM call's latency metrics (see Client#send_messages_with_tools)
|
|
99
|
+
@reasoning_effort = nil # Per-session reasoning effort override; nil = provider default
|
|
82
100
|
@ui = ui # UIController for direct UI interaction
|
|
83
101
|
@debug_logs = [] # Debug logs for troubleshooting
|
|
84
102
|
@pending_injections = [] # Pending inline skill injections to flush after observe()
|
|
@@ -102,6 +120,9 @@ module Clacky
|
|
|
102
120
|
# Background sync: compare remote skill versions and download updates quietly.
|
|
103
121
|
# Runs in a daemon thread so Agent startup is never blocked.
|
|
104
122
|
@brand_config.sync_brand_skills_async!
|
|
123
|
+
# Free-mode counterpart: branded but not activated → fetch unencrypted skills
|
|
124
|
+
# via the public endpoint so users get a working install with no serial number.
|
|
125
|
+
@brand_config.sync_free_skills_async!
|
|
105
126
|
|
|
106
127
|
# Initialize Time Machine
|
|
107
128
|
init_time_machine
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -410,6 +410,86 @@ module Clacky
|
|
|
410
410
|
end
|
|
411
411
|
end
|
|
412
412
|
|
|
413
|
+
# Fetch the list of free (unencrypted, published) skills available for the
|
|
414
|
+
# configured package_name. Anonymous endpoint — no license key required.
|
|
415
|
+
# This is what powers the "no serial number" free mode: a branded install
|
|
416
|
+
# that is not activated still gets the creator's free skills automatically.
|
|
417
|
+
#
|
|
418
|
+
# Returns { success: bool, skills: [], error: }. Each skill in the returned
|
|
419
|
+
# array carries the same shape as fetch_brand_skills! (name, latest_version,
|
|
420
|
+
# description, etc.) so install_brand_skill! can consume it directly.
|
|
421
|
+
def fetch_free_skills!
|
|
422
|
+
return { success: false, error: "Not branded", skills: [] } unless branded?
|
|
423
|
+
if @package_name.nil? || @package_name.strip.empty?
|
|
424
|
+
return { success: false, error: "package_name not configured", skills: [] }
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
encoded_pkg = URI.encode_www_form_component(@package_name.strip)
|
|
428
|
+
response = platform_client.get("/api/v1/distributions/free_skills?package_name=#{encoded_pkg}")
|
|
429
|
+
|
|
430
|
+
if response[:success] && response[:data].is_a?(Hash)
|
|
431
|
+
installed = installed_brand_skills
|
|
432
|
+
skills = (response[:data]["skills"] || []).map do |skill|
|
|
433
|
+
normalized = skill["name"].to_s.downcase.gsub(/[\s_]+/, "-").gsub(/[^a-z0-9-]/, "").gsub(/-+/, "-")
|
|
434
|
+
name = installed.keys.find { |k| k == normalized } || normalized
|
|
435
|
+
local = installed[name]
|
|
436
|
+
latest_ver = (skill["latest_version"] || {})["version"] || skill["version"]
|
|
437
|
+
needs_update = local ? version_older?(local["version"], latest_ver) : false
|
|
438
|
+
skill.merge(
|
|
439
|
+
"name" => name,
|
|
440
|
+
"installed_version" => local ? local["version"] : nil,
|
|
441
|
+
"needs_update" => needs_update
|
|
442
|
+
)
|
|
443
|
+
end
|
|
444
|
+
{ success: true, skills: skills, paid_skills_count: response[:data]["paid_skills_count"].to_i }
|
|
445
|
+
else
|
|
446
|
+
{ success: false, error: response[:error] || "Failed to fetch free skills", skills: [], paid_skills_count: 0 }
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Install a single free (unencrypted) skill. Thin wrapper around
|
|
451
|
+
# install_brand_skill! that records the skill as encrypted: false so the
|
|
452
|
+
# loader reads SKILL.md directly without attempting decryption.
|
|
453
|
+
def install_free_skill!(skill_info)
|
|
454
|
+
install_brand_skill!(skill_info, encrypted: false)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Synchronise free skills in the background for unactivated branded installs.
|
|
458
|
+
#
|
|
459
|
+
# Mirrors sync_brand_skills_async! but uses the public free_skills endpoint
|
|
460
|
+
# so no license is required. Only runs when the install is branded and NOT
|
|
461
|
+
# activated — once a license is activated the regular brand-skill sync
|
|
462
|
+
# takes over (and may include additional encrypted skills).
|
|
463
|
+
#
|
|
464
|
+
# @return [Thread, nil]
|
|
465
|
+
def sync_free_skills_async!(on_complete: nil)
|
|
466
|
+
return nil unless branded?
|
|
467
|
+
return nil if activated?
|
|
468
|
+
return nil if ENV["CLACKY_TEST"] == "1"
|
|
469
|
+
|
|
470
|
+
Thread.new do
|
|
471
|
+
Thread.current.abort_on_exception = false
|
|
472
|
+
|
|
473
|
+
begin
|
|
474
|
+
result = fetch_free_skills!
|
|
475
|
+
next unless result[:success]
|
|
476
|
+
|
|
477
|
+
remote_skill_names = result[:skills].map { |s| s["name"] }
|
|
478
|
+
installed_brand_skills.each_key do |local_name|
|
|
479
|
+
send(:delete_brand_skill!, local_name) unless remote_skill_names.include?(local_name)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
installed = installed_brand_skills
|
|
483
|
+
to_install = result[:skills].select { |s| installed[s["name"]].nil? || s["needs_update"] }
|
|
484
|
+
results = to_install.map { |skill_info| install_free_skill!(skill_info) }
|
|
485
|
+
|
|
486
|
+
on_complete&.call(results)
|
|
487
|
+
rescue StandardError
|
|
488
|
+
# Background sync failures are intentionally swallowed.
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
413
493
|
# Upload (publish) a custom skill ZIP to the OpenClacky Cloud API.
|
|
414
494
|
# Calls POST /api/v1/client/skills (system-license endpoint).
|
|
415
495
|
# zip_data is the raw binary content of the ZIP file.
|
|
@@ -629,7 +709,9 @@ module Clacky
|
|
|
629
709
|
|
|
630
710
|
# Install (or update) a single brand skill by downloading and extracting its zip.
|
|
631
711
|
# skill_info: a hash from fetch_brand_skills! with at least name + latest_version.download_url + version
|
|
632
|
-
|
|
712
|
+
# encrypted: whether the ZIP contains AES-encrypted .enc files + MANIFEST.enc.json (true)
|
|
713
|
+
# or plaintext SKILL.md and supporting files (false, used by free-mode).
|
|
714
|
+
def install_brand_skill!(skill_info, encrypted: true)
|
|
633
715
|
require "net/http"
|
|
634
716
|
require "uri"
|
|
635
717
|
|
|
@@ -698,10 +780,10 @@ module Clacky
|
|
|
698
780
|
|
|
699
781
|
FileUtils.rm_f(tmp_zip)
|
|
700
782
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
783
|
+
record_installed_skill(slug, version, skill_info["description"],
|
|
784
|
+
encrypted: encrypted,
|
|
785
|
+
description_zh: skill_info["description_zh"],
|
|
786
|
+
name_zh: skill_info["name_zh"])
|
|
705
787
|
|
|
706
788
|
{ success: true, name: slug, version: version }
|
|
707
789
|
rescue StandardError, ScriptError => e
|
data/lib/clacky/client.rb
CHANGED
|
@@ -125,7 +125,7 @@ module Clacky
|
|
|
125
125
|
# path. When given but streaming is not yet wired for the active provider,
|
|
126
126
|
# a single synthetic invocation is fired after the response is received,
|
|
127
127
|
# so UI plumbing can be exercised end-to-end without the proxy work.
|
|
128
|
-
def send_messages_with_tools(messages, model:, tools:, max_tokens:, enable_caching: false, on_chunk: nil)
|
|
128
|
+
def send_messages_with_tools(messages, model:, tools:, max_tokens:, enable_caching: false, reasoning_effort: nil, on_chunk: nil)
|
|
129
129
|
caching_enabled = enable_caching && supports_prompt_caching?(model)
|
|
130
130
|
cloned = deep_clone(messages)
|
|
131
131
|
|
|
@@ -140,13 +140,13 @@ module Clacky
|
|
|
140
140
|
response =
|
|
141
141
|
if bedrock?
|
|
142
142
|
streaming_used = !on_chunk.nil?
|
|
143
|
-
send_bedrock_request(cloned, model, tools, max_tokens, caching_enabled, on_chunk: wrapped_on_chunk)
|
|
143
|
+
send_bedrock_request(cloned, model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort, on_chunk: wrapped_on_chunk)
|
|
144
144
|
elsif anthropic_format?
|
|
145
145
|
streaming_used = !on_chunk.nil?
|
|
146
|
-
send_anthropic_request(cloned, model, tools, max_tokens, caching_enabled, on_chunk: wrapped_on_chunk)
|
|
146
|
+
send_anthropic_request(cloned, model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort, on_chunk: wrapped_on_chunk)
|
|
147
147
|
else
|
|
148
148
|
streaming_used = !on_chunk.nil?
|
|
149
|
-
send_openai_request(cloned, model, tools, max_tokens, caching_enabled, on_chunk: wrapped_on_chunk)
|
|
149
|
+
send_openai_request(cloned, model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort, on_chunk: wrapped_on_chunk)
|
|
150
150
|
end
|
|
151
151
|
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
152
152
|
|
|
@@ -217,8 +217,8 @@ module Clacky
|
|
|
217
217
|
|
|
218
218
|
# ── Bedrock Converse request / response ───────────────────────────────────
|
|
219
219
|
|
|
220
|
-
def send_bedrock_request(messages, model, tools, max_tokens, caching_enabled, on_chunk: nil)
|
|
221
|
-
body = MessageFormat::Bedrock.build_request_body(messages, model, tools, max_tokens, caching_enabled)
|
|
220
|
+
def send_bedrock_request(messages, model, tools, max_tokens, caching_enabled, reasoning_effort: nil, on_chunk: nil)
|
|
221
|
+
body = MessageFormat::Bedrock.build_request_body(messages, model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort)
|
|
222
222
|
return send_bedrock_stream_request(body, model, on_chunk) if on_chunk
|
|
223
223
|
|
|
224
224
|
response = bedrock_connection.post(bedrock_endpoint(model)) { |r| r.body = body.to_json }
|
|
@@ -248,7 +248,10 @@ module Clacky
|
|
|
248
248
|
end
|
|
249
249
|
end
|
|
250
250
|
|
|
251
|
-
|
|
251
|
+
unless response.status == 200
|
|
252
|
+
response.env.body = sse_buf if response.body.to_s.empty?
|
|
253
|
+
raise_error(response)
|
|
254
|
+
end
|
|
252
255
|
MessageFormat::Bedrock.parse_response(aggregator.to_h)
|
|
253
256
|
end
|
|
254
257
|
|
|
@@ -263,11 +266,11 @@ module Clacky
|
|
|
263
266
|
|
|
264
267
|
# ── Anthropic request / response ──────────────────────────────────────────
|
|
265
268
|
|
|
266
|
-
def send_anthropic_request(messages, model, tools, max_tokens, caching_enabled, on_chunk: nil)
|
|
269
|
+
def send_anthropic_request(messages, model, tools, max_tokens, caching_enabled, reasoning_effort: nil, on_chunk: nil)
|
|
267
270
|
# Apply cache_control to the message that marks the cache breakpoint
|
|
268
271
|
messages = apply_message_caching(messages) if caching_enabled
|
|
269
272
|
|
|
270
|
-
body = MessageFormat::Anthropic.build_request_body(messages, model, tools, max_tokens, caching_enabled)
|
|
273
|
+
body = MessageFormat::Anthropic.build_request_body(messages, model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort)
|
|
271
274
|
return send_anthropic_stream_request(body, on_chunk) if on_chunk
|
|
272
275
|
|
|
273
276
|
response = anthropic_connection.post(anthropic_messages_path) { |r| r.body = body.to_json }
|
|
@@ -304,14 +307,15 @@ module Clacky
|
|
|
304
307
|
|
|
305
308
|
# ── OpenAI request / response ─────────────────────────────────────────────
|
|
306
309
|
|
|
307
|
-
def send_openai_request(messages, model, tools, max_tokens, caching_enabled, on_chunk: nil)
|
|
310
|
+
def send_openai_request(messages, model, tools, max_tokens, caching_enabled, reasoning_effort: nil, on_chunk: nil)
|
|
308
311
|
# Apply cache_control markers to messages when caching is enabled.
|
|
309
312
|
# OpenRouter proxies Claude with the same cache_control field convention as Anthropic direct.
|
|
310
313
|
messages = apply_message_caching(messages) if caching_enabled
|
|
311
314
|
|
|
312
315
|
body = MessageFormat::OpenAI.build_request_body(
|
|
313
316
|
messages, model, tools, max_tokens, caching_enabled,
|
|
314
|
-
vision_supported: @vision_supported
|
|
317
|
+
vision_supported: @vision_supported,
|
|
318
|
+
reasoning_effort: reasoning_effort
|
|
315
319
|
)
|
|
316
320
|
return send_openai_stream_request(body, on_chunk) if on_chunk
|
|
317
321
|
|
|
@@ -48,7 +48,7 @@ module Clacky
|
|
|
48
48
|
# @param max_tokens [Integer]
|
|
49
49
|
# @param caching_enabled [Boolean]
|
|
50
50
|
# @return [Hash] ready to serialize as JSON body
|
|
51
|
-
def build_request_body(messages, model, tools, max_tokens, caching_enabled)
|
|
51
|
+
def build_request_body(messages, model, tools, max_tokens, caching_enabled, reasoning_effort: nil)
|
|
52
52
|
system_messages = messages.select { |m| m[:role] == "system" }
|
|
53
53
|
regular_messages = messages.reject { |m| m[:role] == "system" }
|
|
54
54
|
|
|
@@ -64,9 +64,21 @@ module Clacky
|
|
|
64
64
|
body = { model: model, max_tokens: max_tokens, messages: api_messages }
|
|
65
65
|
body[:system] = system_text unless system_text.empty?
|
|
66
66
|
body[:tools] = api_tools if api_tools&.any?
|
|
67
|
+
|
|
68
|
+
if (effort = normalized_effort(reasoning_effort))
|
|
69
|
+
body[:thinking] = { type: "adaptive" }
|
|
70
|
+
body[:output_config] = { effort: effort }
|
|
71
|
+
end
|
|
72
|
+
|
|
67
73
|
body
|
|
68
74
|
end
|
|
69
75
|
|
|
76
|
+
private_class_method def self.normalized_effort(effort)
|
|
77
|
+
return nil if effort.nil? || effort.to_s.empty?
|
|
78
|
+
s = effort.to_s
|
|
79
|
+
%w[low medium high].include?(s) ? s : nil
|
|
80
|
+
end
|
|
81
|
+
|
|
70
82
|
# ── Response parsing ──────────────────────────────────────────────────────
|
|
71
83
|
|
|
72
84
|
# Parse Anthropic API response into canonical internal format.
|
|
@@ -52,7 +52,7 @@ module Clacky
|
|
|
52
52
|
# @param max_tokens [Integer]
|
|
53
53
|
# @param caching_enabled [Boolean] (currently unused for Bedrock)
|
|
54
54
|
# @return [Hash] ready to serialize as JSON body
|
|
55
|
-
def build_request_body(messages, model, tools, max_tokens, caching_enabled = false)
|
|
55
|
+
def build_request_body(messages, model, tools, max_tokens, caching_enabled = false, reasoning_effort: nil)
|
|
56
56
|
system_messages = messages.select { |m| m[:role] == "system" }
|
|
57
57
|
regular_messages = messages.reject { |m| m[:role] == "system" }
|
|
58
58
|
|
|
@@ -83,9 +83,21 @@ module Clacky
|
|
|
83
83
|
body[:toolConfig] = { tools: tools.map { |t| to_api_tool(t) } }
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
extra = additional_fields_for_effort(reasoning_effort)
|
|
87
|
+
body[:additionalModelRequestFields] = extra if extra
|
|
88
|
+
|
|
86
89
|
body
|
|
87
90
|
end
|
|
88
91
|
|
|
92
|
+
private_class_method def self.additional_fields_for_effort(effort)
|
|
93
|
+
return nil if effort.nil? || effort.to_s.empty?
|
|
94
|
+
return nil unless %w[low medium high].include?(effort.to_s)
|
|
95
|
+
{
|
|
96
|
+
thinking: { type: "adaptive" },
|
|
97
|
+
output_config: { effort: effort.to_s }
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
89
101
|
# ── Response parsing ──────────────────────────────────────────────────────
|
|
90
102
|
|
|
91
103
|
# Parse Bedrock Converse API response into canonical internal format.
|
|
@@ -44,7 +44,7 @@ module Clacky
|
|
|
44
44
|
# @param vision_supported [Boolean] whether the target model accepts
|
|
45
45
|
# image_url content blocks (default true, conservative)
|
|
46
46
|
# @return [Hash]
|
|
47
|
-
def build_request_body(messages, model, tools, max_tokens, caching_enabled, vision_supported: true)
|
|
47
|
+
def build_request_body(messages, model, tools, max_tokens, caching_enabled, vision_supported: true, reasoning_effort: nil)
|
|
48
48
|
api_messages = messages.map { |msg| normalize_message_content(msg, vision_supported: vision_supported) }
|
|
49
49
|
|
|
50
50
|
body = { model: model, max_tokens: max_tokens, messages: api_messages }
|
|
@@ -59,6 +59,10 @@ module Clacky
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
if reasoning_effort && !reasoning_effort.to_s.empty?
|
|
63
|
+
body[:reasoning_effort] = reasoning_effort.to_s
|
|
64
|
+
end
|
|
65
|
+
|
|
62
66
|
body
|
|
63
67
|
end
|
|
64
68
|
|