openclacky 1.5.20 → 1.5.22

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +110 -0
  3. data/README.md +18 -0
  4. data/lib/clacky/agent/chunk_index.rb +8 -1
  5. data/lib/clacky/agent/feedback.rb +30 -0
  6. data/lib/clacky/agent/history_navigation.rb +17 -3
  7. data/lib/clacky/agent/hook_manager.rb +6 -1
  8. data/lib/clacky/agent/llm_caller.rb +1 -0
  9. data/lib/clacky/agent/message_compressor_helper.rb +28 -2
  10. data/lib/clacky/agent/session_serializer.rb +20 -2
  11. data/lib/clacky/agent.rb +56 -43
  12. data/lib/clacky/agent_config.rb +82 -4
  13. data/lib/clacky/billing/platform_billing.rb +8 -1
  14. data/lib/clacky/brand_config.rb +1 -1
  15. data/lib/clacky/cli.rb +3 -2
  16. data/lib/clacky/client.rb +5 -5
  17. data/lib/clacky/deepseek_web_tools.rb +156 -0
  18. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
  19. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +21 -1
  20. data/lib/clacky/default_extensions/git/panels/git/view.js +1911 -398
  21. data/lib/clacky/default_extensions/preview/panels/preview/view.js +199 -34
  22. data/lib/clacky/extension/loader.rb +5 -3
  23. data/lib/clacky/locales/en.rb +5 -5
  24. data/lib/clacky/locales/zh.rb +5 -5
  25. data/lib/clacky/message_format/anthropic.rb +20 -0
  26. data/lib/clacky/message_format/open_ai.rb +31 -14
  27. data/lib/clacky/message_history.rb +21 -3
  28. data/lib/clacky/platform_http_client.rb +6 -2
  29. data/lib/clacky/providers.rb +66 -31
  30. data/lib/clacky/search_config.rb +71 -15
  31. data/lib/clacky/server/browser_manager.rb +35 -5
  32. data/lib/clacky/server/channel/channel_manager.rb +6 -6
  33. data/lib/clacky/server/discover.rb +5 -4
  34. data/lib/clacky/server/git_panel.rb +360 -34
  35. data/lib/clacky/server/http_server.rb +334 -89
  36. data/lib/clacky/server/model_prices.rb +5 -3
  37. data/lib/clacky/server/pid_files.rb +25 -0
  38. data/lib/clacky/server/preview.rb +118 -17
  39. data/lib/clacky/server/scheduler.rb +81 -2
  40. data/lib/clacky/server/server_master.rb +9 -3
  41. data/lib/clacky/server/session_registry.rb +19 -7
  42. data/lib/clacky/server/web_ui_controller.rb +78 -47
  43. data/lib/clacky/session_manager.rb +58 -5
  44. data/lib/clacky/skill.rb +1 -1
  45. data/lib/clacky/tools/ask_user.rb +4 -1
  46. data/lib/clacky/tools/base.rb +3 -2
  47. data/lib/clacky/tools/browser.rb +18 -2
  48. data/lib/clacky/tools/file_reader.rb +1 -1
  49. data/lib/clacky/tools/terminal/session_manager.rb +19 -1
  50. data/lib/clacky/tools/terminal.rb +65 -23
  51. data/lib/clacky/tools/trash_manager.rb +5 -3
  52. data/lib/clacky/tools/web_fetch.rb +81 -22
  53. data/lib/clacky/tools/web_search.rb +45 -4
  54. data/lib/clacky/ui2/ui_controller.rb +1 -1
  55. data/lib/clacky/ui_interface.rb +4 -3
  56. data/lib/clacky/utils/file_processor.rb +51 -0
  57. data/lib/clacky/utils/mac_app_detector.rb +8 -7
  58. data/lib/clacky/utils/model_pricing.rb +192 -15
  59. data/lib/clacky/utils/windows_app_detector.rb +3 -1
  60. data/lib/clacky/version.rb +1 -1
  61. data/lib/clacky/web/app.css +1019 -204
  62. data/lib/clacky/web/app.js +59 -6
  63. data/lib/clacky/web/auth.js +1 -1
  64. data/lib/clacky/web/components/chat-navigator.js +8 -1
  65. data/lib/clacky/web/components/composer.js +107 -8
  66. data/lib/clacky/web/components/image-zoom.js +165 -0
  67. data/lib/clacky/web/components/mentions.js +2 -13
  68. data/lib/clacky/web/components/model-picker.js +9 -5
  69. data/lib/clacky/web/components/notify.js +2 -2
  70. data/lib/clacky/web/components/onboard.js +13 -10
  71. data/lib/clacky/web/core/aside.js +60 -3
  72. data/lib/clacky/web/features/archive/store.js +30 -16
  73. data/lib/clacky/web/features/archive/view.js +20 -10
  74. data/lib/clacky/web/features/backup/view.js +5 -5
  75. data/lib/clacky/web/features/billing/view.js +2 -2
  76. data/lib/clacky/web/features/channels/view.js +5 -2
  77. data/lib/clacky/web/features/extensions/view.js +2 -2
  78. data/lib/clacky/web/features/new-session/store.js +82 -5
  79. data/lib/clacky/web/features/new-session/view.js +496 -58
  80. data/lib/clacky/web/features/skills/store.js +1 -1
  81. data/lib/clacky/web/features/tasks/view.js +162 -51
  82. data/lib/clacky/web/features/trash/store.js +10 -7
  83. data/lib/clacky/web/features/trash/view.js +9 -4
  84. data/lib/clacky/web/features/workspace/store.js +13 -6
  85. data/lib/clacky/web/features/workspace/view.js +153 -163
  86. data/lib/clacky/web/i18n.js +342 -84
  87. data/lib/clacky/web/index.html +262 -216
  88. data/lib/clacky/web/projects.js +93 -5
  89. data/lib/clacky/web/sessions.js +423 -234
  90. data/lib/clacky/web/settings.js +169 -16
  91. data/lib/clacky/web/skills.js +2 -2
  92. data/lib/clacky/web/theme.js +17 -2
  93. data/lib/clacky/web/utils.js +17 -2
  94. data/lib/clacky/web/weixin-qr.html +2 -1
  95. data/lib/clacky/web/ws-dispatcher.js +17 -42
  96. data/lib/clacky.rb +1 -0
  97. metadata +5 -1
@@ -36,7 +36,7 @@ module Clacky
36
36
  "default_model" => "auto",
37
37
  "models" => [
38
38
  "auto",
39
- "dsk-deepseek-flash",
39
+ "dsk-deepseek-v4.1-flash",
40
40
  "dsk-deepseek-v4-pro",
41
41
  "oc-glm-5.3",
42
42
  "oc-kimi-k3",
@@ -60,8 +60,7 @@ module Clacky
60
60
  "abs-gpt-5.6-luna",
61
61
  "or-gemini-4-argon",
62
62
  "or-gemini-3-1-pro",
63
- "or-gemini-3-8-flash",
64
- "oc-grok-4.7"
63
+ "or-gemini-3-8-flash"
65
64
  ],
66
65
  # Image generation models served by the openclacky platform
67
66
  # gateway. The gateway exposes a standard OpenAI-compatible
@@ -146,11 +145,14 @@ module Clacky
146
145
  # oc-glm-5.3 is likewise text-only: its upstream rejects any content
147
146
  # block other than `type: "text"`, so images must never be sent
148
147
  # inline. oc-kimi-k3 and oc-grok-4.7 are natively multimodal and keep
149
- # vision=true.
148
+ # vision=true; oc-gpt-6.1-sol, oc-gpt-6-astra, oc-gpt-6-sol and
149
+ # oc-gpt-6-luna do too.
150
150
  "model_capabilities" => {
151
- "dsk-deepseek-flash" => { "vision" => true }.freeze,
152
- "dsk-deepseek-v4-pro" => { "vision" => false }.freeze,
153
- "oc-glm-5.3" => { "vision" => false }.freeze
151
+ "dsk-deepseek-v4.1-flash" => { "vision" => true }.freeze,
152
+ # Legacy alias still sent by sessions created before the rename.
153
+ "dsk-deepseek-flash" => { "vision" => true }.freeze,
154
+ "dsk-deepseek-v4-pro" => { "vision" => false }.freeze,
155
+ "oc-glm-5.3" => { "vision" => false }.freeze
154
156
  }.freeze,
155
157
  # Bedrock GPT models (abs-gpt-*) are served through the OpenAI
156
158
  # Responses API — their Chat Completions endpoint rejects function
@@ -166,7 +168,7 @@ module Clacky
166
168
  # themselves, so they're intentionally not listed here as keys —
167
169
  # no injection happens when the default model is already lite-class.
168
170
  "lite_models" => {
169
- "auto" => "dsk-deepseek-flash",
171
+ "auto" => "dsk-deepseek-v4.1-flash",
170
172
  "abs-claude-fable-5-1" => "abs-claude-haiku-5-5",
171
173
  "abs-claude-fable-5" => "abs-claude-haiku-5-5",
172
174
  "abs-claude-opus-5-5" => "abs-claude-haiku-5-5",
@@ -180,7 +182,7 @@ module Clacky
180
182
  "abs-gpt-6-sol" => "abs-gpt-6-luna",
181
183
  "abs-gpt-5.6-sol" => "abs-gpt-5.6-luna",
182
184
  "abs-gpt-5.6-terra" => "abs-gpt-5.6-luna",
183
- "dsk-deepseek-v4-pro" => "dsk-deepseek-flash",
185
+ "dsk-deepseek-v4-pro" => "dsk-deepseek-v4.1-flash",
184
186
  "or-gemini-3-1-pro" => "or-gemini-3-8-flash"
185
187
  },
186
188
  # Fallback chain: if a model is unavailable, try the next one in order.
@@ -199,6 +201,7 @@ module Clacky
199
201
  "endpoint_variants" => [
200
202
  { "label" => "Primary (Global)", "label_key" => "settings.models.baseurl.variant.openclacky_primary", "base_url" => "https://api.openclacky.com" }.freeze
201
203
  ].freeze,
204
+ "deepseek_web_tools" => true,
202
205
  "website_url" => "https://www.openclacky.com/llm_keys"
203
206
  }.freeze,
204
207
 
@@ -212,7 +215,7 @@ module Clacky
212
215
  "base_url" => "https://api.deepseek.com",
213
216
  "api" => "openai-completions",
214
217
  "default_model" => "deepseek-v4-pro",
215
- "lite_model" => "deepseek-flash",
218
+ "lite_model" => "deepseek-v4.1-flash",
216
219
  # Note: deepseek-chat and deepseek-reasoner are legacy aliases being
217
220
  # deprecated on 2026-07-24; they map to V4.1 Flash's non-thinking and
218
221
  # thinking modes respectively.
@@ -220,15 +223,23 @@ module Clacky
220
223
  # longer offered upstream, so they are out of this list: only their
221
224
  # pricing entries remain, to let already-billed history resolve.
222
225
  "models" => [
223
- "deepseek-flash",
226
+ "deepseek-v4.1-flash",
224
227
  "deepseek-v4-pro",
225
228
  ],
226
229
  # DeepSeek V4 API is text-only across all models, except V4.1 Flash
227
- # (deepseek-flash), which is natively multimodal.
230
+ # (deepseek-v4.1-flash), which is natively multimodal.
228
231
  "capabilities" => { "vision" => false }.freeze,
229
232
  "model_capabilities" => {
230
- "deepseek-flash" => { "vision" => true }.freeze
233
+ "deepseek-v4.1-flash" => { "vision" => true }.freeze,
234
+ # Legacy id kept for configs saved before the rename.
235
+ "deepseek-flash" => { "vision" => true }.freeze
236
+ }.freeze,
237
+ # The upstream API still exposes V4.1 Flash as "deepseek-flash"; the
238
+ # picker shows the generation name.
239
+ "api_model_aliases" => {
240
+ "deepseek-v4.1-flash" => "deepseek-flash"
231
241
  }.freeze,
242
+ "deepseek_web_tools" => true,
232
243
  "website_url" => "https://platform.deepseek.com/api_keys"
233
244
  }.freeze,
234
245
 
@@ -269,8 +280,8 @@ module Clacky
269
280
  "name" => "Kimi (Moonshot)",
270
281
  "base_url" => "https://api.moonshot.cn/v1",
271
282
  "api" => "openai-completions",
272
- "default_model" => "kimi-k3",
273
- "models" => ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
283
+ "default_model" => "kimi-k3.1-pro",
284
+ "models" => ["kimi-k3.1-pro", "kimi-k3.1-pro-highspeed", "kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
274
285
  # Moonshot operates two regional endpoints with identical APIs & model
275
286
  # lineup — mainland China (.cn) and international (.ai). These are the
276
287
  # pay-as-you-go Open Platform endpoints; the subscription-billed
@@ -286,7 +297,7 @@ module Clacky
286
297
  { "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://api.moonshot.cn/v1", "region" => "cn" }.freeze,
287
298
  { "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.moonshot.ai/v1", "region" => "intl" }.freeze
288
299
  ].freeze,
289
- # k3 / k2.7-code / k2.6 are multimodal; legacy k2 text-only models need model_capabilities override if added.
300
+ # k3.1-pro / k3 / k2.7-code / k2.6 are multimodal; legacy k2 text-only models need model_capabilities override if added.
290
301
  "capabilities" => { "vision" => true }.freeze,
291
302
  "default_ocr_model" => "kimi-k3",
292
303
  "website_url" => "https://platform.moonshot.cn/console/api-keys"
@@ -334,8 +345,8 @@ module Clacky
334
345
  "name" => "Minimax",
335
346
  "base_url" => "https://api.minimaxi.com/v1",
336
347
  "api" => "openai-completions",
337
- "default_model" => "MiniMax-M3",
338
- "models" => ["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"],
348
+ "default_model" => "MiniMax-M3.1-Flash",
349
+ "models" => ["MiniMax-M3.1-Flash", "MiniMax-M3"],
339
350
  # MiniMax operates two regional endpoints with identical APIs & model
340
351
  # lineup — mainland China (.com) and international (.io). Listing both
341
352
  # lets find_by_base_url identify either one as provider "minimax",
@@ -345,16 +356,15 @@ module Clacky
345
356
  { "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://api.minimaxi.com/v1", "region" => "cn" }.freeze,
346
357
  { "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.minimax.io/v1", "region" => "intl" }.freeze
347
358
  ].freeze,
348
- # MiniMax M2.5/M2.7 are text-only on this endpoint. M3 (released 2026-06-01)
349
- # is natively multimodal — it accepts image input via OpenAI-style
350
- # image_url content parts — so it overrides the provider-level
351
- # vision=false below. M3 exposes a 1,000,000-token context window
352
- # (M2.7: 204,800; M2.5: 40,960).
359
+ # M3.1-Flash and M3 are natively multimodal (image_url content parts,
360
+ # 1M context) and override the provider-level vision=false, which
361
+ # stays as the safe default for retired text-only ids (M2.x).
353
362
  "capabilities" => { "vision" => false }.freeze,
354
363
  "model_capabilities" => {
355
- "MiniMax-M3" => { "vision" => true }.freeze
364
+ "MiniMax-M3.1-Flash" => { "vision" => true }.freeze,
365
+ "MiniMax-M3" => { "vision" => true }.freeze
356
366
  }.freeze,
357
- "default_ocr_model" => "MiniMax-M3",
367
+ "default_ocr_model" => "MiniMax-M3.1-Flash",
358
368
  "website_url" => "https://platform.minimax.io/"
359
369
  }.freeze,
360
370
 
@@ -573,17 +583,20 @@ module Clacky
573
583
  "doubao-seed-evolving",
574
584
  "doubao-seed-2.1-pro",
575
585
  "doubao-seed-2.1-turbo",
586
+ "doubao-seed-2.1-lite",
576
587
  "doubao-seed-2.0-mini",
577
- "doubao-seed-2.0-lite",
578
588
  "minimax-m3",
579
- "kimi-k2.7-code",
589
+ "kimi-k3.1-pro",
580
590
  "kimi-k3",
591
+ "kimi-k2.7-code",
581
592
  # GLM-5.2 is delisted from Ark on 2026-08-31, so new setups only
582
593
  # offer GLM-5.3. It is available on the Coding/Agent Plan endpoints;
583
594
  # the pay-as-you-go endpoint does not carry it yet (no versioned id),
584
595
  # so no payg alias is registered for it.
585
596
  "glm-5.3",
586
597
  "glm-5.3-flash",
598
+ "deepseek-v4.1-flash",
599
+ "deepseek-v4.1-flash-economy",
587
600
  "deepseek-v4-pro",
588
601
  "deepseek-v4-flash"
589
602
  ],
@@ -604,14 +617,21 @@ module Clacky
604
617
  "glm-5.2" => "glm-5-2-260617",
605
618
  "deepseek-v4-pro" => "deepseek-v4-pro-260425",
606
619
  "deepseek-v4-flash" => "deepseek-v4-flash-260425",
620
+ "deepseek-v4.1-flash" => "deepseek-v4-1-flash-261017",
621
+ "deepseek-v4.1-flash-economy" => "deepseek-v4-1-flash-economy-261017",
622
+ "kimi-k3.1-pro" => "kimi-k3-1-pro-261024",
607
623
  "doubao-seed-2.1-pro" => "doubao-seed-2-1-pro-260628",
608
624
  "doubao-seed-2.1-turbo" => "doubao-seed-2-1-turbo-260628",
625
+ "doubao-seed-2.1-lite" => "doubao-seed-2-1-lite-260915",
609
626
  "doubao-seed-2.0-mini" => "doubao-seed-2-0-mini-260428",
627
+ # doubao-seed-2.0-lite is delisted in favor of 2.1-lite; the alias
628
+ # stays so existing configs keep working.
610
629
  "doubao-seed-2.0-lite" => "doubao-seed-2-0-lite-260428"
611
630
  }.freeze
612
631
  }.freeze,
613
632
  # Most Doubao/multimodal models accept image input; GLM-5.2/5.3 and
614
- # DeepSeek-V4 on Ark are text-only. GLM-5.3-Flash inherits vision=true.
633
+ # DeepSeek-V4 on Ark are text-only. GLM-5.3-Flash and DeepSeek-V4.1
634
+ # (incl. economy) inherit vision=true.
615
635
  "capabilities" => { "vision" => true }.freeze,
616
636
  "model_capabilities" => {
617
637
  "glm-5.3" => { "vision" => false }.freeze,
@@ -620,7 +640,7 @@ module Clacky
620
640
  "deepseek-v4-flash" => { "vision" => false }.freeze,
621
641
  "minimax-m2.7" => { "vision" => false }.freeze
622
642
  }.freeze,
623
- "default_ocr_model" => "doubao-seed-2.0-lite",
643
+ "default_ocr_model" => "doubao-seed-2.1-lite",
624
644
  "website_url" => "https://console.volcengine.com/ark/region:cn-beijing/overview"
625
645
  }.freeze,
626
646
 
@@ -642,7 +662,6 @@ module Clacky
642
662
  "glm-5.2",
643
663
  "kimi-k3",
644
664
  "glm-5.1",
645
- "minimax-m2.7",
646
665
  "minimax-m3",
647
666
  "kimi-k2.7-code",
648
667
  "kimi-k2.6",
@@ -664,7 +683,6 @@ module Clacky
664
683
  "lite_models" => {
665
684
  "glm-5.2" => "glm-5.1",
666
685
  "kimi-k3" => "kimi-k2.6",
667
- "minimax-m3" => "minimax-m2.7",
668
686
  "kimi-k2.7-code" => "kimi-k2.6",
669
687
  "deepseek-v4-pro" => "deepseek-v4-flash",
670
688
  "gemini-3-flash-preview" => "deepseek-v4-flash"
@@ -1084,6 +1102,10 @@ module Clacky
1084
1102
  # Plan endpoints accept the short "doubao-seed-2.1-pro"). Users always see
1085
1103
  # and pick the short name; this swaps it just before the request goes out.
1086
1104
  #
1105
+ # The map comes in two shapes:
1106
+ # - grouped by endpoint variant: { "payg" => { short => real } }
1107
+ # - flat, endpoint-independent: { short => real } (e.g. DeepSeek)
1108
+ #
1087
1109
  # Returns the original model unchanged when no alias applies (unknown
1088
1110
  # provider, endpoint without an alias group, or model not in the map).
1089
1111
  #
@@ -1101,6 +1123,9 @@ module Clacky
1101
1123
  aliases = preset && preset["api_model_aliases"]
1102
1124
  return model unless aliases.is_a?(Hash)
1103
1125
 
1126
+ flat = aliases[model.to_s]
1127
+ return flat if flat.is_a?(String)
1128
+
1104
1129
  group = alias_group_for_base_url(preset, base_url)
1105
1130
  return model unless group
1106
1131
 
@@ -1173,6 +1198,16 @@ module Clacky
1173
1198
  return true unless caps.key?(key)
1174
1199
  caps[key] != false
1175
1200
  end
1201
+
1202
+ # True when the preset declares native DeepSeek web tools (search + fetch).
1203
+ # Currently DeepSeek direct ("deepseekv4") and the OpenClacky platform
1204
+ # gateway ("openclacky").
1205
+ def deepseek_web_tools?(provider_id)
1206
+ preset = PRESETS[provider_id]
1207
+ return false unless preset
1208
+
1209
+ preset["deepseek_web_tools"] == true
1210
+ end
1176
1211
  end
1177
1212
  end
1178
1213
  end
@@ -2,19 +2,26 @@
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
+ # keys: — credentials per provider, so switching providers never
20
+ # tavily: tvly-xxx hands one provider's key to another (optional for
21
+ # deepseek_web, which can reuse a configured model key)
22
+ #
23
+ # A missing file, or one without a `provider` key, means "never chosen" and
24
+ # resolves to the free chain.
18
25
  #
19
26
  # The directory is the registry: dropping my_engine.rb into
20
27
  # ~/.clacky/searchers/ and setting `provider: my_engine` is all it takes,
@@ -22,21 +29,65 @@ module Clacky
22
29
  module SearchConfig
23
30
  CONFIG_PATH = File.expand_path("~/.clacky/search.yml").freeze
24
31
 
32
+ # Hosted DeepSeek search + page fetch, billed per successful call upstream.
33
+ # It behaves like a provider but ships with the gem, so it has no script.
34
+ DEEPSEEK_WEB = "deepseek_web"
35
+
25
36
  class << self
26
- # @return [Hash] { "provider" => String, "key" => String }
37
+ # @return [Hash] { "provider" => String, "keys" => { provider => String } }
27
38
  def load
28
39
  raw = File.exist?(CONFIG_PATH) ? YAML.load_file(CONFIG_PATH) : nil
29
- return { "provider" => "", "key" => "" } unless raw.is_a?(Hash)
40
+ return default_config unless raw.is_a?(Hash)
41
+
42
+ provider = raw["provider"].to_s.strip
43
+ keys = {}
44
+ (raw["keys"].is_a?(Hash) ? raw["keys"] : {}).each do |name, value|
45
+ value = value.to_s.strip
46
+ keys[name.to_s] = value unless value.empty?
47
+ end
48
+ # Files written before keys were split hold one key for the provider then selected.
49
+ legacy = raw["key"].to_s.strip
50
+ keys[provider] ||= legacy unless provider.empty? || legacy.empty?
30
51
 
31
- { "provider" => raw["provider"].to_s.strip, "key" => raw["key"].to_s.strip }
52
+ { "provider" => provider, "keys" => keys }
32
53
  rescue StandardError
33
- { "provider" => "", "key" => "" }
54
+ default_config
34
55
  end
35
56
 
36
- def save(provider:, key:)
57
+ # The key is stored under the resulting provider; a blank key removes it.
58
+ def save(provider: nil, key: nil)
59
+ current = load
60
+ new_provider = provider.nil? ? current["provider"] : provider.to_s.strip
61
+ keys = current["keys"]
62
+ unless key.nil? || new_provider.empty?
63
+ key = key.to_s.strip
64
+ if key.empty?
65
+ keys.delete(new_provider)
66
+ else
67
+ keys[new_provider] = key
68
+ end
69
+ end
70
+
71
+ payload = {
72
+ "provider" => new_provider,
73
+ "keys" => keys
74
+ }
75
+
37
76
  FileUtils.mkdir_p(File.dirname(CONFIG_PATH))
38
- File.write(CONFIG_PATH, { "provider" => provider.to_s.strip, "key" => key.to_s.strip }.to_yaml)
77
+ File.write(CONFIG_PATH, payload.to_yaml)
39
78
  File.chmod(0o600, CONFIG_PATH)
79
+
80
+ # A new provider or key invalidates an earlier failure, so the next
81
+ # search retries DeepSeek instead of sitting out the cooldown.
82
+ Clacky::DeepSeekWebTools.reset_cooldown!
83
+ end
84
+
85
+ def deepseek_web?
86
+ load["provider"] == DEEPSEEK_WEB
87
+ end
88
+
89
+ private def default_config
90
+ { "provider" => "", "keys" => {} }
40
91
  end
41
92
 
42
93
  # Absolute path of the configured searcher, or nil when unset/missing.
@@ -44,13 +95,18 @@ module Clacky
44
95
  # web_search silently falls back instead of erroring on every search.
45
96
  def script_path
46
97
  name = load["provider"]
47
- return nil if name.empty?
98
+ return nil if name.empty? || name == DEEPSEEK_WEB
48
99
 
49
100
  Clacky::Utils::SearcherManager.path_for(name)
50
101
  end
51
102
 
52
103
  def key
53
- load["key"]
104
+ config = load
105
+ config["keys"][config["provider"]].to_s
106
+ end
107
+
108
+ def key_for(provider)
109
+ load["keys"][provider.to_s].to_s
54
110
  end
55
111
  end
56
112
  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
 
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tmpdir"
3
+ require_relative "pid_files"
4
4
  require "socket"
5
5
 
6
6
  module Clacky
7
7
  module Server
8
8
  # Discover locally-running Clacky server(s) by scanning PID files
9
- # written by Master at /tmp/clacky-master-<port>.pid.
9
+ # written by Master in the configured PID directory.
10
10
  #
11
11
  # Used by the CLI (bare `clacky agent` mode) to auto-detect a sibling
12
12
  # server process, so skills that call back into the server (channels,
@@ -17,7 +17,6 @@ module Clacky
17
17
  # Does NOT probe the TCP port (avoids false positives from stale files
18
18
  # but also avoids noisy connection attempts).
19
19
  module Discover
20
- PID_FILE_GLOB = File.join(Dir.tmpdir, "clacky-master-*.pid").freeze
21
20
  PID_FILE_REGEX = /clacky-master-(\d+)\.pid\z/.freeze
22
21
 
23
22
  module_function
@@ -41,7 +40,9 @@ module Clacky
41
40
  #
42
41
  # @return [Array<Hash>] sorted by port ascending
43
42
  def find_all_local
44
- Dir.glob(PID_FILE_GLOB).filter_map do |path|
43
+ directory = PidFiles.directory
44
+ Dir.glob("clacky-master-*.pid", base: directory).filter_map do |filename|
45
+ path = File.join(directory, filename)
45
46
  m = path.match(PID_FILE_REGEX)
46
47
  next nil unless m
47
48