openclacky 1.2.8 → 1.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/lib/clacky/agent/llm_caller.rb +3 -0
- data/lib/clacky/agent/message_compressor_helper.rb +6 -5
- data/lib/clacky/agent/session_serializer.rb +4 -0
- data/lib/clacky/agent.rb +9 -0
- data/lib/clacky/agent_config.rb +111 -8
- data/lib/clacky/brand_config.rb +1 -0
- data/lib/clacky/cli.rb +49 -22
- data/lib/clacky/client.rb +6 -2
- data/lib/clacky/default_skills/channel-manager/SKILL.md +33 -110
- data/lib/clacky/default_skills/media-gen/SKILL.md +128 -0
- data/lib/clacky/idle_compression_timer.rb +38 -15
- data/lib/clacky/media/base.rb +68 -0
- data/lib/clacky/media/gemini.rb +36 -0
- data/lib/clacky/media/generator.rb +78 -0
- data/lib/clacky/media/openai_compat.rb +168 -0
- data/lib/clacky/providers.rb +89 -2
- data/lib/clacky/rich_ui_controller.rb +1549 -0
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +24 -2
- data/lib/clacky/server/channel/channel_manager.rb +89 -2
- data/lib/clacky/server/http_server.rb +334 -29
- data/lib/clacky/session_manager.rb +9 -8
- data/lib/clacky/telemetry.rb +26 -6
- data/lib/clacky/ui2/layout_manager.rb +11 -7
- data/lib/clacky/ui2/ui_controller.rb +2 -2
- data/lib/clacky/ui_interface.rb +1 -1
- data/lib/clacky/utils/model_pricing.rb +75 -53
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +393 -14
- data/lib/clacky/web/billing.js +1 -1
- data/lib/clacky/web/i18n.js +86 -4
- data/lib/clacky/web/index.html +23 -3
- data/lib/clacky/web/model-tester.js +58 -0
- data/lib/clacky/web/onboard.js +17 -30
- data/lib/clacky/web/sessions.js +443 -2
- data/lib/clacky/web/settings.js +372 -97
- data/lib/clacky/web/workspace.js +9 -1
- data/lib/clacky.rb +3 -0
- data/scripts/build/lib/network.sh +61 -30
- data/scripts/install.ps1 +16 -4
- data/scripts/install.sh +61 -30
- data/scripts/install_browser.sh +61 -30
- data/scripts/install_full.sh +61 -30
- data/scripts/install_rails_deps.sh +61 -30
- data/scripts/install_system_deps.sh +61 -30
- metadata +12 -3
- data/lib/clacky/default_skills/channel-manager/feishu_setup.rb +0 -574
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b32940868f1d61791afd615ff73dbaf72dc80c111f9f9435ef939ef39ae5dec
|
|
4
|
+
data.tar.gz: be8efa7ee318c3f174ddbbdf1f5b2754705eb6a5d3f263aa11cbe5539b198e8f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abcbed799ca8feed1a41e39a72bd8e6a5e9184c8e76a67bac79f9bee07f88ebf6b639102d6cfcd6527bf80b7b3c9bda5665f131bf81a164b9a14b963cad1ea47
|
|
7
|
+
data.tar.gz: 829bc77c06483853c1d568d04a528a9611c6e7f775b38b6c6fdebe04c5a5ae6974328bb1ac9c0f8cb1afe0765a60795061157b65860eaf26e8bb7368dadb2b8e
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ 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.2.10] - 2026-06-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Compressor concurrency config and Ollama context size auto-detection
|
|
12
|
+
- Channel sessions auto-rebind on server restart
|
|
13
|
+
- Disk space check (4 GB minimum) on Windows install
|
|
14
|
+
- WSL network connectivity pre-check before installation
|
|
15
|
+
- MiniMax M3 provider with vision support and pricing
|
|
16
|
+
- One-click exchange rate update in settings
|
|
17
|
+
- Rich TUI controller for terminal interaction
|
|
18
|
+
|
|
19
|
+
### Improved
|
|
20
|
+
- WebUI working directory selector UX
|
|
21
|
+
- Qwen pricing table: official rates, promo discounts, clean up stale models
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Directory picker interaction and indentation issues
|
|
25
|
+
- File upload for PDF and Excel files
|
|
26
|
+
- Session soft-delete: count-based cleanup and never evict pinned sessions
|
|
27
|
+
- Usage tooltip total value showing unreadable blue background
|
|
28
|
+
- Workspace file list now collapses on session switch
|
|
29
|
+
- Install script network errors use exit code 2 for better error handling
|
|
30
|
+
|
|
31
|
+
## [1.2.9] - 2026-06-01
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- Image generation support via model tool calls
|
|
35
|
+
- Startup telemetry now reports launch source for better usage analytics
|
|
36
|
+
|
|
37
|
+
### Improved
|
|
38
|
+
- Feishu channel setup simplified with Agent App flow — fewer manual steps and no redirect URL config needed
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- Network region detection hardened with CDN fallback to handle edge cases and improve reliability
|
|
42
|
+
|
|
8
43
|
## [1.2.8] - 2026-06-01
|
|
9
44
|
|
|
10
45
|
### Added
|
|
@@ -583,6 +583,9 @@ module Clacky
|
|
|
583
583
|
"exceeds the maximum context", # Portkey & generic gateways
|
|
584
584
|
"exceeds the model's context", # Generic
|
|
585
585
|
"exceeds the model's maximum", # Generic
|
|
586
|
+
"exceeds the available context", # llama.cpp / llama-server
|
|
587
|
+
"available context size", # llama.cpp / llama-server variant
|
|
588
|
+
"try increasing it", # llama.cpp action hint (server.cpp)
|
|
586
589
|
"reduce the length of the input", # Qwen action hint
|
|
587
590
|
"reduce the length of the messages", # OpenAI action hint
|
|
588
591
|
"reduce the length of your", # Generic action hint
|
|
@@ -5,9 +5,10 @@ module Clacky
|
|
|
5
5
|
# Message compression functionality for managing conversation history
|
|
6
6
|
# Handles automatic compression when token limits are exceeded
|
|
7
7
|
module MessageCompressorHelper
|
|
8
|
-
# Compression
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# Compression behavior knobs.
|
|
9
|
+
# Token & message-count thresholds are owned by AgentConfig — see
|
|
10
|
+
# AgentConfig::DEFAULT_COMPRESSION_THRESHOLD / DEFAULT_MESSAGE_COUNT_THRESHOLD.
|
|
11
|
+
# The constants below are tuning parameters not currently exposed as user config.
|
|
11
12
|
MAX_RECENT_MESSAGES = 20 # Keep this many recent message pairs intact
|
|
12
13
|
TARGET_COMPRESSED_TOKENS = 10_000 # Target size after compression
|
|
13
14
|
IDLE_COMPRESSION_THRESHOLD = 20_000 # Minimum messages needed for idle compression
|
|
@@ -141,8 +142,8 @@ module Clacky
|
|
|
141
142
|
else
|
|
142
143
|
# Normal compression - check thresholds
|
|
143
144
|
# Either: token count exceeds threshold OR message count exceeds threshold
|
|
144
|
-
token_threshold_exceeded = total_tokens >=
|
|
145
|
-
message_count_exceeded = message_count >=
|
|
145
|
+
token_threshold_exceeded = total_tokens >= @config.compression_threshold
|
|
146
|
+
message_count_exceeded = message_count >= @config.message_count_threshold
|
|
146
147
|
|
|
147
148
|
# Only compress if we exceed at least one threshold
|
|
148
149
|
return nil unless token_threshold_exceeded || message_count_exceeded
|
|
@@ -25,6 +25,9 @@ module Clacky
|
|
|
25
25
|
# Restore source; fall back to :manual for sessions saved before this field existed
|
|
26
26
|
@source = (session_data[:source] || "manual").to_sym
|
|
27
27
|
|
|
28
|
+
# Restore channel info for IM platform sessions
|
|
29
|
+
@channel_info = session_data[:channel_info]
|
|
30
|
+
|
|
28
31
|
# Restore cache statistics if available
|
|
29
32
|
@cache_stats = session_data.dig(:stats, :cache_stats) || {
|
|
30
33
|
cache_creation_input_tokens: 0,
|
|
@@ -158,6 +161,7 @@ module Clacky
|
|
|
158
161
|
model_base_url: persisted_card_field("base_url"),
|
|
159
162
|
sub_model: @config.session_model_overlay_name
|
|
160
163
|
},
|
|
164
|
+
channel_info: @channel_info,
|
|
161
165
|
stats: stats_data,
|
|
162
166
|
messages: @history.to_a
|
|
163
167
|
}
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -46,6 +46,7 @@ module Clacky
|
|
|
46
46
|
:latest_latency, # Hash of latency metrics from the most recent LLM call (see Client#send_messages_with_tools)
|
|
47
47
|
:reasoning_effort
|
|
48
48
|
attr_accessor :pinned
|
|
49
|
+
attr_accessor :channel_info
|
|
49
50
|
|
|
50
51
|
REASONING_EFFORTS = %w[low medium high].freeze
|
|
51
52
|
|
|
@@ -72,6 +73,7 @@ module Clacky
|
|
|
72
73
|
@config = config.is_a?(AgentConfig) ? config : AgentConfig.new(config)
|
|
73
74
|
@agent_profile = AgentProfile.load(profile)
|
|
74
75
|
@source = source.to_sym # :manual | :cron | :channel
|
|
76
|
+
@channel_info = nil # { platform:, user_id:, user_name:, chat_id: } set by ChannelManager
|
|
75
77
|
@tool_registry = ToolRegistry.new
|
|
76
78
|
@hooks = HookManager.new
|
|
77
79
|
@session_id = session_id
|
|
@@ -1593,6 +1595,13 @@ module Clacky
|
|
|
1593
1595
|
desktop ? "Desktop: #{desktop}" : nil,
|
|
1594
1596
|
"Working directory: #{@working_dir}"
|
|
1595
1597
|
].compact.join(". ")
|
|
1598
|
+
if @channel_info
|
|
1599
|
+
platform = @channel_info[:platform].to_s
|
|
1600
|
+
user_id = @channel_info[:user_id].to_s
|
|
1601
|
+
user_name = @channel_info[:user_name].to_s
|
|
1602
|
+
sender = user_name.empty? ? user_id : "@#{user_name}(#{user_id})"
|
|
1603
|
+
parts = "#{parts}. Channel: #{platform}, Sender: #{sender}"
|
|
1604
|
+
end
|
|
1596
1605
|
|
|
1597
1606
|
content = "[Session context: #{parts}]"
|
|
1598
1607
|
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -151,8 +151,16 @@ module Clacky
|
|
|
151
151
|
|
|
152
152
|
PERMISSION_MODES = [:auto_approve, :confirm_safes, :confirm_all].freeze
|
|
153
153
|
|
|
154
|
+
# Conversation-history compression defaults. Both are user-configurable
|
|
155
|
+
# via `settings.compression_threshold` / `settings.message_count_threshold`
|
|
156
|
+
# in ~/.clacky/config.yml — local-model users (llama.cpp / ollama / vllm)
|
|
157
|
+
# typically lower compression_threshold to fit their server's context window.
|
|
158
|
+
DEFAULT_COMPRESSION_THRESHOLD = 150_000
|
|
159
|
+
DEFAULT_MESSAGE_COUNT_THRESHOLD = 200
|
|
160
|
+
|
|
154
161
|
attr_accessor :permission_mode, :max_tokens, :verbose,
|
|
155
162
|
:enable_compression, :enable_prompt_caching,
|
|
163
|
+
:compression_threshold, :message_count_threshold,
|
|
156
164
|
:models, :current_model_index, :current_model_id,
|
|
157
165
|
:memory_update_enabled, :skill_evolution,
|
|
158
166
|
:max_running_agents, :max_idle_agents,
|
|
@@ -165,6 +173,13 @@ module Clacky
|
|
|
165
173
|
@enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
|
|
166
174
|
# Enable prompt caching by default for cost savings
|
|
167
175
|
@enable_prompt_caching = options[:enable_prompt_caching].nil? ? true : options[:enable_prompt_caching]
|
|
176
|
+
# Token threshold that triggers proactive history compression. Local models
|
|
177
|
+
# (llama.cpp, ollama, vllm) often have small context windows (e.g. 64k);
|
|
178
|
+
# users with such setups should lower this to avoid hitting the server-side ceiling.
|
|
179
|
+
@compression_threshold = options[:compression_threshold] || DEFAULT_COMPRESSION_THRESHOLD
|
|
180
|
+
# Message-count threshold that also triggers compression, independent of token count.
|
|
181
|
+
# Guards against pathological histories with many tiny messages.
|
|
182
|
+
@message_count_threshold = options[:message_count_threshold] || DEFAULT_MESSAGE_COUNT_THRESHOLD
|
|
168
183
|
|
|
169
184
|
# Models configuration
|
|
170
185
|
@models = options[:models] || []
|
|
@@ -318,7 +333,9 @@ module Clacky
|
|
|
318
333
|
end
|
|
319
334
|
end
|
|
320
335
|
|
|
321
|
-
new(**constructor_args)
|
|
336
|
+
instance = new(**constructor_args)
|
|
337
|
+
instance.derive_media_models!
|
|
338
|
+
instance
|
|
322
339
|
end
|
|
323
340
|
|
|
324
341
|
# Auto-injection of provider-preset lite models into @models has been
|
|
@@ -385,7 +402,9 @@ module Clacky
|
|
|
385
402
|
# Settings keys that are persisted to config.yml.
|
|
386
403
|
# These map directly to AgentConfig accessors.
|
|
387
404
|
CONFIG_SETTINGS_KEYS = %w[
|
|
388
|
-
enable_compression enable_prompt_caching
|
|
405
|
+
enable_compression enable_prompt_caching
|
|
406
|
+
compression_threshold message_count_threshold
|
|
407
|
+
memory_update_enabled
|
|
389
408
|
skill_evolution max_running_agents max_idle_agents
|
|
390
409
|
default_working_dir
|
|
391
410
|
].freeze
|
|
@@ -400,6 +419,8 @@ module Clacky
|
|
|
400
419
|
settings = {
|
|
401
420
|
"enable_compression" => @enable_compression,
|
|
402
421
|
"enable_prompt_caching" => @enable_prompt_caching,
|
|
422
|
+
"compression_threshold" => @compression_threshold,
|
|
423
|
+
"message_count_threshold" => @message_count_threshold,
|
|
403
424
|
"memory_update_enabled" => @memory_update_enabled,
|
|
404
425
|
"skill_evolution" => @skill_evolution,
|
|
405
426
|
"max_running_agents" => @max_running_agents,
|
|
@@ -585,12 +606,94 @@ module Clacky
|
|
|
585
606
|
}.compact
|
|
586
607
|
end
|
|
587
608
|
|
|
588
|
-
# Find model by type (default or lite)
|
|
589
|
-
# Returns the model hash or nil if not found
|
|
609
|
+
# Find model by type (default or lite or media kind)
|
|
610
|
+
# Returns the model hash or nil if not found.
|
|
611
|
+
# For media kinds (image/video/audio): explicit user-configured (custom)
|
|
612
|
+
# entries win; otherwise an auto-derived virtual entry is returned
|
|
613
|
+
# based on the default model's provider — mirroring how lite is
|
|
614
|
+
# virtually derived via #lite_model_config_for_current.
|
|
590
615
|
def find_model_by_type(type)
|
|
616
|
+
kind = type.to_s
|
|
617
|
+
if Clacky::Providers::MEDIA_KINDS.include?(kind)
|
|
618
|
+
custom = @models.find { |m| m["type"] == kind }
|
|
619
|
+
return custom if custom
|
|
620
|
+
return derive_media_model(kind)
|
|
621
|
+
end
|
|
591
622
|
@models.find { |m| m["type"] == type }
|
|
592
623
|
end
|
|
593
624
|
|
|
625
|
+
private def derive_media_model(kind)
|
|
626
|
+
default = find_model_by_type("default")
|
|
627
|
+
return nil unless default
|
|
628
|
+
|
|
629
|
+
provider_id = Clacky::Providers.resolve_provider(
|
|
630
|
+
base_url: default["base_url"],
|
|
631
|
+
api_key: default["api_key"]
|
|
632
|
+
)
|
|
633
|
+
return nil unless provider_id
|
|
634
|
+
|
|
635
|
+
model_name = Clacky::Providers.default_media_model(provider_id, kind)
|
|
636
|
+
return nil if model_name.nil? || model_name.to_s.empty?
|
|
637
|
+
|
|
638
|
+
{
|
|
639
|
+
"model" => model_name,
|
|
640
|
+
"base_url" => default["base_url"],
|
|
641
|
+
"api_key" => default["api_key"],
|
|
642
|
+
"type" => kind,
|
|
643
|
+
"auto_injected" => true
|
|
644
|
+
}
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# Kept as a no-op for backward compatibility. Media auto entries are
|
|
648
|
+
# now derived virtually on read; nothing is materialized into @models.
|
|
649
|
+
def derive_media_models!
|
|
650
|
+
@models.reject! { |m| m["auto_injected"] && Clacky::Providers::MEDIA_KINDS.include?(m["type"].to_s) }
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# Returns the configured/derived media model entry for `kind`, plus a
|
|
654
|
+
# hint about its source. UI uses this to render the tri-state control.
|
|
655
|
+
# @param kind [String] one of "image" / "video" / "audio"
|
|
656
|
+
# @return [Hash{String=>Object}] keys:
|
|
657
|
+
# "configured" [Boolean] — anything available?
|
|
658
|
+
# "source" [String] — "off" | "auto" | "custom"
|
|
659
|
+
# "model" [String, nil]
|
|
660
|
+
# "base_url" [String, nil]
|
|
661
|
+
# "provider" [String, nil] — provider id
|
|
662
|
+
# "available" [Array<String>] — auto-source candidates from preset
|
|
663
|
+
def media_state(kind)
|
|
664
|
+
kind = kind.to_s
|
|
665
|
+
custom = @models.find { |m| m["type"] == kind }
|
|
666
|
+
auto = custom ? nil : derive_media_model(kind)
|
|
667
|
+
entry = custom || auto
|
|
668
|
+
|
|
669
|
+
provider_id = if entry
|
|
670
|
+
Clacky::Providers.resolve_provider(
|
|
671
|
+
base_url: entry["base_url"],
|
|
672
|
+
api_key: entry["api_key"]
|
|
673
|
+
)
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
available_provider_id = if custom
|
|
677
|
+
provider_id
|
|
678
|
+
else
|
|
679
|
+
default = find_model_by_type("default")
|
|
680
|
+
default && Clacky::Providers.resolve_provider(
|
|
681
|
+
base_url: default["base_url"],
|
|
682
|
+
api_key: default["api_key"]
|
|
683
|
+
)
|
|
684
|
+
end
|
|
685
|
+
available = available_provider_id ? Clacky::Providers.media_models(available_provider_id, kind) : []
|
|
686
|
+
|
|
687
|
+
{
|
|
688
|
+
"configured" => !entry.nil?,
|
|
689
|
+
"source" => custom ? "custom" : (auto ? "auto" : "off"),
|
|
690
|
+
"model" => entry && entry["model"],
|
|
691
|
+
"base_url" => entry && entry["base_url"],
|
|
692
|
+
"provider" => provider_id,
|
|
693
|
+
"available" => available
|
|
694
|
+
}
|
|
695
|
+
end
|
|
696
|
+
|
|
594
697
|
# Find model by composite key (model name + base_url).
|
|
595
698
|
# Used when restoring a session to match its original model without relying
|
|
596
699
|
# on the runtime-only id (which changes on every process restart).
|
|
@@ -896,14 +999,14 @@ module Clacky
|
|
|
896
999
|
Clacky::Providers.supports?(provider_id, capability, model_name: m["model"])
|
|
897
1000
|
end
|
|
898
1001
|
|
|
899
|
-
# Set a model's type (default or
|
|
900
|
-
#
|
|
1002
|
+
# Set a model's type (default, lite, image, video, or audio).
|
|
1003
|
+
# At most one model carries each type at a time.
|
|
901
1004
|
# @param index [Integer] the model index
|
|
902
|
-
# @param type [String, nil]
|
|
1005
|
+
# @param type [String, nil] type tag, or nil to clear
|
|
903
1006
|
# Returns true if successful
|
|
904
1007
|
def set_model_type(index, type)
|
|
905
1008
|
return false if index < 0 || index >= @models.length
|
|
906
|
-
return false unless ["default", "lite", nil].include?(type)
|
|
1009
|
+
return false unless ["default", "lite", "image", "video", "audio", nil].include?(type)
|
|
907
1010
|
|
|
908
1011
|
if type
|
|
909
1012
|
# Remove type from any other model that has it
|
data/lib/clacky/brand_config.rb
CHANGED
data/lib/clacky/cli.rb
CHANGED
|
@@ -53,6 +53,7 @@ module Clacky
|
|
|
53
53
|
option :list, type: :boolean, aliases: "-l", desc: "List recent sessions"
|
|
54
54
|
option :attach, type: :string, aliases: "-a", desc: "Attach to session by number or keyword"
|
|
55
55
|
option :json, type: :boolean, default: false, desc: "Output NDJSON to stdout (for scripting/piping)"
|
|
56
|
+
option :ui, type: :string, default: nil, desc: "Interactive UI implementation: ui2, rich (default: ui2)"
|
|
56
57
|
option :message, type: :string, aliases: "-m", desc: "Run non-interactively with this message and exit"
|
|
57
58
|
option :file, type: :array, aliases: "-f", desc: "File path(s) to attach (use with -m; supports images and documents)"
|
|
58
59
|
option :image, type: :array, aliases: "-i", desc: "Image file path(s) to attach (alias for --file, kept for compatibility)"
|
|
@@ -725,27 +726,47 @@ module Clacky
|
|
|
725
726
|
# Brand license check — must happen before UI2 starts (raw terminal mode conflict)
|
|
726
727
|
check_brand_license_cli
|
|
727
728
|
|
|
728
|
-
|
|
729
|
-
is_dark_bg = UI2::TerminalDetector.detect_dark_background
|
|
729
|
+
ui_name = (options[:ui] || ENV["OPENCLACKY_UI"] || "ui2").to_s
|
|
730
730
|
|
|
731
|
-
|
|
732
|
-
|
|
731
|
+
ui_controller = if ui_name == "rich"
|
|
732
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.6.0")
|
|
733
|
+
say "Error: Rich UI requires Ruby >= 2.6. Use --ui ui2 on Ruby #{RUBY_VERSION}.", :red
|
|
734
|
+
exit 1
|
|
735
|
+
end
|
|
736
|
+
require_relative "rich_ui_controller"
|
|
737
|
+
RichUIController.new(
|
|
738
|
+
working_dir: working_dir,
|
|
739
|
+
mode: agent_config.permission_mode.to_s,
|
|
740
|
+
model: agent_config.model_name,
|
|
741
|
+
theme: options[:theme]
|
|
742
|
+
)
|
|
743
|
+
else
|
|
744
|
+
unless ui_name == "ui2"
|
|
745
|
+
say "Error: Unknown UI '#{ui_name}'. Available UIs: ui2, rich", :red
|
|
746
|
+
exit 1
|
|
747
|
+
end
|
|
748
|
+
# Detect terminal background BEFORE starting UI2 to avoid output interference
|
|
749
|
+
is_dark_bg = UI2::TerminalDetector.detect_dark_background
|
|
733
750
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
available_themes = UI2::ThemeManager.available_themes.map(&:to_s)
|
|
737
|
-
unless available_themes.include?(theme_name)
|
|
738
|
-
say "Error: Unknown theme '#{theme_name}'. Available themes: #{available_themes.join(', ')}", :red
|
|
739
|
-
exit 1
|
|
740
|
-
end
|
|
751
|
+
# Pass detected background mode to theme manager (singleton)
|
|
752
|
+
UI2::ThemeManager.instance.set_background_mode(is_dark_bg)
|
|
741
753
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
754
|
+
# Validate theme
|
|
755
|
+
theme_name = options[:theme] || "hacker"
|
|
756
|
+
available_themes = UI2::ThemeManager.available_themes.map(&:to_s)
|
|
757
|
+
unless available_themes.include?(theme_name)
|
|
758
|
+
say "Error: Unknown theme '#{theme_name}'. Available themes: #{available_themes.join(', ')}", :red
|
|
759
|
+
exit 1
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# Create UI2 controller with configuration
|
|
763
|
+
UI2::UIController.new(
|
|
764
|
+
working_dir: working_dir,
|
|
765
|
+
mode: agent_config.permission_mode.to_s,
|
|
766
|
+
model: agent_config.model_name,
|
|
767
|
+
theme: theme_name
|
|
768
|
+
)
|
|
769
|
+
end
|
|
749
770
|
|
|
750
771
|
# Inject UI into agent
|
|
751
772
|
agent.instance_variable_set(:@ui, ui_controller)
|
|
@@ -758,6 +779,7 @@ module Clacky
|
|
|
758
779
|
|
|
759
780
|
# Track current working thread (agent or idle compression that can be interrupted)
|
|
760
781
|
current_task_thread = nil
|
|
782
|
+
shutting_down = false
|
|
761
783
|
|
|
762
784
|
# Idle compression timer - triggers compression after 180s of inactivity
|
|
763
785
|
idle_timer = Clacky::IdleCompressionTimer.new(
|
|
@@ -816,7 +838,9 @@ module Clacky
|
|
|
816
838
|
end
|
|
817
839
|
|
|
818
840
|
# Stop UI and exit
|
|
819
|
-
|
|
841
|
+
shutting_down = true
|
|
842
|
+
idle_timer.shutdown
|
|
843
|
+
ui_controller.stop(clear_screen: true)
|
|
820
844
|
exit(0)
|
|
821
845
|
end
|
|
822
846
|
|
|
@@ -867,7 +891,9 @@ module Clacky
|
|
|
867
891
|
ui_controller.update_todos([])
|
|
868
892
|
next
|
|
869
893
|
when "/exit", "/quit"
|
|
870
|
-
|
|
894
|
+
shutting_down = true
|
|
895
|
+
idle_timer.shutdown
|
|
896
|
+
ui_controller.stop(clear_screen: true)
|
|
871
897
|
exit(0)
|
|
872
898
|
when "/help"
|
|
873
899
|
sleep 0.1
|
|
@@ -909,7 +935,7 @@ module Clacky
|
|
|
909
935
|
ensure
|
|
910
936
|
current_task_thread = nil
|
|
911
937
|
# Start idle timer after agent completes
|
|
912
|
-
idle_timer.start
|
|
938
|
+
idle_timer.start unless shutting_down
|
|
913
939
|
end
|
|
914
940
|
end
|
|
915
941
|
end
|
|
@@ -928,7 +954,8 @@ module Clacky
|
|
|
928
954
|
ui_controller.start_input_loop
|
|
929
955
|
|
|
930
956
|
# Cleanup: kill any running threads
|
|
931
|
-
|
|
957
|
+
shutting_down = true
|
|
958
|
+
idle_timer.shutdown
|
|
932
959
|
current_task_thread&.kill
|
|
933
960
|
|
|
934
961
|
# Save final session state
|
data/lib/clacky/client.rb
CHANGED
|
@@ -561,10 +561,14 @@ module Clacky
|
|
|
561
561
|
# ── Error handling ────────────────────────────────────────────────────────
|
|
562
562
|
|
|
563
563
|
def handle_test_response(response)
|
|
564
|
-
return { success: true } if response.status == 200
|
|
564
|
+
return { success: true, status: response.status } if response.status == 200
|
|
565
565
|
|
|
566
566
|
error_body = JSON.parse(response.body) rescue nil
|
|
567
|
-
{
|
|
567
|
+
{
|
|
568
|
+
success: false,
|
|
569
|
+
status: response.status,
|
|
570
|
+
error: extract_error_message(error_body, response.body)
|
|
571
|
+
}
|
|
568
572
|
end
|
|
569
573
|
|
|
570
574
|
def raise_error(response)
|
|
@@ -99,128 +99,51 @@ Ask:
|
|
|
99
99
|
|
|
100
100
|
### Feishu setup
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
Feishu now offers a one-click **Agent App** (智能体应用) that auto-configures all
|
|
103
|
+
required permissions, events, and publishing for you — no Bot capability toggle,
|
|
104
|
+
no permission JSON, no event subscription, no version/release steps. Just create
|
|
105
|
+
the app and copy the credentials. The connection mode is unchanged (long
|
|
106
|
+
connection / WebSocket), handled entirely by the server.
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
ruby "SKILL_DIR/feishu_setup.rb"
|
|
107
|
-
```
|
|
108
|
-
**Important**: call `terminal` with `timeout: 180` — the script may wait up to 90s for a WebSocket connection in Phase 4.
|
|
109
|
-
|
|
110
|
-
**If exit code is 0:**
|
|
111
|
-
- The script completed successfully.
|
|
112
|
-
- Config is already written to `~/.clacky/channels.yml`.
|
|
113
|
-
- Tell the user: "✅ Feishu channel configured automatically! The channel is ready."
|
|
114
|
-
- **Skip Step 2 (manual fallback) and continue to Step 3.**
|
|
115
|
-
|
|
116
|
-
**If exit code is non-0:**
|
|
117
|
-
- Check stdout for the error message.
|
|
118
|
-
- **If the error contains "Browser not configured" or "browser tool":**
|
|
119
|
-
- Tell the user: "The browser tool is not configured yet. Let me help you set it up first..."
|
|
120
|
-
- Invoke the `browser-setup` skill: `invoke_skill("browser-setup", "setup")`.
|
|
121
|
-
- After browser-setup completes, tell the user: "Browser is ready! Let me retry the Feishu setup..."
|
|
122
|
-
- **Retry the script** (same command, same timeout). If it succeeds this time, stop. If it fails again, check the new error and proceed accordingly.
|
|
123
|
-
- **If the error contains "No cookies found" or "Please log in":**
|
|
124
|
-
- Open Feishu login page using browser tool:
|
|
125
|
-
```
|
|
126
|
-
browser(action="navigate", url="https://open.feishu.cn/app")
|
|
127
|
-
```
|
|
128
|
-
- Tell the user: "I've opened Feishu in your browser. Please log in, then reply 'done'."
|
|
129
|
-
- Wait for "done".
|
|
130
|
-
- **Retry the script** (same command, same timeout). Repeat this login-wait-retry loop up to **3 times total**.
|
|
131
|
-
- If any attempt succeeds (exit code 0), stop — setup is complete.
|
|
132
|
-
- If an attempt fails with a **different** error (not a login error), break out of the loop and continue to Step 2.
|
|
133
|
-
- If all 3 attempts fail with login errors, tell the user: "Automated setup was unable to detect a Feishu login after 3 attempts. Switching to guided setup..." and continue to Step 2.
|
|
134
|
-
- **Otherwise (non-login, non-browser error):**
|
|
135
|
-
- Tell the user: "Automated setup encountered an issue: `<error message>`. Switching to guided setup..."
|
|
136
|
-
- Continue to Step 2 (manual flow) below.
|
|
137
|
-
|
|
138
|
-
---
|
|
139
|
-
|
|
140
|
-
#### Step 2 — Manual guided setup (fallback)
|
|
141
|
-
|
|
142
|
-
Only reach here if the automated script failed.
|
|
143
|
-
|
|
144
|
-
##### Phase 1 — Open Feishu Open Platform
|
|
145
|
-
|
|
146
|
-
1. Navigate: `open https://open.feishu.cn/app`. Pass `isolated: true`.
|
|
147
|
-
2. If a login page or QR code is shown, tell the user to log in and wait for "done".
|
|
148
|
-
3. Confirm the app list is visible.
|
|
149
|
-
|
|
150
|
-
##### Phase 2 — Create a new app
|
|
151
|
-
|
|
152
|
-
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".
|
|
153
|
-
|
|
154
|
-
##### Phase 3 — Enable Bot capability
|
|
155
|
-
|
|
156
|
-
5. Feishu opens Add App Capabilities by default after creating an app. Guide the user: "Find the Bot capability card and click the Add button next to it, then reply done." Wait for "done".
|
|
157
|
-
|
|
158
|
-
##### Phase 4 — Get credentials
|
|
159
|
-
|
|
160
|
-
6. Navigate to Credentials & Basic Info in the left menu.
|
|
161
|
-
7. Guide the user: "Copy App ID and App Secret, then paste here. Reply with: App ID: xxx, App Secret: xxx" Wait for the reply. Parse `app_id` and `app_secret`.
|
|
108
|
+
#### Step 1 — Open the Agent App creation page
|
|
162
109
|
|
|
163
|
-
|
|
110
|
+
1. Navigate: `open https://open.feishu.cn/page/launcher?from=backend_oneclick`. Pass `isolated: true`. If the browser is not configured (the `open` call fails), just give the user the URL and ask them to open it manually in any browser — the rest of the flow is fully manual and does not need browser automation.
|
|
111
|
+
2. If a login page or QR code is shown, tell the user to scan/log in and wait for "done".
|
|
164
112
|
|
|
165
|
-
|
|
166
|
-
9. Guide the user: "In the bulk import dialog, clear the existing example first (select all, delete), then paste the following JSON. Reply done." Wait for "done". Do NOT try to clear or edit via browser — user does it.
|
|
113
|
+
#### Step 2 — Create the Agent App
|
|
167
114
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"im:message",
|
|
173
|
-
"im:message.p2p_msg:readonly",
|
|
174
|
-
"im:message:send_as_bot"
|
|
175
|
-
],
|
|
176
|
-
"user": []
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
```
|
|
115
|
+
3. After login, the page lands on **创建飞书智能体应用 (Create Feishu Agent App)**.
|
|
116
|
+
Guide the user: "Enter an app name (e.g. Open Clacky), then click **立即创建 (Create Now)**. Reply done."
|
|
117
|
+
(The avatar is auto-assigned at random and can be changed anytime — it does not affect setup.)
|
|
118
|
+
Wait for "done".
|
|
180
119
|
|
|
181
|
-
|
|
120
|
+
#### Step 3 — Copy credentials
|
|
182
121
|
|
|
183
|
-
|
|
122
|
+
4. The page jumps to **创建成功 (Created Successfully)**, showing `App ID` and `App Secret`.
|
|
123
|
+
The Secret is masked by default. Guide the user: "Click the eye icon next to **App Secret** to reveal it,
|
|
124
|
+
then copy both values and paste here. Reply with: App ID: xxx, App Secret: xxx"
|
|
125
|
+
Wait for the reply. Parse `app_id` (starts with `cli_`) and `app_secret`. Trim whitespace and
|
|
126
|
+
make sure the two values are not swapped.
|
|
184
127
|
|
|
185
|
-
|
|
186
|
-
```bash
|
|
187
|
-
curl -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/feishu \
|
|
188
|
-
-H "Content-Type: application/json" \
|
|
189
|
-
-d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>","domain":"https://open.feishu.cn"}'
|
|
190
|
-
```
|
|
191
|
-
**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.**
|
|
192
|
-
11. **Wait for connection** — Poll until log shows `[feishu-ws] WebSocket connected ✅`:
|
|
193
|
-
```bash
|
|
194
|
-
for i in $(seq 1 20); do
|
|
195
|
-
grep -q "\[feishu-ws\] WebSocket connected" ~/.clacky/logger/clacky-$(date +%Y-%m-%d).log 2>/dev/null && echo "CONNECTED" && break
|
|
196
|
-
sleep 1
|
|
197
|
-
done
|
|
198
|
-
```
|
|
199
|
-
12. **Configure events** — Guide the user: "In Events & Callbacks, select 'Long Connection' mode. Click Save. Then click Add Event, search `im.message.receive_v1`, select it, click Add. Reply done." Wait for "done".
|
|
128
|
+
#### Step 4 — Save credentials
|
|
200
129
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
-d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>"}'
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
Check for `"code":0`. On success: continue to Step 3 (below).
|
|
214
|
-
|
|
215
|
-
##### Phase 9 — done
|
|
130
|
+
5. Run:
|
|
131
|
+
```bash
|
|
132
|
+
curl -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/feishu \
|
|
133
|
+
-H "Content-Type: application/json" \
|
|
134
|
+
-d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>","domain":"https://open.feishu.cn"}'
|
|
135
|
+
```
|
|
136
|
+
**CRITICAL: This curl call is the ONLY way to save credentials. NEVER write `~/.clacky/channels.yml`
|
|
137
|
+
or any file under `~/.clacky/channels/` directly. The server API handles persistence, hot-reload,
|
|
138
|
+
and establishing the long connection.**
|
|
216
139
|
|
|
217
|
-
|
|
140
|
+
On success: tell the user "✅ Feishu channel configured!" and **continue to Step 5 (Feishu CLI)**.
|
|
218
141
|
|
|
219
142
|
---
|
|
220
143
|
|
|
221
|
-
#### Step
|
|
144
|
+
#### Step 5 — Optional: install Feishu CLI
|
|
222
145
|
|
|
223
|
-
Reach here
|
|
146
|
+
Reach here after the channel is configured (Step 4 succeeded). Read `app_id` and `app_secret` from `~/.clacky/channels.yml` (under `channels.feishu`) for the install commands below.
|
|
224
147
|
|
|
225
148
|
Call `request_user_feedback`:
|
|
226
149
|
|
|
@@ -269,7 +192,7 @@ When `lark-cli auth login` returns successfully, tell the user:
|
|
|
269
192
|
|
|
270
193
|
### WeCom setup
|
|
271
194
|
|
|
272
|
-
1. Navigate: `open https://work.weixin.qq.com/wework_admin/frame#/aiHelper/create`. Pass `isolated: true`.
|
|
195
|
+
1. Navigate: `open https://work.weixin.qq.com/wework_admin/frame#/aiHelper/create`. Pass `isolated: true`. If the browser is not configured (the `open` call fails), just give the user the URL and ask them to open it manually in any browser — the rest of the flow is fully manual and does not need browser automation.
|
|
273
196
|
2. If a login page or QR code is shown, tell the user to log in and wait for "done".
|
|
274
197
|
3. Guide the user: "Scroll to the bottom of the right panel and click 'API mode creation'. Reply done." Wait for "done".
|
|
275
198
|
4. Guide the user: "Click 'Add' next to 'Visible Range'. Select the top-level company node. Click Confirm. Reply done." Wait for "done".
|