openclacky 0.9.2 → 0.9.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 +27 -0
- data/docs/security-design.md +109 -0
- data/lib/clacky/agent/message_compressor_helper.rb +82 -69
- data/lib/clacky/agent/session_serializer.rb +9 -1
- data/lib/clacky/agent/skill_manager.rb +7 -0
- data/lib/clacky/agent.rb +11 -3
- data/lib/clacky/banner.rb +65 -0
- data/lib/clacky/block_font.rb +331 -0
- data/lib/clacky/brand_config.rb +73 -5
- data/lib/clacky/client.rb +159 -631
- data/lib/clacky/default_skills/activate-license/SKILL.md +118 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +10 -20
- data/lib/clacky/message_format/anthropic.rb +241 -0
- data/lib/clacky/message_format/open_ai.rb +135 -0
- data/lib/clacky/server/channel/adapters/wecom/adapter.rb +2 -0
- data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +13 -0
- data/lib/clacky/server/http_server.rb +12 -2
- data/lib/clacky/session_manager.rb +7 -2
- data/lib/clacky/tools/browser.rb +109 -280
- data/lib/clacky/ui2/block_font.rb +10 -0
- data/lib/clacky/ui2/components/welcome_banner.rb +23 -22
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +588 -6
- data/lib/clacky/web/app.js +30 -15
- data/lib/clacky/web/brand.js +141 -9
- data/lib/clacky/web/i18n.js +28 -2
- data/lib/clacky/web/index.html +142 -127
- data/lib/clacky/web/onboard.js +192 -225
- data/lib/clacky/web/sessions.js +12 -8
- data/lib/clacky/web/settings.js +57 -4
- data/lib/clacky.rb +2 -0
- data/scripts/install.sh +60 -15
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d842684d3cae23106509a9e47be986d51e518f8b953e66011783b32e1a121a6e
|
|
4
|
+
data.tar.gz: 34a8ef45f736724fd7e8356e032a4cd1102bd1a28b47f412419895bd8575caf7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc9f6ee3d0b8ebf01346261dfd9dbcf6a77654bc40632b5730432fbe5ce23c604f30135739507c1802e837a4baf6097609ba26397c9eea2ec4555b115cd73dc9
|
|
7
|
+
data.tar.gz: 20ad77eb3191fadd1d4340ce7a92b16489be68e83dc4d33fa1d2e802a821074d1964f9a1e7afadcd7cbf9846b81dabebd5cbd918441bb3b3528bfd6212fb5f56
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.3] - 2026-03-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Brand logo banner on web server startup**: a styled block-font logo now displays in the terminal when `clacky server` launches, giving a polished startup experience
|
|
14
|
+
- **BlockFont renderer replaces artii dependency**: the gem now ships its own high-quality block-font engine for rendering large ASCII logos, removing the external `artii` dependency and enabling full offline use
|
|
15
|
+
- **Hover-to-expand token usage and session info bar**: hovering over the token usage line or session info bar in the WebUI now expands it to show full details, keeping the UI compact by default
|
|
16
|
+
- **Redesigned setup panel with Back button and Custom provider support**: the model setup flow now includes a Back button for navigation and a dedicated "Custom provider" path, making it easier to configure non-standard API endpoints; also fixes a dropdown re-entry bug
|
|
17
|
+
- **License activation via non-blocking top banner**: the brand activation flow no longer blocks the entire UI with a full-screen panel — it now shows a slim top banner, and activation is handled through a dedicated skill
|
|
18
|
+
- **`startSoulSession` exposed on Onboard public interface**: third-party integrations can now trigger soul session initialization directly from the onboard module
|
|
19
|
+
|
|
20
|
+
### Improved
|
|
21
|
+
- **Browser tool simplified and config-driven**: the browser tool setup is now handled through a unified config object, removing ~250 lines of complex auto-restart logic and making the tool more predictable and maintainable
|
|
22
|
+
- **Prompt caching more stable**: cache anchoring now uses the last assistant message as the stable boundary, reducing cache misses caused by system prompt variations; caching is correctly restored for both Anthropic and OpenRouter paths
|
|
23
|
+
- **Message format extracted to dedicated modules**: OpenAI and Anthropic message formatting now live in separate modules (`Clacky::MessageFormat::OpenAI` and `Clacky::MessageFormat::Anthropic`), making the client code easier to read and test
|
|
24
|
+
- **WeCom channel reliability**: auth failure handling is improved with proper reconnection logic; the `channel-setup` skill guidance is also updated for clarity
|
|
25
|
+
- **Install script and license expiry handling**: the install script is streamlined, license-expired states are handled gracefully, and encrypted skills are decrypted at load time
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **Prompt cache stability across turns**: cache was occasionally invalidated between turns due to message boundary drift; now anchored reliably to the last assistant message
|
|
29
|
+
- **`request_user_feedback` missing from session history replay**: feedback prompts sent during a session were not rendered when replaying history in the WebUI; they now appear correctly as assistant messages
|
|
30
|
+
- **Brand activation banner not shown when API key is missing**: the banner now correctly appears even when no API key is configured, with a translated skip warning
|
|
31
|
+
- **Zip extraction security**: zip files are now read in chunks with size verification, preventing potential zip-bomb or oversized-file issues
|
|
32
|
+
|
|
33
|
+
### More
|
|
34
|
+
- Remove browser tool auto-restart logic that was causing instability in headless environments
|
|
35
|
+
- Add security design documentation
|
|
36
|
+
|
|
10
37
|
## [0.9.2] - 2026-03-15
|
|
11
38
|
|
|
12
39
|
### Fixed
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# openclacky 安全设计方案
|
|
2
|
+
|
|
3
|
+
> 创建时间:2026-03-14
|
|
4
|
+
> 背景:用户反馈 openclacky 可以操作任意文件和电脑,感觉不安全。本文档梳理现有安全机制和待实施的安全策略。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 一、现有安全机制
|
|
9
|
+
|
|
10
|
+
| 机制 | 说明 |
|
|
11
|
+
|---|---|
|
|
12
|
+
| 权限模式 | CLI 默认 `confirm_safes`,危险操作需用户确认 |
|
|
13
|
+
| SafeShell | `rm` → 软删除到 trash、`sudo` 拦截、`curl \| bash` 拦截 |
|
|
14
|
+
| 文件 diff 预览 | `write`/`edit` 操作前展示变更内容 |
|
|
15
|
+
| 安全日志 | 每个项目有 `~/.clacky/safety_logs/<hash>/safety.log` 记录拦截记录 |
|
|
16
|
+
| 受保护文件 | `.env`、`.ssh/`、`.aws/`、`Gemfile` 等不能被删除 |
|
|
17
|
+
|
|
18
|
+
### 权限模式说明
|
|
19
|
+
|
|
20
|
+
| 模式 | 行为 | 适用场景 |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `confirm_safes` | 只读操作自动执行,写/危险 shell 需用户确认 | **CLI 默认** |
|
|
23
|
+
| `confirm_all` | 遇到 `request_user_feedback` 才等待人工 | WebUI session 默认 |
|
|
24
|
+
| `auto_approve` | 全自动执行所有工具,无需确认 | 定时任务默认 |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 二、现有安全薄弱点
|
|
29
|
+
|
|
30
|
+
1. **`write` 工具无路径限制** — 可写到项目目录外的任意路径(如 `~/.bashrc`)
|
|
31
|
+
2. **`shell` 和 `safe_shell` 同时注册** — AI 可能绕过 safe_shell 直接用 `shell`
|
|
32
|
+
3. **WebUI 无认证** — 任何能访问本地端口的进程或局域网用户都能控制 Agent
|
|
33
|
+
4. **write 覆盖无备份** — 文件被 AI 改写后无法恢复(rm 有软删除,write 没有)
|
|
34
|
+
5. **安全机制不可见** — 用户感知不到现有保护,导致不信任
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 三、安全策略(待实施)
|
|
39
|
+
|
|
40
|
+
### 🟢 第一梯队:低成本、高收益(推荐优先)
|
|
41
|
+
|
|
42
|
+
#### 1. 操作审计日志
|
|
43
|
+
- Agent 执行的每个 `write`/`edit`/`shell` 操作,写入 `~/.clacky/audit.log`
|
|
44
|
+
- 格式:时间戳 + 项目 + 操作类型 + 详情
|
|
45
|
+
- 提供 `clacky audit` 命令查看历史操作
|
|
46
|
+
- **价值**:让用户看得见 AI 做了什么,信任感最强
|
|
47
|
+
|
|
48
|
+
#### 2. 权限模式状态可见化
|
|
49
|
+
- WebUI 顶部常驻显示当前权限级别,颜色区分
|
|
50
|
+
- 🔴 `auto_approve` — 全自动,无需确认
|
|
51
|
+
- 🟡 `confirm_safes` — 危险操作需确认(推荐)
|
|
52
|
+
- 🟢 `confirm_all` — 所有操作均需确认
|
|
53
|
+
- CLI 启动时打印当前权限模式
|
|
54
|
+
|
|
55
|
+
#### 3. 统一走 safe_shell(堵漏洞)
|
|
56
|
+
- 移除裸 `shell` 工具,只保留 `safe_shell`
|
|
57
|
+
- 确保所有 shell 命令都经过安全替换器处理
|
|
58
|
+
- 成本极低,安全提升明显
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### 🟡 第二梯队:中等成本、用户感知强
|
|
63
|
+
|
|
64
|
+
#### 4. write/edit 路径白名单
|
|
65
|
+
- 只允许写项目工作目录内的文件
|
|
66
|
+
- 写项目外路径(如 `~/.bashrc`、`~/.ssh/`)需额外确认,或默认拒绝
|
|
67
|
+
- 防止 AI 越界修改系统/用户配置文件
|
|
68
|
+
|
|
69
|
+
#### 5. WebUI 本地认证 Token
|
|
70
|
+
- 启动 Web 服务时生成随机 token,终端显示带 token 的访问链接
|
|
71
|
+
- 防止本地其他进程或局域网内的人劫持 Agent
|
|
72
|
+
- 参考:Jupyter Notebook 的成熟方案
|
|
73
|
+
|
|
74
|
+
#### 6. write 操作自动备份(撤销支持)
|
|
75
|
+
- `write` 覆盖文件前,自动备份原文件到 `~/.clacky/trash/`
|
|
76
|
+
- 支持 `clacky undo` 恢复被 AI 改写的文件
|
|
77
|
+
- 用户最担心的就是"AI 把我文件改了找不回来"
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### 🔵 第三梯队:大投入、长期规划
|
|
82
|
+
|
|
83
|
+
#### 7. 沙箱模式(Docker)
|
|
84
|
+
- 可选的隔离执行环境,把 Agent 限制在容器内
|
|
85
|
+
- 主机文件系统与 Agent 完全隔离
|
|
86
|
+
- 适合高敏感场景,启动成本高、对体验有影响
|
|
87
|
+
|
|
88
|
+
#### 8. 网络访问白名单
|
|
89
|
+
- 控制 `web_fetch`/`curl` 能访问的域名范围
|
|
90
|
+
- 适合企业级部署场景
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 四、实施优先级建议
|
|
95
|
+
|
|
96
|
+
1. **审计日志** — 让用户"看得见"AI 做了什么,信任感提升最显著
|
|
97
|
+
2. **write/edit 路径白名单** — 堵最大的安全漏洞
|
|
98
|
+
3. **统一走 safe_shell** — 低成本堵漏,可顺手实施
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 五、让用户放心的核心原则
|
|
103
|
+
|
|
104
|
+
> **安全机制不够,用户感知更重要。**
|
|
105
|
+
|
|
106
|
+
- 用户能看到 AI 正在做什么(实时显示)
|
|
107
|
+
- 用户能看到 AI 做过什么(审计日志)
|
|
108
|
+
- 用户能撤销 AI 的操作(备份+恢复)
|
|
109
|
+
- 用户能控制 AI 的权限(权限模式可见+可切换)
|
|
@@ -153,113 +153,126 @@ module Clacky
|
|
|
153
153
|
)
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
-
# Get recent messages while preserving tool_calls/tool_results pairs
|
|
157
|
-
#
|
|
156
|
+
# Get recent messages while preserving tool_calls/tool_results pairs.
|
|
157
|
+
# Handles both canonical format (role: "tool") and legacy Anthropic-native
|
|
158
|
+
# format (role: "user" with tool_result content blocks).
|
|
158
159
|
# @param messages [Array] All messages
|
|
159
160
|
# @param count [Integer] Target number of recent messages to keep
|
|
160
161
|
# @return [Array] Recent messages with complete tool pairs
|
|
161
162
|
def get_recent_messages_with_tool_pairs(messages, count)
|
|
162
|
-
# This method ensures that assistant messages with tool_calls are always kept together
|
|
163
|
-
# with ALL their corresponding tool_results, maintaining the correct order.
|
|
164
|
-
# This is critical for Bedrock Claude API which validates the tool_calls/tool_results pairing.
|
|
165
|
-
|
|
166
163
|
return [] if messages.nil? || messages.empty?
|
|
167
164
|
|
|
168
|
-
# Track which messages to include
|
|
169
165
|
messages_to_include = Set.new
|
|
170
|
-
|
|
171
|
-
# Start from the end and work backwards
|
|
172
166
|
i = messages.size - 1
|
|
173
167
|
messages_collected = 0
|
|
174
168
|
|
|
175
169
|
while i >= 0 && messages_collected < count
|
|
176
170
|
msg = messages[i]
|
|
177
171
|
|
|
178
|
-
# Skip if already marked for inclusion
|
|
179
172
|
if messages_to_include.include?(i)
|
|
180
173
|
i -= 1
|
|
181
174
|
next
|
|
182
175
|
end
|
|
183
176
|
|
|
184
|
-
# Mark this message for inclusion
|
|
185
177
|
messages_to_include.add(i)
|
|
186
178
|
messages_collected += 1
|
|
187
179
|
|
|
188
|
-
#
|
|
189
|
-
if msg[:role] == "assistant" && msg[:tool_calls]
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
# Find all tool results that belong to this assistant message
|
|
193
|
-
# They should be in the messages immediately following this assistant message
|
|
194
|
-
j = i + 1
|
|
195
|
-
while j < messages.size
|
|
196
|
-
next_msg = messages[j]
|
|
197
|
-
|
|
198
|
-
# If we find a tool result for one of our tool_calls, include it
|
|
199
|
-
if next_msg[:role] == "tool" && tool_call_ids.include?(next_msg[:tool_call_id])
|
|
200
|
-
messages_to_include.add(j)
|
|
201
|
-
elsif next_msg[:role] != "tool"
|
|
202
|
-
# Stop when we hit a non-tool message (start of next turn)
|
|
203
|
-
break
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
j += 1
|
|
207
|
-
end
|
|
180
|
+
# assistant with tool_calls → also pull in all following tool results
|
|
181
|
+
if msg[:role] == "assistant" && msg[:tool_calls]&.any?
|
|
182
|
+
pull_tool_results_after(messages, i, messages_to_include)
|
|
208
183
|
end
|
|
209
184
|
|
|
210
|
-
#
|
|
211
|
-
if msg
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
while j >= 0
|
|
215
|
-
prev_msg = messages[j]
|
|
216
|
-
if prev_msg[:role] == "assistant" && prev_msg[:tool_calls]
|
|
217
|
-
# Check if this assistant has the matching tool_call
|
|
218
|
-
has_matching_call = prev_msg[:tool_calls].any? { |tc| tc[:id] == msg[:tool_call_id] }
|
|
219
|
-
if has_matching_call
|
|
220
|
-
unless messages_to_include.include?(j)
|
|
221
|
-
messages_to_include.add(j)
|
|
222
|
-
messages_collected += 1
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
# Also include all other tool results for this assistant message
|
|
226
|
-
tool_call_ids = prev_msg[:tool_calls].map { |tc| tc[:id] }
|
|
227
|
-
k = j + 1
|
|
228
|
-
while k < messages.size
|
|
229
|
-
result_msg = messages[k]
|
|
230
|
-
if result_msg[:role] == "tool" && tool_call_ids.include?(result_msg[:tool_call_id])
|
|
231
|
-
messages_to_include.add(k)
|
|
232
|
-
elsif result_msg[:role] != "tool"
|
|
233
|
-
break
|
|
234
|
-
end
|
|
235
|
-
k += 1
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
break
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
j -= 1
|
|
185
|
+
# tool result (canonical or legacy Anthropic) → also pull in its assistant
|
|
186
|
+
if tool_result_message?(msg)
|
|
187
|
+
pull_assistant_before(messages, i, messages_to_include) do |added|
|
|
188
|
+
messages_collected += 1 if added
|
|
242
189
|
end
|
|
243
190
|
end
|
|
244
191
|
|
|
245
192
|
i -= 1
|
|
246
193
|
end
|
|
247
194
|
|
|
248
|
-
# Extract the messages in their original order
|
|
249
195
|
recent_messages = messages_to_include.to_a.sort.map { |idx| messages[idx] }
|
|
250
196
|
|
|
251
197
|
# Truncate large tool results to prevent token bloat
|
|
252
198
|
recent_messages.map do |msg|
|
|
253
|
-
|
|
254
|
-
msg.merge(content: msg[:content][0..2000] + "...\n[Content truncated - exceeded 2000 characters]")
|
|
255
|
-
else
|
|
256
|
-
msg
|
|
257
|
-
end
|
|
199
|
+
truncate_tool_result(msg)
|
|
258
200
|
end
|
|
259
201
|
end
|
|
260
202
|
|
|
261
203
|
private
|
|
262
204
|
|
|
205
|
+
# Returns true if msg is a tool result, regardless of storage format.
|
|
206
|
+
# Canonical: role:"tool" | Legacy Anthropic-native: role:"user" + tool_result blocks
|
|
207
|
+
def tool_result_message?(msg)
|
|
208
|
+
MessageFormat::OpenAI.tool_result_message?(msg) ||
|
|
209
|
+
MessageFormat::Anthropic.tool_result_message?(msg)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Returns the tool_call IDs referenced in a tool result message.
|
|
213
|
+
def tool_result_ids(msg)
|
|
214
|
+
if MessageFormat::OpenAI.tool_result_message?(msg)
|
|
215
|
+
MessageFormat::OpenAI.tool_call_ids(msg)
|
|
216
|
+
else
|
|
217
|
+
MessageFormat::Anthropic.tool_use_ids(msg)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Returns true if msg is a tool result that matches any of the given call IDs.
|
|
222
|
+
def tool_result_for?(msg, call_ids)
|
|
223
|
+
tool_result_message?(msg) && (tool_result_ids(msg) & call_ids).any?
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Mark all tool results immediately following messages[assistant_idx].
|
|
227
|
+
# Stops at the first non-tool-result message.
|
|
228
|
+
def pull_tool_results_after(messages, assistant_idx, include_set)
|
|
229
|
+
call_ids = messages[assistant_idx][:tool_calls].map { |tc| tc[:id] }
|
|
230
|
+
j = assistant_idx + 1
|
|
231
|
+
while j < messages.size
|
|
232
|
+
nxt = messages[j]
|
|
233
|
+
if tool_result_for?(nxt, call_ids)
|
|
234
|
+
include_set.add(j)
|
|
235
|
+
elsif !tool_result_message?(nxt)
|
|
236
|
+
break
|
|
237
|
+
end
|
|
238
|
+
j += 1
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Walk backwards from tool_result_idx to find and mark its assistant message.
|
|
243
|
+
# Also marks all sibling tool results for that assistant.
|
|
244
|
+
# Yields true if the assistant was newly added (for caller to increment count).
|
|
245
|
+
def pull_assistant_before(messages, tool_result_idx, include_set)
|
|
246
|
+
result_ids = tool_result_ids(messages[tool_result_idx])
|
|
247
|
+
|
|
248
|
+
j = tool_result_idx - 1
|
|
249
|
+
while j >= 0
|
|
250
|
+
prev = messages[j]
|
|
251
|
+
if prev[:role] == "assistant" && prev[:tool_calls]&.any?
|
|
252
|
+
call_ids = prev[:tool_calls].map { |tc| tc[:id] }
|
|
253
|
+
if (call_ids & result_ids).any?
|
|
254
|
+
newly_added = include_set.add?(j)
|
|
255
|
+
yield newly_added
|
|
256
|
+
|
|
257
|
+
# Also pull all sibling tool results for this assistant
|
|
258
|
+
pull_tool_results_after(messages, j, include_set)
|
|
259
|
+
break
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
j -= 1
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Truncate oversized tool result content to avoid token bloat.
|
|
267
|
+
def truncate_tool_result(msg)
|
|
268
|
+
if MessageFormat::OpenAI.tool_result_message?(msg) &&
|
|
269
|
+
msg[:content].is_a?(String) && msg[:content].length > 2000
|
|
270
|
+
msg.merge(content: msg[:content][0..2000] + "...\n[Content truncated - exceeded 2000 characters]")
|
|
271
|
+
else
|
|
272
|
+
msg
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
263
276
|
# Save the messages being compressed to a chunk MD file for future recall
|
|
264
277
|
# File path: ~/.clacky/sessions/{datetime}-{short_id}-chunk-{n}.md
|
|
265
278
|
# @param original_messages [Array<Hash>] All messages before compression (excluding compression instruction)
|
|
@@ -191,7 +191,15 @@ module Clacky
|
|
|
191
191
|
name = tc[:name] || tc.dig(:function, :name) || ""
|
|
192
192
|
args_raw = tc[:arguments] || tc.dig(:function, :arguments) || {}
|
|
193
193
|
args = args_raw.is_a?(String) ? (JSON.parse(args_raw) rescue args_raw) : args_raw
|
|
194
|
-
|
|
194
|
+
|
|
195
|
+
# Special handling: request_user_feedback question is shown as an
|
|
196
|
+
# assistant message (matching real-time behavior), not as a tool call.
|
|
197
|
+
if name == "request_user_feedback"
|
|
198
|
+
question = args.is_a?(Hash) ? (args[:question] || args["question"]).to_s : ""
|
|
199
|
+
ui.show_assistant_message(question) unless question.empty?
|
|
200
|
+
else
|
|
201
|
+
ui.show_tool_call(name, args)
|
|
202
|
+
end
|
|
195
203
|
end
|
|
196
204
|
|
|
197
205
|
# Emit token usage stored on this message (for history replay display)
|
|
@@ -155,6 +155,13 @@ module Clacky
|
|
|
155
155
|
skill = parsed[:skill]
|
|
156
156
|
arguments = parsed[:arguments]
|
|
157
157
|
|
|
158
|
+
# Encrypted brand skills and fork-agent skills must run in an isolated subagent.
|
|
159
|
+
# Injecting their plaintext into @messages would expose confidential content to the LLM.
|
|
160
|
+
if skill.encrypted? || skill.fork_agent?
|
|
161
|
+
execute_skill_with_subagent(skill, arguments)
|
|
162
|
+
return
|
|
163
|
+
end
|
|
164
|
+
|
|
158
165
|
# Expand skill content (substitutes $ARGUMENTS if present)
|
|
159
166
|
expanded_content = skill.process_content(arguments, template_context: build_template_context)
|
|
160
167
|
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -671,16 +671,24 @@ module Clacky
|
|
|
671
671
|
private def format_tool_calls_for_api(tool_calls)
|
|
672
672
|
return nil unless tool_calls
|
|
673
673
|
|
|
674
|
-
tool_calls.
|
|
674
|
+
valid = tool_calls.filter_map do |call|
|
|
675
|
+
func = call[:function] || call
|
|
676
|
+
name = func[:name] || call[:name]
|
|
677
|
+
arguments = func[:arguments] || call[:arguments]
|
|
678
|
+
# Skip malformed tool calls with nil name or arguments
|
|
679
|
+
next if name.nil? || arguments.nil?
|
|
680
|
+
|
|
675
681
|
{
|
|
676
682
|
id: call[:id],
|
|
677
683
|
type: call[:type] || "function",
|
|
678
684
|
function: {
|
|
679
|
-
name:
|
|
680
|
-
arguments:
|
|
685
|
+
name: name,
|
|
686
|
+
arguments: arguments
|
|
681
687
|
}
|
|
682
688
|
}
|
|
683
689
|
end
|
|
690
|
+
|
|
691
|
+
valid.any? ? valid : nil
|
|
684
692
|
end
|
|
685
693
|
|
|
686
694
|
private def register_builtin_tools
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
require_relative "version"
|
|
5
|
+
require_relative "brand_config"
|
|
6
|
+
require_relative "block_font"
|
|
7
|
+
|
|
8
|
+
module Clacky
|
|
9
|
+
# Banner provides logo and branding for CLI and Web UI startup.
|
|
10
|
+
# Lightweight — no terminal UI dependencies.
|
|
11
|
+
class Banner
|
|
12
|
+
DEFAULT_CLI_LOGO = <<~'LOGO'
|
|
13
|
+
██████╗ ██████╗ ███████╗███╗ ██╗ ██████╗██╗ █████╗ ██████╗██╗ ██╗██╗ ██╗
|
|
14
|
+
██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔════╝██║ ██╔══██╗██╔════╝██║ ██╔╝╚██╗ ██╔╝
|
|
15
|
+
██║ ██║██████╔╝█████╗ ██╔██╗ ██║██║ ██║ ███████║██║ █████╔╝ ╚████╔╝
|
|
16
|
+
██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██║ ██║ ██╔══██║██║ ██╔═██╗ ╚██╔╝
|
|
17
|
+
╚██████╔╝██║ ███████╗██║ ╚████║╚██████╗███████╗██║ ██║╚██████╗██║ ██╗ ██║
|
|
18
|
+
╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝
|
|
19
|
+
LOGO
|
|
20
|
+
|
|
21
|
+
TAGLINE = "[>] Your personal Assistant & Technical Co-founder"
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@pastel = Pastel.new
|
|
25
|
+
@brand = BrandConfig.load
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the CLI logo text.
|
|
29
|
+
# If branded, renders brand_command using BlockFont (big Unicode art).
|
|
30
|
+
# Falls back to default OPENCLACKY logo when not branded.
|
|
31
|
+
def cli_logo
|
|
32
|
+
if @brand.branded?
|
|
33
|
+
render_key = @brand.brand_command.to_s.strip
|
|
34
|
+
render_key = "clacky" if render_key.empty?
|
|
35
|
+
Clacky::BlockFont.render(render_key)
|
|
36
|
+
else
|
|
37
|
+
DEFAULT_CLI_LOGO
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the tagline string.
|
|
42
|
+
def tagline
|
|
43
|
+
if @brand.branded?
|
|
44
|
+
@brand.brand_name.to_s
|
|
45
|
+
else
|
|
46
|
+
TAGLINE
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Renders the CLI logo as colored text
|
|
51
|
+
def colored_cli_logo
|
|
52
|
+
@pastel.bright_green(cli_logo)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Renders the tagline as colored text
|
|
56
|
+
def colored_tagline
|
|
57
|
+
@pastel.bright_cyan(tagline)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Renders a URL with bold + underline for emphasis
|
|
61
|
+
def highlight(url)
|
|
62
|
+
@pastel.bold.underline(url)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|