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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +110 -0
- data/README.md +18 -0
- data/lib/clacky/agent/chunk_index.rb +8 -1
- data/lib/clacky/agent/feedback.rb +30 -0
- data/lib/clacky/agent/history_navigation.rb +17 -3
- data/lib/clacky/agent/hook_manager.rb +6 -1
- data/lib/clacky/agent/llm_caller.rb +1 -0
- data/lib/clacky/agent/message_compressor_helper.rb +28 -2
- data/lib/clacky/agent/session_serializer.rb +20 -2
- data/lib/clacky/agent.rb +56 -43
- data/lib/clacky/agent_config.rb +82 -4
- data/lib/clacky/billing/platform_billing.rb +8 -1
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/cli.rb +3 -2
- data/lib/clacky/client.rb +5 -5
- data/lib/clacky/deepseek_web_tools.rb +156 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +21 -1
- data/lib/clacky/default_extensions/git/panels/git/view.js +1911 -398
- data/lib/clacky/default_extensions/preview/panels/preview/view.js +199 -34
- data/lib/clacky/extension/loader.rb +5 -3
- data/lib/clacky/locales/en.rb +5 -5
- data/lib/clacky/locales/zh.rb +5 -5
- data/lib/clacky/message_format/anthropic.rb +20 -0
- data/lib/clacky/message_format/open_ai.rb +31 -14
- data/lib/clacky/message_history.rb +21 -3
- data/lib/clacky/platform_http_client.rb +6 -2
- data/lib/clacky/providers.rb +66 -31
- data/lib/clacky/search_config.rb +71 -15
- data/lib/clacky/server/browser_manager.rb +35 -5
- data/lib/clacky/server/channel/channel_manager.rb +6 -6
- data/lib/clacky/server/discover.rb +5 -4
- data/lib/clacky/server/git_panel.rb +360 -34
- data/lib/clacky/server/http_server.rb +334 -89
- data/lib/clacky/server/model_prices.rb +5 -3
- data/lib/clacky/server/pid_files.rb +25 -0
- data/lib/clacky/server/preview.rb +118 -17
- data/lib/clacky/server/scheduler.rb +81 -2
- data/lib/clacky/server/server_master.rb +9 -3
- data/lib/clacky/server/session_registry.rb +19 -7
- data/lib/clacky/server/web_ui_controller.rb +78 -47
- data/lib/clacky/session_manager.rb +58 -5
- data/lib/clacky/skill.rb +1 -1
- data/lib/clacky/tools/ask_user.rb +4 -1
- data/lib/clacky/tools/base.rb +3 -2
- data/lib/clacky/tools/browser.rb +18 -2
- data/lib/clacky/tools/file_reader.rb +1 -1
- data/lib/clacky/tools/terminal/session_manager.rb +19 -1
- data/lib/clacky/tools/terminal.rb +65 -23
- data/lib/clacky/tools/trash_manager.rb +5 -3
- data/lib/clacky/tools/web_fetch.rb +81 -22
- data/lib/clacky/tools/web_search.rb +45 -4
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +4 -3
- data/lib/clacky/utils/file_processor.rb +51 -0
- data/lib/clacky/utils/mac_app_detector.rb +8 -7
- data/lib/clacky/utils/model_pricing.rb +192 -15
- data/lib/clacky/utils/windows_app_detector.rb +3 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1019 -204
- data/lib/clacky/web/app.js +59 -6
- data/lib/clacky/web/auth.js +1 -1
- data/lib/clacky/web/components/chat-navigator.js +8 -1
- data/lib/clacky/web/components/composer.js +107 -8
- data/lib/clacky/web/components/image-zoom.js +165 -0
- data/lib/clacky/web/components/mentions.js +2 -13
- data/lib/clacky/web/components/model-picker.js +9 -5
- data/lib/clacky/web/components/notify.js +2 -2
- data/lib/clacky/web/components/onboard.js +13 -10
- data/lib/clacky/web/core/aside.js +60 -3
- data/lib/clacky/web/features/archive/store.js +30 -16
- data/lib/clacky/web/features/archive/view.js +20 -10
- data/lib/clacky/web/features/backup/view.js +5 -5
- data/lib/clacky/web/features/billing/view.js +2 -2
- data/lib/clacky/web/features/channels/view.js +5 -2
- data/lib/clacky/web/features/extensions/view.js +2 -2
- data/lib/clacky/web/features/new-session/store.js +82 -5
- data/lib/clacky/web/features/new-session/view.js +496 -58
- data/lib/clacky/web/features/skills/store.js +1 -1
- data/lib/clacky/web/features/tasks/view.js +162 -51
- data/lib/clacky/web/features/trash/store.js +10 -7
- data/lib/clacky/web/features/trash/view.js +9 -4
- data/lib/clacky/web/features/workspace/store.js +13 -6
- data/lib/clacky/web/features/workspace/view.js +153 -163
- data/lib/clacky/web/i18n.js +342 -84
- data/lib/clacky/web/index.html +262 -216
- data/lib/clacky/web/projects.js +93 -5
- data/lib/clacky/web/sessions.js +423 -234
- data/lib/clacky/web/settings.js +169 -16
- data/lib/clacky/web/skills.js +2 -2
- data/lib/clacky/web/theme.js +17 -2
- data/lib/clacky/web/utils.js +17 -2
- data/lib/clacky/web/weixin-qr.html +2 -1
- data/lib/clacky/web/ws-dispatcher.js +17 -42
- data/lib/clacky.rb +1 -0
- metadata +5 -1
data/lib/clacky/providers.rb
CHANGED
|
@@ -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"
|
|
152
|
-
|
|
153
|
-
"
|
|
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
|
|
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
|
-
#
|
|
349
|
-
#
|
|
350
|
-
#
|
|
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-
|
|
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
|
|
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.
|
|
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
|
data/lib/clacky/search_config.rb
CHANGED
|
@@ -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
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
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 —
|
|
16
|
-
# blank
|
|
17
|
-
#
|
|
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, "
|
|
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
|
|
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" =>
|
|
52
|
+
{ "provider" => provider, "keys" => keys }
|
|
32
53
|
rescue StandardError
|
|
33
|
-
|
|
54
|
+
default_config
|
|
34
55
|
end
|
|
35
56
|
|
|
36
|
-
|
|
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,
|
|
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
|
|
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:
|
|
120
|
-
daemon_running:
|
|
121
|
-
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|