openclacky 1.5.19 → 1.5.21

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +95 -0
  3. data/README.md +19 -1
  4. data/README_CN.md +1 -1
  5. data/README_JA.md +1 -1
  6. data/lib/clacky/agent/chunk_index.rb +8 -1
  7. data/lib/clacky/agent/feedback.rb +30 -0
  8. data/lib/clacky/agent/history_navigation.rb +17 -3
  9. data/lib/clacky/agent/hook_manager.rb +6 -1
  10. data/lib/clacky/agent/llm_caller.rb +17 -3
  11. data/lib/clacky/agent/message_compressor_helper.rb +36 -2
  12. data/lib/clacky/agent/session_serializer.rb +45 -4
  13. data/lib/clacky/agent.rb +135 -55
  14. data/lib/clacky/agent_config.rb +82 -4
  15. data/lib/clacky/billing/platform_billing.rb +10 -5
  16. data/lib/clacky/brand_config.rb +1 -1
  17. data/lib/clacky/cli.rb +5 -4
  18. data/lib/clacky/client.rb +12 -5
  19. data/lib/clacky/deepseek_web_tools.rb +156 -0
  20. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
  21. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +97 -20
  22. data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -0
  23. data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
  24. data/lib/clacky/extension/loader.rb +5 -3
  25. data/lib/clacky/locales/en.rb +5 -5
  26. data/lib/clacky/locales/zh.rb +5 -5
  27. data/lib/clacky/message_format/anthropic.rb +20 -0
  28. data/lib/clacky/message_format/open_ai.rb +31 -14
  29. data/lib/clacky/message_history.rb +45 -4
  30. data/lib/clacky/platform_http_client.rb +6 -2
  31. data/lib/clacky/providers.rb +38 -138
  32. data/lib/clacky/search_config.rb +47 -13
  33. data/lib/clacky/server/browser_manager.rb +35 -5
  34. data/lib/clacky/server/channel/channel_manager.rb +6 -6
  35. data/lib/clacky/server/discover.rb +5 -4
  36. data/lib/clacky/server/http_server.rb +285 -53
  37. data/lib/clacky/server/model_prices.rb +10 -5
  38. data/lib/clacky/server/pid_files.rb +25 -0
  39. data/lib/clacky/server/server_master.rb +9 -3
  40. data/lib/clacky/server/session_registry.rb +37 -7
  41. data/lib/clacky/server/web_ui_controller.rb +72 -0
  42. data/lib/clacky/session_manager.rb +28 -6
  43. data/lib/clacky/skill.rb +1 -1
  44. data/lib/clacky/tools/ask_user.rb +4 -1
  45. data/lib/clacky/tools/base.rb +3 -2
  46. data/lib/clacky/tools/browser.rb +18 -2
  47. data/lib/clacky/tools/file_reader.rb +1 -1
  48. data/lib/clacky/tools/terminal/session_manager.rb +19 -1
  49. data/lib/clacky/tools/terminal.rb +65 -23
  50. data/lib/clacky/tools/trash_manager.rb +5 -3
  51. data/lib/clacky/tools/web_fetch.rb +81 -22
  52. data/lib/clacky/tools/web_search.rb +45 -4
  53. data/lib/clacky/ui2/ui_controller.rb +1 -1
  54. data/lib/clacky/ui_interface.rb +4 -3
  55. data/lib/clacky/utils/file_processor.rb +51 -0
  56. data/lib/clacky/utils/model_pricing.rb +242 -39
  57. data/lib/clacky/version.rb +1 -1
  58. data/lib/clacky/web/app.css +1158 -249
  59. data/lib/clacky/web/app.js +82 -13
  60. data/lib/clacky/web/components/chat-navigator.js +8 -1
  61. data/lib/clacky/web/components/code-editor.js +6 -2
  62. data/lib/clacky/web/components/mentions.js +2 -13
  63. data/lib/clacky/web/components/model-picker.js +92 -97
  64. data/lib/clacky/web/components/onboard.js +13 -10
  65. data/lib/clacky/web/components/sidebar.js +6 -2
  66. data/lib/clacky/web/features/archive/store.js +196 -0
  67. data/lib/clacky/web/features/archive/view.js +409 -0
  68. data/lib/clacky/web/features/backup/view.js +17 -7
  69. data/lib/clacky/web/features/billing/view.js +84 -10
  70. data/lib/clacky/web/features/channels/view.js +75 -29
  71. data/lib/clacky/web/features/new-session/store.js +3 -3
  72. data/lib/clacky/web/features/new-session/view.js +1 -0
  73. data/lib/clacky/web/features/trash/store.js +14 -11
  74. data/lib/clacky/web/features/trash/view.js +66 -13
  75. data/lib/clacky/web/features/workspace/view.js +421 -16
  76. data/lib/clacky/web/i18n.js +242 -70
  77. data/lib/clacky/web/index.html +270 -117
  78. data/lib/clacky/web/projects.js +56 -1
  79. data/lib/clacky/web/sessions.js +609 -267
  80. data/lib/clacky/web/settings.js +186 -32
  81. data/lib/clacky/web/ws-dispatcher.js +33 -21
  82. data/lib/clacky.rb +4 -2
  83. metadata +6 -1
@@ -11,11 +11,11 @@ module Clacky
11
11
  class MessageHistory
12
12
  # Fields that are internal to the agent and must not be sent to the API.
13
13
  INTERNAL_FIELDS = %i[
14
- task_id created_at system_injected session_context memory_update
14
+ message_id task_id created_at system_injected session_context memory_update
15
15
  subagent_instructions subagent_result subagent_transcript token_usage
16
16
  compressed_summary chunk_path truncated transient
17
17
  chunk_index chunk_count ext_events skill_command skill_command_display
18
- display_files display_references
18
+ display_files display_references feedback_requests
19
19
  ].freeze
20
20
 
21
21
  INTERNAL_CONTENT_BLOCK_FIELDS = %i[image_path image_name].freeze
@@ -37,7 +37,8 @@ module Clacky
37
37
  # Write operations
38
38
  # ─────────────────────────────────────────────
39
39
 
40
- # Append a single message hash to the history.
40
+ # Append a single message hash to the history. An optional block receives
41
+ # the stored, sanitized message so callers can retain its identity.
41
42
  #
42
43
  # When appending a user message, automatically drop any trailing assistant
43
44
  # message that has unanswered tool_calls (no tool_result follows it).
@@ -48,7 +49,9 @@ module Clacky
48
49
  if message[:role] == "user"
49
50
  drop_dangling_tool_calls!
50
51
  end
51
- @messages << deep_sanitize_utf8(message)
52
+ stored_message = deep_sanitize_utf8(message)
53
+ @messages << stored_message
54
+ yield stored_message if block_given?
52
55
  self
53
56
  end
54
57
 
@@ -65,6 +68,18 @@ module Clacky
65
68
  self
66
69
  end
67
70
 
71
+ # Replace one real user turn after its attachments have been resolved.
72
+ # message_id is the stable identity; created_at remains presentation data.
73
+ def replace_user_message(message_id:, message:)
74
+ idx = @messages.index do |m|
75
+ m[:role] == "user" && !m[:system_injected] && m[:message_id] == message_id
76
+ end
77
+ return false unless idx
78
+
79
+ @messages[idx] = deep_sanitize_utf8(message)
80
+ true
81
+ end
82
+
68
83
  # Replace the entire message list (used by compression rebuild).
69
84
  def replace_all(new_messages)
70
85
  @messages = new_messages.map { |m| deep_sanitize_utf8(m) }
@@ -99,6 +114,16 @@ module Clacky
99
114
  self
100
115
  end
101
116
 
117
+ # Save the effective request before waiting, including while no tool result
118
+ # exists yet. The original model-authored call remains unchanged.
119
+ def attach_feedback_request(message:, tool_call_id:, request:)
120
+ return false unless message
121
+
122
+ message[:feedback_requests] ||= {}
123
+ message[:feedback_requests][tool_call_id.to_s] = deep_sanitize_utf8(request)
124
+ true
125
+ end
126
+
102
127
  # Settle an interrupted fan-out whose assistant.tool_calls turn is dangling
103
128
  # because the batch was cancelled before any tool result was written. Mirrors
104
129
  # repair_tool_call_pairing (same helpers, same scan) but PERSISTS the result:
@@ -179,6 +204,17 @@ module Clacky
179
204
  truncate_from(idx)
180
205
  end
181
206
 
207
+ # Truncate history starting from the message with the given stable ID.
208
+ # Used to roll back an attachment turn that failed before resolution.
209
+ def truncate_from_message(message_id)
210
+ idx = @messages.index do |m|
211
+ m[:role] == "user" && !m[:system_injected] && m[:message_id] == message_id
212
+ end
213
+ return self unless idx
214
+
215
+ truncate_from(idx)
216
+ end
217
+
182
218
  # Roll back the history to just before the given message object.
183
219
  # Removes the message and anything appended after it.
184
220
  # Used to undo a failed speculative append (e.g. compression message that errored).
@@ -292,6 +328,11 @@ module Clacky
292
328
  @messages
293
329
  end
294
330
  msgs = source.map { |m| strip_for_api(m) }
331
+ # A tool message with a blank id answers no announced call, and one of
332
+ # them makes every later request fail (422 "invalid type: null"). Drop
333
+ # these before pairing: a blank id is not recognized as a tool result,
334
+ # so pairing would otherwise insert a placeholder and orphan the real one.
335
+ msgs.reject! { |m| m[:role] == "tool" && m[:tool_call_id].to_s.strip.empty? }
295
336
  msgs = repair_tool_call_pairing(msgs)
296
337
  ensure_reasoning_content_consistency(msgs, force: force_reasoning_content_pad)
297
338
  end
@@ -50,9 +50,13 @@ module Clacky
50
50
  # Auto-detects the platform endpoint:
51
51
  # - When CLACKY_LICENSE_SERVER is set → that URL (dev override)
52
52
  # - Otherwise → PRIMARY_HOST
53
+ def self.base_url
54
+ override = ENV["CLACKY_LICENSE_SERVER"]
55
+ override && !override.empty? ? override.chomp("/") : PRIMARY_HOST
56
+ end
57
+
53
58
  def initialize
54
- override = ENV["CLACKY_LICENSE_SERVER"]
55
- @base_url = override && !override.empty? ? override : PRIMARY_HOST
59
+ @base_url = self.class.base_url
56
60
  end
57
61
 
58
62
  # Send a POST request with a JSON body and return a normalised result hash.
@@ -40,7 +40,6 @@ module Clacky
40
40
  "dsk-deepseek-v4-pro",
41
41
  "oc-glm-5.3",
42
42
  "oc-kimi-k3",
43
- "oc-minimax-m2.7",
44
43
  "abs-claude-fable-5-1",
45
44
  "abs-claude-fable-5",
46
45
  "abs-claude-opus-5-5",
@@ -50,17 +49,18 @@ module Clacky
50
49
  "abs-claude-sonnet-5-5",
51
50
  "abs-claude-sonnet-5",
52
51
  "abs-claude-sonnet-4-6",
52
+ "abs-claude-haiku-5-5",
53
53
  "abs-claude-haiku-4-5",
54
- "abs-gpt-6-astra",
55
54
  "abs-gpt-6.1-sol",
55
+ "abs-gpt-6-astra",
56
56
  "abs-gpt-6-sol",
57
57
  "abs-gpt-6-luna",
58
58
  "abs-gpt-5.6-sol",
59
59
  "abs-gpt-5.6-terra",
60
60
  "abs-gpt-5.6-luna",
61
+ "or-gemini-4-argon",
61
62
  "or-gemini-3-1-pro",
62
- "or-gemini-3-8-flash",
63
- "or-gemini-3-7-flash"
63
+ "or-gemini-3-8-flash"
64
64
  ],
65
65
  # Image generation models served by the openclacky platform
66
66
  # gateway. The gateway exposes a standard OpenAI-compatible
@@ -112,12 +112,10 @@ module Clacky
112
112
  # audio parts). The gateway returns transcription text.
113
113
  "stt_models" => [
114
114
  "or-stt-gemini-3-8-flash",
115
- "or-stt-gemini-3-7-flash",
116
115
  "or-stt-gemini-1-5-pro"
117
116
  ],
118
117
  "stt_model_aliases" => {
119
118
  "or-stt-gemini-3-8-flash" => "Gemini 3.8 Flash STT",
120
- "or-stt-gemini-3-7-flash" => "Gemini 3.7 Flash STT",
121
119
  "or-stt-gemini-1-5-pro" => "Gemini 1.5 Pro STT"
122
120
  },
123
121
  "default_stt_model" => "or-stt-gemini-3-8-flash",
@@ -126,12 +124,10 @@ module Clacky
126
124
  # parts). The gateway returns analysis text.
127
125
  "video_understanding_models" => [
128
126
  "or-gemini-3-8-flash",
129
- "or-gemini-3-7-flash",
130
127
  "or-gemini-3-1-pro"
131
128
  ],
132
129
  "video_understanding_model_aliases" => {
133
130
  "or-gemini-3-8-flash" => "Gemini 3.8 Flash",
134
- "or-gemini-3-7-flash" => "Gemini 3.7 Flash",
135
131
  "or-gemini-3-1-pro" => "Gemini 3.1 Pro"
136
132
  },
137
133
  "default_video_understanding_model" => "or-gemini-3-8-flash",
@@ -148,15 +144,13 @@ module Clacky
148
144
  # (it accepts image/audio/video input natively via OpenRouter).
149
145
  # oc-glm-5.3 is likewise text-only: its upstream rejects any content
150
146
  # block other than `type: "text"`, so images must never be sent
151
- # inline. oc-minimax-m2.7 is also text-only, but its upstream accepts
152
- # the image block and silently ignores it — a worse failure mode,
153
- # since the model then answers as if no image had been attached.
154
- # oc-kimi-k3 is natively multimodal and keeps vision=true.
147
+ # inline. oc-kimi-k3 and oc-grok-4.7 are natively multimodal and keep
148
+ # vision=true; oc-gpt-6.1-sol, oc-gpt-6-astra, oc-gpt-6-sol and
149
+ # oc-gpt-6-luna do too.
155
150
  "model_capabilities" => {
156
151
  "dsk-deepseek-flash" => { "vision" => true }.freeze,
157
152
  "dsk-deepseek-v4-pro" => { "vision" => false }.freeze,
158
- "oc-glm-5.3" => { "vision" => false }.freeze,
159
- "oc-minimax-m2.7" => { "vision" => false }.freeze
153
+ "oc-glm-5.3" => { "vision" => false }.freeze
160
154
  }.freeze,
161
155
  # Bedrock GPT models (abs-gpt-*) are served through the OpenAI
162
156
  # Responses API — their Chat Completions endpoint rejects function
@@ -173,15 +167,15 @@ module Clacky
173
167
  # no injection happens when the default model is already lite-class.
174
168
  "lite_models" => {
175
169
  "auto" => "dsk-deepseek-flash",
176
- "abs-claude-fable-5-1" => "abs-claude-haiku-4-5",
177
- "abs-claude-fable-5" => "abs-claude-haiku-4-5",
178
- "abs-claude-opus-5-5" => "abs-claude-haiku-4-5",
179
- "abs-claude-opus-5" => "abs-claude-haiku-4-5",
180
- "abs-claude-opus-4-8" => "abs-claude-haiku-4-5",
181
- "abs-claude-opus-4-7" => "abs-claude-haiku-4-5",
182
- "abs-claude-sonnet-5-5" => "abs-claude-haiku-4-5",
183
- "abs-claude-sonnet-5" => "abs-claude-haiku-4-5",
184
- "abs-claude-sonnet-4-6" => "abs-claude-haiku-4-5",
170
+ "abs-claude-fable-5-1" => "abs-claude-haiku-5-5",
171
+ "abs-claude-fable-5" => "abs-claude-haiku-5-5",
172
+ "abs-claude-opus-5-5" => "abs-claude-haiku-5-5",
173
+ "abs-claude-opus-5" => "abs-claude-haiku-5-5",
174
+ "abs-claude-opus-4-8" => "abs-claude-haiku-5-5",
175
+ "abs-claude-opus-4-7" => "abs-claude-haiku-5-5",
176
+ "abs-claude-sonnet-5-5" => "abs-claude-haiku-5-5",
177
+ "abs-claude-sonnet-5" => "abs-claude-haiku-5-5",
178
+ "abs-claude-sonnet-4-6" => "abs-claude-haiku-5-5",
185
179
  "abs-gpt-6-astra" => "abs-gpt-6-luna",
186
180
  "abs-gpt-6-sol" => "abs-gpt-6-luna",
187
181
  "abs-gpt-5.6-sol" => "abs-gpt-5.6-luna",
@@ -205,7 +199,8 @@ module Clacky
205
199
  "endpoint_variants" => [
206
200
  { "label" => "Primary (Global)", "label_key" => "settings.models.baseurl.variant.openclacky_primary", "base_url" => "https://api.openclacky.com" }.freeze
207
201
  ].freeze,
208
- "website_url" => "https://www.openclacky.com/ai-keys"
202
+ "deepseek_web_tools" => true,
203
+ "website_url" => "https://www.openclacky.com/llm_keys"
209
204
  }.freeze,
210
205
 
211
206
 
@@ -235,6 +230,7 @@ module Clacky
235
230
  "model_capabilities" => {
236
231
  "deepseek-flash" => { "vision" => true }.freeze
237
232
  }.freeze,
233
+ "deepseek_web_tools" => true,
238
234
  "website_url" => "https://platform.deepseek.com/api_keys"
239
235
  }.freeze,
240
236
 
@@ -340,8 +336,8 @@ module Clacky
340
336
  "name" => "Minimax",
341
337
  "base_url" => "https://api.minimaxi.com/v1",
342
338
  "api" => "openai-completions",
343
- "default_model" => "MiniMax-M3",
344
- "models" => ["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"],
339
+ "default_model" => "MiniMax-M3.1-Flash",
340
+ "models" => ["MiniMax-M3.1-Flash", "MiniMax-M3"],
345
341
  # MiniMax operates two regional endpoints with identical APIs & model
346
342
  # lineup — mainland China (.com) and international (.io). Listing both
347
343
  # lets find_by_base_url identify either one as provider "minimax",
@@ -351,16 +347,15 @@ module Clacky
351
347
  { "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://api.minimaxi.com/v1", "region" => "cn" }.freeze,
352
348
  { "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.minimax.io/v1", "region" => "intl" }.freeze
353
349
  ].freeze,
354
- # MiniMax M2.5/M2.7 are text-only on this endpoint. M3 (released 2026-06-01)
355
- # is natively multimodal — it accepts image input via OpenAI-style
356
- # image_url content parts — so it overrides the provider-level
357
- # vision=false below. M3 exposes a 1,000,000-token context window
358
- # (M2.7: 204,800; M2.5: 40,960).
350
+ # M3.1-Flash and M3 are natively multimodal (image_url content parts,
351
+ # 1M context) and override the provider-level vision=false, which
352
+ # stays as the safe default for retired text-only ids (M2.x).
359
353
  "capabilities" => { "vision" => false }.freeze,
360
354
  "model_capabilities" => {
361
- "MiniMax-M3" => { "vision" => true }.freeze
355
+ "MiniMax-M3.1-Flash" => { "vision" => true }.freeze,
356
+ "MiniMax-M3" => { "vision" => true }.freeze
362
357
  }.freeze,
363
- "default_ocr_model" => "MiniMax-M3",
358
+ "default_ocr_model" => "MiniMax-M3.1-Flash",
364
359
  "website_url" => "https://platform.minimax.io/"
365
360
  }.freeze,
366
361
 
@@ -648,7 +643,6 @@ module Clacky
648
643
  "glm-5.2",
649
644
  "kimi-k3",
650
645
  "glm-5.1",
651
- "minimax-m2.7",
652
646
  "minimax-m3",
653
647
  "kimi-k2.7-code",
654
648
  "kimi-k2.6",
@@ -670,114 +664,12 @@ module Clacky
670
664
  "lite_models" => {
671
665
  "glm-5.2" => "glm-5.1",
672
666
  "kimi-k3" => "kimi-k2.6",
673
- "minimax-m3" => "minimax-m2.7",
674
667
  "kimi-k2.7-code" => "kimi-k2.6",
675
668
  "deepseek-v4-pro" => "deepseek-v4-flash",
676
669
  "gemini-3-flash-preview" => "deepseek-v4-flash"
677
670
  },
678
671
  "default_ocr_model" => "kimi-k2.7-code",
679
672
  "website_url" => "https://ollama.com/settings/keys"
680
- }.freeze,
681
-
682
- "orcarouter" => {
683
- "name" => "OrcaRouter",
684
- "base_url" => "https://api.orcarouter.ai/v1",
685
- "api" => "openai-completions",
686
- "default_model" => "openai/gpt-5.5",
687
- # Curated default lineup. OrcaRouter exposes 190+ models from OpenAI,
688
- # Anthropic, Google, DeepSeek, Qwen, MiniMax, Zhipu and others behind a
689
- # single OpenAI-compatible endpoint. Shipping a small list of the
690
- # mainstream Claude + GPT entries gives users a working dropdown out
691
- # of the box; users can still type any other OrcaRouter model id manually.
692
- "models" => [
693
- "anthropic/claude-sonnet-5",
694
- "anthropic/claude-opus-4.8",
695
- "anthropic/claude-haiku-4.5",
696
- "openai/gpt-5.5",
697
- "openai/gpt-5.4",
698
- "openai/gpt-5.4-mini",
699
- "google/gemini-3.5-flash",
700
- "deepseek/deepseek-v4-flash",
701
- "z-ai/glm-5.2"
702
- ],
703
- # Per-primary lite pairing — Claude family pairs with Haiku, GPT
704
- # family pairs with the mini variant. Mirrors the openrouter preset
705
- # so subagents on OrcaRouter get a sensible cheap/fast sidekick.
706
- "lite_models" => {
707
- "anthropic/claude-sonnet-5" => "anthropic/claude-haiku-4.5",
708
- "anthropic/claude-opus-4.8" => "anthropic/claude-haiku-4.5",
709
- "openai/gpt-5.5" => "openai/gpt-5.4-mini",
710
- "openai/gpt-5.4" => "openai/gpt-5.4-mini"
711
- },
712
- # Per-model API type overrides. OrcaRouter proxies Claude through a
713
- # native Anthropic /v1/messages endpoint (https://api.orcarouter.ai/v1/messages)
714
- # in addition to its OpenAI-compatible /chat/completions endpoint.
715
- # Routing "anthropic/*" via the native endpoint preserves cache_control
716
- # fidelity, matching what Claude Code CLI does internally (same rationale
717
- # as the openrouter preset). Non-Claude models keep the OpenAI shim.
718
- "model_api_overrides" => {
719
- /\Aanthropic\// => "anthropic-messages"
720
- }.freeze,
721
- # Most models on OrcaRouter are vision-capable; DeepSeek and GLM-5.2
722
- # are text-only.
723
- "capabilities" => { "vision" => true }.freeze,
724
- "model_capabilities" => {
725
- "deepseek/deepseek-v4-flash" => { "vision" => false }.freeze,
726
- "z-ai/glm-5.2" => { "vision" => false }.freeze
727
- }.freeze,
728
- "default_ocr_model" => "google/gemini-3.5-flash",
729
- "website_url" => "https://www.orcarouter.ai"
730
- }.freeze,
731
-
732
- "requesty" => {
733
- "name" => "Requesty",
734
- "base_url" => "https://router.requesty.ai/v1",
735
- "api" => "openai-completions",
736
- "default_model" => "claude-sonnet-4-6",
737
- # Curated default lineup of Requesty managed policies (GET /v1/models/managed).
738
- # A managed policy is a short model id that Requesty routes across several
739
- # upstream providers. Users can still type any other id manually, including
740
- # the full "vendor/model" catalogue ids (e.g. "openai/gpt-4o-mini").
741
- "models" => [
742
- "claude-sonnet-4-6",
743
- "claude-opus-4-8",
744
- "claude-opus-4-7",
745
- "claude-opus-4-6",
746
- "claude-haiku-4-5",
747
- "gpt-5.6-sol",
748
- "gpt-5.6-terra",
749
- "gpt-5.6-luna",
750
- "gpt-5.5",
751
- "gpt-5.4",
752
- "gpt-5.4-mini",
753
- "gemini-3.8-flash"
754
- ],
755
- # Per-primary lite pairing, same shape as the openrouter preset.
756
- "lite_models" => {
757
- "claude-sonnet-4-6" => "claude-haiku-4-5",
758
- "claude-opus-4-8" => "claude-haiku-4-5",
759
- "claude-opus-4-7" => "claude-haiku-4-5",
760
- "claude-opus-4-6" => "claude-haiku-4-5",
761
- "gpt-5.6-sol" => "gpt-5.6-luna",
762
- "gpt-5.6-terra" => "gpt-5.6-luna",
763
- "gpt-5.5" => "gpt-5.4-mini",
764
- "gpt-5.4" => "gpt-5.4-mini"
765
- },
766
- # Requesty also serves the native Anthropic /v1/messages endpoint, so
767
- # Claude models route there to preserve cache_control fidelity (same
768
- # rationale as the openrouter preset). Other models keep the OpenAI shim.
769
- "model_api_overrides" => {
770
- /\Aanthropic\// => "anthropic-messages",
771
- /\Aclaude[-.]/ => "anthropic-messages"
772
- }.freeze,
773
- # Same API key works on every regional endpoint; the EU one keeps
774
- # traffic inside the EU (Frankfurt).
775
- "endpoint_variants" => [
776
- { "label" => "Global", "base_url" => "https://router.requesty.ai/v1" }.freeze,
777
- { "label" => "EU", "base_url" => "https://router.eu.requesty.ai/v1", "region" => "eu" }.freeze
778
- ].freeze,
779
- "default_ocr_model" => "gemini-3.8-flash",
780
- "website_url" => "https://app.requesty.ai/api-keys"
781
673
  }.freeze
782
674
 
783
675
  }.freeze
@@ -797,8 +689,6 @@ module Clacky
797
689
  MIMO_ID = "mimo"
798
690
  VOLCENGINE_ARK_ID = "volcengine-ark"
799
691
  OLLAMA_ID = "ollama"
800
- ORCAROUTER_ID = "orcarouter"
801
- REQUESTY_ID = "requesty"
802
692
 
803
693
  MEDIA_KINDS = %w[image video audio stt video_understanding].freeze
804
694
 
@@ -1282,6 +1172,16 @@ module Clacky
1282
1172
  return true unless caps.key?(key)
1283
1173
  caps[key] != false
1284
1174
  end
1175
+
1176
+ # True when the preset declares native DeepSeek web tools (search + fetch).
1177
+ # Currently DeepSeek direct ("deepseekv4") and the OpenClacky platform
1178
+ # gateway ("openclacky").
1179
+ def deepseek_web_tools?(provider_id)
1180
+ preset = PRESETS[provider_id]
1181
+ return false unless preset
1182
+
1183
+ preset["deepseek_web_tools"] == true
1184
+ end
1285
1185
  end
1286
1186
  end
1287
1187
  end
@@ -2,19 +2,25 @@
2
2
 
3
3
  require "yaml"
4
4
  require "fileutils"
5
+ require_relative "deepseek_web_tools"
5
6
 
6
7
  module Clacky
7
8
  # Search provider selection, backed by ~/.clacky/search.yml.
8
9
  #
9
- # The built-in Parallel/DuckDuckGo/Bing providers need no configuration and
10
- # stay the default. Pointing `provider` at a script in ~/.clacky/searchers/ routes
11
- # web_search through an API-backed engine instead, which is what users who
12
- # want a provider-specific API or policy controls reach for.
10
+ # The built-in Parallel/DuckDuckGo/Bing chain is the default: it needs no key
11
+ # and no configuration, so a fresh install searches the web out of the box.
12
+ # DeepSeek's hosted web tools are opt-in — they render JS-heavy pages to
13
+ # Markdown but bill per call, so they only run once the user picks them.
13
14
  #
14
15
  # search.yml schema:
15
- # provider: tavily — basename of a script in ~/.clacky/searchers/;
16
- # blank or missing means built-in engines only
17
- # key: tvly-xxx — credential handed to that script via env
16
+ # provider: tavily — "deepseek_web" for DeepSeek's hosted
17
+ # search/fetch, blank for built-in engines only,
18
+ # otherwise the basename of a script in ~/.clacky/searchers/
19
+ # key: tvly-xxx — credential for the selected provider (optional for
20
+ # deepseek_web, which can reuse a configured model key)
21
+ #
22
+ # A missing file, or one without a `provider` key, means "never chosen" and
23
+ # resolves to the free chain.
18
24
  #
19
25
  # The directory is the registry: dropping my_engine.rb into
20
26
  # ~/.clacky/searchers/ and setting `provider: my_engine` is all it takes,
@@ -22,21 +28,49 @@ module Clacky
22
28
  module SearchConfig
23
29
  CONFIG_PATH = File.expand_path("~/.clacky/search.yml").freeze
24
30
 
31
+ # Hosted DeepSeek search + page fetch, billed per successful call upstream.
32
+ # It behaves like a provider but ships with the gem, so it has no script.
33
+ DEEPSEEK_WEB = "deepseek_web"
34
+
25
35
  class << self
26
36
  # @return [Hash] { "provider" => String, "key" => String }
27
37
  def load
28
38
  raw = File.exist?(CONFIG_PATH) ? YAML.load_file(CONFIG_PATH) : nil
29
- return { "provider" => "", "key" => "" } unless raw.is_a?(Hash)
39
+ return default_config unless raw.is_a?(Hash)
30
40
 
31
- { "provider" => raw["provider"].to_s.strip, "key" => raw["key"].to_s.strip }
41
+ {
42
+ "provider" => raw["provider"].to_s.strip,
43
+ "key" => raw["key"].to_s.strip
44
+ }
32
45
  rescue StandardError
33
- { "provider" => "", "key" => "" }
46
+ default_config
34
47
  end
35
48
 
36
- def save(provider:, key:)
49
+ def save(provider: nil, key: nil)
50
+ current = load
51
+ new_provider = provider.nil? ? current["provider"] : provider.to_s.strip
52
+ new_key = key.nil? ? current["key"] : key.to_s.strip
53
+
54
+ payload = {
55
+ "provider" => new_provider,
56
+ "key" => new_key
57
+ }
58
+
37
59
  FileUtils.mkdir_p(File.dirname(CONFIG_PATH))
38
- File.write(CONFIG_PATH, { "provider" => provider.to_s.strip, "key" => key.to_s.strip }.to_yaml)
60
+ File.write(CONFIG_PATH, payload.to_yaml)
39
61
  File.chmod(0o600, CONFIG_PATH)
62
+
63
+ # A new provider or key invalidates an earlier failure, so the next
64
+ # search retries DeepSeek instead of sitting out the cooldown.
65
+ Clacky::DeepSeekWebTools.reset_cooldown!
66
+ end
67
+
68
+ def deepseek_web?
69
+ load["provider"] == DEEPSEEK_WEB
70
+ end
71
+
72
+ private def default_config
73
+ { "provider" => "", "key" => "" }
40
74
  end
41
75
 
42
76
  # Absolute path of the configured searcher, or nil when unset/missing.
@@ -44,7 +78,7 @@ module Clacky
44
78
  # web_search silently falls back instead of erroring on every search.
45
79
  def script_path
46
80
  name = load["provider"]
47
- return nil if name.empty?
81
+ return nil if name.empty? || name == DEEPSEEK_WEB
48
82
 
49
83
  Clacky::Utils::SearcherManager.path_for(name)
50
84
  end
@@ -19,6 +19,7 @@ module Clacky
19
19
  # enabled: true/false — whether the browser tool is active
20
20
  # chrome_version: "146" — detected Chrome version (set by browser-setup skill)
21
21
  # configured_at: date — when setup was last run
22
+ # screenshot_max_base64_bytes: Integer — screenshot size ceiling (settings UI)
22
23
  #
23
24
  # Liveness check strategy:
24
25
  # process_alive? sends an MCP `ping` (standard in MCP spec 2024-11-05) and
@@ -116,27 +117,49 @@ module Clacky
116
117
  enabled = cfg["enabled"] == true
117
118
  running = @process && @process[:wait_thr]&.alive?
118
119
  {
119
- enabled: enabled,
120
- daemon_running: !!running,
121
- chrome_version: cfg["chrome_version"]
120
+ enabled: enabled,
121
+ daemon_running: !!running,
122
+ chrome_version: cfg["chrome_version"],
123
+ screenshot_max_base64_bytes: effective_screenshot_limit(cfg)
122
124
  }
123
125
  end
124
126
 
125
127
  # Write browser.yml with the given config and reload the daemon.
126
128
  # Called by HttpServer POST /api/browser/configure.
129
+ # Existing keys are preserved so re-running browser-setup does not wipe
130
+ # user preferences such as the screenshot limits.
127
131
  # @param chrome_version [String] detected Chrome major version
128
132
  def configure(chrome_version:)
129
- cfg = {
133
+ cfg = load_config.merge(
130
134
  "enabled" => true,
131
135
  "browser" => "chrome",
132
136
  "chrome_version" => chrome_version.to_s,
133
137
  "configured_at" => Date.today.to_s
134
- }
138
+ )
135
139
  FileUtils.mkdir_p(File.dirname(BROWSER_CONFIG_PATH))
136
140
  File.write(BROWSER_CONFIG_PATH, cfg.to_yaml)
137
141
  reload
138
142
  end
139
143
 
144
+ # Persist the screenshot preferences written by the settings UI.
145
+ # No daemon reload: the browser tool re-reads browser.yml on every screenshot.
146
+ # @param max_base64_bytes [Integer, nil] screenshot size ceiling in bytes
147
+ # @return [Hash] the effective settings after the update
148
+ def update_screenshot_settings(max_base64_bytes: nil)
149
+ raise "Browser not configured. Run /browser-setup first." unless File.exist?(BROWSER_CONFIG_PATH)
150
+
151
+ if !max_base64_bytes.nil? && !(max_base64_bytes.is_a?(Integer) && max_base64_bytes.positive?)
152
+ raise ArgumentError, "screenshot_max_base64_bytes must be a positive integer"
153
+ end
154
+
155
+ cfg = load_config
156
+ cfg["screenshot_max_base64_bytes"] = max_base64_bytes unless max_base64_bytes.nil?
157
+
158
+ File.write(BROWSER_CONFIG_PATH, cfg.to_yaml)
159
+ @config = cfg
160
+ { screenshot_max_base64_bytes: effective_screenshot_limit(cfg) }
161
+ end
162
+
140
163
  # Toggle the browser tool on/off by flipping `enabled` in browser.yml.
141
164
  # Raises if browser.yml doesn't exist (not yet set up).
142
165
  # @return [Boolean] new enabled state
@@ -224,6 +247,13 @@ module Clacky
224
247
  {}
225
248
  end
226
249
 
250
+ private def effective_screenshot_limit(cfg)
251
+ limit = cfg["screenshot_max_base64_bytes"]
252
+ return limit if limit.is_a?(Integer) && limit.positive?
253
+
254
+ Clacky::Tools::Browser::SCREENSHOT_MAX_BASE64_BYTES
255
+ end
256
+
227
257
  # Must be called inside @mutex
228
258
  def ensure_process!
229
259
  return if process_alive?
@@ -152,7 +152,7 @@ module Clacky
152
152
  # receive_id for outbound messages), so we extract the chat portion.
153
153
  prefix = "#{platform}:"
154
154
  ids = []
155
- @registry.list.each do |summary|
155
+ @registry.list(archived: nil).each do |summary|
156
156
  @registry.with_session(summary[:id]) do |s|
157
157
  (s[:channel_keys] || []).each do |key|
158
158
  next unless key.start_with?(prefix)
@@ -463,7 +463,7 @@ module Clacky
463
463
  when "/unbind"
464
464
  unbound = false
465
465
  cleared_agents = []
466
- @registry.list.each do |summary|
466
+ @registry.list(archived: nil).each do |summary|
467
467
  @registry.with_session(summary[:id]) do |s|
468
468
  # Set#delete always returns self — use delete? so an unknown key
469
469
  # reports "No binding found." instead of a false "Unbound.".
@@ -726,7 +726,7 @@ module Clacky
726
726
  # /bind and /unbind keep agent.channel_info strictly in sync with channel_keys
727
727
  # (see handle_bind / handle_unbind), so the two sources never disagree on the
728
728
  # same key for two different sessions — a single pass is sufficient.
729
- @registry.list.each do |summary|
729
+ @registry.list(archived: nil).each do |summary|
730
730
  found = nil
731
731
  @registry.with_session(summary[:id]) { |s| found = s[:channel_keys]&.include?(key) }
732
732
  return summary[:id] if found
@@ -814,7 +814,7 @@ module Clacky
814
814
  # The on-disk summary is authoritative here, so sessions evicted from
815
815
  # memory are cleaned too — which is exactly the case /bind used to miss.
816
816
  def clear_channel_info_for_key(key, keep_session_id: nil)
817
- @registry.list(limit: nil).each do |summary|
817
+ @registry.list(limit: nil, archived: nil).each do |summary|
818
818
  next if summary[:id] == keep_session_id
819
819
 
820
820
  info = summary[:channel_info]
@@ -839,7 +839,7 @@ module Clacky
839
839
  end
840
840
 
841
841
  def bind_key_to_session(key, session_id)
842
- @registry.list.each do |summary|
842
+ @registry.list(archived: nil).each do |summary|
843
843
  @registry.with_session(summary[:id]) { |s| s[:channel_keys]&.delete(key) }
844
844
  end
845
845
  @registry.with_session(session_id) do |s|
@@ -932,7 +932,7 @@ module Clacky
932
932
  def restore_channel_bindings
933
933
  bound_keys = Set.new
934
934
  restored_count = 0
935
- @registry.list(limit: nil).each do |summary|
935
+ @registry.list(limit: nil, archived: nil).each do |summary|
936
936
  info = summary[:channel_info]
937
937
  next unless info.is_a?(Hash) && info[:platform] && info[:user_id] && info[:chat_id]
938
938