openclacky 1.5.4 → 1.5.6
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 +63 -0
- data/lib/clacky/agent/goal_manager.rb +305 -0
- data/lib/clacky/agent/goal_state.rb +88 -0
- data/lib/clacky/agent/message_compressor_helper.rb +4 -1
- data/lib/clacky/agent/session_serializer.rb +27 -3
- data/lib/clacky/agent.rb +202 -3
- data/lib/clacky/agent_config.rb +4 -2
- data/lib/clacky/agent_profile.rb +25 -3
- data/lib/clacky/cli.rb +15 -15
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +29 -1
- data/lib/clacky/default_extensions/ext-studio/ext.yml +4 -4
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +130 -59
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +12 -4
- data/lib/clacky/default_extensions/git/ext.yml +3 -0
- data/lib/clacky/default_extensions/meeting/ext.yml +3 -0
- data/lib/clacky/default_extensions/time_machine/ext.yml +4 -0
- data/lib/clacky/default_searchers/tavily.rb +98 -0
- data/lib/clacky/default_skills/new/SKILL.md +17 -12
- data/lib/clacky/extension/api_extension.rb +29 -1
- data/lib/clacky/extension/loader.rb +1 -0
- data/lib/clacky/extension/packager.rb +114 -20
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/idle_compression_timer.rb +1 -3
- data/lib/clacky/message_format/anthropic.rb +1 -1
- data/lib/clacky/message_format/bedrock.rb +1 -1
- data/lib/clacky/message_format/open_ai.rb +8 -0
- data/lib/clacky/providers.rb +65 -8
- data/lib/clacky/search_config.rb +57 -0
- data/lib/clacky/server/http_server.rb +249 -31
- data/lib/clacky/server/session_registry.rb +10 -3
- data/lib/clacky/server/web_ui_controller.rb +12 -2
- data/lib/clacky/session_manager.rb +1 -0
- data/lib/clacky/tools/terminal.rb +30 -3
- 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/ui_controller.rb +17 -0
- data/lib/clacky/ui_interface.rb +9 -0
- data/lib/clacky/utils/gitignore_parser.rb +7 -1
- data/lib/clacky/utils/model_pricing.rb +0 -10
- data/lib/clacky/utils/searcher_manager.rb +87 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +530 -197
- data/lib/clacky/web/app.js +28 -3
- 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/store.js +214 -9
- data/lib/clacky/web/features/extensions/view.js +158 -13
- data/lib/clacky/web/features/new-session/store.js +25 -3
- data/lib/clacky/web/features/new-session/view.js +337 -140
- data/lib/clacky/web/features/skills/store.js +4 -2
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/i18n.js +72 -18
- data/lib/clacky/web/index.html +71 -64
- data/lib/clacky/web/projects.js +36 -19
- data/lib/clacky/web/sessions.js +173 -8
- data/lib/clacky/web/settings.js +85 -0
- data/lib/clacky/web/skills.js +24 -4
- data/lib/clacky/web/utils.js +1 -0
- data/lib/clacky/web/ws-dispatcher.js +63 -3
- data/lib/clacky.rb +2 -0
- metadata +7 -1
|
@@ -258,14 +258,31 @@ module Clacky
|
|
|
258
258
|
@http_server&.instance_variable_get(:@registry)
|
|
259
259
|
end
|
|
260
260
|
|
|
261
|
+
def project_manager
|
|
262
|
+
@http_server&.instance_variable_get(:@project_manager)
|
|
263
|
+
end
|
|
264
|
+
|
|
261
265
|
# Create a brand-new session and optionally kick off its first task.
|
|
262
266
|
# Returns the new session_id. When a prompt is given, the task is
|
|
263
267
|
# submitted immediately (the session starts running); display_message
|
|
264
268
|
# controls the user-facing bubble shown in place of the raw prompt.
|
|
269
|
+
# When `project_id` is given, the project's working_dir is inherited
|
|
270
|
+
# (unless an explicit working_dir overrides it) and the session is
|
|
271
|
+
# associated with the project.
|
|
265
272
|
def create_session(name: nil, prompt: nil, working_dir: nil, profile: "general",
|
|
266
|
-
source: :manual, display_message: nil, hidden: false)
|
|
273
|
+
source: :manual, display_message: nil, hidden: false, project_id: nil)
|
|
267
274
|
error!("server not ready", status: 503) unless @http_server
|
|
268
275
|
|
|
276
|
+
if project_id
|
|
277
|
+
project_id = project_id.to_s.strip
|
|
278
|
+
project_id = nil if project_id.empty?
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
project = project_id ? project_manager&.find(project_id) : nil
|
|
282
|
+
error!("Project not found", status: 404) if project_id && project.nil?
|
|
283
|
+
|
|
284
|
+
working_dir = File.expand_path(project[:working_dir]) if working_dir.nil? && project && project[:working_dir].to_s.strip != ""
|
|
285
|
+
|
|
269
286
|
session_id = @http_server.send(
|
|
270
287
|
:build_session,
|
|
271
288
|
name: name,
|
|
@@ -275,8 +292,19 @@ module Clacky
|
|
|
275
292
|
hidden: hidden
|
|
276
293
|
)
|
|
277
294
|
|
|
295
|
+
if project_id
|
|
296
|
+
agent = nil
|
|
297
|
+
registry.with_session(session_id) { |s| agent = s[:agent] }
|
|
298
|
+
if agent
|
|
299
|
+
agent.project_id = project_id
|
|
300
|
+
session_manager&.save(agent.to_session_data)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
278
304
|
submit_task(session_id, prompt, display_message: display_message) if prompt && !prompt.strip.empty?
|
|
279
305
|
|
|
306
|
+
@http_server.send(:broadcast_session_update, session_id)
|
|
307
|
+
|
|
280
308
|
session_id
|
|
281
309
|
end
|
|
282
310
|
|
|
@@ -422,6 +422,7 @@ module Clacky
|
|
|
422
422
|
"description" => spec["description"].to_s,
|
|
423
423
|
"description_zh" => spec["description_zh"].to_s,
|
|
424
424
|
"order" => spec["order"],
|
|
425
|
+
"hidden" => !!spec["hidden"],
|
|
425
426
|
"author" => container[:author],
|
|
426
427
|
"homepage" => container[:homepage],
|
|
427
428
|
"license" => container[:license],
|
|
@@ -18,9 +18,20 @@ 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
|
|
|
32
|
+
# VCS and IDE metadata that must never ship in a pack, even without a .gitignore.
|
|
33
|
+
NEVER_PACK_DIRS = [".git", ".svn", ".hg", ".idea", ".vscode"].freeze
|
|
34
|
+
|
|
24
35
|
Result = Struct.new(:ext_id, :path, :units, keyword_init: true)
|
|
25
36
|
|
|
26
37
|
class Error < StandardError; end
|
|
@@ -48,6 +59,7 @@ module Clacky
|
|
|
48
59
|
File.delete(zip_path) if File.exist?(zip_path)
|
|
49
60
|
|
|
50
61
|
write_zip(container_dir, zip_path)
|
|
62
|
+
enforce_zip_size!(zip_path)
|
|
51
63
|
|
|
52
64
|
Result.new(ext_id: slug, path: zip_path, units: nil)
|
|
53
65
|
end
|
|
@@ -56,12 +68,19 @@ module Clacky
|
|
|
56
68
|
# local zip path or an http(s) URL. Validates the archive (single top
|
|
57
69
|
# container with an ext.yml, no path traversal), extracts it, then runs
|
|
58
70
|
# verify on the resolved installed layer.
|
|
59
|
-
|
|
71
|
+
# on_progress is an optional callable that receives a stage hash, e.g.:
|
|
72
|
+
# { stage: "downloading", progress: 45 }
|
|
73
|
+
# { stage: "extracting" }
|
|
74
|
+
# { stage: "verifying" }
|
|
75
|
+
# Caller can use this for progress reporting without coupling to internals.
|
|
76
|
+
def install(source, installed_dir: Clacky::ExtensionLoader::INSTALLED_DIR, force: false, on_progress: nil)
|
|
60
77
|
Dir.mktmpdir("clacky-ext-install") do |tmp|
|
|
61
|
-
|
|
78
|
+
on_progress&.call({ stage: "downloading", progress: 0 })
|
|
79
|
+
zip_path = local_zip_for(source, tmp, on_progress: on_progress)
|
|
80
|
+
|
|
81
|
+
on_progress&.call({ stage: "extracting" })
|
|
62
82
|
extract_root = File.join(tmp, "unpacked")
|
|
63
83
|
FileUtils.mkdir_p(extract_root)
|
|
64
|
-
|
|
65
84
|
extract_zip(zip_path, extract_root)
|
|
66
85
|
|
|
67
86
|
ext_id, container_src = locate_container(extract_root)
|
|
@@ -74,6 +93,7 @@ module Clacky
|
|
|
74
93
|
FileUtils.mkdir_p(target)
|
|
75
94
|
FileUtils.cp_r(Dir.glob("#{container_src}/*"), target)
|
|
76
95
|
|
|
96
|
+
on_progress&.call({ stage: "verifying" })
|
|
77
97
|
Clacky::ExtensionLoader.invalidate_cache!
|
|
78
98
|
units = verify_installed(ext_id, installed_dir)
|
|
79
99
|
|
|
@@ -124,12 +144,9 @@ module Clacky
|
|
|
124
144
|
end
|
|
125
145
|
|
|
126
146
|
private def write_zip(container_dir, zip_path)
|
|
147
|
+
gitignore = load_container_gitignore(container_dir)
|
|
127
148
|
Zip::File.open(zip_path, create: true) do |zip|
|
|
128
|
-
|
|
129
|
-
files.each do |abs|
|
|
130
|
-
base = File.basename(abs)
|
|
131
|
-
next if base == "." || base == ".."
|
|
132
|
-
|
|
149
|
+
each_packable(container_dir, gitignore) do |abs|
|
|
133
150
|
rel = relative(container_dir, abs)
|
|
134
151
|
next if system_metadata?(rel)
|
|
135
152
|
|
|
@@ -143,17 +160,65 @@ module Clacky
|
|
|
143
160
|
end
|
|
144
161
|
end
|
|
145
162
|
|
|
163
|
+
# Same budget the installer enforces on download/extract - without it a
|
|
164
|
+
# creator could publish something no one (themselves included) can install.
|
|
165
|
+
private def enforce_zip_size!(zip_path)
|
|
166
|
+
size = File.size(zip_path)
|
|
167
|
+
return if size <= MAX_ZIP_SIZE
|
|
168
|
+
|
|
169
|
+
raise Error, "packed zip is #{size} bytes, exceeds #{MAX_ZIP_SIZE} bytes (#{MAX_ZIP_SIZE / 1024 / 1024}MB) limit"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private def load_container_gitignore(container_dir)
|
|
173
|
+
path = File.join(container_dir, ".gitignore")
|
|
174
|
+
Clacky::GitignoreParser.new(path) if File.exist?(path)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Walk the container yielding every path that should ship. Honors .gitignore
|
|
178
|
+
# strictly: ignored files are excluded even when they are config files
|
|
179
|
+
# (.env, secrets) - packing must never leak gitignored content, unlike the
|
|
180
|
+
# search tools which surface config files. Nested .gitignore files are
|
|
181
|
+
# honored per-subtree without leaking rules into sibling directories.
|
|
182
|
+
private def each_packable(dir, gitignore, root: dir)
|
|
183
|
+
Dir.children(dir).sort.each do |name|
|
|
184
|
+
full = File.join(dir, name)
|
|
185
|
+
rel = relative(root, full)
|
|
186
|
+
|
|
187
|
+
if File.directory?(full)
|
|
188
|
+
next if File.symlink?(full)
|
|
189
|
+
next if NEVER_PACK_DIRS.include?(name)
|
|
190
|
+
next if gitignore&.ignored?("#{rel}/")
|
|
191
|
+
|
|
192
|
+
yield full
|
|
193
|
+
each_packable(full, gitignore_for_subdir(full, rel, gitignore), root: root) { |p| yield p }
|
|
194
|
+
else
|
|
195
|
+
next if gitignore&.ignored?(rel)
|
|
196
|
+
|
|
197
|
+
yield full
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private def gitignore_for_subdir(dir, rel, parent)
|
|
203
|
+
nested = File.join(dir, ".gitignore")
|
|
204
|
+
return parent unless File.exist?(nested)
|
|
205
|
+
|
|
206
|
+
merged = parent ? parent.dup : Clacky::GitignoreParser.new(nil)
|
|
207
|
+
merged.merge!(nested, prefix: rel)
|
|
208
|
+
merged
|
|
209
|
+
end
|
|
210
|
+
|
|
146
211
|
# True if any path segment is a platform metadata file/dir (e.g. a nested
|
|
147
212
|
# agents/.DS_Store or a whole __MACOSX/ tree).
|
|
148
213
|
private def system_metadata?(rel)
|
|
149
214
|
rel.split(File::SEPARATOR).any? { |seg| SYSTEM_METADATA.include?(seg) }
|
|
150
215
|
end
|
|
151
216
|
|
|
152
|
-
private def local_zip_for(source, tmp)
|
|
217
|
+
private def local_zip_for(source, tmp, on_progress: nil)
|
|
153
218
|
src = source.to_s
|
|
154
219
|
if src.match?(%r{\Ahttps?://})
|
|
155
220
|
dest = File.join(tmp, "download.zip")
|
|
156
|
-
download(src, dest)
|
|
221
|
+
download(src, dest, on_progress: on_progress)
|
|
157
222
|
dest
|
|
158
223
|
else
|
|
159
224
|
path = File.expand_path(src)
|
|
@@ -162,19 +227,48 @@ module Clacky
|
|
|
162
227
|
end
|
|
163
228
|
end
|
|
164
229
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
230
|
+
# Download url → dest file.
|
|
231
|
+
# IMPORTANT: OpenURI's block form (`URI.open(url) { |io| ... }`) buffers
|
|
232
|
+
# the *entire* response into a StringIO/Tempfile via Net::HTTP's
|
|
233
|
+
# read_body before ever yielding `io` — so reading `io` in chunks after
|
|
234
|
+
# the fact reports progress only after the download has already
|
|
235
|
+
# finished. To get progress callbacks that fire while bytes are still
|
|
236
|
+
# arriving on the socket, we hook OpenURI's own `progress_proc` /
|
|
237
|
+
# `content_length_proc`, which run inside read_body as each chunk lands.
|
|
238
|
+
# Respects whatever proxy env vars the user has configured (some CDN
|
|
239
|
+
# domains are only reachable at usable speed through a proxy).
|
|
240
|
+
private def download(url, dest, on_progress: nil)
|
|
241
|
+
content_length = nil
|
|
242
|
+
|
|
243
|
+
content_length_proc = lambda do |len|
|
|
244
|
+
content_length = len if len&.positive?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
progress_proc = lambda do |size|
|
|
248
|
+
raise Error, "download exceeds #{MAX_ZIP_SIZE} bytes" if size > MAX_ZIP_SIZE
|
|
249
|
+
next unless on_progress
|
|
250
|
+
|
|
251
|
+
pct = if content_length&.positive?
|
|
252
|
+
[(size * 100 / content_length).floor, 99].min
|
|
253
|
+
else
|
|
254
|
+
(90 - 90 * Math.exp(-size.to_f / (1024 * 1024))).floor
|
|
255
|
+
end
|
|
256
|
+
on_progress.call({ stage: "downloading", progress: pct })
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
URI.open(
|
|
260
|
+
url, "rb",
|
|
261
|
+
open_timeout: OPEN_TIMEOUT,
|
|
262
|
+
read_timeout: READ_TIMEOUT,
|
|
263
|
+
content_length_proc: content_length_proc,
|
|
264
|
+
progress_proc: progress_proc
|
|
265
|
+
) do |io| # rubocop:disable Security/Open
|
|
266
|
+
File.open(dest, "wb") { |out| out.write(io.read) }
|
|
175
267
|
end
|
|
176
268
|
rescue OpenURI::HTTPError, SocketError => e
|
|
177
269
|
raise Error, "failed to download #{url}: #{e.message}"
|
|
270
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
271
|
+
raise Error, "download timed out: #{e.message}"
|
|
178
272
|
end
|
|
179
273
|
|
|
180
274
|
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
|
|
@@ -121,9 +121,7 @@ module Clacky
|
|
|
121
121
|
success = @agent.trigger_idle_compression
|
|
122
122
|
|
|
123
123
|
if success && @session_manager
|
|
124
|
-
|
|
125
|
-
original_updated_at = existing&.dig(:updated_at) ? Time.parse(existing[:updated_at].to_s) : nil
|
|
126
|
-
@session_manager.save(@agent.to_session_data(status: :success, updated_at: original_updated_at))
|
|
124
|
+
@session_manager.save(@agent.to_session_data(status: :success))
|
|
127
125
|
end
|
|
128
126
|
|
|
129
127
|
@on_compress&.call(success)
|
|
@@ -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 }
|
|
@@ -279,6 +279,14 @@ module Clacky
|
|
|
279
279
|
elsif !effort_str.empty?
|
|
280
280
|
body[:thinking] = { type: "adaptive" }
|
|
281
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?
|
|
282
290
|
elsif reasoning_effort && !effort_str.empty?
|
|
283
291
|
body[:reasoning_effort] = effort_str
|
|
284
292
|
end
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -427,13 +427,11 @@ module Clacky
|
|
|
427
427
|
"name_key" => "provider.name.volcengine_ark",
|
|
428
428
|
"base_url" => "https://ark.cn-beijing.volces.com/api/v3",
|
|
429
429
|
"api" => "openai-completions",
|
|
430
|
-
"default_model" => "doubao-seed-2.
|
|
430
|
+
"default_model" => "doubao-seed-2.1-pro",
|
|
431
431
|
"models" => [
|
|
432
432
|
"doubao-seed-evolving",
|
|
433
433
|
"doubao-seed-2.1-pro",
|
|
434
434
|
"doubao-seed-2.1-turbo",
|
|
435
|
-
"doubao-seed-2.0-code",
|
|
436
|
-
"doubao-seed-2.0-pro",
|
|
437
435
|
"doubao-seed-2.0-lite",
|
|
438
436
|
"minimax-m3",
|
|
439
437
|
"minimax-m2.7",
|
|
@@ -460,9 +458,7 @@ module Clacky
|
|
|
460
458
|
"deepseek-v4-flash" => "deepseek-v4-flash-260425",
|
|
461
459
|
"doubao-seed-2.1-pro" => "doubao-seed-2-1-pro-260628",
|
|
462
460
|
"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"
|
|
461
|
+
"doubao-seed-2.0-lite" => "doubao-seed-2-0-lite-260428"
|
|
466
462
|
}.freeze
|
|
467
463
|
}.freeze,
|
|
468
464
|
# Most Doubao/multimodal models accept image input; GLM-5.2 and
|
|
@@ -477,6 +473,67 @@ module Clacky
|
|
|
477
473
|
"website_url" => "https://console.volcengine.com/ark/region:cn-beijing/overview"
|
|
478
474
|
}.freeze,
|
|
479
475
|
|
|
476
|
+
"ollama" => {
|
|
477
|
+
"name" => "Ollama (Cloud)",
|
|
478
|
+
# Ollama Cloud API — OpenAI-compatible endpoint serving both local and
|
|
479
|
+
# cloud models. Cloud models are available immediately without pulling.
|
|
480
|
+
# Authentication: API key via Authorization: Bearer <key> header.
|
|
481
|
+
# Create API keys at: https://ollama.com/settings/keys
|
|
482
|
+
"base_url" => "https://ollama.com",
|
|
483
|
+
"api" => "openai-completions",
|
|
484
|
+
"default_model" => "deepseek-v4-flash",
|
|
485
|
+
# Curated list of cloud-enabled models from the Ollama library.
|
|
486
|
+
# Users can type any Ollama model name manually; this list seeds the picker.
|
|
487
|
+
"models" => [
|
|
488
|
+
"glm-5.2",
|
|
489
|
+
"kimi-k3",
|
|
490
|
+
"gemma4",
|
|
491
|
+
"qwen3.5",
|
|
492
|
+
"glm-5.1",
|
|
493
|
+
"minimax-m2.7",
|
|
494
|
+
"nemotron-3-super",
|
|
495
|
+
"minimax-m3",
|
|
496
|
+
"kimi-k2.7-code",
|
|
497
|
+
"kimi-k2.6",
|
|
498
|
+
"deepseek-v4-flash",
|
|
499
|
+
"deepseek-v4-pro",
|
|
500
|
+
"nemotron-3-ultra",
|
|
501
|
+
"gemini-3-flash-preview",
|
|
502
|
+
"nemotron-3-nano",
|
|
503
|
+
"mistral-large-3",
|
|
504
|
+
"gpt-oss"
|
|
505
|
+
],
|
|
506
|
+
# Provider-level default: many Ollama cloud models are vision-capable.
|
|
507
|
+
"capabilities" => { "vision" => true }.freeze,
|
|
508
|
+
# Model-level overrides: text-only models that don't accept image input.
|
|
509
|
+
"model_capabilities" => {
|
|
510
|
+
"glm-5.2" => { "vision" => false }.freeze,
|
|
511
|
+
"glm-5.1" => { "vision" => false }.freeze,
|
|
512
|
+
"minimax-m2.7" => { "vision" => false }.freeze,
|
|
513
|
+
"deepseek-v4-pro" => { "vision" => false }.freeze,
|
|
514
|
+
"deepseek-v4-flash" => { "vision" => false }.freeze,
|
|
515
|
+
"nemotron-3-super" => { "vision" => false }.freeze,
|
|
516
|
+
"nemotron-3-ultra" => { "vision" => false }.freeze,
|
|
517
|
+
"nemotron-3-nano" => { "vision" => false }.freeze,
|
|
518
|
+
"gpt-oss" => { "vision" => false }.freeze
|
|
519
|
+
}.freeze,
|
|
520
|
+
# Per-primary lite pairing: subagents use smaller models for cheap/fast work.
|
|
521
|
+
"lite_models" => {
|
|
522
|
+
"glm-5.2" => "glm-5.1",
|
|
523
|
+
"kimi-k3" => "kimi-k2.6",
|
|
524
|
+
"gemma4" => "nemotron-3-nano",
|
|
525
|
+
"qwen3.5" => "gemini-3-flash-preview",
|
|
526
|
+
"minimax-m3" => "minimax-m2.7",
|
|
527
|
+
"kimi-k2.7-code" => "kimi-k2.6",
|
|
528
|
+
"deepseek-v4-pro" => "deepseek-v4-flash",
|
|
529
|
+
"mistral-large-3" => "nemotron-3-nano",
|
|
530
|
+
"gpt-oss" => "nemotron-3-nano",
|
|
531
|
+
"gemini-3-flash-preview" => "nemotron-3-nano"
|
|
532
|
+
},
|
|
533
|
+
"default_ocr_model" => "kimi-k2.7-code",
|
|
534
|
+
"website_url" => "https://ollama.com/settings/keys"
|
|
535
|
+
}.freeze,
|
|
536
|
+
|
|
480
537
|
"openai" => {
|
|
481
538
|
"name" => "OpenAI (GPT)",
|
|
482
539
|
"base_url" => "https://api.openai.com/v1",
|
|
@@ -930,8 +987,8 @@ module Clacky
|
|
|
930
987
|
# Translate a display model name into the real model id the target
|
|
931
988
|
# endpoint expects. Some providers expose the same model under different
|
|
932
989
|
# ids per billing endpoint (e.g. Volcengine Ark's pay-as-you-go API needs
|
|
933
|
-
# versioned ids like "doubao-seed-2-
|
|
934
|
-
# Plan endpoints accept the short "doubao-seed-2.
|
|
990
|
+
# versioned ids like "doubao-seed-2-1-pro-260628" while the Coding/Agent
|
|
991
|
+
# Plan endpoints accept the short "doubao-seed-2.1-pro"). Users always see
|
|
935
992
|
# and pick the short name; this swaps it just before the request goes out.
|
|
936
993
|
#
|
|
937
994
|
# 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
|