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 +4 -4
- data/.clacky/skills/gem-release/SKILL.md +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +5 -5
- data/clacky-legacy/clacky.gemspec +3 -3
- data/clacky-legacy/clarky.gemspec +3 -3
- data/docs/HOW-TO-USE-CN.md +2 -2
- data/docs/HOW-TO-USE.md +2 -2
- data/docs/why-developer.md +2 -2
- data/docs/why-openclacky.md +2 -2
- data/homebrew/openclacky.rb +1 -1
- data/lib/clacky/agent/cost_tracker.rb +0 -64
- data/lib/clacky/agent/message_compressor_helper.rb +4 -6
- data/lib/clacky/agent/session_serializer.rb +7 -12
- data/lib/clacky/agent.rb +17 -0
- data/lib/clacky/cli.rb +2 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +47 -19
- data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +4 -16
- data/lib/clacky/idle_compression_timer.rb +22 -7
- data/lib/clacky/message_history.rb +41 -0
- data/lib/clacky/providers.rb +2 -2
- data/lib/clacky/server/browser_manager.rb +28 -24
- data/lib/clacky/server/channel/channel_manager.rb +1 -1
- data/lib/clacky/server/http_server.rb +13 -8
- data/lib/clacky/server/server_master.rb +18 -11
- data/lib/clacky/server/session_registry.rb +45 -7
- data/lib/clacky/tools/browser.rb +188 -305
- data/lib/clacky/tools/shell.rb +119 -97
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/sessions.js +1 -0
- data/scripts/install.ps1 +180 -0
- data/scripts/install.sh +209 -163
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5db22957073a294578efa60af05e69277891f976bb24d6d9397d5abbefeaf6b8
|
|
4
|
+
data.tar.gz: 3103ec5cf881eb0ebe16f79a5f136c01ef890255ebeda9757d4aa5f592071470
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
[](https://github.com/clacky-ai/openclacky/actions)
|
|
4
4
|
[](https://rubygems.org/gems/openclacky)
|
|
5
5
|
[](https://www.ruby-lang.org)
|
|
6
6
|
[](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/
|
|
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/
|
|
123
|
-
cd
|
|
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/
|
|
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/
|
|
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/
|
|
17
|
-
spec.metadata["changelog_uri"] = "https://github.com/clacky-ai/
|
|
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/
|
|
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/
|
|
17
|
-
spec.metadata["changelog_uri"] = "https://github.com/clacky-ai/
|
|
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"]
|
data/docs/HOW-TO-USE-CN.md
CHANGED
|
@@ -91,6 +91,6 @@ Clacky 可以自动执行复杂任务,内置多种工具:
|
|
|
91
91
|
|
|
92
92
|
## 了解更多
|
|
93
93
|
|
|
94
|
-
- GitHub:https://github.com/clacky-ai/
|
|
95
|
-
- 问题反馈:https://github.com/clacky-ai/
|
|
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/
|
|
93
|
-
- Report Issues: https://github.com/clacky-ai/
|
|
92
|
+
- GitHub: https://github.com/clacky-ai/openclacky
|
|
93
|
+
- Report Issues: https://github.com/clacky-ai/openclacky/issues
|
|
94
94
|
- Version: 0.7.0
|
data/docs/why-developer.md
CHANGED
|
@@ -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/
|
|
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/
|
|
365
|
+
- **GitHub**: https://github.com/clacky-ai/openclacky
|
|
366
366
|
- **Documentation**: https://docs.clacky.ai
|
|
367
367
|
- **Discord**: https://discord.gg/clacky
|
|
368
368
|
|
data/docs/why-openclacky.md
CHANGED
|
@@ -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/
|
|
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/
|
|
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
|
|
data/homebrew/openclacky.rb
CHANGED
|
@@ -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/
|
|
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
|
-
#
|
|
54
|
-
total_tokens =
|
|
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
|
-
|
|
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]} -> ~#{
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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...
|
|
48
|
-
wecom ❌ no (not configured)
|
|
49
|
-
weixin ✅ yes
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
259
|
+
Call the server API to re-enable the platform (this reads from disk, sets enabled, saves, and hot-reloads):
|
|
242
260
|
|
|
243
|
-
|
|
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.
|
|
269
|
+
Say: "✅ `<platform>` channel enabled."
|
|
246
270
|
|
|
247
271
|
---
|
|
248
272
|
|
|
249
273
|
## `disable`
|
|
250
274
|
|
|
251
|
-
|
|
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.
|
|
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.
|
|
263
|
-
5. Say: "
|
|
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
|
|
128
|
-
@target_id = nil
|
|
125
|
+
@client = client
|
|
129
126
|
end
|
|
130
127
|
|
|
131
128
|
def navigate(url)
|
|
132
|
-
|
|
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",
|
|
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
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
#
|
|
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&.
|
|
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.
|