openclacky 1.5.5 → 1.5.7
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/.clacky/skills/gem-release/SKILL.md +18 -1
- data/.clackyrules +25 -1
- data/CHANGELOG.md +58 -0
- data/CONTRIBUTING.md +9 -0
- data/Rakefile +15 -0
- data/docs/design-emit-and-background-skills.md +253 -0
- data/docs/mcp-architecture.md +48 -9
- data/docs/mcp.example.json +9 -0
- data/docs/superpowers/plans/2026-08-09-chatcut-extension-bridge.md +191 -0
- data/docs/superpowers/plans/2026-08-09-native-mcp-oauth.md +243 -0
- data/docs/superpowers/specs/2026-08-09-mcp-oauth-extension-fallback-design.md +115 -0
- data/lib/clacky/agent/cost_tracker.rb +17 -2
- data/lib/clacky/agent/goal_manager.rb +305 -0
- data/lib/clacky/agent/goal_state.rb +88 -0
- data/lib/clacky/agent/hook_manager.rb +14 -3
- data/lib/clacky/agent/memory_updater.rb +17 -18
- data/lib/clacky/agent/message_compressor_helper.rb +23 -0
- data/lib/clacky/agent/session_serializer.rb +162 -25
- data/lib/clacky/agent/skill_auto_creator.rb +1 -2
- data/lib/clacky/agent/skill_evolution.rb +11 -7
- data/lib/clacky/agent/skill_manager.rb +20 -18
- data/lib/clacky/agent/skill_reflector.rb +3 -6
- data/lib/clacky/agent.rb +312 -22
- data/lib/clacky/anthropic_stream_aggregator.rb +6 -1
- data/lib/clacky/cli.rb +19 -15
- data/lib/clacky/client.rb +1 -1
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +29 -1
- data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +88 -26
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +21 -7
- data/lib/clacky/default_searchers/tavily.rb +98 -0
- data/lib/clacky/extension/api_extension.rb +37 -3
- data/lib/clacky/extension/packager.rb +54 -5
- data/lib/clacky/fanout.rb +100 -0
- data/lib/clacky/idle_compression_timer.rb +1 -3
- data/lib/clacky/locales/en.rb +2 -0
- data/lib/clacky/locales/zh.rb +2 -0
- data/lib/clacky/mcp/cli_commands.rb +61 -0
- data/lib/clacky/mcp/client.rb +18 -2
- data/lib/clacky/mcp/connection_manager.rb +85 -0
- data/lib/clacky/mcp/http_transport.rb +25 -9
- data/lib/clacky/mcp/oauth/authorization_manager.rb +257 -0
- data/lib/clacky/mcp/oauth/config.rb +51 -0
- data/lib/clacky/mcp/oauth/credential_store.rb +82 -0
- data/lib/clacky/mcp/oauth/session.rb +88 -0
- data/lib/clacky/message_format/anthropic.rb +27 -13
- data/lib/clacky/message_history.rb +32 -1
- data/lib/clacky/providers.rb +15 -13
- data/lib/clacky/search_config.rb +57 -0
- data/lib/clacky/server/http_server.rb +136 -37
- data/lib/clacky/server/session_registry.rb +21 -18
- data/lib/clacky/server/web_ui_controller.rb +1 -1
- data/lib/clacky/session_manager.rb +13 -11
- data/lib/clacky/shell_hook_loader.rb +14 -8
- data/lib/clacky/tools/invoke_skill.rb +3 -2
- data/lib/clacky/tools/terminal.rb +11 -4
- data/lib/clacky/tools/web_search.rb +45 -6
- data/lib/clacky/ui2/components/command_suggestions.rb +17 -0
- data/lib/clacky/ui2/components/input_area.rb +26 -2
- data/lib/clacky/ui2/progress_handle.rb +27 -3
- data/lib/clacky/ui2/ui_controller.rb +223 -6
- data/lib/clacky/ui_interface.rb +22 -3
- data/lib/clacky/utils/arguments_parser.rb +73 -4
- data/lib/clacky/utils/gitignore_parser.rb +7 -1
- data/lib/clacky/utils/model_pricing.rb +21 -10
- data/lib/clacky/utils/searcher_manager.rb +87 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +243 -33
- data/lib/clacky/web/app.js +12 -1
- data/lib/clacky/web/components/chat-navigator.js +260 -0
- data/lib/clacky/web/features/billing/view.js +2 -2
- data/lib/clacky/web/features/brand/view.js +5 -1
- data/lib/clacky/web/features/extensions/view.js +19 -19
- data/lib/clacky/web/features/new-session/view.js +19 -2
- data/lib/clacky/web/features/skills/store.js +4 -2
- data/lib/clacky/web/i18n.js +64 -14
- data/lib/clacky/web/index.html +40 -9
- data/lib/clacky/web/projects.js +62 -19
- data/lib/clacky/web/sessions.js +329 -115
- data/lib/clacky/web/settings.js +86 -0
- data/lib/clacky/web/skills.js +24 -4
- data/lib/clacky/web/ws-dispatcher.js +86 -35
- data/lib/clacky.rb +4 -0
- metadata +18 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module Mcp
|
|
7
|
+
module OAuth
|
|
8
|
+
class Config
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
attr_reader :resource
|
|
12
|
+
|
|
13
|
+
def self.from_server_spec(spec)
|
|
14
|
+
spec ||= {}
|
|
15
|
+
auth = spec["auth"]
|
|
16
|
+
enabled = auth.is_a?(Hash) && !auth.empty?
|
|
17
|
+
if enabled && auth["type"].to_s != "oauth"
|
|
18
|
+
raise Error, "unsupported MCP authentication type '#{auth['type']}'"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
resource = enabled ? (auth["resource"] || spec["url"]) : spec["url"]
|
|
22
|
+
new(enabled: enabled, resource: resource, endpoint: spec["url"])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(enabled:, resource:, endpoint: nil)
|
|
26
|
+
@enabled = enabled
|
|
27
|
+
@resource = resource.to_s
|
|
28
|
+
@endpoint = (endpoint || resource).to_s
|
|
29
|
+
validate! if enabled?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def enabled?
|
|
33
|
+
@enabled == true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private def validate!
|
|
37
|
+
validate_https_url(@endpoint, "OAuth MCP endpoint")
|
|
38
|
+
validate_https_url(@resource, "OAuth MCP resource")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private def validate_https_url(value, label)
|
|
42
|
+
uri = URI.parse(value)
|
|
43
|
+
raise Error, "#{label} must be an HTTPS URL without credentials" unless
|
|
44
|
+
uri.scheme == "https" && uri.host && !uri.host.empty? && !uri.user && !uri.password
|
|
45
|
+
rescue URI::InvalidURIError
|
|
46
|
+
raise Error, "#{label} must be an HTTPS URL"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "digest"
|
|
6
|
+
|
|
7
|
+
module Clacky
|
|
8
|
+
module Mcp
|
|
9
|
+
module OAuth
|
|
10
|
+
class CredentialStore
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
attr_reader :path
|
|
14
|
+
|
|
15
|
+
def initialize(server_name:, home: Dir.home)
|
|
16
|
+
original_name = server_name.to_s
|
|
17
|
+
safe_name = original_name.gsub(/[^a-zA-Z0-9_.-]/, "_")
|
|
18
|
+
safe_name = "server" if safe_name.empty?
|
|
19
|
+
safe_name = "#{safe_name}-#{Digest::SHA256.hexdigest(original_name)[0, 12]}" if safe_name != original_name
|
|
20
|
+
@path = File.join(File.expand_path(home), ".clacky", "mcp", "oauth", "#{safe_name}.json")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def load
|
|
24
|
+
return nil unless File.file?(@path)
|
|
25
|
+
|
|
26
|
+
with_lock(File::LOCK_SH) do
|
|
27
|
+
value = JSON.parse(File.read(@path))
|
|
28
|
+
raise JSON::ParserError unless value.is_a?(Hash)
|
|
29
|
+
value
|
|
30
|
+
end
|
|
31
|
+
rescue JSON::ParserError
|
|
32
|
+
raise Error, "invalid OAuth credential file; log in again"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def save(value)
|
|
36
|
+
ensure_directory
|
|
37
|
+
with_lock(File::LOCK_EX) do
|
|
38
|
+
temporary = "#{@path}.tmp-#{Process.pid}-#{rand(1_000_000)}"
|
|
39
|
+
File.open(temporary, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
|
|
40
|
+
file.write(JSON.generate(value))
|
|
41
|
+
file.write("\n")
|
|
42
|
+
file.flush
|
|
43
|
+
file.fsync
|
|
44
|
+
end
|
|
45
|
+
File.rename(temporary, @path)
|
|
46
|
+
File.chmod(0o600, @path)
|
|
47
|
+
ensure
|
|
48
|
+
FileUtils.rm_f(temporary) if defined?(temporary) && temporary
|
|
49
|
+
end
|
|
50
|
+
value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def delete
|
|
54
|
+
return unless File.exist?(@path)
|
|
55
|
+
|
|
56
|
+
ensure_directory
|
|
57
|
+
with_lock(File::LOCK_EX) { FileUtils.rm_f(@path) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def inspect
|
|
61
|
+
"#<#{self.class} path=#{@path.inspect}>"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private def ensure_directory
|
|
65
|
+
FileUtils.mkdir_p(File.dirname(@path), mode: 0o700)
|
|
66
|
+
File.chmod(0o700, File.dirname(@path))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private def with_lock(mode)
|
|
70
|
+
ensure_directory
|
|
71
|
+
lock_path = "#{@path}.lock"
|
|
72
|
+
File.open(lock_path, File::RDWR | File::CREAT, 0o600) do |lock|
|
|
73
|
+
lock.flock(mode)
|
|
74
|
+
yield
|
|
75
|
+
ensure
|
|
76
|
+
lock.flock(File::LOCK_UN) rescue nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module Mcp
|
|
7
|
+
module OAuth
|
|
8
|
+
class Session
|
|
9
|
+
EXPIRY_SKEW = 60
|
|
10
|
+
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
class NotConnectedError < Error; end
|
|
13
|
+
|
|
14
|
+
def initialize(store:, manager:, clock: nil)
|
|
15
|
+
@store = store
|
|
16
|
+
@manager = manager
|
|
17
|
+
@clock = clock || -> { Time.now.to_i }
|
|
18
|
+
@force_refresh = false
|
|
19
|
+
@lock = Monitor.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def authorization_headers
|
|
23
|
+
@lock.synchronize do
|
|
24
|
+
grant = @store.load
|
|
25
|
+
raise NotConnectedError, "OAuth MCP is not connected; run `clacky mcp login SERVER`" unless grant
|
|
26
|
+
|
|
27
|
+
if @force_refresh || expiring?(grant)
|
|
28
|
+
grant = @manager.refresh(grant)
|
|
29
|
+
@store.save(grant)
|
|
30
|
+
@force_refresh = false
|
|
31
|
+
end
|
|
32
|
+
token = grant["access_token"].to_s
|
|
33
|
+
raise Error, "stored OAuth grant has no access token; log in again" if token.empty?
|
|
34
|
+
{ "Authorization" => "Bearer #{token}" }
|
|
35
|
+
end
|
|
36
|
+
rescue NotConnectedError, Error
|
|
37
|
+
raise
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
raise Error, "OAuth session failed (#{e.class}); log in again"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def invalidate!
|
|
43
|
+
@lock.synchronize { @force_refresh = true }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def login
|
|
47
|
+
@lock.synchronize do
|
|
48
|
+
grant = @manager.login
|
|
49
|
+
@force_refresh = false
|
|
50
|
+
grant
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def logout
|
|
55
|
+
@lock.synchronize do
|
|
56
|
+
@store.delete
|
|
57
|
+
@force_refresh = false
|
|
58
|
+
end
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def connected?
|
|
63
|
+
!@store.load.nil?
|
|
64
|
+
rescue StandardError
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def status
|
|
69
|
+
grant = @store.load
|
|
70
|
+
return { "connected" => false } unless grant
|
|
71
|
+
{
|
|
72
|
+
"connected" => true,
|
|
73
|
+
"expires_at" => grant["expires_at"],
|
|
74
|
+
"expired" => grant.fetch("expires_at", 0).to_i <= @clock.call
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def inspect
|
|
79
|
+
"#<#{self.class} connected=#{connected?}>"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private def expiring?(grant)
|
|
83
|
+
grant.fetch("expires_at", 0).to_i <= @clock.call + EXPIRY_SKEW
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -19,6 +19,25 @@ module Clacky
|
|
|
19
19
|
module Anthropic
|
|
20
20
|
module_function
|
|
21
21
|
|
|
22
|
+
# Normalise a /v1/messages usage block to the codebase's canonical
|
|
23
|
+
# (OpenAI-style) prompt_tokens: total input including cache_read but
|
|
24
|
+
# excluding cache_creation.
|
|
25
|
+
#
|
|
26
|
+
# Anthropic reports input_tokens as the post-breakpoint tail only, with
|
|
27
|
+
# the cache buckets disjoint from it. Some /v1/messages-compatible
|
|
28
|
+
# gateways instead report input_tokens as the FULL input with both cache
|
|
29
|
+
# buckets already folded in; adding cache_read there would bill the whole
|
|
30
|
+
# cached prefix twice. Detect that by whether the cache buckets already
|
|
31
|
+
# fit inside input_tokens.
|
|
32
|
+
def normalise_prompt_tokens(input_tokens, cache_read, cache_creation)
|
|
33
|
+
cached_input = cache_read + cache_creation
|
|
34
|
+
if cached_input.positive? && input_tokens >= cached_input
|
|
35
|
+
input_tokens - cache_creation
|
|
36
|
+
else
|
|
37
|
+
input_tokens + cache_read
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
22
41
|
# ── Message type identification ───────────────────────────────────────────
|
|
23
42
|
|
|
24
43
|
# Returns true if the message is an Anthropic-native tool result stored in
|
|
@@ -169,22 +188,16 @@ module Clacky
|
|
|
169
188
|
else data["stop_reason"]
|
|
170
189
|
end
|
|
171
190
|
|
|
172
|
-
# Anthropic native `input_tokens` counts ONLY the non-cached, freshly-billed
|
|
173
|
-
# input — cache_read_input_tokens and cache_creation_input_tokens are
|
|
174
|
-
# reported separately and are disjoint from input_tokens.
|
|
175
|
-
#
|
|
176
191
|
# Normalise to the codebase's canonical shape (OpenAI-style) so downstream
|
|
177
192
|
# (ModelPricing.calculate_cost, CostTracker, show_token_usage) stays
|
|
178
193
|
# provider-agnostic:
|
|
179
194
|
#
|
|
180
|
-
# prompt_tokens =
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
# ModelPricing does
|
|
184
|
-
# `regular_input = prompt_tokens - cache_read`.)
|
|
195
|
+
# prompt_tokens = total input incl. cache_read, excl. cache_write
|
|
196
|
+
# (ModelPricing does
|
|
197
|
+
# `regular_input = prompt_tokens - cache_read`.)
|
|
185
198
|
# completion_tokens = output
|
|
186
199
|
# total_tokens = THIS TURN'S new compute volume
|
|
187
|
-
# =
|
|
200
|
+
# = non_cached + cache_creation + output
|
|
188
201
|
# (cache_read is excluded because hits are ~free /
|
|
189
202
|
# already-paid-for; cache_creation IS new work this
|
|
190
203
|
# turn even though it's billed at write_rate.)
|
|
@@ -199,14 +212,15 @@ module Clacky
|
|
|
199
212
|
cache_creation = usage["cache_creation_input_tokens"].to_i
|
|
200
213
|
output_tokens = usage["output_tokens"].to_i
|
|
201
214
|
|
|
202
|
-
prompt_tokens = raw_input_tokens
|
|
215
|
+
prompt_tokens = normalise_prompt_tokens(raw_input_tokens, cache_read, cache_creation)
|
|
203
216
|
|
|
204
217
|
usage_data = {
|
|
205
218
|
prompt_tokens: prompt_tokens,
|
|
206
219
|
completion_tokens: output_tokens,
|
|
207
220
|
# Per-turn new compute: what the server freshly processed this request.
|
|
208
|
-
# Excludes cache_read (nearly free, already-paid-for).
|
|
209
|
-
|
|
221
|
+
# Excludes cache_read (nearly free, already-paid-for). Derived from the
|
|
222
|
+
# normalised prompt_tokens so it stays correct under both conventions.
|
|
223
|
+
total_tokens: (prompt_tokens - cache_read) + cache_creation + output_tokens,
|
|
210
224
|
# Signal to CostTracker: total_tokens above is already the per-turn
|
|
211
225
|
# delta (not a running cumulative like OpenAI's). CostTracker should
|
|
212
226
|
# NOT subtract previous_total when this flag is truthy.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module Clacky
|
|
4
6
|
# MessageHistory wraps the conversation message list and exposes
|
|
5
7
|
# business-meaningful operations instead of raw array manipulation.
|
|
@@ -12,9 +14,18 @@ module Clacky
|
|
|
12
14
|
task_id created_at system_injected session_context memory_update
|
|
13
15
|
subagent_instructions subagent_result subagent_transcript token_usage
|
|
14
16
|
compressed_summary chunk_path truncated transient
|
|
15
|
-
chunk_index chunk_count
|
|
17
|
+
chunk_index chunk_count ext_events
|
|
16
18
|
].freeze
|
|
17
19
|
|
|
20
|
+
# Cap on persisted ext_events per message. These are milestone events
|
|
21
|
+
# (progress chatter is transient), so a handful per message is the norm —
|
|
22
|
+
# the cap only exists to stop a runaway extension. Oldest are dropped first.
|
|
23
|
+
MAX_EXT_EVENTS_PER_MESSAGE = 50
|
|
24
|
+
|
|
25
|
+
# Oversized payloads belong in a file, not in session.json (which is
|
|
26
|
+
# rewritten in full on every save).
|
|
27
|
+
MAX_EXT_EVENT_BYTES = 8 * 1024
|
|
28
|
+
|
|
18
29
|
def initialize(messages = [])
|
|
19
30
|
@messages = messages.dup
|
|
20
31
|
end
|
|
@@ -85,6 +96,26 @@ module Clacky
|
|
|
85
96
|
self
|
|
86
97
|
end
|
|
87
98
|
|
|
99
|
+
# Append a custom extension event onto the most recent message so it is
|
|
100
|
+
# persisted with the session and can be replayed after a reload.
|
|
101
|
+
# Events are anchored to the last message: on replay they are re-emitted
|
|
102
|
+
# right after that message, preserving chronological order.
|
|
103
|
+
# No-op when the history is still empty (nothing to anchor to).
|
|
104
|
+
def append_ext_event(event)
|
|
105
|
+
msg = @messages.last
|
|
106
|
+
return self unless msg
|
|
107
|
+
|
|
108
|
+
if JSON.generate(event).bytesize > MAX_EXT_EVENT_BYTES
|
|
109
|
+
Clacky::Logger.warn("ext event #{event[:type].inspect} dropped: payload exceeds #{MAX_EXT_EVENT_BYTES} bytes")
|
|
110
|
+
return self
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
list = (msg[:ext_events] ||= [])
|
|
114
|
+
list.shift while list.size >= MAX_EXT_EVENTS_PER_MESSAGE
|
|
115
|
+
list << deep_sanitize_utf8(event)
|
|
116
|
+
self
|
|
117
|
+
end
|
|
118
|
+
|
|
88
119
|
# Mutate the last message matching the predicate lambda in-place.
|
|
89
120
|
# Used by execute_skill_with_subagent to update instruction messages.
|
|
90
121
|
def mutate_last_matching(predicate, &block)
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -43,6 +43,7 @@ module Clacky
|
|
|
43
43
|
"dsk-deepseek-v4-pro",
|
|
44
44
|
"dsk-deepseek-v4-flash",
|
|
45
45
|
"or-gemini-3-1-pro",
|
|
46
|
+
"or-gemini-3-6-flash",
|
|
46
47
|
"or-gemini-3-5-flash"
|
|
47
48
|
],
|
|
48
49
|
# Image generation models served by the openclacky platform
|
|
@@ -94,30 +95,34 @@ module Clacky
|
|
|
94
95
|
# routes them to Vertex AI Gemini (generateContent with inline
|
|
95
96
|
# audio parts). The gateway returns transcription text.
|
|
96
97
|
"stt_models" => [
|
|
98
|
+
"or-stt-gemini-3-6-flash",
|
|
97
99
|
"or-stt-gemini-3-5-flash",
|
|
98
100
|
"or-stt-gemini-1-5-pro"
|
|
99
101
|
],
|
|
100
102
|
"stt_model_aliases" => {
|
|
103
|
+
"or-stt-gemini-3-6-flash" => "Gemini 3.6 Flash STT",
|
|
101
104
|
"or-stt-gemini-3-5-flash" => "Gemini 3.5 Flash STT",
|
|
102
105
|
"or-stt-gemini-1-5-pro" => "Gemini 1.5 Pro STT"
|
|
103
106
|
},
|
|
104
|
-
"default_stt_model" => "or-stt-gemini-3-
|
|
107
|
+
"default_stt_model" => "or-stt-gemini-3-6-flash",
|
|
105
108
|
# Video understanding models served by the openclacky gateway, which
|
|
106
109
|
# routes video frames to Gemini (generateContent with inline image
|
|
107
110
|
# parts). The gateway returns analysis text.
|
|
108
111
|
"video_understanding_models" => [
|
|
112
|
+
"or-gemini-3-6-flash",
|
|
109
113
|
"or-gemini-3-5-flash",
|
|
110
114
|
"or-gemini-3-1-pro"
|
|
111
115
|
],
|
|
112
116
|
"video_understanding_model_aliases" => {
|
|
117
|
+
"or-gemini-3-6-flash" => "Gemini 3.6 Flash",
|
|
113
118
|
"or-gemini-3-5-flash" => "Gemini 3.5 Flash",
|
|
114
119
|
"or-gemini-3-1-pro" => "Gemini 3.1 Pro"
|
|
115
120
|
},
|
|
116
|
-
"default_video_understanding_model" => "or-gemini-3-
|
|
121
|
+
"default_video_understanding_model" => "or-gemini-3-6-flash",
|
|
117
122
|
# Default OCR sidecar — used when the primary model is text-only.
|
|
118
123
|
# Candidates are derived from the provider's vision-capable models;
|
|
119
124
|
# this just picks the cheap+fast default to surface in "auto" mode.
|
|
120
|
-
"default_ocr_model" => "or-gemini-3-
|
|
125
|
+
"default_ocr_model" => "or-gemini-3-6-flash",
|
|
121
126
|
# Provider-level default: the Claude family served here is vision-capable.
|
|
122
127
|
"capabilities" => { "vision" => true }.freeze,
|
|
123
128
|
# Model-level overrides: DeepSeek models routed through this provider
|
|
@@ -144,7 +149,7 @@ module Clacky
|
|
|
144
149
|
"abs-claude-sonnet-4-6" => "abs-claude-haiku-4-5",
|
|
145
150
|
"abs-claude-sonnet-4-5" => "abs-claude-haiku-4-5",
|
|
146
151
|
"dsk-deepseek-v4-pro" => "dsk-deepseek-v4-flash",
|
|
147
|
-
"or-gemini-3-1-pro" => "or-gemini-3-
|
|
152
|
+
"or-gemini-3-1-pro" => "or-gemini-3-6-flash"
|
|
148
153
|
},
|
|
149
154
|
# Fallback chain: if a model is unavailable, try the next one in order.
|
|
150
155
|
# Keys are primary model names; values are the fallback model to use instead.
|
|
@@ -427,13 +432,11 @@ module Clacky
|
|
|
427
432
|
"name_key" => "provider.name.volcengine_ark",
|
|
428
433
|
"base_url" => "https://ark.cn-beijing.volces.com/api/v3",
|
|
429
434
|
"api" => "openai-completions",
|
|
430
|
-
"default_model" => "doubao-seed-2.
|
|
435
|
+
"default_model" => "doubao-seed-2.1-pro",
|
|
431
436
|
"models" => [
|
|
432
437
|
"doubao-seed-evolving",
|
|
433
438
|
"doubao-seed-2.1-pro",
|
|
434
439
|
"doubao-seed-2.1-turbo",
|
|
435
|
-
"doubao-seed-2.0-code",
|
|
436
|
-
"doubao-seed-2.0-pro",
|
|
437
440
|
"doubao-seed-2.0-lite",
|
|
438
441
|
"minimax-m3",
|
|
439
442
|
"minimax-m2.7",
|
|
@@ -460,9 +463,7 @@ module Clacky
|
|
|
460
463
|
"deepseek-v4-flash" => "deepseek-v4-flash-260425",
|
|
461
464
|
"doubao-seed-2.1-pro" => "doubao-seed-2-1-pro-260628",
|
|
462
465
|
"doubao-seed-2.1-turbo" => "doubao-seed-2-1-turbo-260628",
|
|
463
|
-
"doubao-seed-2.0-
|
|
464
|
-
"doubao-seed-2.0-lite" => "doubao-seed-2-0-lite-260428",
|
|
465
|
-
"doubao-seed-2.0-code" => "doubao-seed-2-0-code-preview-260215"
|
|
466
|
+
"doubao-seed-2.0-lite" => "doubao-seed-2-0-lite-260428"
|
|
466
467
|
}.freeze
|
|
467
468
|
}.freeze,
|
|
468
469
|
# Most Doubao/multimodal models accept image input; GLM-5.2 and
|
|
@@ -619,7 +620,8 @@ module Clacky
|
|
|
619
620
|
{ pattern: /glm/i, limit: 65_536 }, # GLM-5.2: 128K output ceiling; 64K ample for reasoning+answer
|
|
620
621
|
{ pattern: /kimi-k3/i, limit: 65_536 }, # Kimi K3: max_completion_tokens=131072 (max 1M); 64K ample
|
|
621
622
|
{ pattern: /mimo-v2\.5-pro/i, limit: 65_536 }, # MiMo-V2.5-Pro: max_completion_tokens=131072; 64K ample
|
|
622
|
-
{ pattern: /mimo/i, limit: 32_768 }
|
|
623
|
+
{ pattern: /mimo/i, limit: 32_768 }, # MiMo-V2.5: max_completion_tokens=32768; full default ceiling
|
|
624
|
+
{ pattern: /gemini/i, limit: 65_536 } # Gemini 3.x: 64K max output ceiling
|
|
623
625
|
].freeze
|
|
624
626
|
|
|
625
627
|
class << self
|
|
@@ -991,8 +993,8 @@ module Clacky
|
|
|
991
993
|
# Translate a display model name into the real model id the target
|
|
992
994
|
# endpoint expects. Some providers expose the same model under different
|
|
993
995
|
# ids per billing endpoint (e.g. Volcengine Ark's pay-as-you-go API needs
|
|
994
|
-
# versioned ids like "doubao-seed-2-
|
|
995
|
-
# Plan endpoints accept the short "doubao-seed-2.
|
|
996
|
+
# versioned ids like "doubao-seed-2-1-pro-260628" while the Coding/Agent
|
|
997
|
+
# Plan endpoints accept the short "doubao-seed-2.1-pro"). Users always see
|
|
996
998
|
# and pick the short name; this swaps it just before the request goes out.
|
|
997
999
|
#
|
|
998
1000
|
# Returns the original model unchanged when no alias applies (unknown
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Clacky
|
|
7
|
+
# Search provider selection, backed by ~/.clacky/search.yml.
|
|
8
|
+
#
|
|
9
|
+
# The built-in DuckDuckGo/Bing scrapers need no configuration and stay the
|
|
10
|
+
# 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 higher-quality results (or China-reachable ones) reach for.
|
|
13
|
+
#
|
|
14
|
+
# 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
|
|
18
|
+
#
|
|
19
|
+
# The directory is the registry: dropping my_engine.rb into
|
|
20
|
+
# ~/.clacky/searchers/ and setting `provider: my_engine` is all it takes,
|
|
21
|
+
# mirroring how ~/.clacky/skills/ and ~/.clacky/parsers/ work.
|
|
22
|
+
module SearchConfig
|
|
23
|
+
CONFIG_PATH = File.expand_path("~/.clacky/search.yml").freeze
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# @return [Hash] { "provider" => String, "key" => String }
|
|
27
|
+
def load
|
|
28
|
+
raw = File.exist?(CONFIG_PATH) ? YAML.load_file(CONFIG_PATH) : nil
|
|
29
|
+
return { "provider" => "", "key" => "" } unless raw.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
{ "provider" => raw["provider"].to_s.strip, "key" => raw["key"].to_s.strip }
|
|
32
|
+
rescue StandardError
|
|
33
|
+
{ "provider" => "", "key" => "" }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def save(provider:, key:)
|
|
37
|
+
FileUtils.mkdir_p(File.dirname(CONFIG_PATH))
|
|
38
|
+
File.write(CONFIG_PATH, { "provider" => provider.to_s.strip, "key" => key.to_s.strip }.to_yaml)
|
|
39
|
+
File.chmod(0o600, CONFIG_PATH)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Absolute path of the configured searcher, or nil when unset/missing.
|
|
43
|
+
# A provider naming a script that isn't installed is treated as unset so
|
|
44
|
+
# web_search silently falls back instead of erroring on every search.
|
|
45
|
+
def script_path
|
|
46
|
+
name = load["provider"]
|
|
47
|
+
return nil if name.empty?
|
|
48
|
+
|
|
49
|
+
Clacky::Utils::SearcherManager.path_for(name)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def key
|
|
53
|
+
load["key"]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|