openclacky 1.5.3 → 1.5.5

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +13 -0
  3. data/CHANGELOG.md +57 -0
  4. data/Dockerfile +21 -1
  5. data/README.md +26 -7
  6. data/README_CN.md +26 -7
  7. data/README_JA.md +26 -7
  8. data/lib/clacky/agent/message_compressor.rb +4 -29
  9. data/lib/clacky/agent/message_compressor_helper.rb +17 -16
  10. data/lib/clacky/agent/session_serializer.rb +7 -0
  11. data/lib/clacky/agent.rb +14 -2
  12. data/lib/clacky/agent_config.rb +10 -5
  13. data/lib/clacky/agent_profile.rb +25 -3
  14. data/lib/clacky/brand_config.rb +32 -12
  15. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +15 -0
  16. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +1 -5
  17. data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -2
  18. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +43 -34
  19. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +9 -3
  20. data/lib/clacky/default_extensions/git/ext.yml +3 -0
  21. data/lib/clacky/default_extensions/meeting/ext.yml +3 -0
  22. data/lib/clacky/default_extensions/time_machine/ext.yml +4 -0
  23. data/lib/clacky/default_parsers/xlsx_parser.py +66 -0
  24. data/lib/clacky/default_skills/new/SKILL.md +17 -12
  25. data/lib/clacky/extension/api_extension.rb +5 -2
  26. data/lib/clacky/extension/loader.rb +1 -0
  27. data/lib/clacky/extension/packager.rb +60 -15
  28. data/lib/clacky/extension/verifier.rb +1 -1
  29. data/lib/clacky/message_format/anthropic.rb +1 -1
  30. data/lib/clacky/message_format/bedrock.rb +1 -1
  31. data/lib/clacky/message_format/open_ai.rb +34 -3
  32. data/lib/clacky/providers.rb +77 -8
  33. data/lib/clacky/server/http_server.rb +495 -39
  34. data/lib/clacky/server/project_manager.rb +150 -0
  35. data/lib/clacky/server/server_master.rb +7 -0
  36. data/lib/clacky/server/session_registry.rb +14 -1
  37. data/lib/clacky/server/web_ui_controller.rb +12 -2
  38. data/lib/clacky/session_manager.rb +6 -4
  39. data/lib/clacky/tools/file_reader.rb +2 -1
  40. data/lib/clacky/tools/terminal.rb +30 -3
  41. data/lib/clacky/utils/model_pricing.rb +69 -18
  42. data/lib/clacky/utils/parser_manager.rb +115 -7
  43. data/lib/clacky/version.rb +1 -1
  44. data/lib/clacky/web/app.css +1216 -318
  45. data/lib/clacky/web/app.js +27 -4
  46. data/lib/clacky/web/components/notify.js +139 -22
  47. data/lib/clacky/web/core/aside.js +21 -0
  48. data/lib/clacky/web/features/brand/store.js +3 -1
  49. data/lib/clacky/web/features/brand/view.js +29 -5
  50. data/lib/clacky/web/features/extensions/store.js +214 -9
  51. data/lib/clacky/web/features/extensions/view.js +156 -11
  52. data/lib/clacky/web/features/new-session/store.js +25 -3
  53. data/lib/clacky/web/features/new-session/view.js +339 -142
  54. data/lib/clacky/web/features/skills/view.js +1 -1
  55. data/lib/clacky/web/features/trash/store.js +23 -9
  56. data/lib/clacky/web/features/trash/view.js +51 -0
  57. data/lib/clacky/web/i18n.js +124 -15
  58. data/lib/clacky/web/index.html +105 -79
  59. data/lib/clacky/web/projects.js +1143 -0
  60. data/lib/clacky/web/sessions.js +320 -538
  61. data/lib/clacky/web/settings.js +138 -5
  62. data/lib/clacky/web/utils.js +1 -0
  63. data/lib/clacky/web/ws-dispatcher.js +98 -4
  64. data/scripts/build/src/install_system_deps.sh.cc +11 -3
  65. data/scripts/install_system_deps.sh +11 -3
  66. metadata +5 -2
  67. data/lib/clacky/default_parsers/xlsx_parser.rb +0 -121
@@ -18,6 +18,14 @@ module Clacky
18
18
  MANIFEST = "ext.yml"
19
19
  MAX_ZIP_SIZE = 50 * 1024 * 1024
20
20
 
21
+ # Network timeouts for the download step. `read_timeout` is an *idle*
22
+ # timeout (resets on every chunk received), so a stalled connection with
23
+ # no new bytes flowing for this long is treated as dead — not a "download
24
+ # still going, just slow" situation, which large files on a slow link
25
+ # would otherwise trigger.
26
+ OPEN_TIMEOUT = 15 # seconds to establish the connection
27
+ READ_TIMEOUT = 60 # seconds of silence before giving up mid-download
28
+
21
29
  # Platform metadata that leaks in from the developer's OS; never ship it.
22
30
  SYSTEM_METADATA = [".DS_Store", "__MACOSX", "Thumbs.db", "desktop.ini"].freeze
23
31
 
@@ -56,12 +64,19 @@ module Clacky
56
64
  # local zip path or an http(s) URL. Validates the archive (single top
57
65
  # container with an ext.yml, no path traversal), extracts it, then runs
58
66
  # verify on the resolved installed layer.
59
- def install(source, installed_dir: Clacky::ExtensionLoader::INSTALLED_DIR, force: false)
67
+ # on_progress is an optional callable that receives a stage hash, e.g.:
68
+ # { stage: "downloading", progress: 45 }
69
+ # { stage: "extracting" }
70
+ # { stage: "verifying" }
71
+ # Caller can use this for progress reporting without coupling to internals.
72
+ def install(source, installed_dir: Clacky::ExtensionLoader::INSTALLED_DIR, force: false, on_progress: nil)
60
73
  Dir.mktmpdir("clacky-ext-install") do |tmp|
61
- zip_path = local_zip_for(source, tmp)
74
+ on_progress&.call({ stage: "downloading", progress: 0 })
75
+ zip_path = local_zip_for(source, tmp, on_progress: on_progress)
76
+
77
+ on_progress&.call({ stage: "extracting" })
62
78
  extract_root = File.join(tmp, "unpacked")
63
79
  FileUtils.mkdir_p(extract_root)
64
-
65
80
  extract_zip(zip_path, extract_root)
66
81
 
67
82
  ext_id, container_src = locate_container(extract_root)
@@ -74,6 +89,7 @@ module Clacky
74
89
  FileUtils.mkdir_p(target)
75
90
  FileUtils.cp_r(Dir.glob("#{container_src}/*"), target)
76
91
 
92
+ on_progress&.call({ stage: "verifying" })
77
93
  Clacky::ExtensionLoader.invalidate_cache!
78
94
  units = verify_installed(ext_id, installed_dir)
79
95
 
@@ -149,11 +165,11 @@ module Clacky
149
165
  rel.split(File::SEPARATOR).any? { |seg| SYSTEM_METADATA.include?(seg) }
150
166
  end
151
167
 
152
- private def local_zip_for(source, tmp)
168
+ private def local_zip_for(source, tmp, on_progress: nil)
153
169
  src = source.to_s
154
170
  if src.match?(%r{\Ahttps?://})
155
171
  dest = File.join(tmp, "download.zip")
156
- download(src, dest)
172
+ download(src, dest, on_progress: on_progress)
157
173
  dest
158
174
  else
159
175
  path = File.expand_path(src)
@@ -162,19 +178,48 @@ module Clacky
162
178
  end
163
179
  end
164
180
 
165
- private def download(url, dest)
166
- total = 0
167
- URI.open(url, "rb") do |io| # rubocop:disable Security/Open
168
- File.open(dest, "wb") do |out|
169
- while (chunk = io.read(64 * 1024))
170
- total += chunk.bytesize
171
- raise Error, "download exceeds #{MAX_ZIP_SIZE} bytes" if total > MAX_ZIP_SIZE
172
- out.write(chunk)
173
- end
174
- end
181
+ # Download url → dest file.
182
+ # IMPORTANT: OpenURI's block form (`URI.open(url) { |io| ... }`) buffers
183
+ # the *entire* response into a StringIO/Tempfile via Net::HTTP's
184
+ # read_body before ever yielding `io` — so reading `io` in chunks after
185
+ # the fact reports progress only after the download has already
186
+ # finished. To get progress callbacks that fire while bytes are still
187
+ # arriving on the socket, we hook OpenURI's own `progress_proc` /
188
+ # `content_length_proc`, which run inside read_body as each chunk lands.
189
+ # Respects whatever proxy env vars the user has configured (some CDN
190
+ # domains are only reachable at usable speed through a proxy).
191
+ private def download(url, dest, on_progress: nil)
192
+ content_length = nil
193
+
194
+ content_length_proc = lambda do |len|
195
+ content_length = len if len&.positive?
196
+ end
197
+
198
+ progress_proc = lambda do |size|
199
+ raise Error, "download exceeds #{MAX_ZIP_SIZE} bytes" if size > MAX_ZIP_SIZE
200
+ next unless on_progress
201
+
202
+ pct = if content_length&.positive?
203
+ [(size * 100 / content_length).floor, 99].min
204
+ else
205
+ (90 - 90 * Math.exp(-size.to_f / (1024 * 1024))).floor
206
+ end
207
+ on_progress.call({ stage: "downloading", progress: pct })
208
+ end
209
+
210
+ URI.open(
211
+ url, "rb",
212
+ open_timeout: OPEN_TIMEOUT,
213
+ read_timeout: READ_TIMEOUT,
214
+ content_length_proc: content_length_proc,
215
+ progress_proc: progress_proc
216
+ ) do |io| # rubocop:disable Security/Open
217
+ File.open(dest, "wb") { |out| out.write(io.read) }
175
218
  end
176
219
  rescue OpenURI::HTTPError, SocketError => e
177
220
  raise Error, "failed to download #{url}: #{e.message}"
221
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
222
+ raise Error, "download timed out: #{e.message}"
178
223
  end
179
224
 
180
225
  private def extract_zip(zip_path, dest_root)
@@ -22,7 +22,7 @@ module Clacky
22
22
  PANEL_KEYS = %w[id title title_zh description description_zh view order attach entry_points].freeze
23
23
  API_KEYS = %w[id handler].freeze
24
24
  SKILL_KEYS = %w[id dir protected].freeze
25
- AGENT_KEYS = %w[id title title_zh description description_zh order prompt panels skills avatar].freeze
25
+ AGENT_KEYS = %w[id title title_zh description description_zh order prompt panels skills avatar hidden].freeze
26
26
  CHANNEL_KEYS = %w[id platform adapter].freeze
27
27
  PATCH_KEYS = %w[target file fingerprint on_mismatch].freeze
28
28
  HOOK_KEYS = %w[event file].freeze
@@ -114,7 +114,7 @@ module Clacky
114
114
  private_class_method def self.normalized_effort(effort)
115
115
  return nil if effort.nil? || effort.to_s.empty?
116
116
  s = effort.to_s
117
- %w[low medium high].include?(s) ? s : nil
117
+ %w[low medium high xhigh max].include?(s) ? s : nil
118
118
  end
119
119
 
120
120
  # Merge consecutive tool_result user messages into a single user message.
@@ -91,7 +91,7 @@ module Clacky
91
91
 
92
92
  private_class_method def self.additional_fields_for_effort(effort)
93
93
  return nil if effort.nil? || effort.to_s.empty?
94
- return nil unless %w[low medium high].include?(effort.to_s)
94
+ return nil unless %w[low medium high xhigh max].include?(effort.to_s)
95
95
  {
96
96
  thinking: { type: "adaptive" },
97
97
  output_config: { effort: effort.to_s }
@@ -195,10 +195,16 @@ module Clacky
195
195
  # ── Private helpers ───────────────────────────────────────────────────────
196
196
 
197
197
  # Returns the token-limit field name for the given model.
198
- # Most OpenAI-compatible APIs use :max_tokens; MiMo-V2.5 (Xiaomi) uses
199
- # :max_completion_tokens to cap the combined thinking + answer length.
198
+ # Most OpenAI-compatible APIs use :max_tokens; MiMo-V2.5 (Xiaomi) and
199
+ # MiniMax-M3 use :max_completion_tokens to cap the combined thinking +
200
+ # answer length (M3 documents max_completion_tokens over the legacy
201
+ # max_tokens field).
200
202
  private_class_method def self.token_field_for(model)
201
- model.to_s.match?(/^mimo-v2/i) ? :max_completion_tokens : :max_tokens
203
+ if model.to_s.match?(/^mimo-v2/i) || model.to_s.match?(/^minimax-m3$/i)
204
+ :max_completion_tokens
205
+ else
206
+ :max_tokens
207
+ end
202
208
  end
203
209
 
204
210
  # Apply model-specific reasoning / thinking parameters to the request body.
@@ -256,6 +262,31 @@ module Clacky
256
262
  elsif !effort_str.empty?
257
263
  body[:thinking] = { type: "enabled" }
258
264
  end
265
+ elsif model.to_s.match?(/^minimax-m3$/i)
266
+ # MiniMax-M3 controls thinking via a top-level "thinking" field with
267
+ # { type: "adaptive" | "disabled" } — the same envelope shape MiMo
268
+ # uses, but with MiniMax-specific values. Adaptive thinking is the
269
+ # server default when the field is omitted, so we only send the
270
+ # field to explicitly switch modes:
271
+ # - reasoning_effort "off"/"disabled" → thinking disabled
272
+ # (M3 answers directly; M2.7 keeps thinking on regardless).
273
+ # - any other effort → adaptive thinking (M3
274
+ # decides per request); we let the server pick the intensity.
275
+ # M2.7 cannot disable thinking and ignores this field, so sending
276
+ # it is harmless for the rest of the MiniMax lineup.
277
+ if %w[off nothink disabled].include?(effort_str)
278
+ body[:thinking] = { type: "disabled" }
279
+ elsif !effort_str.empty?
280
+ body[:thinking] = { type: "adaptive" }
281
+ end
282
+ elsif model.to_s.match?(/deepseek/i)
283
+ # DeepSeek V4 reasoning_effort only accepts low/high/max.
284
+ # xhigh is not recognized — map it to max.
285
+ effort = case effort_str
286
+ when "xhigh" then "max"
287
+ else effort_str
288
+ end
289
+ body[:reasoning_effort] = effort unless effort.empty?
259
290
  elsif reasoning_effort && !effort_str.empty?
260
291
  body[:reasoning_effort] = effort_str
261
292
  end
@@ -29,12 +29,14 @@ module Clacky
29
29
  "name" => "OpenClacky",
30
30
  "base_url" => "https://api.openclacky.com",
31
31
  "api" => "bedrock",
32
- "default_model" => "abs-claude-sonnet-4-6",
32
+ "default_model" => "abs-claude-sonnet-5",
33
33
  "models" => [
34
34
  "abs-claude-fable-5",
35
+ "abs-claude-opus-5",
35
36
  "abs-claude-opus-4-8",
36
37
  "abs-claude-opus-4-7",
37
38
  "abs-claude-opus-4-6",
39
+ "abs-claude-sonnet-5",
38
40
  "abs-claude-sonnet-4-6",
39
41
  "abs-claude-sonnet-4-5",
40
42
  "abs-claude-haiku-4-5",
@@ -134,9 +136,11 @@ module Clacky
134
136
  # no injection happens when the default model is already lite-class.
135
137
  "lite_models" => {
136
138
  "abs-claude-fable-5" => "abs-claude-haiku-4-5",
139
+ "abs-claude-opus-5" => "abs-claude-haiku-4-5",
137
140
  "abs-claude-opus-4-8" => "abs-claude-haiku-4-5",
138
141
  "abs-claude-opus-4-7" => "abs-claude-haiku-4-5",
139
142
  "abs-claude-opus-4-6" => "abs-claude-haiku-4-5",
143
+ "abs-claude-sonnet-5" => "abs-claude-haiku-4-5",
140
144
  "abs-claude-sonnet-4-6" => "abs-claude-haiku-4-5",
141
145
  "abs-claude-sonnet-4-5" => "abs-claude-haiku-4-5",
142
146
  "dsk-deepseek-v4-pro" => "dsk-deepseek-v4-flash",
@@ -145,7 +149,9 @@ module Clacky
145
149
  # Fallback chain: if a model is unavailable, try the next one in order.
146
150
  # Keys are primary model names; values are the fallback model to use instead.
147
151
  "fallback_models" => {
148
- "abs-claude-fable-5" => "abs-claude-opus-4-8",
152
+ "abs-claude-fable-5" => "abs-claude-opus-5",
153
+ "abs-claude-opus-5" => "abs-claude-opus-4-8",
154
+ "abs-claude-sonnet-5" => "abs-claude-sonnet-4-6",
149
155
  "abs-claude-sonnet-4-6" => "abs-claude-sonnet-4-5"
150
156
  },
151
157
  # Secondary gateway URL used when the primary base_url is unreachable
@@ -254,18 +260,20 @@ module Clacky
254
260
  # so capability checks (vision=false) fire correctly regardless of
255
261
  # which endpoint the user configured.
256
262
  "endpoint_variants" => [
257
- { "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://api.minimaxi.com/v1", "region" => "cn" }.freeze,
258
- { "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.minimax.io/v1", "region" => "intl" }.freeze
263
+ { "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://api.minimaxi.com/v1", "region" => "cn" }.freeze,
264
+ { "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.minimax.io/v1", "region" => "intl" }.freeze
259
265
  ].freeze,
260
- # MiniMax M2.x does not support multimodal/vision input on this endpoint.
261
- # M3 (released 2026-06-01) is natively multimodal and accepts image
262
- # input, so it overrides the provider-level vision=false below.
266
+ # MiniMax M2.5/M2.7 are text-only on this endpoint. M3 (released 2026-06-01)
267
+ # is natively multimodal — it accepts image input via OpenAI-style
268
+ # image_url content parts — so it overrides the provider-level
269
+ # vision=false below. M3 exposes a 1,000,000-token context window
270
+ # (M2.7: 204,800; M2.5: 40,960).
263
271
  "capabilities" => { "vision" => false }.freeze,
264
272
  "model_capabilities" => {
265
273
  "MiniMax-M3" => { "vision" => true }.freeze
266
274
  }.freeze,
267
275
  "default_ocr_model" => "MiniMax-M3",
268
- "website_url" => "https://www.minimaxi.com/user-center/basic-information/interface-key"
276
+ "website_url" => "https://platform.minimax.io/"
269
277
  }.freeze,
270
278
 
271
279
  "kimi" => {
@@ -469,6 +477,67 @@ module Clacky
469
477
  "website_url" => "https://console.volcengine.com/ark/region:cn-beijing/overview"
470
478
  }.freeze,
471
479
 
480
+ "ollama" => {
481
+ "name" => "Ollama (Cloud)",
482
+ # Ollama Cloud API — OpenAI-compatible endpoint serving both local and
483
+ # cloud models. Cloud models are available immediately without pulling.
484
+ # Authentication: API key via Authorization: Bearer <key> header.
485
+ # Create API keys at: https://ollama.com/settings/keys
486
+ "base_url" => "https://ollama.com",
487
+ "api" => "openai-completions",
488
+ "default_model" => "deepseek-v4-flash",
489
+ # Curated list of cloud-enabled models from the Ollama library.
490
+ # Users can type any Ollama model name manually; this list seeds the picker.
491
+ "models" => [
492
+ "glm-5.2",
493
+ "kimi-k3",
494
+ "gemma4",
495
+ "qwen3.5",
496
+ "glm-5.1",
497
+ "minimax-m2.7",
498
+ "nemotron-3-super",
499
+ "minimax-m3",
500
+ "kimi-k2.7-code",
501
+ "kimi-k2.6",
502
+ "deepseek-v4-flash",
503
+ "deepseek-v4-pro",
504
+ "nemotron-3-ultra",
505
+ "gemini-3-flash-preview",
506
+ "nemotron-3-nano",
507
+ "mistral-large-3",
508
+ "gpt-oss"
509
+ ],
510
+ # Provider-level default: many Ollama cloud models are vision-capable.
511
+ "capabilities" => { "vision" => true }.freeze,
512
+ # Model-level overrides: text-only models that don't accept image input.
513
+ "model_capabilities" => {
514
+ "glm-5.2" => { "vision" => false }.freeze,
515
+ "glm-5.1" => { "vision" => false }.freeze,
516
+ "minimax-m2.7" => { "vision" => false }.freeze,
517
+ "deepseek-v4-pro" => { "vision" => false }.freeze,
518
+ "deepseek-v4-flash" => { "vision" => false }.freeze,
519
+ "nemotron-3-super" => { "vision" => false }.freeze,
520
+ "nemotron-3-ultra" => { "vision" => false }.freeze,
521
+ "nemotron-3-nano" => { "vision" => false }.freeze,
522
+ "gpt-oss" => { "vision" => false }.freeze
523
+ }.freeze,
524
+ # Per-primary lite pairing: subagents use smaller models for cheap/fast work.
525
+ "lite_models" => {
526
+ "glm-5.2" => "glm-5.1",
527
+ "kimi-k3" => "kimi-k2.6",
528
+ "gemma4" => "nemotron-3-nano",
529
+ "qwen3.5" => "gemini-3-flash-preview",
530
+ "minimax-m3" => "minimax-m2.7",
531
+ "kimi-k2.7-code" => "kimi-k2.6",
532
+ "deepseek-v4-pro" => "deepseek-v4-flash",
533
+ "mistral-large-3" => "nemotron-3-nano",
534
+ "gpt-oss" => "nemotron-3-nano",
535
+ "gemini-3-flash-preview" => "nemotron-3-nano"
536
+ },
537
+ "default_ocr_model" => "kimi-k2.7-code",
538
+ "website_url" => "https://ollama.com/settings/keys"
539
+ }.freeze,
540
+
472
541
  "openai" => {
473
542
  "name" => "OpenAI (GPT)",
474
543
  "base_url" => "https://api.openai.com/v1",