openclacky 1.0.4 → 1.1.0

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/gem-release/SKILL.md +99 -356
  3. data/.clacky/skills/gem-release/scripts/release.sh +304 -0
  4. data/CHANGELOG.md +42 -0
  5. data/docs/system-skill-authoring-guide.md +1 -1
  6. data/lib/clacky/agent/tool_executor.rb +3 -1
  7. data/lib/clacky/agent.rb +12 -7
  8. data/lib/clacky/agent_config.rb +9 -3
  9. data/lib/clacky/brand_config.rb +19 -4
  10. data/lib/clacky/cli.rb +1 -1
  11. data/lib/clacky/default_skills/{channel-setup → channel-manager}/SKILL.md +180 -18
  12. data/lib/clacky/default_skills/channel-manager/dingtalk_setup.rb +191 -0
  13. data/lib/clacky/default_skills/channel-manager/discord_setup.rb +199 -0
  14. data/lib/clacky/default_skills/channel-manager/install_feishu_skills.rb +105 -0
  15. data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
  16. data/lib/clacky/default_skills/onboard/scripts/import_external_skills.rb +2 -4
  17. data/lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb +18 -96
  18. data/lib/clacky/default_skills/product-help/SKILL.md +10 -2
  19. data/lib/clacky/message_history.rb +26 -1
  20. data/lib/clacky/providers.rb +29 -4
  21. data/lib/clacky/server/channel/adapters/dingtalk/adapter.rb +177 -0
  22. data/lib/clacky/server/channel/adapters/dingtalk/api_client.rb +82 -0
  23. data/lib/clacky/server/channel/adapters/dingtalk/stream_client.rb +205 -0
  24. data/lib/clacky/server/channel/adapters/discord/adapter.rb +229 -0
  25. data/lib/clacky/server/channel/adapters/discord/api_client.rb +108 -0
  26. data/lib/clacky/server/channel/adapters/discord/gateway_client.rb +272 -0
  27. data/lib/clacky/server/channel/adapters/telegram/adapter.rb +375 -0
  28. data/lib/clacky/server/channel/adapters/telegram/api_client.rb +205 -0
  29. data/lib/clacky/server/channel/channel_config.rb +26 -0
  30. data/lib/clacky/server/channel.rb +3 -0
  31. data/lib/clacky/server/http_server.rb +75 -4
  32. data/lib/clacky/server/server_master.rb +35 -13
  33. data/lib/clacky/server/session_registry.rb +54 -3
  34. data/lib/clacky/server/web_ui_controller.rb +7 -1
  35. data/lib/clacky/telemetry.rb +1 -16
  36. data/lib/clacky/tools/browser.rb +8 -5
  37. data/lib/clacky/tools/glob.rb +11 -38
  38. data/lib/clacky/tools/grep.rb +7 -16
  39. data/lib/clacky/ui2/markdown_renderer.rb +1 -1
  40. data/lib/clacky/ui2/ui_controller.rb +2 -1
  41. data/lib/clacky/utils/file_ignore_helper.rb +49 -0
  42. data/lib/clacky/utils/gitignore_parser.rb +27 -0
  43. data/lib/clacky/version.rb +1 -1
  44. data/lib/clacky/web/app.css +248 -31
  45. data/lib/clacky/web/app.js +51 -1
  46. data/lib/clacky/web/channels.js +98 -28
  47. data/lib/clacky/web/datepicker.js +205 -0
  48. data/lib/clacky/web/i18n.js +48 -9
  49. data/lib/clacky/web/index.html +33 -6
  50. data/lib/clacky/web/onboard.js +46 -4
  51. data/lib/clacky/web/sessions.js +33 -72
  52. data/lib/clacky/web/settings.js +42 -4
  53. data/lib/clacky/web/version.js +52 -1
  54. metadata +21 -10
  55. data/docs/proposals/2026-05-11-system-prompt-alignment.md +0 -325
  56. data/docs/proposals/2026-05-12-memory-mechanism-optimization.md +0 -89
  57. /data/lib/clacky/default_skills/{channel-setup → channel-manager}/feishu_setup.rb +0 -0
  58. /data/lib/clacky/default_skills/{channel-setup → channel-manager}/import_lark_skills.rb +0 -0
  59. /data/lib/clacky/default_skills/{channel-setup → channel-manager}/weixin_setup.rb +0 -0
@@ -0,0 +1,304 @@
1
+ #!/bin/bash
2
+ # release.sh — openclacky gem release automation
3
+ #
4
+ # Usage:
5
+ # bash release.sh <version> [--prerelease] [--update-latest]
6
+ #
7
+ # Examples:
8
+ # bash release.sh 1.0.6 # stable release
9
+ # bash release.sh 1.0.6 --dry-run # preview without executing
10
+ # bash release.sh 2.0.0.beta.1 --prerelease # pre-release, skip latest.txt
11
+ # bash release.sh 2.0.0.rc1 --prerelease --update-latest # pre-release, update latest.txt
12
+ #
13
+ # Prerequisites:
14
+ # - gh CLI installed and authenticated
15
+ # - coscli installed at /usr/local/bin/coscli with ~/.cos.yaml
16
+ # - RubyGems credentials configured (gem push)
17
+
18
+ set -euo pipefail
19
+
20
+ # ── Colors ──────────────────────────────────────────────────────────────
21
+ RED='\033[0;31m'
22
+ GREEN='\033[0;32m'
23
+ YELLOW='\033[1;33m'
24
+ BLUE='\033[0;34m'
25
+ CYAN='\033[0;36m'
26
+ NC='\033[0m'
27
+
28
+ info() { echo -e "${BLUE}ℹ${NC} $1"; }
29
+ success() { echo -e "${GREEN}✓${NC} $1"; }
30
+ warn() { echo -e "${YELLOW}⚠${NC} $1"; }
31
+ error() { echo -e "${RED}✗${NC} $1" >&2; }
32
+ step() { echo -e "\n${CYAN}▶ Step $1:${NC} $2\n"; }
33
+ die() { error "$1"; exit 1; }
34
+
35
+ # ── Parse args ──────────────────────────────────────────────────────────
36
+ VERSION=""
37
+ PRERELEASE=false
38
+ UPDATE_LATEST=true
39
+ DRY_RUN=false
40
+
41
+ while [[ $# -gt 0 ]]; do
42
+ case "$1" in
43
+ --prerelease) PRERELEASE=true; UPDATE_LATEST=false ;;
44
+ --update-latest) UPDATE_LATEST=true ;;
45
+ --dry-run) DRY_RUN=true ;;
46
+ --help|-h)
47
+ echo "Usage: bash release.sh <version> [--prerelease] [--update-latest] [--dry-run]"
48
+ exit 0
49
+ ;;
50
+ -*) die "Unknown option: $1" ;;
51
+ *) VERSION="$1" ;;
52
+ esac
53
+ shift
54
+ done
55
+
56
+ [[ -z "$VERSION" ]] && die "Version argument required. Usage: bash release.sh <version>"
57
+
58
+ # ── Resolve paths ───────────────────────────────────────────────────────
59
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || die "Not inside a git repository"
60
+ cd "$REPO_ROOT"
61
+
62
+ VERSION_FILE="lib/clacky/version.rb"
63
+ GEMSPEC="openclacky.gemspec"
64
+ CHANGELOG="CHANGELOG.md"
65
+ GEM_FILE="openclacky-${VERSION}.gem"
66
+ OSS_BUCKET="cos://clackyai-1258723534"
67
+
68
+ [[ -f "$VERSION_FILE" ]] || die "Version file not found: $VERSION_FILE"
69
+ [[ -f "$GEMSPEC" ]] || die "Gemspec not found: $GEMSPEC"
70
+
71
+ CURRENT_VERSION=$(ruby -ne 'puts $1 if /VERSION\s*=\s*"([^"]+)"/' "$VERSION_FILE")
72
+ info "Current version: ${CURRENT_VERSION}"
73
+ info "Target version: ${VERSION}"
74
+ info "Pre-release: ${PRERELEASE}"
75
+ info "Update latest: ${UPDATE_LATEST}"
76
+ info "Dry run: ${DRY_RUN}"
77
+ echo ""
78
+
79
+ if [[ "$DRY_RUN" == true ]]; then
80
+ warn "DRY RUN mode — no changes will be made"
81
+ echo ""
82
+ fi
83
+
84
+ # ── Helper: run or preview ──────────────────────────────────────────────
85
+ run() {
86
+ if [[ "$DRY_RUN" == true ]]; then
87
+ echo -e " ${YELLOW}[dry-run]${NC} $*"
88
+ else
89
+ "$@"
90
+ fi
91
+ }
92
+
93
+ # ════════════════════════════════════════════════════════════════════════
94
+ # Step 1: Pre-release checks
95
+ # ════════════════════════════════════════════════════════════════════════
96
+ step 1 "Pre-release checks"
97
+
98
+ if [[ -n "$(git status --porcelain)" ]]; then
99
+ die "Working directory is not clean. Commit or stash changes first."
100
+ fi
101
+ success "Working directory is clean"
102
+
103
+ BRANCH=$(git branch --show-current)
104
+ if [[ "$BRANCH" != "main" ]]; then
105
+ warn "Not on main branch (currently on '${BRANCH}')"
106
+ fi
107
+
108
+ command -v gh >/dev/null 2>&1 || die "gh CLI not found. Install with: brew install gh"
109
+ command -v coscli >/dev/null 2>&1 || die "coscli not found. Install at /usr/local/bin/coscli"
110
+ success "Required tools available (gh, coscli)"
111
+
112
+ # ════════════════════════════════════════════════════════════════════════
113
+ # Step 2: Run tests
114
+ # ════════════════════════════════════════════════════════════════════════
115
+ step 2 "Running test suite"
116
+
117
+ if [[ "$DRY_RUN" == true ]]; then
118
+ echo -e " ${YELLOW}[dry-run]${NC} bundle exec rspec"
119
+ else
120
+ bundle exec rspec || die "Tests failed — aborting release"
121
+ fi
122
+ success "All tests passed"
123
+
124
+ # ════════════════════════════════════════════════════════════════════════
125
+ # Step 3: Bump version
126
+ # ════════════════════════════════════════════════════════════════════════
127
+ step 3 "Bumping version to ${VERSION}"
128
+
129
+ run sed -i '' "s/VERSION = \"${CURRENT_VERSION}\"/VERSION = \"${VERSION}\"/" "$VERSION_FILE"
130
+
131
+ if [[ "$DRY_RUN" != true ]]; then
132
+ grep -q "VERSION = \"${VERSION}\"" "$VERSION_FILE" || die "Version bump failed"
133
+ fi
134
+ success "Updated ${VERSION_FILE}"
135
+
136
+ # ════════════════════════════════════════════════════════════════════════
137
+ # Step 4: Update Gemfile.lock
138
+ # ════════════════════════════════════════════════════════════════════════
139
+ step 4 "Updating Gemfile.lock"
140
+
141
+ run bundle install --quiet
142
+ success "Gemfile.lock updated"
143
+
144
+ # ════════════════════════════════════════════════════════════════════════
145
+ # Step 5: Commit version bump
146
+ # ════════════════════════════════════════════════════════════════════════
147
+ step 5 "Committing version bump"
148
+
149
+ run git add "$VERSION_FILE" Gemfile.lock
150
+ run git commit -m "chore: bump version to ${VERSION}"
151
+ success "Version bump committed"
152
+
153
+ # ════════════════════════════════════════════════════════════════════════
154
+ # Step 6: Push and wait for CI
155
+ # ════════════════════════════════════════════════════════════════════════
156
+ step 6 "Pushing to origin and checking CI"
157
+
158
+ run git push origin "$BRANCH"
159
+ success "Pushed to origin/${BRANCH}"
160
+
161
+ if [[ "$DRY_RUN" != true ]]; then
162
+ info "Waiting for CI to complete (this may take a few minutes)..."
163
+ if gh run list --branch "$BRANCH" --limit 1 --json status -q '.[0].status' 2>/dev/null | grep -q "completed"; then
164
+ success "CI already completed"
165
+ else
166
+ gh run watch --exit-status 2>/dev/null || warn "Could not watch CI run — verify manually at GitHub Actions"
167
+ fi
168
+ fi
169
+
170
+ # ════════════════════════════════════════════════════════════════════════
171
+ # Step 7: Build gem
172
+ # ════════════════════════════════════════════════════════════════════════
173
+ step 7 "Building gem"
174
+
175
+ run gem build "$GEMSPEC"
176
+
177
+ if [[ "$DRY_RUN" != true ]]; then
178
+ [[ -f "$GEM_FILE" ]] || die "Gem file not found: $GEM_FILE"
179
+ fi
180
+ success "Built ${GEM_FILE}"
181
+
182
+ # ════════════════════════════════════════════════════════════════════════
183
+ # Step 8: Publish to RubyGems
184
+ # ════════════════════════════════════════════════════════════════════════
185
+ step 8 "Publishing to RubyGems"
186
+
187
+ run gem push "$GEM_FILE"
188
+ success "Published to RubyGems"
189
+
190
+ # ════════════════════════════════════════════════════════════════════════
191
+ # Step 9: Git tag
192
+ # ════════════════════════════════════════════════════════════════════════
193
+ step 9 "Creating git tag v${VERSION}"
194
+
195
+ run git tag "v${VERSION}"
196
+ run git push origin --tags
197
+ success "Tag v${VERSION} pushed"
198
+
199
+ # ════════════════════════════════════════════════════════════════════════
200
+ # Step 10: GitHub Release
201
+ # ════════════════════════════════════════════════════════════════════════
202
+ step 10 "Creating GitHub Release"
203
+
204
+ RELEASE_NOTES_FILE="/tmp/release_notes_${VERSION}.md"
205
+
206
+ if [[ "$DRY_RUN" != true ]]; then
207
+ if [[ -f "$CHANGELOG" ]]; then
208
+ # Extract section for this version from CHANGELOG
209
+ awk "/^## \\[${VERSION}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" "$CHANGELOG" > "$RELEASE_NOTES_FILE"
210
+ if [[ ! -s "$RELEASE_NOTES_FILE" ]]; then
211
+ echo "Release v${VERSION}" > "$RELEASE_NOTES_FILE"
212
+ warn "No CHANGELOG entry found for ${VERSION} — using placeholder"
213
+ fi
214
+ else
215
+ echo "Release v${VERSION}" > "$RELEASE_NOTES_FILE"
216
+ warn "CHANGELOG.md not found — using placeholder"
217
+ fi
218
+ fi
219
+
220
+ if [[ "$PRERELEASE" == true ]]; then
221
+ run gh release create "v${VERSION}" \
222
+ --title "v${VERSION}" \
223
+ --notes-file "$RELEASE_NOTES_FILE" \
224
+ --prerelease \
225
+ "$GEM_FILE"
226
+ else
227
+ run gh release create "v${VERSION}" \
228
+ --title "v${VERSION}" \
229
+ --notes-file "$RELEASE_NOTES_FILE" \
230
+ --latest \
231
+ "$GEM_FILE"
232
+ fi
233
+ success "GitHub Release created"
234
+
235
+ # ════════════════════════════════════════════════════════════════════════
236
+ # Step 11: Upload to OSS CDN
237
+ # ════════════════════════════════════════════════════════════════════════
238
+ step 11 "Syncing to Tencent Cloud OSS"
239
+
240
+ run coscli cp "$GEM_FILE" "${OSS_BUCKET}/openclacky/${GEM_FILE}"
241
+ success "Uploaded ${GEM_FILE} to OSS"
242
+
243
+ if [[ "$UPDATE_LATEST" == true ]]; then
244
+ if [[ "$DRY_RUN" != true ]]; then
245
+ echo "${VERSION}" > /tmp/latest.txt
246
+ fi
247
+ run coscli cp /tmp/latest.txt "${OSS_BUCKET}/openclacky/latest.txt"
248
+ success "Updated latest.txt → ${VERSION}"
249
+
250
+ if [[ "$DRY_RUN" != true ]]; then
251
+ VERIFY=$(curl -fsSL https://oss.1024code.com/openclacky/latest.txt 2>/dev/null || echo "FAILED")
252
+ if [[ "$VERIFY" == "$VERSION" ]]; then
253
+ success "Verified latest.txt = ${VERSION}"
254
+ else
255
+ warn "latest.txt verification returned: ${VERIFY}"
256
+ fi
257
+ fi
258
+ else
259
+ info "Skipping latest.txt update (pre-release or not requested)"
260
+ fi
261
+
262
+ # ════════════════════════════════════════════════════════════════════════
263
+ # Step 12: Sync scripts to OSS
264
+ # ════════════════════════════════════════════════════════════════════════
265
+ step 12 "Rebuilding and syncing scripts to OSS"
266
+
267
+ run bash scripts/build/build.sh
268
+
269
+ if [[ "$DRY_RUN" != true ]]; then
270
+ for script in scripts/*.sh scripts/*.ps1; do
271
+ [[ -f "$script" ]] || continue
272
+ coscli cp "$script" "${OSS_BUCKET}/clacky-ai/openclacky/main/scripts/$(basename "$script")"
273
+ success "Uploaded $(basename "$script")"
274
+ done
275
+ else
276
+ echo -e " ${YELLOW}[dry-run]${NC} Upload scripts/*.sh and scripts/*.ps1 to OSS"
277
+ fi
278
+ success "Scripts synced to OSS"
279
+
280
+ # ════════════════════════════════════════════════════════════════════════
281
+ # Step 13: Cleanup
282
+ # ════════════════════════════════════════════════════════════════════════
283
+ step 13 "Cleanup"
284
+
285
+ [[ -f "$GEM_FILE" ]] && rm -f "$GEM_FILE" && info "Removed ${GEM_FILE}"
286
+ [[ -f "$RELEASE_NOTES_FILE" ]] && rm -f "$RELEASE_NOTES_FILE"
287
+ [[ -f "/tmp/latest.txt" ]] && rm -f /tmp/latest.txt
288
+
289
+ # ════════════════════════════════════════════════════════════════════════
290
+ # Done
291
+ # ════════════════════════════════════════════════════════════════════════
292
+ echo ""
293
+ echo "╔═══════════════════════════════════════════════════════════╗"
294
+ echo "║ ║"
295
+ echo -e "║ ${GREEN}🎉 v${VERSION} released successfully!${NC} ║"
296
+ echo "║ ║"
297
+ echo "╠═══════════════════════════════════════════════════════════╣"
298
+ echo "║ ║"
299
+ echo "║ 📦 RubyGems: rubygems.org/gems/openclacky ║"
300
+ echo "║ 🏷️ GitHub: github.com/clacky-ai/openclacky/releases ║"
301
+ echo "║ ☁️ OSS CDN: oss.1024code.com/openclacky/ ║"
302
+ echo "║ ║"
303
+ echo "╚═══════════════════════════════════════════════════════════╝"
304
+ echo ""
data/CHANGELOG.md CHANGED
@@ -5,6 +5,48 @@ 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.0] - 2026-05-15
9
+
10
+ ### Added
11
+ - **DingTalk channel adapter.** New IM channel adapter connects openclacky to DingTalk via Stream Mode WebSocket. Includes DingTalk API client for text/markdown messages, Device Flow QR setup script, and full Web UI integration with channel config, HTTP server routes, and i18n strings. (#112)
12
+ - **Feishu channel-manager skill setup & onboard improvements.** Channel-manager now includes a dedicated Feishu skills installation flow (`install_feishu_skills.rb`) and updated setup instructions. Skill installation is serialized for reliability. (#122)
13
+ - **Custom datepicker component with i18n support.** New reusable datepicker component with CSS variable theming and full English/Chinese localization, replacing browser-native date inputs. (#119)
14
+ - **Rename sessions via modal dialog.** Session rename now uses a proper modal dialog with i18n support instead of inline editing, for a cleaner UX. (#113)
15
+ - **Channel enable/disable toggle.** Configured channels can now be individually enabled or disabled from the Channels page without removing credentials. Distinguishes "disabled" from "not configured" in badge and hint text. (#108)
16
+ - **Provider promo hint for OpenClacky.** When OpenClacky is selected as provider, a contextual promo hint appears below the dropdown on both settings and onboarding pages, with dark mode support and localized copy. (#109)
17
+ - **Running config for concurrent agent limits.** New `AgentConfig` running configuration and `SessionRegistry` concurrency controls to limit the number of simultaneously active agents, preventing resource exhaustion on busy servers.
18
+
19
+ ### Improved
20
+ - **Channel page and sidebar nav polish.** Visual refinements to the Channels page layout and sidebar navigation styling.
21
+ - **Telegram group chat skill guidance.** Channel-setup skill now clarifies Privacy Mode requirements for Telegram group chats, preventing common misconfiguration. (#117)
22
+
23
+ ### Fixed
24
+ - **Channel skill trigger matching.** Renamed `channel-setup` to `channel-manager` so the agent's send-message intent matches the correct skill more reliably. (C-5584, #120)
25
+ - **Markdown image overflow in chat bubbles.** Images in assistant messages are now width-constrained to fit within the message bubble instead of overflowing. (C-5585, #118)
26
+ - **Channel image rewriting scoped to Web UI.** Local image URL rewriting is now applied only in the Web UI context; IM channel messages use the file basename as attachment name instead. (C-5590, #115)
27
+ - **Discord file upload.** Added multipart middleware to the Discord Faraday connection so file attachments upload correctly. (C-5589, #116)
28
+ - **File walk respects ignore patterns.** Fixed glob/walk to apply ignore patterns before traversal, resolving cases where ignored files were still visited. (#102)
29
+ - **Server restart kills stale PIDs.** Improved process cleanup on restart with better PID management and user-facing hints when restart fails.
30
+ - **Device ID persistence.** Device ID is now persisted in `BrandConfig` instead of being regenerated, ensuring stable telemetry identity across restarts.
31
+ - **Terminal markdown rendering on Ruby 4.0.** Fixed compatibility issue with Ruby 4.0's stricter method dispatch that broke terminal markdown output. (#99)
32
+
33
+ ## [1.0.5] - 2026-05-12
34
+
35
+ ### Added
36
+ - **Telegram channel adapter.** New IM channel adapter that connects openclacky to Telegram via the Bot API. Setup is just a bot token from @BotFather — no browser automation, no QR. Mirrors the existing Feishu / WeCom / Weixin contract: HTTPS long-poll inbound, `sendMessage` / `sendPhoto` / `sendDocument` outbound, photo + document download routed through the standard FileProcessor + vision pipeline, group `@-mention` filtering and `allowed_users` whitelist. `base_url` is configurable to support self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) for networks where `api.telegram.org` is unreachable. Frontend Channels panel, `channel-setup` skill, English/Chinese i18n, and `app.css` logo class added. 32 new specs in `spec/clacky/server/channel/adapters/telegram/`.
37
+ - **Discord channel adapter.** Full Discord integration via REST API + Gateway (WebSocket), with channel-setup support, Web UI Channels panel entry, and i18n strings. Connect Clacky to Discord servers for bot interactions through slash commands and message events.
38
+ - **OpenRouter curated model list.** The OpenRouter provider now ships with a curated dropdown of mainstream Claude and GPT models (Sonnet, Opus, Haiku, GPT-5.5/5.4), so users can pick from the list instead of typing model IDs manually. Full catalogue still accessible by typing any model ID.
39
+ - **OpenRouter lite model pairing.** Subagents on OpenRouter now automatically get a sensible cheap/fast sidekick — Claude family pairs with Haiku, GPT family pairs with the mini variant — matching the behavior already available on the native OpenAI and OpenClacky providers.
40
+ - **MiMo 2.5 Pro (Xiaomi) model support.** Added `mimo-v2.5-pro` to the MiMo provider preset alongside existing MiMo models.
41
+ - **AI key setup guide link.** New users and those configuring API keys now see a "New to AI keys? See the guide →" link on both onboarding and settings pages, pointing to the official documentation.
42
+
43
+ ### Improved
44
+ - **Default model upgraded to claude-sonnet-4-6.** The OpenClacky provider now defaults to the latest Claude Sonnet model for better performance out of the box.
45
+
46
+ ### Fixed
47
+ - **Linux server restart stability.** Fixed an inherited socket cleanup bug where WEBrick's shutdown would propagate `SHUT_RDWR` to the shared kernel socket, breaking subsequent `accept()` calls on Linux. The server now detaches inherited sockets before shutdown so worker restarts work reliably.
48
+ - **Upgrade failure recovery UI.** When an in-app upgrade restart fails, the UI now shows both tray icon and CLI recovery paths (`gem update ...`) instead of leaving users stranded. Also added branded CLI command info to the version check API for white-label builds.
49
+
8
50
  ## [1.0.4] - 2026-05-11
9
51
 
10
52
  ### Added
@@ -23,7 +23,7 @@ Skills must not read local config files directly.
23
23
  - ❌ `cat ~/.clacky/browser.yml`
24
24
  - ✅ `curl http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/browser/status`
25
25
 
26
- Exception: lightweight `enable` / `disable` operations may read/write yml directly (see `channel-setup`).
26
+ Exception: lightweight `enable` / `disable` operations may read/write yml directly (see `channel-manager`).
27
27
 
28
28
  ---
29
29
 
@@ -178,6 +178,9 @@ module Clacky
178
178
  if formatted_result.is_a?(Hash) && formatted_result[:image_inject]
179
179
  image_inject = formatted_result[:image_inject]
180
180
  formatted_result = formatted_result.reject { |k, _| k == :image_inject }
181
+ if formatted_result[:content_string]
182
+ formatted_result = formatted_result[:content_string]
183
+ end
181
184
  end
182
185
 
183
186
  # If the tool returned a plain string, use it directly (avoids double-escaping).
@@ -187,7 +190,6 @@ module Clacky
187
190
  content = if formatted_result.is_a?(String)
188
191
  formatted_result
189
192
  elsif formatted_result.is_a?(Array)
190
- # Multipart content (e.g. screenshot image blocks) — keep as Array
191
193
  formatted_result
192
194
  else
193
195
  JSON.generate(formatted_result)
data/lib/clacky/agent.rb CHANGED
@@ -979,9 +979,12 @@ module Clacky
979
979
  next unless mime_type && base64_data
980
980
 
981
981
  data_url = "data:#{mime_type};base64,#{base64_data}"
982
+ label = path ? File.basename(path.to_s) : "image"
983
+ image_block = { type: "image_url", image_url: { url: data_url } }
984
+ image_block[:image_path] = path if path
982
985
  image_content = [
983
- { type: "text", text: "[Image from file_reader: #{File.basename(path.to_s)}]" },
984
- { type: "image_url", image_url: { url: data_url } }
986
+ { type: "text", text: "[Image: #{label}]" },
987
+ image_block
985
988
  ]
986
989
  @history.append({
987
990
  role: "user",
@@ -1514,7 +1517,7 @@ module Clacky
1514
1517
  inline = $1 == "!"
1515
1518
  # URL-decode percent-encoded characters (e.g. Chinese filenames encoded by AI)
1516
1519
  raw_path = CGI.unescape($3)
1517
- name = $2.empty? ? File.basename(raw_path) : $2
1520
+ name = File.basename(raw_path)
1518
1521
  path = File.expand_path(raw_path)
1519
1522
  Clacky::Logger.info("[parse_file_links] raw=#{$3.inspect} expanded=#{path.inspect} exist=#{File.exist?(path)}")
1520
1523
  files << { name: name, path: path, inline: inline }
@@ -1523,13 +1526,15 @@ module Clacky
1523
1526
  end
1524
1527
 
1525
1528
  # Emit assistant message to UI, parsing any embedded file:// links first.
1529
+ #
1530
+ # Local image URL rewriting (file:// → /api/local-image) is intentionally
1531
+ # NOT done here. It is browser-specific (the Web UI runs on http://localhost
1532
+ # and cannot load file:// directly) and must stay scoped to the Web UI
1533
+ # controller. IM channel subscribers need the original file:// markdown so
1534
+ # parse_file_links can extract paths and deliver images as native attachments.
1526
1535
  private def emit_assistant_message(content)
1527
1536
  return if content.nil? || content.empty?
1528
1537
 
1529
- # Rewrite local image paths (file:// and bare absolute) to /api/local-image proxy URLs
1530
- # so the browser can render them without file:// security blocks.
1531
- content = Clacky::Utils::FileProcessor.rewrite_local_image_urls(content)
1532
-
1533
1538
  parsed = parse_file_links(content)
1534
1539
  @ui&.show_assistant_message(parsed[:text], files: parsed[:files])
1535
1540
  end
@@ -154,7 +154,8 @@ module Clacky
154
154
  attr_accessor :permission_mode, :max_tokens, :verbose,
155
155
  :enable_compression, :enable_prompt_caching,
156
156
  :models, :current_model_index, :current_model_id,
157
- :memory_update_enabled, :skill_evolution
157
+ :memory_update_enabled, :skill_evolution,
158
+ :max_running_agents, :max_idle_agents
158
159
 
159
160
  def initialize(options = {})
160
161
  @permission_mode = validate_permission_mode(options[:permission_mode])
@@ -195,6 +196,9 @@ module Clacky
195
196
  @skill_evolution = @skill_evolution.transform_keys(&:to_sym)
196
197
  @skill_evolution.transform_values! { |v| v.is_a?(Hash) ? v.transform_keys(&:to_sym) : v }
197
198
 
199
+ @max_running_agents = options[:max_running_agents] || 10
200
+ @max_idle_agents = options[:max_idle_agents] || 10
201
+
198
202
  # Per-session virtual model overlay.
199
203
  # When set, #current_model returns a *merged* hash (the resolved @models
200
204
  # entry merged with this overlay) without mutating the shared @models
@@ -368,7 +372,7 @@ module Clacky
368
372
  # These map directly to AgentConfig accessors.
369
373
  CONFIG_SETTINGS_KEYS = %w[
370
374
  enable_compression enable_prompt_caching memory_update_enabled
371
- skill_evolution
375
+ skill_evolution max_running_agents max_idle_agents
372
376
  ].freeze
373
377
 
374
378
  # Serialize the current agent configuration to YAML.
@@ -382,7 +386,9 @@ module Clacky
382
386
  "enable_compression" => @enable_compression,
383
387
  "enable_prompt_caching" => @enable_prompt_caching,
384
388
  "memory_update_enabled" => @memory_update_enabled,
385
- "skill_evolution" => @skill_evolution
389
+ "skill_evolution" => @skill_evolution,
390
+ "max_running_agents" => @max_running_agents,
391
+ "max_idle_agents" => @max_idle_agents
386
392
  }
387
393
  YAML.dump("settings" => settings, "models" => persistable_models)
388
394
  end
@@ -76,13 +76,28 @@ module Clacky
76
76
 
77
77
  # Load brand configuration from ~/.clacky/brand.yml.
78
78
  # Returns an empty BrandConfig (no brand) if the file does not exist.
79
+ # Always ensures a stable device_id is present and persisted.
79
80
  def self.load
80
- return new({}) unless File.exist?(BRAND_FILE)
81
+ if File.exist?(BRAND_FILE)
82
+ data = YAML.safe_load(File.read(BRAND_FILE)) || {}
83
+ else
84
+ data = {}
85
+ end
81
86
 
82
- data = YAML.safe_load(File.read(BRAND_FILE)) || {}
83
- new(data)
87
+ instance = new(data)
88
+ instance.ensure_device_id!
89
+ instance
84
90
  rescue StandardError
85
- new({})
91
+ instance = new({})
92
+ instance.ensure_device_id!
93
+ instance
94
+ end
95
+
96
+ def ensure_device_id!
97
+ return if @device_id && !@device_id.strip.empty?
98
+
99
+ @device_id = generate_device_id
100
+ save
86
101
  end
87
102
 
88
103
  # Returns true when this installation has a product name configured.
data/lib/clacky/cli.rb CHANGED
@@ -172,7 +172,7 @@ module Clacky
172
172
  # is booted by this process), and only when the user hasn't already set
173
173
  # CLACKY_SERVER_HOST / CLACKY_SERVER_PORT explicitly.
174
174
  #
175
- # Why: skills like `channel-setup` and `browser-setup` call back into
175
+ # Why: skills like `channel-manager` and `browser-setup` call back into
176
176
  # http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/*. In server
177
177
  # mode those vars are injected by HTTPServer#start. In CLI mode they
178
178
  # would be blank, so the skill templates expand to an unreachable URL.