openclacky 1.5.12 → 1.5.13
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 +48 -0
- data/lib/clacky/access_key.rb +59 -0
- data/lib/clacky/agent/message_compressor_helper.rb +52 -1
- data/lib/clacky/agent/session_serializer.rb +35 -2
- data/lib/clacky/agent/time_machine.rb +9 -1
- data/lib/clacky/agent.rb +11 -7
- data/lib/clacky/agent_config.rb +40 -0
- data/lib/clacky/agent_profile.rb +5 -5
- data/lib/clacky/billing/platform_billing.rb +84 -1
- data/lib/clacky/brand_config.rb +4 -3
- data/lib/clacky/cli.rb +4 -2
- data/lib/clacky/client.rb +107 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +3 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +40 -21
- data/lib/clacky/default_extensions/git/ext.yml +1 -1
- data/lib/clacky/default_extensions/git/panels/git/view.js +483 -77
- data/lib/clacky/default_extensions/meeting/ext.yml +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +1 -1
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +186 -208
- data/lib/clacky/extension/packager.rb +4 -2
- data/lib/clacky/extension/verifier.rb +14 -1
- data/lib/clacky/message_format/open_ai.rb +5 -1
- data/lib/clacky/message_format/open_ai_responses.rb +409 -0
- data/lib/clacky/message_history.rb +6 -2
- data/lib/clacky/openai_responses_stream_aggregator.rb +294 -0
- data/lib/clacky/providers.rb +18 -6
- data/lib/clacky/server/dir_picker.rb +154 -0
- data/lib/clacky/server/git_panel.rb +61 -9
- data/lib/clacky/server/http_server.rb +147 -103
- data/lib/clacky/tools/terminal.rb +27 -3
- data/lib/clacky/ui2/markdown_renderer.rb +15 -12
- data/lib/clacky/ui2/strings_cjk_patch.rb +132 -0
- data/lib/clacky/ui2/themes/hacker_theme.rb +1 -1
- data/lib/clacky/ui2/themes/minimal_theme.rb +1 -1
- data/lib/clacky/utils/model_pricing.rb +70 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +412 -108
- data/lib/clacky/web/components/composer.js +45 -7
- data/lib/clacky/web/components/custom-select.js +136 -0
- data/lib/clacky/web/components/mentions.js +58 -2
- data/lib/clacky/web/components/model-picker.js +527 -0
- data/lib/clacky/web/components/onboard.js +29 -55
- data/lib/clacky/web/core/ext.js +38 -0
- data/lib/clacky/web/features/billing/view.js +20 -6
- data/lib/clacky/web/features/extensions/view.js +22 -10
- data/lib/clacky/web/features/new-session/store.js +15 -1
- data/lib/clacky/web/features/new-session/view.js +31 -22
- data/lib/clacky/web/features/trash/view.js +31 -19
- data/lib/clacky/web/features/workspace/store.js +1 -0
- data/lib/clacky/web/features/workspace/view.js +54 -3
- data/lib/clacky/web/i18n.js +73 -11
- data/lib/clacky/web/index.html +92 -37
- data/lib/clacky/web/sessions.js +550 -592
- data/lib/clacky/web/settings.js +163 -190
- data/lib/clacky.rb +3 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca259b2f17df9695e21c4d4c4f18ee83affec0fc41d67db829d903e1c3bb008d
|
|
4
|
+
data.tar.gz: b0bfd31378c6b452a7aef246d3d316242a91fa6366f543c45edcff8c3f44ac6c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfcb5b28264aeb2acb6b121b89c395e4e9241b3a3135c2e9fb7d565365ed263c701f939448a2c01cb229359cca11402b36b081d4edff6231ce8e46e72d253498
|
|
7
|
+
data.tar.gz: 95cca310ebd8e0a23b1f1e5aed241b6e5fbc19aa206fdb16301dfd3b90f07e33424a44686c10b2d6cf4d3344c75eab2c033d7b3e4e7ef6c9a40b7879dd07a88b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,54 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
|
|
7
|
+
## [1.5.13] - 2026-09-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Add Gemini 3.8 Flash model and set it as the trial default
|
|
11
|
+
- Add or-stt-gemini-3-8-flash speech-to-text model; default STT and video understanding to 3.8
|
|
12
|
+
- Add OpenAI Responses API as a fourth API format (#490 - @kylezhang)
|
|
13
|
+
- Add access-key API and config-reload API
|
|
14
|
+
- Add Claude Fable 5.1, GLM-5.3-Flash and qwen3.8-max models with pricing
|
|
15
|
+
- Default the Web UI to the OS/browser language on first visit
|
|
16
|
+
- Make the workspace file tree resizable
|
|
17
|
+
- Expose mounted drives in the directory picker
|
|
18
|
+
- Personalize the new-session page title and empty-chat greeting with the AI's name
|
|
19
|
+
- Localise built-in setup session titles
|
|
20
|
+
|
|
21
|
+
### Improved
|
|
22
|
+
- Replace native selects with shared CustomSelect dropdowns across settings
|
|
23
|
+
- Overhaul the git & time-machine panel
|
|
24
|
+
- Improve the directory picker (#521)
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Merge OpenClacky billing across all configured API keys
|
|
28
|
+
- Preserve custom provider and API format when editing a model
|
|
29
|
+
- Reclaim PTY file descriptors when a session shell dies
|
|
30
|
+
- Wrap ANSI-coloured CJK text without IndexError
|
|
31
|
+
- Prevent history edits while a session is running (#520)
|
|
32
|
+
- Preserve attachment badges after compression (#516)
|
|
33
|
+
- Flag hidden agents instead of dropping them
|
|
34
|
+
- Install upgrades next to the running gem's base directory
|
|
35
|
+
- Skip extension auto-sync for administrators (#515)
|
|
36
|
+
- Support the mention menu with Windows IME (#513)
|
|
37
|
+
- Validate extension version input (#514)
|
|
38
|
+
- Keep mention chips anchored and on one line while typing
|
|
39
|
+
- Fall back to uncolored markdown for CJK tables
|
|
40
|
+
- Stabilise billing toolbar layout (#517)
|
|
41
|
+
- Light the group unread dot for backend-created sessions (#518)
|
|
42
|
+
- Translate maximum reasoning effort (#522)
|
|
43
|
+
- Harden WSL Windows profile detection in the directory picker
|
|
44
|
+
- Wrap download errors so the CDN fallback fires
|
|
45
|
+
- Surface the git tab badge eagerly
|
|
46
|
+
- Show the meeting panel only in general sessions
|
|
47
|
+
- Whitelist the config top-level key in the extension verifier
|
|
48
|
+
|
|
49
|
+
### More
|
|
50
|
+
- Extract shared ModelPicker model dropdown component
|
|
51
|
+
- Refine dropdown chevrons and submenu selection
|
|
52
|
+
- Adjust sidebar panel order and labels
|
|
53
|
+
- Update trial model copy to Gemini 3.7 Flash
|
|
54
|
+
|
|
7
55
|
## [1.5.12] - 2026-08-27
|
|
8
56
|
|
|
9
57
|
### Added
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Clacky
|
|
7
|
+
# Access key for server-mode authentication.
|
|
8
|
+
#
|
|
9
|
+
# Two sources, in priority order:
|
|
10
|
+
# 1. ~/.clacky/access_key — a file holding the raw key, writable from
|
|
11
|
+
# outside the process (e.g. a sandbox host pushing a key into a
|
|
12
|
+
# pre-started container). This is what makes rotation possible:
|
|
13
|
+
# ENV is a boot-time snapshot the running process can never change.
|
|
14
|
+
# 2. CLACKY_ACCESS_KEY env var — the original bootstrap mechanism.
|
|
15
|
+
module AccessKey
|
|
16
|
+
ENV_VAR = "CLACKY_ACCESS_KEY"
|
|
17
|
+
|
|
18
|
+
def self.resolve
|
|
19
|
+
from_file || from_env
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.from_file
|
|
23
|
+
return nil unless File.exist?(key_file)
|
|
24
|
+
|
|
25
|
+
key = File.read(key_file).strip
|
|
26
|
+
key.empty? ? nil : key
|
|
27
|
+
rescue StandardError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.from_env
|
|
32
|
+
key = ENV.fetch(ENV_VAR, "").strip
|
|
33
|
+
key.empty? ? nil : key
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.write(key)
|
|
37
|
+
key = key.to_s.strip
|
|
38
|
+
raise ArgumentError, "access key must not be blank" if key.empty?
|
|
39
|
+
|
|
40
|
+
FileUtils.mkdir_p(File.dirname(key_file))
|
|
41
|
+
File.write(key_file, "#{key}\n")
|
|
42
|
+
FileUtils.chmod(0o600, key_file)
|
|
43
|
+
key
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.generate
|
|
47
|
+
SecureRandom.hex(32)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.configured?
|
|
51
|
+
!resolve.nil?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Resolved lazily so specs that stub Dir.home in a before-hook still work.
|
|
55
|
+
def self.key_file
|
|
56
|
+
File.join(Dir.home, ".clacky", "access_key")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -624,10 +624,12 @@ module Clacky
|
|
|
624
624
|
|
|
625
625
|
case role
|
|
626
626
|
when "user"
|
|
627
|
+
display_files = display_files_for_archive(msg)
|
|
627
628
|
lines << (msg[:task_id] ? "## User [Task #{msg[:task_id]}]" : "## User")
|
|
628
629
|
lines << ""
|
|
629
|
-
lines <<
|
|
630
|
+
lines << format_archived_user_content(content, display_files)
|
|
630
631
|
lines << ""
|
|
632
|
+
append_display_files_line(lines, display_files)
|
|
631
633
|
append_ext_events_line(lines, msg)
|
|
632
634
|
when "assistant"
|
|
633
635
|
# If this message is itself a compressed summary, annotate the header
|
|
@@ -676,6 +678,55 @@ module Clacky
|
|
|
676
678
|
lines
|
|
677
679
|
end
|
|
678
680
|
|
|
681
|
+
# Serialize only the lightweight UI metadata needed to reconstruct file
|
|
682
|
+
# badges after compression. File paths, previews, sizes, MIME types, and
|
|
683
|
+
# contents deliberately stay out of the chunk archive.
|
|
684
|
+
def display_files_for_archive(msg)
|
|
685
|
+
files = Array(msg[:display_files]).dup
|
|
686
|
+
Array(msg[:content]).each do |block|
|
|
687
|
+
next unless block.is_a?(Hash)
|
|
688
|
+
next unless %w[image image_url].include?((block[:type] || block["type"]).to_s)
|
|
689
|
+
|
|
690
|
+
name = block[:image_name] || block["image_name"]
|
|
691
|
+
if name.nil? || name.to_s.strip.empty?
|
|
692
|
+
path = block[:image_path] || block["image_path"]
|
|
693
|
+
name = File.basename(path.to_s).sub(/\A[0-9a-f]{16}_/, "") unless path.to_s.empty?
|
|
694
|
+
end
|
|
695
|
+
name = "image" if name.nil? || name.to_s.strip.empty?
|
|
696
|
+
files << { name: name, type: "image" }
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
files.filter_map do |file|
|
|
700
|
+
next unless file.is_a?(Hash)
|
|
701
|
+
|
|
702
|
+
name = file[:name] || file["name"]
|
|
703
|
+
next if name.nil? || name.to_s.strip.empty?
|
|
704
|
+
|
|
705
|
+
type = file[:type] || file["type"] || "file"
|
|
706
|
+
type = "file" if type.to_s.strip.empty?
|
|
707
|
+
{ name: name.to_s, type: type.to_s }
|
|
708
|
+
end.uniq { |file| [file[:name], file[:type]] }
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Once an image has a badge in the archive, omit the generic [image_url]
|
|
712
|
+
# placeholder from the visible replay text. Text blocks remain unchanged.
|
|
713
|
+
def format_archived_user_content(content, display_files)
|
|
714
|
+
return format_message_content(content) unless content.is_a?(Array)
|
|
715
|
+
return format_message_content(content) unless display_files.any? { |file| file[:type] == "image" }
|
|
716
|
+
|
|
717
|
+
visible_blocks = content.reject do |block|
|
|
718
|
+
block.is_a?(Hash) && %w[image image_url].include?((block[:type] || block["type"]).to_s)
|
|
719
|
+
end
|
|
720
|
+
format_message_content(visible_blocks)
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
def append_display_files_line(lines, files)
|
|
724
|
+
return if files.empty?
|
|
725
|
+
|
|
726
|
+
lines << "_Display files: #{files.to_json}_"
|
|
727
|
+
lines << ""
|
|
728
|
+
end
|
|
729
|
+
|
|
679
730
|
# Serialize persisted extension events into the chunk MD so they survive
|
|
680
731
|
# compression and can be replayed in place. Mirrors the "_Tool calls:"
|
|
681
732
|
# convention; parse_chunk_md_to_rounds reads it back.
|
|
@@ -534,6 +534,7 @@ module Clacky
|
|
|
534
534
|
sections.each do |sec|
|
|
535
535
|
text = sec[:lines].join.strip
|
|
536
536
|
text, sec_ext_events = extract_ext_events_from_text(text)
|
|
537
|
+
text, sec_display_files = extract_display_files_from_text(text)
|
|
537
538
|
|
|
538
539
|
# Nested chunk: expand it recursively, prepend before current rounds
|
|
539
540
|
if sec[:nested_chunk]
|
|
@@ -546,7 +547,7 @@ module Clacky
|
|
|
546
547
|
next
|
|
547
548
|
end
|
|
548
549
|
|
|
549
|
-
next if text.empty? && sec_ext_events.empty?
|
|
550
|
+
next if text.empty? && sec_ext_events.empty? && sec_display_files.empty?
|
|
550
551
|
|
|
551
552
|
if sec[:role] == "user"
|
|
552
553
|
round_index += 1
|
|
@@ -560,6 +561,7 @@ module Clacky
|
|
|
560
561
|
_from_chunk: true
|
|
561
562
|
}
|
|
562
563
|
user_msg[:task_id] = sec[:task_id] if sec[:task_id]
|
|
564
|
+
user_msg[:display_files] = sec_display_files unless sec_display_files.empty?
|
|
563
565
|
current_round = {
|
|
564
566
|
user_msg: user_msg,
|
|
565
567
|
events: [],
|
|
@@ -618,6 +620,35 @@ module Clacky
|
|
|
618
620
|
end
|
|
619
621
|
|
|
620
622
|
|
|
623
|
+
# Pull lightweight attachment badge metadata out of a chunk section.
|
|
624
|
+
# The marker is internal archive data and must not appear in replayed text.
|
|
625
|
+
# Only name + type are accepted; paths and file contents are never restored.
|
|
626
|
+
def extract_display_files_from_text(text)
|
|
627
|
+
return [text, []] unless text.include?("_Display files:")
|
|
628
|
+
|
|
629
|
+
files = []
|
|
630
|
+
kept = text.each_line.reject do |line|
|
|
631
|
+
match = line.strip.match(/\A_Display files:\s*(\[.*\])_?\z/i)
|
|
632
|
+
next false unless match
|
|
633
|
+
|
|
634
|
+
parsed = JSON.parse(match[1]) rescue []
|
|
635
|
+
Array(parsed).each do |file|
|
|
636
|
+
next unless file.is_a?(Hash)
|
|
637
|
+
|
|
638
|
+
name = file["name"] || file[:name]
|
|
639
|
+
next if name.nil? || name.to_s.strip.empty?
|
|
640
|
+
|
|
641
|
+
type = file["type"] || file[:type] || "file"
|
|
642
|
+
type = "file" if type.to_s.strip.empty?
|
|
643
|
+
files << { name: name.to_s, type: type.to_s }
|
|
644
|
+
end
|
|
645
|
+
true
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
[kept.join.strip, files]
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
|
|
621
652
|
# Pull the "_Ext events: type | {json}; ..._" line out of a chunk section,
|
|
622
653
|
# returning the remaining text plus the parsed events. Counterpart to
|
|
623
654
|
# MessageCompressorHelper#append_ext_events_line.
|
|
@@ -948,7 +979,9 @@ module Clacky
|
|
|
948
979
|
|
|
949
980
|
mime_type = (url || "")[/\Adata:([^;]+);/, 1] || "image/jpeg"
|
|
950
981
|
ext = mime_type.split("/").last
|
|
951
|
-
|
|
982
|
+
name = block[:image_name] || block["image_name"]
|
|
983
|
+
name = "image_#{idx + 1}.#{ext}" if name.nil? || name.to_s.strip.empty?
|
|
984
|
+
{ name: name, mime_type: mime_type, data_url: url, path: path }
|
|
952
985
|
end
|
|
953
986
|
end
|
|
954
987
|
|
|
@@ -529,7 +529,15 @@ module Clacky
|
|
|
529
529
|
private def looks_binary?(path)
|
|
530
530
|
return false if path.nil? || !File.exist?(path)
|
|
531
531
|
sample = File.binread(path, 8000)
|
|
532
|
-
sample.include?("\x00")
|
|
532
|
+
return true if sample.include?("\x00")
|
|
533
|
+
sample.force_encoding("UTF-8")
|
|
534
|
+
# The 8000-byte window can cut a multibyte char in half; drop up to
|
|
535
|
+
# three dangling tail bytes before judging the encoding.
|
|
536
|
+
3.times do
|
|
537
|
+
break if sample.valid_encoding?
|
|
538
|
+
sample = sample.byteslice(0, sample.bytesize - 1)
|
|
539
|
+
end
|
|
540
|
+
!sample.valid_encoding?
|
|
533
541
|
rescue StandardError
|
|
534
542
|
true
|
|
535
543
|
end
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -568,9 +568,12 @@ module Clacky
|
|
|
568
568
|
all_disk_files = disk_files + downgraded
|
|
569
569
|
|
|
570
570
|
# Format user message — text + inline vision images
|
|
571
|
-
# Store the tmp path alongside the data_url
|
|
572
|
-
#
|
|
573
|
-
user_content = format_user_content(
|
|
571
|
+
# Store the tmp path and original name alongside the data_url: the path supports
|
|
572
|
+
# normal replay, while the name becomes the lightweight badge after compression.
|
|
573
|
+
user_content = format_user_content(
|
|
574
|
+
user_input,
|
|
575
|
+
vision_images.map { |v| { url: v[:url], path: v[:path], name: v[:name] } }
|
|
576
|
+
)
|
|
574
577
|
|
|
575
578
|
# Parse disk files — agent's responsibility, not the upload layer.
|
|
576
579
|
# process_path runs the parser script and returns a FileRef with preview_path or parse_error.
|
|
@@ -2351,10 +2354,10 @@ module Clacky
|
|
|
2351
2354
|
|
|
2352
2355
|
# Build user message content for LLM.
|
|
2353
2356
|
# Returns plain String when no vision images; Array of content parts otherwise.
|
|
2354
|
-
#
|
|
2355
|
-
#
|
|
2356
|
-
#
|
|
2357
|
-
#
|
|
2357
|
+
# vision_images: Array of String (plain url) OR Hash { url:, path:, name: }
|
|
2358
|
+
# path is stored so normal history replay can reconstruct the image; name is
|
|
2359
|
+
# lightweight metadata used for an archived badge after compression. Both
|
|
2360
|
+
# fields are stripped by MessageHistory before the content reaches the API.
|
|
2358
2361
|
private def format_user_content(text, vision_images)
|
|
2359
2362
|
vision_images ||= []
|
|
2360
2363
|
|
|
@@ -2366,6 +2369,7 @@ module Clacky
|
|
|
2366
2369
|
if img.is_a?(Hash)
|
|
2367
2370
|
block = { type: "image_url", image_url: { url: img[:url] } }
|
|
2368
2371
|
block[:image_path] = img[:path] if img[:path]
|
|
2372
|
+
block[:image_name] = img[:name] if img[:name]
|
|
2369
2373
|
content << block
|
|
2370
2374
|
else
|
|
2371
2375
|
content << { type: "image_url", image_url: { url: img } }
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -402,6 +402,46 @@ module Clacky
|
|
|
402
402
|
Clacky::ProxyConfig.reset_cache! if defined?(Clacky::ProxyConfig)
|
|
403
403
|
end
|
|
404
404
|
|
|
405
|
+
# Re-read config.yml from disk and apply it to this instance in place.
|
|
406
|
+
#
|
|
407
|
+
# Two invariants make this safe for a running server:
|
|
408
|
+
# 1. @models is mutated with #replace, never reassigned — every session
|
|
409
|
+
# built via #deep_copy shares that array by design (see #deep_copy).
|
|
410
|
+
# 2. Model ids are regenerated on every load and are not persisted, so
|
|
411
|
+
# fresh entries inherit the id of the matching old entry (same model
|
|
412
|
+
# name + base_url). Otherwise every session's @current_model_id
|
|
413
|
+
# would dangle after a reload.
|
|
414
|
+
#
|
|
415
|
+
# Returns true when applied, false when the file could not be parsed —
|
|
416
|
+
# in-memory config is left untouched rather than wiped.
|
|
417
|
+
def reload!(config_file = CONFIG_FILE)
|
|
418
|
+
fresh = self.class.load(config_file)
|
|
419
|
+
|
|
420
|
+
fresh.models.each do |m|
|
|
421
|
+
previous = find_model_by_name_and_url(m["model"], m["base_url"])
|
|
422
|
+
m["id"] = previous["id"] if previous
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
@models.replace(fresh.models)
|
|
426
|
+
|
|
427
|
+
CONFIG_SETTINGS_KEYS.each do |key|
|
|
428
|
+
send("#{key}=", fresh.send(key))
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# A session may have been pinned to a model the user just deleted.
|
|
432
|
+
unless @models.any? { |m| m["id"] == @current_model_id }
|
|
433
|
+
fallback = find_model_by_type("default") || @models.first
|
|
434
|
+
@current_model_id = fallback && fallback["id"]
|
|
435
|
+
@current_model_index = fallback ? @models.index(fallback).to_i : 0
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
Clacky::ProxyConfig.reset_cache! if defined?(Clacky::ProxyConfig)
|
|
439
|
+
true
|
|
440
|
+
rescue StandardError => e
|
|
441
|
+
Clacky::Logger.warn("[AgentConfig] reload! failed: #{e.class}: #{e.message}") if defined?(Clacky::Logger)
|
|
442
|
+
false
|
|
443
|
+
end
|
|
444
|
+
|
|
405
445
|
# Convert to YAML format (top-level array)
|
|
406
446
|
# Auto-injected lite models (auto_injected: true) are excluded from persistence —
|
|
407
447
|
# they are regenerated at load time from the provider preset.
|
data/lib/clacky/agent_profile.rb
CHANGED
|
@@ -88,11 +88,11 @@ module Clacky
|
|
|
88
88
|
# @param recency [Hash<String, Integer>] agent id => epoch of last use,
|
|
89
89
|
# used to order third-party extension agents by most-recently-used
|
|
90
90
|
# instead of their static `order` field.
|
|
91
|
-
# @return [Array<Hash>] each: { id:, title:, title_zh:, description:, description_zh:, source:, order:, layer:, author:, avatar: }
|
|
91
|
+
# @return [Array<Hash>] each: { id:, title:, title_zh:, description:, description_zh:, source:, order:, layer:, author:, avatar:, hidden: }
|
|
92
92
|
def self.all(recency: {})
|
|
93
93
|
out = {}
|
|
94
94
|
|
|
95
|
-
add = lambda do |id, title, title_zh, description, description_zh, source, order, layer, author, avatar|
|
|
95
|
+
add = lambda do |id, title, title_zh, description, description_zh, source, order, layer, author, avatar, hidden|
|
|
96
96
|
next if id.nil? || id.empty?
|
|
97
97
|
out[id] = {
|
|
98
98
|
id: id,
|
|
@@ -105,13 +105,13 @@ module Clacky
|
|
|
105
105
|
layer: layer,
|
|
106
106
|
author: author,
|
|
107
107
|
avatar: avatar,
|
|
108
|
+
hidden: hidden,
|
|
108
109
|
}
|
|
109
110
|
end
|
|
110
111
|
|
|
111
112
|
ext_result = ExtensionLoader.last_result || ExtensionLoader.load_all
|
|
112
113
|
ext_result&.agents&.each do |unit|
|
|
113
114
|
spec = unit.spec || {}
|
|
114
|
-
next if spec["hidden"]
|
|
115
115
|
title = spec["title"].to_s
|
|
116
116
|
title = unit.id if title.empty?
|
|
117
117
|
avatar = spec["avatar_abs"].to_s.empty? ? nil : "/agent_avatar/#{unit.id}"
|
|
@@ -119,7 +119,7 @@ module Clacky
|
|
|
119
119
|
unit.id, title, spec["title_zh"].to_s,
|
|
120
120
|
spec["description"].to_s, spec["description_zh"].to_s,
|
|
121
121
|
"extension", spec["order"], unit.layer.to_s,
|
|
122
|
-
spec["author"].to_s, avatar
|
|
122
|
+
spec["author"].to_s, avatar, !!spec["hidden"]
|
|
123
123
|
)
|
|
124
124
|
end
|
|
125
125
|
|
|
@@ -134,7 +134,7 @@ module Clacky
|
|
|
134
134
|
id, meta["title"] || meta["name"] || id, meta["title_zh"].to_s,
|
|
135
135
|
meta["description"].to_s, meta["description_zh"].to_s,
|
|
136
136
|
"user", meta["order"], "user",
|
|
137
|
-
meta["author"].to_s.empty? ? "You" : meta["author"].to_s, user_avatar
|
|
137
|
+
meta["author"].to_s.empty? ? "You" : meta["author"].to_s, user_avatar, false
|
|
138
138
|
)
|
|
139
139
|
end
|
|
140
140
|
|
|
@@ -21,6 +21,7 @@ module Clacky
|
|
|
21
21
|
"dsk-deepseek-v4-flash" => "deepseek-v4-flash",
|
|
22
22
|
"dsk-deepseek-v4-flash-vision-exp" => "deepseek-v4-flash-vision-exp",
|
|
23
23
|
# claude via bedrock (abs-)
|
|
24
|
+
"abs-claude-fable-5-1" => "global.anthropic.claude-fable-5-1",
|
|
24
25
|
"abs-claude-fable-5" => "global.anthropic.claude-fable-5",
|
|
25
26
|
"abs-claude-opus-5" => "global.anthropic.claude-opus-5",
|
|
26
27
|
"abs-claude-opus-4-8" => "global.anthropic.claude-opus-4-8",
|
|
@@ -31,7 +32,8 @@ module Clacky
|
|
|
31
32
|
"abs-claude-sonnet-4-5" => "global.anthropic.claude-sonnet-4-5",
|
|
32
33
|
"abs-claude-haiku-4-5" => "global.anthropic.claude-haiku-4-5",
|
|
33
34
|
# gemini chat (or-)
|
|
34
|
-
"or-gemini-3-1-pro"
|
|
35
|
+
"or-gemini-3-1-pro" => "gemini-3.1-pro-preview",
|
|
36
|
+
"or-gemini-3-8-flash" => "gemini-3.8-flash",
|
|
35
37
|
"or-gemini-3-7-flash" => "gemini-3.7-flash",
|
|
36
38
|
"or-gemini-3-6-flash" => "gemini-3.6-flash",
|
|
37
39
|
"or-gemini-3-5-flash" => "gemini-3.5-flash",
|
|
@@ -48,6 +50,7 @@ module Clacky
|
|
|
48
50
|
"or-tts-gemini-2-5-flash" => "gemini-2.5-flash-tts",
|
|
49
51
|
"or-tts-gemini-2-5-pro" => "gemini-2.5-pro-tts",
|
|
50
52
|
# speech-to-text (or-)
|
|
53
|
+
"or-stt-gemini-3-8-flash" => "gemini-3.8-flash",
|
|
51
54
|
"or-stt-gemini-3-7-flash" => "gemini-3.7-flash",
|
|
52
55
|
"or-stt-gemini-3-6-flash" => "gemini-3.6-flash",
|
|
53
56
|
"or-stt-gemini-3-5-flash" => "gemini-3.5-flash",
|
|
@@ -62,6 +65,7 @@ module Clacky
|
|
|
62
65
|
"google/gemini-3-pro-image-preview" => "or-gemini-3-pro-image",
|
|
63
66
|
# STT aliases reuse the chat real id; Hash#invert keeps the later STT
|
|
64
67
|
# key, so pin these back to the chat alias for display.
|
|
68
|
+
"gemini-3.8-flash" => "or-gemini-3-8-flash",
|
|
65
69
|
"gemini-3.7-flash" => "or-gemini-3-7-flash",
|
|
66
70
|
"gemini-3.6-flash" => "or-gemini-3-6-flash",
|
|
67
71
|
"gemini-3.5-flash" => "or-gemini-3-5-flash"
|
|
@@ -80,6 +84,26 @@ module Clacky
|
|
|
80
84
|
ALIAS_TO_REAL[alias_name]
|
|
81
85
|
end
|
|
82
86
|
|
|
87
|
+
# Fetch and merge usage summaries across multiple openclacky keys
|
|
88
|
+
# (e.g. several accounts). The platform bills per key, so summing
|
|
89
|
+
# per-key results yields the combined total. Failing keys
|
|
90
|
+
# (invalid/revoked) are skipped; returns nil only when every key fails.
|
|
91
|
+
def fetch_summary_merged(api_keys, period:, model: nil)
|
|
92
|
+
summaries = Array(api_keys).uniq.map { |key| fetch_summary(key, period: period, model: model) }.compact
|
|
93
|
+
return nil if summaries.empty?
|
|
94
|
+
|
|
95
|
+
summaries.reduce { |acc, summary| merge_summaries(acc, summary) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Fetch and merge daily breakdowns across multiple openclacky keys.
|
|
99
|
+
# Same skip-on-failure semantics as fetch_summary_merged.
|
|
100
|
+
def fetch_daily_merged(api_keys, days:, model: nil)
|
|
101
|
+
dailies = Array(api_keys).uniq.map { |key| fetch_daily(key, days: days, model: model) }.compact
|
|
102
|
+
return nil if dailies.empty?
|
|
103
|
+
|
|
104
|
+
{ days: merge_daily_entries(dailies.flat_map { |daily| daily[:days] }) }
|
|
105
|
+
end
|
|
106
|
+
|
|
83
107
|
# Fetch authoritative usage summary for the platform (openclacky).
|
|
84
108
|
# Returns a hash shaped like BillingStore#summary, or nil on failure.
|
|
85
109
|
def fetch_summary(api_key, period:, model: nil)
|
|
@@ -116,6 +140,65 @@ module Clacky
|
|
|
116
140
|
nil
|
|
117
141
|
end
|
|
118
142
|
|
|
143
|
+
private def merge_summaries(a, b)
|
|
144
|
+
merged = {}
|
|
145
|
+
(a.keys | b.keys).each do |key|
|
|
146
|
+
merged[key] =
|
|
147
|
+
case key
|
|
148
|
+
when :by_model then merge_by_model(a[:by_model], b[:by_model])
|
|
149
|
+
when :by_day then merge_by_day(a[:by_day], b[:by_day])
|
|
150
|
+
else merge_value(a[key], b[key])
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
merged
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private def merge_by_model(a, b)
|
|
157
|
+
merged = {}
|
|
158
|
+
(a || {}).each { |model, entry| merged[model] = entry }
|
|
159
|
+
(b || {}).each do |model, entry|
|
|
160
|
+
merged[model] = merged.key?(model) ? merge_entries(merged[model], entry) : entry
|
|
161
|
+
end
|
|
162
|
+
merged
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private def merge_by_day(a, b)
|
|
166
|
+
merged = {}
|
|
167
|
+
(a || {}).each { |date, cost| merged[date] = cost.to_f }
|
|
168
|
+
(b || {}).each { |date, cost| merged[date] = (merged[date] || 0.0) + cost.to_f }
|
|
169
|
+
merged
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private def merge_daily_entries(entries)
|
|
173
|
+
by_date = {}
|
|
174
|
+
entries.each do |entry|
|
|
175
|
+
next unless entry.is_a?(Hash)
|
|
176
|
+
|
|
177
|
+
date = entry[:date] || entry["date"]
|
|
178
|
+
next if date.nil?
|
|
179
|
+
|
|
180
|
+
by_date[date] = by_date.key?(date) ? merge_entries(by_date[date], entry) : entry
|
|
181
|
+
end
|
|
182
|
+
by_date.values.sort_by { |entry| entry[:date].to_s }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private def merge_entries(a, b)
|
|
186
|
+
merged = {}
|
|
187
|
+
(a.keys | b.keys).each { |key| merged[key] = merge_value(a[key], b[key]) }
|
|
188
|
+
merged
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Sum numeric leaf fields. Non-numeric fields (period, from/to,
|
|
192
|
+
# source) are identical across keys — they come from the same query
|
|
193
|
+
# shape — so the first value wins.
|
|
194
|
+
private def merge_value(a, b)
|
|
195
|
+
return b if a.nil?
|
|
196
|
+
return a if b.nil?
|
|
197
|
+
return a + b if a.is_a?(Numeric) && b.is_a?(Numeric)
|
|
198
|
+
|
|
199
|
+
a
|
|
200
|
+
end
|
|
201
|
+
|
|
119
202
|
# Normalise the platform summary (string keys) to the same shape as
|
|
120
203
|
# BillingStore#summary (symbol keys, symbol-keyed by_model entries).
|
|
121
204
|
private def normalize_summary(data)
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -912,14 +912,15 @@ module Clacky
|
|
|
912
912
|
{ success: false, error: e.message }
|
|
913
913
|
end
|
|
914
914
|
|
|
915
|
-
# Synchronise brand extensions in the background for activated
|
|
915
|
+
# Synchronise brand extensions in the background for activated consumer
|
|
916
|
+
# installs. Brand administrators manage extensions through ext-studio and
|
|
917
|
+
# must not auto-install the extensions they publish for consumers.
|
|
916
918
|
# Mirrors sync_brand_skills_async! but installs into the extension layer.
|
|
917
|
-
# Unlike brand skills, new extensions are auto-installed because a bundled
|
|
918
|
-
# extension is chosen by the brand administrator, not the end user.
|
|
919
919
|
#
|
|
920
920
|
# @return [Thread, nil]
|
|
921
921
|
def sync_brand_extensions_async!(on_complete: nil)
|
|
922
922
|
return nil unless activated?
|
|
923
|
+
return nil if user_licensed?
|
|
923
924
|
return nil if ENV["CLACKY_TEST"] == "1"
|
|
924
925
|
|
|
925
926
|
Clacky::ThreadRegistry.spawn(name: "brand-fetch-extensions") do
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -1349,8 +1349,8 @@ module Clacky
|
|
|
1349
1349
|
|
|
1350
1350
|
# ── Security gate ──────────────────────────────────────────────────────
|
|
1351
1351
|
# Binding to 0.0.0.0 exposes the server to the public network.
|
|
1352
|
-
# Refuse to start unless
|
|
1353
|
-
if options[:host] == "0.0.0.0" && !ENV.key?("CLACKY_ACCESS_KEY")
|
|
1352
|
+
# Refuse to start unless an access key is available.
|
|
1353
|
+
if options[:host] == "0.0.0.0" && !ENV.key?("CLACKY_ACCESS_KEY") && Clacky::AccessKey.from_file.nil?
|
|
1354
1354
|
puts <<~MSG
|
|
1355
1355
|
╔══════════════════════════════════════════════════════════════╗
|
|
1356
1356
|
║ ⚠️ Security Warning: Refusing to start ║
|
|
@@ -1365,6 +1365,8 @@ module Clacky
|
|
|
1365
1365
|
║ Then export it: ║
|
|
1366
1366
|
║ export CLACKY_ACCESS_KEY=<your-generated-key> ║
|
|
1367
1367
|
║ ║
|
|
1368
|
+
║ Or write it to ~/.clacky/access_key ║
|
|
1369
|
+
║ ║
|
|
1368
1370
|
╚══════════════════════════════════════════════════════════════╝
|
|
1369
1371
|
MSG
|
|
1370
1372
|
exit(1)
|