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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/lib/clacky/agent/llm_caller.rb +3 -0
  4. data/lib/clacky/agent/message_compressor_helper.rb +6 -5
  5. data/lib/clacky/agent/session_serializer.rb +4 -0
  6. data/lib/clacky/agent.rb +9 -0
  7. data/lib/clacky/agent_config.rb +111 -8
  8. data/lib/clacky/brand_config.rb +1 -0
  9. data/lib/clacky/cli.rb +49 -22
  10. data/lib/clacky/client.rb +6 -2
  11. data/lib/clacky/default_skills/channel-manager/SKILL.md +33 -110
  12. data/lib/clacky/default_skills/media-gen/SKILL.md +128 -0
  13. data/lib/clacky/idle_compression_timer.rb +38 -15
  14. data/lib/clacky/media/base.rb +68 -0
  15. data/lib/clacky/media/gemini.rb +36 -0
  16. data/lib/clacky/media/generator.rb +78 -0
  17. data/lib/clacky/media/openai_compat.rb +168 -0
  18. data/lib/clacky/providers.rb +89 -2
  19. data/lib/clacky/rich_ui_controller.rb +1549 -0
  20. data/lib/clacky/server/channel/adapters/weixin/adapter.rb +24 -2
  21. data/lib/clacky/server/channel/channel_manager.rb +89 -2
  22. data/lib/clacky/server/http_server.rb +334 -29
  23. data/lib/clacky/session_manager.rb +9 -8
  24. data/lib/clacky/telemetry.rb +26 -6
  25. data/lib/clacky/ui2/layout_manager.rb +11 -7
  26. data/lib/clacky/ui2/ui_controller.rb +2 -2
  27. data/lib/clacky/ui_interface.rb +1 -1
  28. data/lib/clacky/utils/model_pricing.rb +75 -53
  29. data/lib/clacky/version.rb +1 -1
  30. data/lib/clacky/web/app.css +393 -14
  31. data/lib/clacky/web/billing.js +1 -1
  32. data/lib/clacky/web/i18n.js +86 -4
  33. data/lib/clacky/web/index.html +23 -3
  34. data/lib/clacky/web/model-tester.js +58 -0
  35. data/lib/clacky/web/onboard.js +17 -30
  36. data/lib/clacky/web/sessions.js +443 -2
  37. data/lib/clacky/web/settings.js +372 -97
  38. data/lib/clacky/web/workspace.js +9 -1
  39. data/lib/clacky.rb +3 -0
  40. data/scripts/build/lib/network.sh +61 -30
  41. data/scripts/install.ps1 +16 -4
  42. data/scripts/install.sh +61 -30
  43. data/scripts/install_browser.sh +61 -30
  44. data/scripts/install_full.sh +61 -30
  45. data/scripts/install_rails_deps.sh +61 -30
  46. data/scripts/install_system_deps.sh +61 -30
  47. metadata +12 -3
  48. 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: '080944ed788d584c01c97ba01a27a63b2d2ab341ecf88140a63424460dcb105e'
4
- data.tar.gz: 3c2b51bde81be7c18b3384297609f0163d5e6ed40de7121185a4cc374e576f10
3
+ metadata.gz: 0b32940868f1d61791afd615ff73dbaf72dc80c111f9f9435ef939ef39ae5dec
4
+ data.tar.gz: be8efa7ee318c3f174ddbbdf1f5b2754705eb6a5d3f263aa11cbe5539b198e8f
5
5
  SHA512:
6
- metadata.gz: c21e88b443f05ae75979ca4b890142ad15283382ba39fba434a3478f9f3a7f08b13c50fd5760d128662dee26374d69695e6b35e0d25e8f92cdad7351517f564f
7
- data.tar.gz: fd37b0b3d64bc68ac777c84beef22be2c6f10c260b5e959273b953aae739c1f176bce7dae5c01c738acb332c5eded97eb973250f51576accbce58ee5f9e12139
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 thresholds
9
- COMPRESSION_THRESHOLD = 150_000 # Trigger compression when exceeding this (in tokens)
10
- MESSAGE_COUNT_THRESHOLD = 200 # Trigger compression when exceeding this (in message count)
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 >= COMPRESSION_THRESHOLD
145
- message_count_exceeded = message_count >= MESSAGE_COUNT_THRESHOLD
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
 
@@ -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 memory_update_enabled
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 lite)
900
- # Ensures only one model has each type
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] "default", "lite", or nil to remove type
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
@@ -783,6 +783,7 @@ module Clacky
783
783
 
784
784
  FileUtils.rm_f(tmp_zip)
785
785
 
786
+
786
787
  record_installed_skill(slug, version, skill_info["description"],
787
788
  encrypted: encrypted,
788
789
  description_zh: skill_info["description_zh"],
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
- # Detect terminal background BEFORE starting UI2 to avoid output interference
729
- is_dark_bg = UI2::TerminalDetector.detect_dark_background
729
+ ui_name = (options[:ui] || ENV["OPENCLACKY_UI"] || "ui2").to_s
730
730
 
731
- # Pass detected background mode to theme manager (singleton)
732
- UI2::ThemeManager.instance.set_background_mode(is_dark_bg)
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
- # Validate theme
735
- theme_name = options[:theme] || "hacker"
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
- # Create UI2 controller with configuration
743
- ui_controller = UI2::UIController.new(
744
- working_dir: working_dir,
745
- mode: agent_config.permission_mode.to_s,
746
- model: agent_config.model_name,
747
- theme: theme_name
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
- ui_controller.stop
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
- ui_controller.stop
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
- idle_timer.cancel
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
- { success: false, error: extract_error_message(error_body, response.body) }
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
- #### Step 1 Try automated setup (script)
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
- Run the setup script (full path is available in the supporting files list above):
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
- ##### Phase 5Add message permissions
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
- 8. Navigate to Permission Management and open the bulk import dialog.
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
- ```json
169
- {
170
- "scopes": {
171
- "tenant": [
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
- ##### Phase 6Configure event subscription (Long Connection)
120
+ #### Step 3Copy credentials
182
121
 
183
- **CRITICAL**: Feishu requires the long connection to be established *before* you can save the event config. The platform shows "No application connection detected" until `clacky server` is running and connected.
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
- 10. **Apply config and establish connection** — Run:
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
- ##### Phase 7 — Publish the app
202
-
203
- 13. Navigate to Version Management & Release. Guide the user: "Create a new version (e.g. 1.0.0, note: Initial release for Open Clacky) and publish it. Reply done." Wait for "done".
204
-
205
- ##### Phase 8 — Validate
206
-
207
- ```bash
208
- curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" \
209
- -H "Content-Type: application/json" \
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
- Step 2 ends here. **Continue to Step 3.**
140
+ On success: tell the user "✅ Feishu channel configured!" and **continue to Step 5 (Feishu CLI)**.
218
141
 
219
142
  ---
220
143
 
221
- #### Step 3 — Optional: install Feishu CLI
144
+ #### Step 5 — Optional: install Feishu CLI
222
145
 
223
- Reach here from either Step 1 success or end of Step 2. Read `app_id` and `app_secret` from `~/.clacky/channels.yml` (under `channels.feishu`) for the install commands below.
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".