openclacky 0.9.10 → 0.9.11

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: 938c620eb9886a37c1e190c5bf2d8c804a6ed86eb496332a26f5f225e832a5ec
4
- data.tar.gz: c165097e40b1bde5339e9819f7ccdd2a3191ecd24f9233a758a4a2406f5d74db
3
+ metadata.gz: 5db22957073a294578efa60af05e69277891f976bb24d6d9397d5abbefeaf6b8
4
+ data.tar.gz: 3103ec5cf881eb0ebe16f79a5f136c01ef890255ebeda9757d4aa5f592071470
5
5
  SHA512:
6
- metadata.gz: 845fef0c86bca42adcb00d2fe34946acd3b72b2d750412a744e78a080cef34b653457ae98318ddad5b623f22325b654ad2a77a61d39abc544edbfd11f44891f3
7
- data.tar.gz: ea770a8ac65695d64affd358b4af0788cccc2c4e70eb95d0afaebea61a639ba8829dbcf61af84d0d8229dac9b4af8e969316a5e5c8fb8ec75f9f430647f9b1cc
6
+ metadata.gz: 55fe30f89017da76ac2a9348fea1a1f4d701006d4c882878e09e915600ac80dcf72131d546b7e786c3588c1c848e47db4628478ce018e619a2d7781bd0cdbfe3
7
+ data.tar.gz: a6923ec51d5a5d60c2a5bd07b7fa269a76087456ea5adf4d7b33a9dc1c8a43f043570a6d4c6f6309b24dc2ad60850dd4d993cc9444fd22c76445e3749952ca09
@@ -105,7 +105,7 @@ To use this skill, simply say:
105
105
  - Parse the CHANGELOG.md section for `[{version}]`
106
106
  - Write it to a temp file (e.g., `/tmp/release_notes_{version}.md`) to avoid shell escaping issues
107
107
  - Run `gh release create` with `--notes-file`
108
- - Verify the release appears at: `https://github.com/clacky-ai/open-clacky/releases`
108
+ - Verify the release appears at: `https://github.com/clacky-ai/openclacky/releases`
109
109
 
110
110
  > **Prerequisite**: `gh` CLI must be installed (`brew install gh`) and authenticated (`gh auth login`)
111
111
 
@@ -238,14 +238,14 @@ Present a clear, user-facing release summary after all steps complete:
238
238
 
239
239
  🔗 Links:
240
240
  - RubyGems: https://rubygems.org/gems/openclacky/versions/{version}
241
- - GitHub Release: https://github.com/clacky-ai/open-clacky/releases/tag/v{version}
241
+ - GitHub Release: https://github.com/clacky-ai/openclacky/releases/tag/v{version}
242
242
 
243
243
  ⬆️ Upgrade:
244
244
  - In the Clacky UI, click "Upgrade" in the bottom-left → detect new version → click upgrade → done
245
245
  - Manual upgrade (CLI): `gem update openclacky`
246
246
 
247
247
  🆕 Fresh install:
248
- /bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/clacky-ai/open-clacky/main/scripts/install.sh)"
248
+ /bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/clacky-ai/openclacky/main/scripts/install.sh)"
249
249
  ```
250
250
 
251
251
  **Rules for writing the summary:**
@@ -308,7 +308,7 @@ gh release create vX.Y.Z \
308
308
  - New version successfully published to RubyGems
309
309
  - Git repository updated with version tag
310
310
  - CHANGELOG.md updated with release notes
311
- - GitHub Release created and visible at https://github.com/clacky-ai/open-clacky/releases
311
+ - GitHub Release created and visible at https://github.com/clacky-ai/openclacky/releases
312
312
  - No build or deployment errors
313
313
  - User-facing release summary presented at the end
314
314
 
data/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.11] - 2026-03-25
11
+
12
+ ### Added
13
+ - **Network-aware installer mirrors**: the install script now automatically detects whether you're in China and picks the fastest mirror (RubyGems China mirror, GitHub, etc.) — no manual configuration needed
14
+ - **Shell rc-file loading**: the shell tool now sources your `.zshrc` / `.bashrc` so commands that depend on environment variables or aliases set in your shell profile work correctly
15
+
16
+ ### Improved
17
+ - **Browser tool `evaluate` targets active page**: JavaScript evaluation now automatically targets the currently active browser tab instead of the last opened one, so `evaluate` always runs in the right context
18
+ - **Browser MCP process cleaned up on server shutdown**: the `chrome-devtools-mcp` node process is now stopped when the server shuts down, preventing orphaned processes that held onto port 7070
19
+ - **Server worker process isolation**: workers are now spawned in their own process group, ensuring grandchild processes (e.g. browser MCP) are fully cleaned up during zero-downtime restarts
20
+ - **Channel status via live API**: `channel status` now queries the running server API instead of reading `~/.clacky/channels.yml` directly, so it reflects the actual runtime state
21
+ - **Idle compression timer race fix**: the compression thread is now registered inside a mutex before starting, eliminating a race where `cancel()` could miss an in-flight compression and leave history in an inconsistent state
22
+ - **Compression token display accuracy**: the post-compression token count now uses the rebuilt history estimate instead of the stale pre-compression API count
23
+ - **Shell process group signals**: `SIGTERM`/`SIGKILL` are now sent to the entire process group (`-pgid`) instead of just the child PID, ensuring backgrounded subprocesses are also killed on timeout
24
+
25
+ ### Fixed
26
+ - **Task error session save**: sessions are now correctly saved to disk even when a task ends with an error, preventing session loss on agent failures
27
+ - **History load and model load bugs**: fixed crashes when loading sessions with missing or malformed history/model fields
28
+ - **Default model updated to Claude claude-sonnet-4-6**: bumped the default Gemini model reference from `gemini-2.5-flash` → `gemini-2.7-flash`
29
+
30
+ ### More
31
+ - Renamed gem references from `open-clacky` to `openclacky` across docs, gemspec, and scripts
32
+
10
33
  ## [0.9.10] - 2026-03-24
11
34
 
12
35
  ### Added
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenClacky
2
2
 
3
- [![Build](https://img.shields.io/github/actions/workflow/status/clacky-ai/open-clacky/main.yml?label=build&style=flat-square)](https://github.com/clacky-ai/open-clacky/actions)
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
4
  [![Release](https://img.shields.io/gem/v/openclacky?label=release&style=flat-square&color=blue)](https://rubygems.org/gems/openclacky)
5
5
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.1.0-red?style=flat-square)](https://www.ruby-lang.org)
6
6
  [![Downloads](https://img.shields.io/gem/dt/openclacky?label=downloads&style=flat-square&color=brightgreen)](https://rubygems.org/gems/openclacky)
@@ -73,7 +73,7 @@ Built on a production-ready Rails architecture with one-click deployment, dev/pr
73
73
  ### Method 1: One-line Install (Recommended)
74
74
 
75
75
  ```bash
76
- /bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/clacky-ai/open-clacky/main/scripts/install.sh)"
76
+ /bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/clacky-ai/openclacky/main/scripts/install.sh)"
77
77
  ```
78
78
 
79
79
  ### Method 2: RubyGems
@@ -119,15 +119,15 @@ You'll be prompted to set your **API Key**, **Model**, and **Base URL** (any Ope
119
119
  ## Install from Source
120
120
 
121
121
  ```bash
122
- git clone https://github.com/clacky-ai/open-clacky.git
123
- cd open-clacky
122
+ git clone https://github.com/clacky-ai/openclacky.git
123
+ cd openclacky
124
124
  bundle install
125
125
  bin/clacky
126
126
  ```
127
127
 
128
128
  ## Contributing
129
129
 
130
- Bug reports and pull requests are welcome on GitHub at https://github.com/clacky-ai/open-clacky. Contributors are expected to adhere to the [code of conduct](https://github.com/clacky-ai/open-clacky/blob/main/CODE_OF_CONDUCT.md).
130
+ Bug reports and pull requests are welcome on GitHub at https://github.com/clacky-ai/openclacky. Contributors are expected to adhere to the [code of conduct](https://github.com/clacky-ai/openclacky/blob/main/CODE_OF_CONDUCT.md).
131
131
 
132
132
  ## License
133
133
 
@@ -8,13 +8,13 @@ Gem::Specification.new do |spec|
8
8
 
9
9
  spec.summary = "Legacy name for openclacky gem"
10
10
  spec.description = "This is a transitional gem that depends on openclacky. The clacky project has been renamed to openclacky. Installing this gem will automatically install openclacky."
11
- spec.homepage = "https://github.com/clacky-ai/open-clacky"
11
+ spec.homepage = "https://github.com/clacky-ai/openclacky"
12
12
  spec.license = "MIT"
13
13
  spec.required_ruby_version = ">= 3.1.0"
14
14
 
15
15
  spec.metadata["homepage_uri"] = spec.homepage
16
- spec.metadata["source_code_uri"] = "https://github.com/clacky-ai/open-clacky"
17
- spec.metadata["changelog_uri"] = "https://github.com/clacky-ai/open-clacky/blob/main/CHANGELOG.md"
16
+ spec.metadata["source_code_uri"] = "https://github.com/clacky-ai/openclacky"
17
+ spec.metadata["changelog_uri"] = "https://github.com/clacky-ai/openclacky/blob/main/CHANGELOG.md"
18
18
 
19
19
  spec.files = Dir["lib/**/*", "bin/*", "README.md", "LICENSE.txt"]
20
20
  spec.require_paths = ["lib"]
@@ -8,13 +8,13 @@ Gem::Specification.new do |spec|
8
8
 
9
9
  spec.summary = "Legacy name for openclacky - AI agent command-line interface"
10
10
  spec.description = "This is a placeholder gem. Installing 'clarky' will automatically install 'openclacky'. The clarky command is maintained for backward compatibility."
11
- spec.homepage = "https://github.com/clacky-ai/open-clacky"
11
+ spec.homepage = "https://github.com/clacky-ai/openclacky"
12
12
  spec.license = "MIT"
13
13
  spec.required_ruby_version = ">= 3.1.0"
14
14
 
15
15
  spec.metadata["homepage_uri"] = spec.homepage
16
- spec.metadata["source_code_uri"] = "https://github.com/clacky-ai/open-clacky"
17
- spec.metadata["changelog_uri"] = "https://github.com/clacky-ai/open-clacky/blob/main/CHANGELOG.md"
16
+ spec.metadata["source_code_uri"] = "https://github.com/clacky-ai/openclacky"
17
+ spec.metadata["changelog_uri"] = "https://github.com/clacky-ai/openclacky/blob/main/CHANGELOG.md"
18
18
 
19
19
  spec.files = Dir["lib/**/*", "bin/*", "README.md", "LICENSE.txt"]
20
20
  spec.require_paths = ["lib"]
@@ -91,6 +91,6 @@ Clacky 可以自动执行复杂任务,内置多种工具:
91
91
 
92
92
  ## 了解更多
93
93
 
94
- - GitHub:https://github.com/clacky-ai/open-clacky
95
- - 问题反馈:https://github.com/clacky-ai/open-clacky/issues
94
+ - GitHub:https://github.com/clacky-ai/openclacky
95
+ - 问题反馈:https://github.com/clacky-ai/openclacky/issues
96
96
  - 当前版本:0.7.0
data/docs/HOW-TO-USE.md CHANGED
@@ -89,6 +89,6 @@ Create your own skills in `.clacky/skills/` directory!
89
89
 
90
90
  ## Learn More
91
91
 
92
- - GitHub: https://github.com/clacky-ai/open-clacky
93
- - Report Issues: https://github.com/clacky-ai/open-clacky/issues
92
+ - GitHub: https://github.com/clacky-ai/openclacky
93
+ - Report Issues: https://github.com/clacky-ai/openclacky/issues
94
94
  - Version: 0.7.0
@@ -285,7 +285,7 @@ models:
285
285
 
286
286
  ```bash
287
287
  # One-line installation (macOS/Linux)
288
- curl -sSL https://raw.githubusercontent.com/clacky-ai/open-clacky/main/scripts/install.sh | bash
288
+ curl -sSL https://raw.githubusercontent.com/clacky-ai/openclacky/main/scripts/install.sh | bash
289
289
 
290
290
  # Or via Ruby gem
291
291
  gem install openclacky
@@ -362,7 +362,7 @@ clacky tools
362
362
 
363
363
  ## Get Started
364
364
 
365
- - **GitHub**: https://github.com/clacky-ai/open-clacky
365
+ - **GitHub**: https://github.com/clacky-ai/openclacky
366
366
  - **Documentation**: https://docs.clacky.ai
367
367
  - **Discord**: https://discord.gg/clacky
368
368
 
@@ -211,7 +211,7 @@ We believe AI development tools should be accessible to everyone.
211
211
 
212
212
  ```bash
213
213
  # One-line installation (macOS/Linux)
214
- curl -sSL https://raw.githubusercontent.com/clacky-ai/open-clacky/main/scripts/install.sh | bash
214
+ curl -sSL https://raw.githubusercontent.com/clacky-ai/openclacky/main/scripts/install.sh | bash
215
215
 
216
216
  # Or if you have Ruby 3.1+
217
217
  gem install openclacky
@@ -239,7 +239,7 @@ clacky tools
239
239
 
240
240
  Clacky is an open-source project. We welcome contributions!
241
241
 
242
- - **GitHub**: https://github.com/clacky-ai/open-clacky
242
+ - **GitHub**: https://github.com/clacky-ai/openclacky
243
243
  - **Discord**: https://discord.gg/clacky
244
244
  - **Twitter**: https://twitter.com/clacky_ai
245
245
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  class Openclacky < Formula
4
4
  desc "Command-line interface for AI models with autonomous agent capabilities"
5
- homepage "https://github.com/clacky-ai/open-clacky"
5
+ homepage "https://github.com/clacky-ai/openclacky"
6
6
  url "https://rubygems.org/downloads/openclacky-0.6.1.gem"
7
7
  sha256 "" # Will be updated when gem is published
8
8
  license "MIT"
@@ -82,71 +82,7 @@ module Clacky
82
82
 
83
83
  # Estimate token count for a message content
84
84
  # Simple approximation: characters / 4 (English text)
85
- # For Chinese/other languages, characters / 2 is more accurate
86
- # This is a rough estimate for compression triggering purposes
87
- # @param content [String, Array, Object] Message content
88
- # @return [Integer] Estimated token count
89
- def estimate_tokens(content)
90
- return 0 if content.nil?
91
-
92
- text = if content.is_a?(String)
93
- content
94
- elsif content.is_a?(Array)
95
- # Handle content arrays (e.g., with images)
96
- # Add safety check to prevent nil.compact error
97
- mapped = content.map { |c| c[:text] if c.is_a?(Hash) }
98
- (mapped || []).compact.join
99
- else
100
- content.to_s
101
- end
102
-
103
- return 0 if text.empty?
104
-
105
- # Detect language mix - count non-ASCII characters
106
- ascii_count = text.bytes.count { |b| b < 128 }
107
- total_bytes = text.bytes.length
108
-
109
- # Mix ratio (1.0 = all English, 0.5 = all Chinese)
110
- mix_ratio = total_bytes > 0 ? ascii_count.to_f / total_bytes : 1.0
111
-
112
- # English: ~4 chars/token, Chinese: ~2 chars/token
113
- base_chars_per_token = mix_ratio * 4 + (1 - mix_ratio) * 2
114
-
115
- (text.length / base_chars_per_token).to_i + 50 # Add overhead for message structure
116
- end
117
-
118
- # Calculate total token count for all messages
119
- # Returns estimated tokens and breakdown by category
120
- # @return [Hash] Token counts by role and total
121
- def total_message_tokens
122
- system_tokens = 0
123
- user_tokens = 0
124
- assistant_tokens = 0
125
- tool_tokens = 0
126
- summary_tokens = 0
127
-
128
- @history.to_a.each do |msg|
129
- tokens = estimate_tokens(msg[:content])
130
- case msg[:role]
131
- when "system"
132
- system_tokens += tokens
133
- when "user"
134
- user_tokens += tokens
135
- when "assistant"
136
- assistant_tokens += tokens
137
- when "tool"
138
- tool_tokens += tokens
139
- end
140
- end
141
85
 
142
- {
143
- total: system_tokens + user_tokens + assistant_tokens + tool_tokens,
144
- system: system_tokens,
145
- user: user_tokens,
146
- assistant: assistant_tokens,
147
- tool: tool_tokens
148
- }
149
- end
150
86
 
151
87
  private
152
88
 
@@ -50,8 +50,8 @@ module Clacky
50
50
  # Check if compression is enabled
51
51
  return nil unless @config.enable_compression
52
52
 
53
- # Calculate total tokens and message count
54
- total_tokens = total_message_tokens[:total]
53
+ # Use actual API-reported tokens from last request
54
+ total_tokens = @previous_total_tokens
55
55
  message_count = @history.size
56
56
 
57
57
  # Force compression (for idle compression) - use lower threshold
@@ -144,11 +144,9 @@ module Clacky
144
144
  chunk_path: chunk_path
145
145
  }
146
146
 
147
- final_tokens = total_message_tokens[:total]
148
-
149
- # Show compression info
147
+ # Show compression info (use estimated tokens from rebuilt history)
150
148
  @ui&.show_info(
151
- "History compressed (~#{compression_context[:original_token_count]} -> ~#{final_tokens} tokens, " \
149
+ "History compressed (~#{compression_context[:original_token_count]} -> ~#{@history.estimate_tokens} tokens, " \
152
150
  "level #{compression_context[:compression_level]})"
153
151
  )
154
152
  end
@@ -36,22 +36,17 @@ module Clacky
36
36
  @current_task_id = session_data.dig(:time_machine, :current_task_id) || 0
37
37
  @active_task_id = session_data.dig(:time_machine, :active_task_id) || 0
38
38
 
39
- # Check if the session ended with an error
39
+ # Check if the session ended with an error.
40
+ # We record the rollback intent here but do NOT truncate history immediately —
41
+ # truncating at restore time causes the history replay to return empty results,
42
+ # leaving the chat panel blank on first open.
43
+ # Instead, the rollback is deferred: history is trimmed lazily when the user
44
+ # actually sends the next message (see run() / handle_user_message).
40
45
  last_status = session_data.dig(:stats, :last_status)
41
46
  last_error = session_data.dig(:stats, :last_error)
42
47
 
43
48
  if last_status == "error" && last_error
44
- # Trim back to just before the last real user message that caused the error
45
- last_user_index = @history.last_real_user_index
46
- if last_user_index
47
- @history.truncate_from(last_user_index)
48
-
49
- @hooks.trigger(:session_rollback, {
50
- reason: "Previous session ended with error",
51
- error_message: last_error,
52
- rolled_back_message_index: last_user_index
53
- })
54
- end
49
+ @pending_error_rollback = true
55
50
  end
56
51
 
57
52
  # Rebuild and refresh the system prompt so any newly installed skills
data/lib/clacky/agent.rb CHANGED
@@ -72,6 +72,7 @@ module Clacky
72
72
  @debug_logs = [] # Debug logs for troubleshooting
73
73
  @pending_injections = [] # Pending inline skill injections to flush after observe()
74
74
  @pending_script_tmpdirs = [] # Decrypted-script tmpdirs to shred when agent.run completes
75
+ @pending_error_rollback = false # Deferred rollback flag set by restore_session on error
75
76
 
76
77
  # Compression tracking
77
78
  @compression_level = 0 # Tracks how many times we've compressed (for progressive summarization)
@@ -180,6 +181,22 @@ module Clacky
180
181
  cache_hit_requests: 0
181
182
  }
182
183
 
184
+ # Deferred error rollback: if the previous session ended with an error,
185
+ # trim history back to just before that failed user message now — at the
186
+ # point the user actually sends a new message, not at restore time.
187
+ # (Trimming at restore time caused replay_history to return empty results.)
188
+ if @pending_error_rollback
189
+ @pending_error_rollback = false
190
+ last_user_index = @history.last_real_user_index
191
+ if last_user_index
192
+ @history.truncate_from(last_user_index)
193
+ @hooks.trigger(:session_rollback, {
194
+ reason: "Previous session ended with error — rolling back before new message",
195
+ rolled_back_message_index: last_user_index
196
+ })
197
+ end
198
+ end
199
+
183
200
  # Add system prompt as the first message if this is the first run
184
201
  if @history.empty?
185
202
  system_prompt = build_system_prompt
data/lib/clacky/cli.rb CHANGED
@@ -580,8 +580,10 @@ module Clacky
580
580
  session_manager&.save(agent.to_session_data(status: :success))
581
581
  json_ui.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
582
582
  rescue Clacky::AgentInterrupted
583
+ session_manager&.save(agent.to_session_data(status: :interrupted))
583
584
  json_ui.emit("interrupted")
584
585
  rescue => e
586
+ session_manager&.save(agent.to_session_data(status: :error, error_message: e.message))
585
587
  json_ui.emit("error", message: e.message)
586
588
  ensure
587
589
  json_ui.set_idle_status
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: channel-setup
3
3
  description: |
4
- Configure IM platform channels (Feishu, WeCom, Weixin) for open-clacky.
4
+ Configure IM platform channels (Feishu, WeCom, Weixin) for openclacky.
5
5
  Uses browser automation for navigation; guides the user to paste credentials and perform UI steps.
6
6
  Trigger on: "channel setup", "setup feishu", "setup wecom", "setup weixin", "setup wechat", "channel config",
7
7
  "channel status", "channel enable", "channel disable", "channel reconfigure", "channel doctor".
@@ -19,7 +19,7 @@ allowed-tools:
19
19
 
20
20
  # Channel Setup Skill
21
21
 
22
- Configure IM platform channels for open-clacky. Config is stored at `~/.clacky/channels.yml`.
22
+ Configure IM platform channels for openclacky.
23
23
 
24
24
  ---
25
25
 
@@ -38,21 +38,38 @@ Configure IM platform channels for open-clacky. Config is stored at `~/.clacky/c
38
38
 
39
39
  ## `status`
40
40
 
41
- Read `~/.clacky/channels.yml` and display:
41
+ Call the server API:
42
+
43
+ ```bash
44
+ curl -s http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels
45
+ ```
46
+
47
+ Response shape (example):
48
+ ```json
49
+ {"channels":[
50
+ {"platform":"feishu","enabled":true,"running":true,"has_config":true,"app_id":"cli_xxx","domain":"https://open.feishu.cn","allowed_users":[]},
51
+ {"platform":"wecom","enabled":false,"running":false,"has_config":false,"bot_id":""},
52
+ {"platform":"weixin","enabled":true,"running":true,"has_config":true,"has_token":true,"base_url":"https://ilinkai.weixin.qq.com","allowed_users":[]}
53
+ ]}
54
+ ```
55
+
56
+ Display the result:
42
57
 
43
58
  ```
44
59
  Channel Status
45
60
  ─────────────────────────────────────────────────────
46
- Platform Enabled Details
47
- feishu ✅ yes app_id: cli_xxx... domain: feishu.cn
48
- wecom ❌ no (not configured)
49
- weixin ✅ yes 2 account(s) logged in
61
+ Platform Enabled Running Details
62
+ feishu ✅ yes ✅ yes app_id: cli_xxx...
63
+ wecom ❌ no ❌ no (not configured)
64
+ weixin ✅ yes yes has_token: true
50
65
  ─────────────────────────────────────────────────────
51
66
  ```
52
67
 
53
- For Weixin, show `has_token: true/false` from the channels.yml entry (token is never displayed).
68
+ - Feishu: show `app_id` (truncated to 12 chars)
69
+ - WeCom: show `bot_id` if present
70
+ - Weixin: show `has_token: true/false` (token value is never displayed)
54
71
 
55
- If the file doesn't exist: "No channels configured yet. Run `/channel-setup setup` to get started."
72
+ If the API is unreachable or returns an empty list: "No channels configured yet. Run `/channel-setup setup` to get started."
56
73
 
57
74
  ---
58
75
 
@@ -101,7 +118,7 @@ Only reach here if the automated script failed.
101
118
 
102
119
  ##### Phase 2 — Create a new app
103
120
 
104
- 4. **Always create a new app** — do NOT reuse existing apps. Guide the user: "Click 'Create Enterprise Self-Built App', fill in name (e.g. Open Clacky) and description (e.g. AI assistant powered by open-clacky), then submit. Reply done." Wait for "done".
121
+ 4. **Always create a new app** — do NOT reuse existing apps. Guide the user: "Click 'Create Enterprise Self-Built App', fill in name (e.g. Open Clacky) and description (e.g. AI assistant powered by openclacky), then submit. Reply done." Wait for "done".
105
122
 
106
123
  ##### Phase 3 — Enable Bot capability
107
124
 
@@ -140,6 +157,7 @@ Only reach here if the automated script failed.
140
157
  -H "Content-Type: application/json" \
141
158
  -d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>","domain":"https://open.feishu.cn"}'
142
159
  ```
160
+ **CRITICAL: This curl call is the ONLY way to save credentials. NEVER write `~/.clacky/channels.yml` or any file under `~/.clacky/channels/` directly. The server API handles persistence and hot-reload.**
143
161
  11. **Wait for connection** — Poll until log shows `[feishu-ws] WebSocket connected ✅`:
144
162
  ```bash
145
163
  for i in $(seq 1 20); do
@@ -238,29 +256,39 @@ Tell the user while waiting:
238
256
 
239
257
  ## `enable`
240
258
 
241
- Read `~/.clacky/channels.yml`, set `channels.<platform>.enabled: true`, write back.
259
+ Call the server API to re-enable the platform (this reads from disk, sets enabled, saves, and hot-reloads):
242
260
 
243
- If the platform has no credentials, redirect to `setup`.
261
+ ```bash
262
+ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/<platform> \
263
+ -H "Content-Type: application/json" \
264
+ -d '{"enabled": true}'
265
+ ```
266
+
267
+ If the platform has no credentials (404 or error), redirect to `setup`.
244
268
 
245
- Say: "✅ `<platform>` channel enabled. Restart `clacky server` to activate."
269
+ Say: "✅ `<platform>` channel enabled."
246
270
 
247
271
  ---
248
272
 
249
273
  ## `disable`
250
274
 
251
- Read `~/.clacky/channels.yml`, set `channels.<platform>.enabled: false`, write back.
275
+ Call the server API to disable the platform:
276
+
277
+ ```bash
278
+ curl -s -X DELETE http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/<platform>
279
+ ```
252
280
 
253
- Say: "❌ `<platform>` channel disabled. Restart `clacky server` to deactivate."
281
+ Say: "❌ `<platform>` channel disabled."
254
282
 
255
283
  ---
256
284
 
257
285
  ## `reconfigure`
258
286
 
259
- 1. Show current config (mask secrets).
287
+ 1. Show current config via `GET /api/channels` (mask secrets — show last 4 chars only).
260
288
  2. Ask: update credentials / change allowed users / add a new platform / enable or disable a platform.
261
- 3. For credential updates, re-run the relevant setup flow.
262
- 4. Write atomically: write to `~/.clacky/channels.yml.tmp` then rename to `~/.clacky/channels.yml`.
263
- 5. Say: "Restart `clacky server` to apply changes."
289
+ 3. For credential updates, re-run the relevant setup flow (which calls `POST /api/channels/<platform>`).
290
+ 4. **NEVER write `~/.clacky/channels.yml` directly** always use the server API.
291
+ 5. Say: "Channel reconfigured."
264
292
 
265
293
  ---
266
294
 
@@ -121,33 +121,24 @@ end
121
121
  # ---------------------------------------------------------------------------
122
122
 
123
123
  class BrowserSession
124
- attr_reader :target_id
125
-
126
124
  def initialize(client)
127
- @client = client
128
- @target_id = nil
125
+ @client = client
129
126
  end
130
127
 
131
128
  def navigate(url)
132
- if @target_id
133
- @client.call("navigate", target_id: @target_id, url: url)
134
- else
135
- result = @client.call("open", url: url)
136
- @target_id = result["targetId"]
137
- end
129
+ @client.call("open", url: url)
138
130
  sleep 2
139
131
  snapshot
140
132
  end
141
133
 
142
134
  def snapshot(interactive: true, compact: true)
143
- result = @client.call("snapshot",
144
- target_id: @target_id, interactive: interactive, compact: compact)
135
+ result = @client.call("snapshot", interactive: interactive, compact: compact)
145
136
  result["output"].to_s
146
137
  end
147
138
 
148
139
  # Run JavaScript in the page context and return the raw output string.
149
140
  def evaluate(js)
150
- result = @client.call("act", target_id: @target_id, kind: "evaluate", js: js)
141
+ result = @client.call("act", kind: "evaluate", js: js)
151
142
  result["output"].to_s
152
143
  end
153
144
 
@@ -155,7 +146,6 @@ class BrowserSession
155
146
  # The browser evaluate output may be wrapped in MCP markdown — parse it out.
156
147
  def cookies
157
148
  result = @client.call("act",
158
- target_id: @target_id,
159
149
  kind: "evaluate",
160
150
  js: "document.cookie")
161
151
  raw = result["output"].to_s
@@ -173,7 +163,6 @@ class BrowserSession
173
163
 
174
164
  # Try lark_oapi_csrf_token specifically via JS
175
165
  result = @client.call("act",
176
- target_id: @target_id,
177
166
  kind: "evaluate",
178
167
  js: "document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('lark_oapi_csrf_token='))?.split('=')[1] || document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('lgw_csrf_token='))?.split('=')[1] || document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('swp_csrf_token='))?.split('=')[1] || ''")
179
168
  token = extract_string_value(result["output"].to_s)
@@ -181,7 +170,6 @@ class BrowserSession
181
170
 
182
171
  # Try from window object
183
172
  result = @client.call("act",
184
- target_id: @target_id,
185
173
  kind: "evaluate",
186
174
  js: "window.csrfToken || ''")
187
175
  extract_string_value(result["output"].to_s)
@@ -41,27 +41,42 @@ module Clacky
41
41
  Thread.current.name = "idle-compression-timer"
42
42
  sleep IDLE_DELAY
43
43
 
44
- # Kick off compression in a separate thread so it can be interrupted
45
- compress_thread = Thread.new do
46
- Thread.current.name = "idle-compression-work"
47
- run_compression
44
+ # Register @compress_thread inside the mutex BEFORE the thread starts running,
45
+ # so cancel() can always find and interrupt it even if it fires immediately.
46
+ compress_thread = nil
47
+ @mutex.synchronize do
48
+ compress_thread = Thread.new do
49
+ Thread.current.name = "idle-compression-work"
50
+ run_compression
51
+ end
52
+ @compress_thread = compress_thread
48
53
  end
49
54
 
50
- @mutex.synchronize { @compress_thread = compress_thread }
51
55
  compress_thread.join
52
56
  @mutex.synchronize { @compress_thread = nil; @timer_thread = nil }
53
57
  end
54
58
  end
55
59
 
56
60
  # Cancel the timer and any in-progress compression.
57
- # Safe to call even if the timer is not running.
61
+ # Raises AgentInterrupted on the compress thread and waits for it to fully exit,
62
+ # ensuring history rollback completes before the caller starts a new agent.run.
58
63
  def cancel
64
+ compress_thread_to_join = nil
65
+
59
66
  @mutex.synchronize do
60
67
  @timer_thread&.kill
61
- @compress_thread&.raise(Clacky::AgentInterrupted, "Idle timer cancelled")
68
+ if @compress_thread&.alive?
69
+ @compress_thread.raise(Clacky::AgentInterrupted, "Idle timer cancelled")
70
+ compress_thread_to_join = @compress_thread
71
+ end
62
72
  @timer_thread = nil
63
73
  @compress_thread = nil
64
74
  end
75
+
76
+ # Join outside the mutex to avoid deadlock.
77
+ # This blocks until the compress thread has finished rolling back history,
78
+ # so the subsequent agent.run sees a clean, consistent history.
79
+ compress_thread_to_join&.join(5)
65
80
  end
66
81
 
67
82
  # True if the timer or compression is currently active.