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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca451cdde2e1cc068bda60f6b56f3d534a0187a7fad0d0b42308ad391c028bdf
4
- data.tar.gz: e828f029d229e20e4e41edbc6773c2f6e9d0bf66aae3ad5b088f563fa8bf4c51
3
+ metadata.gz: 4107ac9d894f9af9647f19152855f13854d4cfebd8013d2d22057ca98de87274
4
+ data.tar.gz: 3c86e0b865cccc27f8c785f39c49384744ad4e6cce6cdf257ca0485f1d62c260
5
5
  SHA512:
6
- metadata.gz: 32ee351b327f14a64e66fa6d27926e99c148ee69965a41360528e8e68d69d5ea40e2377cb94f851d4f00bb1d445fed8e35474c35c620cdfe4cd2ae2a924af186
7
- data.tar.gz: 8a08b30877664568e264f823fd328cab521dc7d3362c10dfb30b50aac76d951bb3c753df23f2d47f4ed631128f490810497c5166378d43c688bf814c81040867
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
  [![Downloads](https://img.shields.io/gem/dt/openclacky?label=downloads&style=flat-square&color=brightgreen)](https://rubygems.org/gems/openclacky)
7
7
  [![License](https://img.shields.io/badge/license-MIT-lightgrey?style=flat-square)](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
+ [![Build](https://img.shields.io/github/actions/workflow/status/clacky-ai/openclacky/main.yml?label=build&style=flat-square)](https://github.com/clacky-ai/openclacky/actions)
4
+ [![Release](https://img.shields.io/gem/v/openclacky?label=release&style=flat-square&color=blue)](https://rubygems.org/gems/openclacky)
5
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.1.0-red?style=flat-square)](https://www.ruby-lang.org)
6
+ [![Downloads](https://img.shields.io/gem/dt/openclacky?label=downloads&style=flat-square&color=brightgreen)](https://rubygems.org/gems/openclacky)
7
+ [![License](https://img.shields.io/badge/license-MIT-lightgrey?style=flat-square)](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) 开源发布。
@@ -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 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.
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
 
@@ -104,6 +104,7 @@ module Clacky
104
104
  tools: tools_to_send,
105
105
  max_tokens: @config.max_tokens,
106
106
  enable_caching: @config.enable_prompt_caching,
107
+ reasoning_effort: @reasoning_effort,
107
108
  on_chunk: build_progress_on_chunk
108
109
  )
109
110
 
@@ -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
@@ -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
- def install_brand_skill!(skill_info)
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
- # Record installed version in brand_skills.json (including description for
702
- # offline display when the remote API is unreachable).
703
- # encrypted: true because the ZIP contains MANIFEST.enc.json + AES-256-GCM encrypted files.
704
- record_installed_skill(slug, version, skill_info["description"], encrypted: true, description_zh: skill_info["description_zh"], name_zh: skill_info["name_zh"])
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
- raise_error(response) unless response.status == 200
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