openclacky 0.8.5 → 0.8.6

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/docs/channel-architecture.md +235 -0
  4. data/lib/clacky/agent/memory_updater.rb +1 -0
  5. data/lib/clacky/agent/session_serializer.rb +41 -3
  6. data/lib/clacky/agent/skill_manager.rb +1 -1
  7. data/lib/clacky/brand_config.rb +352 -43
  8. data/lib/clacky/cli.rb +5 -4
  9. data/lib/clacky/client.rb +2 -2
  10. data/lib/clacky/default_skills/channel-setup/SKILL.md +277 -0
  11. data/lib/clacky/default_skills/cron-task-creator/SKILL.md +250 -0
  12. data/lib/clacky/default_skills/cron-task-creator/evals/evals.json +38 -0
  13. data/lib/clacky/default_skills/cron-task-creator/scripts/list_tasks.rb +121 -0
  14. data/lib/clacky/default_skills/cron-task-creator/scripts/manage_schedule.rb +149 -0
  15. data/lib/clacky/default_skills/cron-task-creator/scripts/manage_task.rb +81 -0
  16. data/lib/clacky/default_skills/cron-task-creator/scripts/task_history.rb +137 -0
  17. data/lib/clacky/default_skills/skill-add/SKILL.md +21 -260
  18. data/lib/clacky/default_skills/skill-add/scripts/install_from_github.rb +143 -99
  19. data/lib/clacky/default_skills/skill-creator/SKILL.md +547 -0
  20. data/lib/clacky/default_skills/skill-creator/agents/analyzer.md +274 -0
  21. data/lib/clacky/default_skills/skill-creator/agents/comparator.md +202 -0
  22. data/lib/clacky/default_skills/skill-creator/agents/grader.md +223 -0
  23. data/lib/clacky/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
  24. data/lib/clacky/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
  25. data/lib/clacky/default_skills/skill-creator/references/schemas.md +430 -0
  26. data/lib/clacky/default_skills/skill-creator/scripts/__init__.py +0 -0
  27. data/lib/clacky/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  28. data/lib/clacky/default_skills/skill-creator/scripts/generate_report.py +326 -0
  29. data/lib/clacky/default_skills/skill-creator/scripts/improve_description.py +310 -0
  30. data/lib/clacky/default_skills/skill-creator/scripts/quick_validate.py +103 -0
  31. data/lib/clacky/default_skills/skill-creator/scripts/run_eval.py +317 -0
  32. data/lib/clacky/default_skills/skill-creator/scripts/run_loop.py +331 -0
  33. data/lib/clacky/default_skills/skill-creator/scripts/utils.py +47 -0
  34. data/lib/clacky/server/channel/adapters/base.rb +82 -0
  35. data/lib/clacky/server/channel/adapters/feishu/adapter.rb +172 -0
  36. data/lib/clacky/server/channel/adapters/feishu/bot.rb +191 -0
  37. data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +106 -0
  38. data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +385 -0
  39. data/lib/clacky/server/channel/adapters/wecom/adapter.rb +106 -0
  40. data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +188 -0
  41. data/lib/clacky/server/channel/channel_config.rb +146 -0
  42. data/lib/clacky/server/channel/channel_manager.rb +230 -0
  43. data/lib/clacky/server/channel/channel_ui_controller.rb +179 -0
  44. data/lib/clacky/server/channel.rb +29 -0
  45. data/lib/clacky/server/http_server.rb +323 -9
  46. data/lib/clacky/server/web_ui_controller.rb +73 -1
  47. data/lib/clacky/skill_loader.rb +1 -0
  48. data/lib/clacky/tools/browser.rb +281 -43
  49. data/lib/clacky/utils/logger.rb +20 -0
  50. data/lib/clacky/version.rb +1 -1
  51. data/lib/clacky/web/app.css +452 -17
  52. data/lib/clacky/web/app.js +53 -18
  53. data/lib/clacky/web/channels.js +196 -0
  54. data/lib/clacky/web/index.html +29 -6
  55. data/lib/clacky/web/sessions.js +10 -1
  56. data/lib/clacky/web/settings.js +2 -2
  57. data/lib/clacky/web/skills.js +307 -92
  58. data/lib/clacky/web/tasks.js +2 -2
  59. metadata +36 -2
  60. data/lib/clacky/default_skills/create-task/SKILL.md +0 -102
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eafcf68d56923cdd3aaacc446277756c77661eaf5da6348ac7f54c565a141bd3
4
- data.tar.gz: 5f09c5ffdfba608554be327b9e7bf6ea1e7df57ceedf2ead6402ae8f74ce8780
3
+ metadata.gz: '0190a435c18d666a4d2cc7c65b301bd24f18cc6a0159f5d7a4ae25ee552883d7'
4
+ data.tar.gz: 62eecd22f6cc112aa00674c683002f14ada01d637b95551a5ca7ff29d408ad75
5
5
  SHA512:
6
- metadata.gz: 223dc788074dc74f3e61f1980eb9bab3ef0fb1d15d1644bf500013ae058212d52b8184be51762c08adeeee3d856d2eab4c78d7ea142fa69b3273e81003c80d7c
7
- data.tar.gz: 58fbeba9eb4f17f02d61a61707c82e16f3a5d152d0ae95cc9ad745d356470a677ab471093271f54ca3e25ddffe9203060e8612698269c96d18015f24288f912a
6
+ metadata.gz: 8b0dcb369eeb481fc32dd8e02d4cc22ed6b21f3fb5115d9145623444bb88be1cf95cf1c3f8b62988bd54c28888512ee90ecf4df74f98250a04f2f0fcbd9d77d8
7
+ data.tar.gz: a6d72920b58547540dd6b389cb8c5dadafc4cf9face794217a7d4c2245bb4f2b46fce4e83616074d1054b9f7542ee0601cad1f9e43fc9358563f6d0dc8b97124
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.8.6] - 2026-03-12
11
+
12
+ ### Added
13
+ - **Channel system with Feishu & WeCom support**: integrated IM platform adapters — agents can now receive and reply to messages via Feishu (WebSocket) and WeCom channels
14
+ - **Skill encryption (brand skills)**: brand skills can be distributed as encrypted `.enc` files, decrypted on-the-fly using license keys; includes a full key management and manifest system
15
+ - **Cron task creator & skill creator default skills**: two new built-in skills for creating scheduled tasks and new skills directly from chat
16
+ - **Image messages in session history restore**: session restore now correctly replays image-containing messages, including thumbnail display in the UI
17
+ - **Skill auto-upload to cloud**: skills can be uploaded to the cloud store from within the UI
18
+
19
+ ### Improved
20
+ - **WeCom setup flow**: improved step-by-step WeCom channel configuration UX (#11)
21
+ - **Skill autocomplete UI**: enhanced slash-command autocomplete interaction — better keyboard navigation, input behavior, and visual feedback (#6)
22
+ - **Chrome setup UX**: simplified Chrome installation flow with improved error messages and progress indicators (#8)
23
+ - **WebUI colors and layout**: polished light/dark mode colors, sidebar alignment, and badge styles for a more consistent look
24
+ - **Test suite speed**: `CLACKY_TEST` guard prevents brand skill network calls during tests — suite now runs ~60× faster per example
25
+
26
+ ### Fixed
27
+ - **Duplicate user bubble on skill install**: prevented an extra chat bubble appearing when installing a skill from the store
28
+ - **Image thumbnails in session replay**: restored missing image thumbnails when replaying historical sessions
29
+ - **WebUI permission mode**: Web UI sessions now correctly use `confirm_all` permission mode
30
+ - **Feishu WS log noise**: removed emoji characters from WebSocket connection log messages
31
+
32
+ ### More
33
+ - Subagent memory update disabled to reduce noise
34
+ - Ping request `max_tokens` bumped from 10 to 16
35
+ - WebUI updated to use new cron-task-creator and skill-creator skills
36
+
10
37
  ## [0.8.5] - 2026-03-11
11
38
 
12
39
  ### Fixed
@@ -0,0 +1,235 @@
1
+ # Channel Architecture
2
+
3
+ ## Overview
4
+
5
+ Channel is a feature that bridges Clacky's Server Sessions to IM platforms
6
+ (Feishu, WeCom, DingTalk, etc.). It reuses the existing Agent + SessionRegistry
7
+ infrastructure — the Agent knows nothing about IM; the Channel layer is purely
8
+ a transport adapter.
9
+
10
+ ## Design Principles
11
+
12
+ - **Zero Agent intrusion** — Agent only speaks `UIInterface`; swap the controller, get IM output
13
+ - **Reuse SessionRegistry** — IM chats resolve to the same `SessionRegistry` sessions as Web UI
14
+ - **WebSocket long connection** — No public domain required; adapters hold a persistent WSS connection to the IM platform
15
+ - **One platform = 2 threads** — read loop thread + ping/heartbeat thread (constant, small footprint)
16
+
17
+ ---
18
+
19
+ ## Layer Diagram
20
+
21
+ ```
22
+ IM Platforms (Feishu / WeCom / DingTalk)
23
+ │ WebSocket long connection (wss://)
24
+
25
+ ┌─────────────────────────────────────┐
26
+ │ Channel Adapter Layer │
27
+ │ Feishu::Adapter │
28
+ │ ├── WSClient (read loop + ping) │
29
+ │ ├── Bot (send API) │
30
+ │ └── MessageParser │
31
+ │ Wecom::Adapter │
32
+ │ └── WSClient (read loop + ping) │
33
+ │ (future) Dingtalk::Adapter │
34
+ └──────────────┬──────────────────────┘
35
+ │ standardized event Hash
36
+
37
+ ┌─────────────────────────────────────┐
38
+ │ ChannelManager │
39
+ │ • Owns adapter threads │
40
+ │ • Routes inbound event → │
41
+ │ ChannelBinding → session_id │
42
+ │ • Calls agent.run in Thread.new │
43
+ └──────────────┬──────────────────────┘
44
+
45
+ ┌───────┴────────┐
46
+ ▼ ▼
47
+ SessionRegistry ChannelUIController
48
+ (existing) (implements UIInterface)
49
+ │ │
50
+ ▼ ▼
51
+ Agent IM Platform reply
52
+ (unchanged) via adapter.send_text
53
+ ```
54
+
55
+ ---
56
+
57
+ ## File Structure
58
+
59
+ ```
60
+ lib/clacky/channel/
61
+ ├── adapters/
62
+ │ ├── base.rb # Adapter abstract base + registry
63
+ │ ├── feishu/
64
+ │ │ ├── adapter.rb # Feishu::Adapter < Base
65
+ │ │ ├── bot.rb # HTTP send API (token cache, Markdown/card)
66
+ │ │ ├── message_parser.rb # Raw WS event → standardized Hash
67
+ │ │ └── ws_client.rb # Feishu protobuf WS long connection
68
+ │ └── wecom/
69
+ │ ├── adapter.rb # Wecom::Adapter < Base
70
+ │ └── ws_client.rb # WeCom JSON WS long connection
71
+ ├── channel_message.rb # Struct: standardized inbound message
72
+ ├── channel_binding.rb # (platform, user_id) → session_id mapping
73
+ ├── channel_ui_controller.rb # UIInterface impl — pushes events to IM
74
+ └── channel_manager.rb # Lifecycle: start/stop adapters, route messages
75
+ lib/clacky/channel.rb # Top-level require entry point
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Standardized Inbound Event
81
+
82
+ All adapters yield the same Hash shape to `ChannelManager`:
83
+
84
+ ```ruby
85
+ {
86
+ platform: :feishu, # Symbol
87
+ chat_id: "oc_xxx", # String — IM chat/group identifier
88
+ user_id: "ou_xxx", # String — IM user identifier
89
+ text: "deploy now", # String — cleaned user text
90
+ message_id: "om_xxx", # String — for threading / update
91
+ timestamp: Time, # Time object
92
+ chat_type: :direct | :group, # Symbol
93
+ raw: { ... } # Original platform payload
94
+ }
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Adapter Interface (Base)
100
+
101
+ ```ruby
102
+ class Adapters::Base
103
+ def self.platform_id → Symbol
104
+ def self.platform_config(raw_config) → Hash # symbol-keyed
105
+ def self.env_keys → Array<String> # for config serialization
106
+
107
+ def start(&on_message) # blocks; yields event Hash per inbound message
108
+ def stop # graceful shutdown
109
+ def send_text(chat_id, text, reply_to: nil) → Hash
110
+ def update_message(chat_id, message_id, text) → Boolean
111
+ def supports_message_updates? → Boolean
112
+ def validate_config(config) → Array<String> # error messages
113
+ end
114
+ ```
115
+
116
+ ---
117
+
118
+ ## ChannelManager
119
+
120
+ ```ruby
121
+ class ChannelManager
122
+ def initialize(session_registry:, session_builder:, channel_config:, agent_config:)
123
+
124
+ def start # Thread.new per enabled platform adapter
125
+ def stop # kills all adapter threads gracefully
126
+
127
+ private
128
+
129
+ def route_message(adapter, event)
130
+ session_id = @binding.resolve_or_create(event, session_builder: @session_builder)
131
+ ui = ChannelUIController.new(event, adapter)
132
+ Thread.new { run_agent(session_id, event[:text], ui) }
133
+ end
134
+ end
135
+ ```
136
+
137
+ ---
138
+
139
+ ## ChannelBinding
140
+
141
+ Maps `(platform, user_id)` → `session_id`. Persisted to `~/.clacky/channel_bindings.yml`.
142
+
143
+ Binding modes (configurable per platform):
144
+
145
+ | Mode | Key | Description |
146
+ |------|-----|-------------|
147
+ | `user` | `(platform, user_id)` | Each IM user gets their own session (default) |
148
+ | `chat` | `(platform, chat_id)` | Whole group shares one session |
149
+
150
+ ---
151
+
152
+ ## ChannelUIController
153
+
154
+ Implements `UIInterface`. Key behaviours:
155
+
156
+ - `show_assistant_message` → `adapter.send_text(chat_id, content)`
157
+ - `show_tool_call` → buffers as `⚙️ \`tool summary\`` (flushed on next message)
158
+ - `show_progress` → `adapter.update_message(...)` if `supports_message_updates?`
159
+ - `show_complete` → sends `✅ Complete • N iterations • $cost`
160
+ - `request_confirmation` → **not supported in IM** (returns auto-approved / raises)
161
+
162
+ ---
163
+
164
+ ## Thread Model
165
+
166
+ ```
167
+ Main thread (WEBrick server.start — blocks)
168
+ ├── WEBrick request threads (existing)
169
+ ├── Agent task threads (existing, per task)
170
+ ├── Scheduler thread (existing, clacky-scheduler)
171
+ └── ChannelManager
172
+ ├── feishu-adapter thread (WSClient read loop, constant)
173
+ │ └── feishu-ping thread (heartbeat, 90s)
174
+ └── wecom-adapter thread (WSClient read loop, constant)
175
+ └── wecom-ping thread (heartbeat, 30s)
176
+ ```
177
+
178
+ Per enabled platform: **2 constant threads**. Agent task threads are spawned
179
+ on demand (same as Web UI path) and exit when done.
180
+
181
+ ---
182
+
183
+ ## Configuration
184
+
185
+ Channel credentials live in `~/.clacky/channels.yml` (managed by `ChannelConfig`
186
+ which already exists in main branch):
187
+
188
+ ```yaml
189
+ channels:
190
+ feishu:
191
+ enabled: true
192
+ app_id: cli_xxx
193
+ app_secret: xxx
194
+ allowed_users:
195
+ - ou_xxx
196
+ wecom:
197
+ enabled: false
198
+ bot_id: xxx
199
+ secret: xxx
200
+ ```
201
+
202
+ `ChannelManager` reads this via `ChannelConfig#platform_config(platform)`.
203
+
204
+ ---
205
+
206
+ ## Integration with HttpServer
207
+
208
+ ```ruby
209
+ # HttpServer#initialize
210
+ @channel_manager = ChannelManager.new(
211
+ session_registry: @registry,
212
+ session_builder: method(:build_session),
213
+ channel_config: Clacky::ChannelConfig.load,
214
+ agent_config: @agent_config
215
+ )
216
+
217
+ # HttpServer#start (after scheduler.start)
218
+ @channel_manager.start
219
+ ```
220
+
221
+ `ChannelManager#start` is non-blocking (spawns threads internally),
222
+ mirroring `Scheduler#start` behaviour.
223
+
224
+ ---
225
+
226
+ ## Future: DingTalk
227
+
228
+ DingTalk also supports a WebSocket Stream mode. Adding it means:
229
+
230
+ 1. `lib/clacky/channel/adapters/dingtalk/adapter.rb` inheriting `Base`
231
+ 2. `lib/clacky/channel/adapters/dingtalk/ws_client.rb`
232
+ 3. Register: `Adapters.register(:dingtalk, Adapter)`
233
+ 4. Add credentials to `ChannelConfig`
234
+
235
+ No changes needed to `ChannelManager`, `ChannelUIController`, or `ChannelBinding`.
@@ -26,6 +26,7 @@ module Clacky
26
26
  # @return [Boolean]
27
27
  def should_update_memory?
28
28
  return false unless memory_update_enabled?
29
+ return false if @is_subagent # Subagents never update memory
29
30
 
30
31
  task_iterations = @iterations - (@task_start_iterations || 0)
31
32
  task_iterations >= MEMORY_UPDATE_MIN_ITERATIONS
@@ -153,8 +153,20 @@ module Clacky
153
153
  @messages.each do |msg|
154
154
  role = msg[:role].to_s
155
155
 
156
- if role == "user" && !msg[:system_injected] && msg[:content].is_a?(String) &&
157
- !msg[:content].to_s.start_with?("[SYSTEM]")
156
+ # A real user message can have either a String content or an Array content
157
+ # (Array = multipart: text + image blocks). Exclude system-injected messages
158
+ # and synthetic [SYSTEM] text messages.
159
+ is_real_user_msg = role == "user" && !msg[:system_injected] &&
160
+ if msg[:content].is_a?(String)
161
+ !msg[:content].start_with?("[SYSTEM]")
162
+ elsif msg[:content].is_a?(Array)
163
+ # Must contain at least one text or image block (not a tool_result array)
164
+ msg[:content].any? { |b| b.is_a?(Hash) && %w[text image].include?(b[:type].to_s) }
165
+ else
166
+ false
167
+ end
168
+
169
+ if is_real_user_msg
158
170
  # Start a new round at each real user message
159
171
  current_round = { user_msg: msg, events: [] }
160
172
  rounds << current_round
@@ -175,8 +187,10 @@ module Clacky
175
187
  page.each do |round|
176
188
  msg = round[:user_msg]
177
189
  display_text = extract_text_from_content(msg[:content])
190
+ # Extract image data URLs from multipart content (for history replay rendering)
191
+ images = extract_images_from_content(msg[:content])
178
192
  # Emit user message with its timestamp for dedup on the frontend
179
- ui.show_user_message(display_text, created_at: msg[:created_at])
193
+ ui.show_user_message(display_text, created_at: msg[:created_at], images: images)
180
194
 
181
195
  round[:events].each do |ev|
182
196
  # Skip system-injected messages (e.g. synthetic skill content, memory prompts)
@@ -241,6 +255,30 @@ module Clacky
241
255
  Clacky::Logger.warn("refresh_system_prompt failed during session restore: #{e.message}")
242
256
  end
243
257
 
258
+ # Extract base64 data URLs from multipart content (image blocks).
259
+ # Returns an empty array when there are no images or content is plain text.
260
+ # @param content [String, Array, Object] Message content
261
+ # @return [Array<String>] Array of data URLs (e.g. "data:image/png;base64,...")
262
+ def extract_images_from_content(content)
263
+ return [] unless content.is_a?(Array)
264
+
265
+ content.filter_map do |block|
266
+ next unless block.is_a?(Hash)
267
+
268
+ case block[:type].to_s
269
+ when "image_url"
270
+ # OpenAI format: { type: "image_url", image_url: { url: "data:image/png;base64,..." } }
271
+ block.dig(:image_url, :url)
272
+ when "image"
273
+ # Anthropic format: { type: "image", source: { type: "base64", media_type: "image/png", data: "..." } }
274
+ source = block[:source]
275
+ next unless source.is_a?(Hash) && source[:type].to_s == "base64"
276
+
277
+ "data:#{source[:media_type]};base64,#{source[:data]}"
278
+ end
279
+ end
280
+ end
281
+
244
282
  # Extract text from message content (handles string and array formats)
245
283
  # @param content [String, Array, Object] Message content
246
284
  # @return [String] Extracted text
@@ -184,7 +184,7 @@ module Clacky
184
184
  system_injected: true
185
185
  }
186
186
 
187
- @ui&.log("Injected skill content for /#{skill.identifier}", level: :info)
187
+ @ui&.show_info("Injected skill content for /#{skill.identifier}")
188
188
  end
189
189
 
190
190
  private