openclacky 1.3.6 → 1.3.8
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 +54 -0
- data/lib/clacky/agent/hook_manager.rb +10 -1
- data/lib/clacky/agent/system_prompt_builder.rb +1 -1
- data/lib/clacky/agent.rb +17 -3
- data/lib/clacky/agent_profile.rb +124 -53
- data/lib/clacky/billing/billing_store.rb +3 -2
- data/lib/clacky/brand_config.rb +309 -6
- data/lib/clacky/cli.rb +4 -57
- data/lib/clacky/default_extensions/coding/agents/coding/avatar.png +0 -0
- data/lib/clacky/default_extensions/coding/ext.yml +19 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/avatar.png +0 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
- data/lib/clacky/default_extensions/ext-studio/ext.yml +33 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1288 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
- data/lib/clacky/default_extensions/general/agents/general/avatar.png +0 -0
- data/lib/clacky/default_extensions/general/ext.yml +19 -0
- data/lib/clacky/default_extensions/git/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +4 -5
- data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
- data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
- data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -90
- data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
- data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
- data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
- data/lib/clacky/extension/api_loader.rb +136 -0
- data/lib/clacky/extension/cli_commands.rb +226 -0
- data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
- data/lib/clacky/extension/hook_loader.rb +77 -0
- data/lib/clacky/extension/loader.rb +489 -0
- data/lib/clacky/extension/packager.rb +226 -0
- data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
- data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
- data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
- data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
- data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
- data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
- data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
- data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
- data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
- data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
- data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
- data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
- data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
- data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
- data/lib/clacky/extension/scaffold.rb +55 -0
- data/lib/clacky/extension/verifier.rb +196 -0
- data/lib/clacky/identity.rb +70 -0
- data/lib/clacky/locales/en.rb +19 -1
- data/lib/clacky/locales/zh.rb +19 -1
- data/lib/clacky/platform_http_client.rb +18 -15
- data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
- data/lib/clacky/server/channel.rb +6 -0
- data/lib/clacky/server/http_server.rb +439 -146
- data/lib/clacky/server/session_registry.rb +9 -2
- data/lib/clacky/shell_hook_loader.rb +266 -22
- data/lib/clacky/skill_loader.rb +28 -1
- data/lib/clacky/ui2/components/welcome_banner.rb +1 -1
- data/lib/clacky/ui2/ui_controller.rb +9 -6
- data/lib/clacky/utils/workspace_rules.rb +2 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1034 -675
- data/lib/clacky/web/app.js +63 -15
- data/lib/clacky/web/auth.js +2 -0
- data/lib/clacky/web/components/notify.js +2 -0
- data/lib/clacky/web/components/sidebar.js +1 -5
- data/lib/clacky/web/core/ext.js +432 -89
- data/lib/clacky/web/features/backup/store.js +1 -0
- data/lib/clacky/web/features/backup/view.js +0 -1
- data/lib/clacky/web/features/brand/view.js +13 -5
- data/lib/clacky/web/features/extensions/store.js +184 -0
- data/lib/clacky/web/features/extensions/view.js +390 -0
- data/lib/clacky/web/features/new-session/store.js +199 -0
- data/lib/clacky/web/features/new-session/view.js +619 -0
- data/lib/clacky/web/features/share/view.js +63 -14
- data/lib/clacky/web/features/skills/store.js +3 -1
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/features/tasks/view.js +12 -0
- data/lib/clacky/web/features/trash/store.js +24 -6
- data/lib/clacky/web/features/trash/view.js +48 -12
- data/lib/clacky/web/features/workspace/store.js +2 -0
- data/lib/clacky/web/features/workspace/view.js +0 -1
- data/lib/clacky/web/i18n.js +152 -106
- data/lib/clacky/web/index.html +172 -190
- data/lib/clacky/web/sessions.js +67 -272
- data/lib/clacky/web/settings.js +1 -1
- data/lib/clacky/web/skills.js +88 -48
- data/lib/clacky/web/theme.js +5 -0
- data/lib/clacky/web/ws-dispatcher.js +12 -2
- data/lib/clacky/web/ws.js +2 -0
- data/lib/clacky.rb +21 -10
- metadata +52 -21
- data/benchmark/runner.rb +0 -441
- data/lib/clacky/api_extension_loader.rb +0 -168
- data/lib/clacky/default_agents/SOUL.md +0 -3
- data/lib/clacky/default_agents/USER.md +0 -1
- data/lib/clacky/default_agents/coding/profile.yml +0 -5
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_agents/general/profile.yml +0 -2
- data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
- data/lib/clacky/web/features/creator/store.js +0 -81
- data/lib/clacky/web/features/creator/view.js +0 -380
- /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
|
@@ -367,14 +367,12 @@ module Clacky
|
|
|
367
367
|
res["Cache-Control"] = "no-store"
|
|
368
368
|
res["Pragma"] = "no-cache"
|
|
369
369
|
res.body = html
|
|
370
|
-
elsif req.path.start_with?("/webui_ext/")
|
|
371
|
-
self.send(:serve_webui_ext, req, res)
|
|
372
|
-
elsif req.path.start_with?("/builtin_ext/")
|
|
373
|
-
self.send(:serve_builtin_ext, req, res)
|
|
374
370
|
elsif req.path.start_with?("/agent_ui/")
|
|
375
371
|
self.send(:serve_agent_ui, req, res)
|
|
376
|
-
elsif req.path.start_with?("/
|
|
377
|
-
self.send(:
|
|
372
|
+
elsif req.path.start_with?("/agent_avatar/")
|
|
373
|
+
self.send(:serve_agent_avatar, req, res)
|
|
374
|
+
elsif req.path.start_with?("/ext_ui/")
|
|
375
|
+
self.send(:serve_ext_ui, req, res)
|
|
378
376
|
else
|
|
379
377
|
file_handler.service(req, res)
|
|
380
378
|
res["Cache-Control"] = "no-store"
|
|
@@ -385,12 +383,18 @@ module Clacky
|
|
|
385
383
|
# Auto-create a default session on startup
|
|
386
384
|
create_default_session
|
|
387
385
|
|
|
388
|
-
# Load
|
|
389
|
-
#
|
|
390
|
-
#
|
|
391
|
-
#
|
|
386
|
+
# Load api backends contributed by ext.yml containers into the shared
|
|
387
|
+
# ApiExtension registry. Each unit mounts at /api/ext/<ext_id>/. Done
|
|
388
|
+
# here (not at gem load) so handlers can resolve session_manager and
|
|
389
|
+
# other host helpers as soon as they are wired up.
|
|
392
390
|
Clacky::ApiExtensionLoader.load_all
|
|
393
391
|
|
|
392
|
+
# Static verification of every ext.yml container: unknown keys, bad
|
|
393
|
+
# scopes, dangling agent→panel references, layer-shadow warnings.
|
|
394
|
+
# Errors from the loader itself are already surfaced elsewhere; this
|
|
395
|
+
# pass covers the whole-program checks an author needs to see at boot.
|
|
396
|
+
report_extension_issues
|
|
397
|
+
|
|
394
398
|
# Start the background scheduler
|
|
395
399
|
@scheduler.start
|
|
396
400
|
puts " Scheduler: #{@scheduler.schedules.size} task(s) loaded"
|
|
@@ -490,9 +494,9 @@ module Clacky
|
|
|
490
494
|
path = req.path
|
|
491
495
|
method = req.request_method
|
|
492
496
|
|
|
493
|
-
#
|
|
494
|
-
#
|
|
495
|
-
#
|
|
497
|
+
# HTTP API extensions declared in an ext.yml container mount at
|
|
498
|
+
# /api/ext/<ext_id>/... Routed through a separate dispatcher so the
|
|
499
|
+
# host's giant case table stays focused on built-in endpoints.
|
|
496
500
|
if path.start_with?(Clacky::Server::ApiExtensionDispatcher::MOUNT_PREFIX)
|
|
497
501
|
return if Clacky::Server::ApiExtensionDispatcher.handle(req, res, http_server: self)
|
|
498
502
|
end
|
|
@@ -503,6 +507,7 @@ module Clacky
|
|
|
503
507
|
when ["GET", "/api/cron-tasks"] then api_list_cron_tasks(res)
|
|
504
508
|
when ["POST", "/api/cron-tasks"] then api_create_cron_task(req, res)
|
|
505
509
|
when ["GET", "/api/skills"] then api_list_skills(res)
|
|
510
|
+
when ["GET", "/api/agents"] then api_list_agents(res)
|
|
506
511
|
when ["GET", "/api/config"] then api_get_config(req, res)
|
|
507
512
|
when ["GET", "/api/config/settings"] then api_get_settings(res)
|
|
508
513
|
when ["GET", "/api/exchange-rate"] then api_exchange_rate(req, res)
|
|
@@ -533,6 +538,11 @@ module Clacky
|
|
|
533
538
|
when ["POST", "/api/onboard/complete"] then api_onboard_complete(req, res)
|
|
534
539
|
when ["POST", "/api/onboard/skip-soul"] then api_onboard_skip_soul(req, res)
|
|
535
540
|
when ["GET", "/api/store/skills"] then api_store_skills(res)
|
|
541
|
+
when ["GET", "/api/store/extensions"] then api_store_extensions(req, res)
|
|
542
|
+
when ["GET", "/api/store/extension"] then api_store_extension_detail(req, res)
|
|
543
|
+
when ["POST", "/api/store/extension/disable"] then api_store_extension_disable(req, res)
|
|
544
|
+
when ["POST", "/api/store/extension/enable"] then api_store_extension_enable(req, res)
|
|
545
|
+
when ["DELETE", "/api/store/extension"] then api_store_extension_uninstall(req, res)
|
|
536
546
|
when ["GET", "/api/brand/status"] then api_brand_status(res)
|
|
537
547
|
when ["POST", "/api/brand/activate"] then api_brand_activate(req, res)
|
|
538
548
|
when ["DELETE", "/api/brand/license"] then api_brand_deactivate(res)
|
|
@@ -615,6 +625,9 @@ module Clacky
|
|
|
615
625
|
elsif method == "GET" && path.match?(%r{^/api/sessions/[^/]+/skills$})
|
|
616
626
|
session_id = path.sub("/api/sessions/", "").sub("/skills", "")
|
|
617
627
|
api_session_skills(session_id, res)
|
|
628
|
+
elsif method == "GET" && path.match?(%r{^/api/agents/[^/]+/skills$})
|
|
629
|
+
agent_id = path[%r{^/api/agents/([^/]+)/skills$}, 1]
|
|
630
|
+
api_agent_skills(agent_id, res)
|
|
618
631
|
elsif method == "GET" && path.match?(%r{^/api/sessions/[^/]+/git/[a-z]+$})
|
|
619
632
|
session_id = path[%r{^/api/sessions/([^/]+)/git/}, 1]
|
|
620
633
|
action = path[%r{/git/([a-z]+)$}, 1]
|
|
@@ -997,7 +1010,13 @@ module Clacky
|
|
|
997
1010
|
api_key: data["api_key"],
|
|
998
1011
|
base_url: data["base_url"],
|
|
999
1012
|
model: data["default_model"]
|
|
1000
|
-
)
|
|
1013
|
+
) if data["api_key"]
|
|
1014
|
+
if data["device_token"]
|
|
1015
|
+
Clacky::Identity.load.bind!(
|
|
1016
|
+
device_token: data["device_token"],
|
|
1017
|
+
user_id: data["user_id"]
|
|
1018
|
+
)
|
|
1019
|
+
end
|
|
1001
1020
|
json_response(res, 200, {
|
|
1002
1021
|
ok: true,
|
|
1003
1022
|
status: "approved",
|
|
@@ -1039,28 +1058,42 @@ module Clacky
|
|
|
1039
1058
|
@agent_config.save
|
|
1040
1059
|
end
|
|
1041
1060
|
|
|
1042
|
-
# Absolute path to the user's WebUI extension directory.
|
|
1043
|
-
WEBUI_EXT_ROOT = File.expand_path("~/.clacky/webui_ext")
|
|
1044
|
-
|
|
1045
|
-
# Built-in extensions bundled with the gem (default_extensions/*/**.js).
|
|
1046
|
-
BUILTIN_EXT_ROOT = File.expand_path("../default_extensions", __dir__)
|
|
1047
|
-
|
|
1048
1061
|
# Build the full <script> payload injected at {{EXT_SCRIPTS}}:
|
|
1049
|
-
# 1.
|
|
1050
|
-
# 2. agent-scoped UI — agents/<name>/webui/**/*.js
|
|
1051
|
-
# 3.
|
|
1052
|
-
#
|
|
1053
|
-
# agent profiles reference each official panel. Never raises.
|
|
1062
|
+
# 1. panel→agents registry (which agent profiles reference each panel)
|
|
1063
|
+
# 2. agent-scoped UI — user ~/.clacky/agents/<name>/webui/**/*.js (data-agent)
|
|
1064
|
+
# 3. ext.yml containers — panel view.js served from /ext_ui/
|
|
1065
|
+
# Never raises.
|
|
1054
1066
|
private def webui_ext_script_tags
|
|
1055
1067
|
[
|
|
1056
1068
|
panel_agents_script,
|
|
1057
|
-
|
|
1058
|
-
global_ext_script_tags,
|
|
1069
|
+
ext_contributions_script,
|
|
1059
1070
|
agent_webui_script_tags,
|
|
1060
|
-
|
|
1071
|
+
container_ext_script_tags,
|
|
1061
1072
|
].reject(&:empty?).join("\n")
|
|
1062
1073
|
end
|
|
1063
1074
|
|
|
1075
|
+
# Container extensions (ext.yml): inject each declared panel's view.js.
|
|
1076
|
+
# Visibility is entirely driven by `agent.panels: [id]` references in
|
|
1077
|
+
# profile.yml / ext.yml agents (see `panel_agents_map`). A panel that no
|
|
1078
|
+
# agent references stays hidden — panels never mount themselves.
|
|
1079
|
+
# Served from /ext_ui/<ext_id>/<rel> with no-store so edits go live on
|
|
1080
|
+
# page refresh. Never raises.
|
|
1081
|
+
private def container_ext_script_tags
|
|
1082
|
+
result = Clacky::ExtensionLoader.load_all
|
|
1083
|
+
result.panels.map do |unit|
|
|
1084
|
+
rel = unit.spec["view"]
|
|
1085
|
+
ext_id = "ext/#{unit.ext_id}/#{unit.id}"
|
|
1086
|
+
panel_id = "#{unit.ext_id}/#{unit.id}"
|
|
1087
|
+
src = "/ext_ui/#{unit.ext_id}/#{rel}"
|
|
1088
|
+
"<script>Clacky.ext._extBegin(#{ext_id.to_json}, #{nil.to_json}, #{panel_id.to_json})</script>" \
|
|
1089
|
+
"<script src=#{src.to_json} data-ext-id=#{ext_id.to_json}></script>" \
|
|
1090
|
+
"<script>Clacky.ext._extEnd()</script>"
|
|
1091
|
+
end.join("\n")
|
|
1092
|
+
rescue StandardError => e
|
|
1093
|
+
Clacky::Logger.warn("[ExtensionLoader] panel injection failed: #{e.message}")
|
|
1094
|
+
""
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1064
1097
|
# Inline script registering the panel→agents map (which agent profiles
|
|
1065
1098
|
# reference each official panel, from their profile.yml `panels:`).
|
|
1066
1099
|
private def panel_agents_script
|
|
@@ -1070,62 +1103,116 @@ module Clacky
|
|
|
1070
1103
|
"<script>Clacky.ext.registerPanelAgents(#{map.to_json})</script>"
|
|
1071
1104
|
end
|
|
1072
1105
|
|
|
1073
|
-
#
|
|
1074
|
-
#
|
|
1106
|
+
# Inline script mapping agent id => the panels & skills its extension
|
|
1107
|
+
# contributes (straight from ext.yml). Built-in extensions are excluded so
|
|
1108
|
+
# the new-session page only advertises what a third-party agent adds.
|
|
1109
|
+
private def ext_contributions_script
|
|
1110
|
+
map = ext_contributions_map
|
|
1111
|
+
return "" if map.empty?
|
|
1112
|
+
|
|
1113
|
+
"<script>Clacky.ext.registerAgentContributions(#{map.to_json})</script>"
|
|
1114
|
+
end
|
|
1115
|
+
|
|
1116
|
+
private def ext_contributions_map
|
|
1117
|
+
result = Clacky::ExtensionLoader.load_all
|
|
1118
|
+
by_ext = Hash.new { |h, k| h[k] = { "panels" => [], "skills" => [] } }
|
|
1119
|
+
label = lambda do |unit|
|
|
1120
|
+
{ "id" => unit.id, "title" => unit.spec["title"] || unit.id, "title_zh" => unit.spec["title_zh"] }
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
result.panels.each { |u| by_ext[u.ext_id]["panels"] << label.call(u) unless u.layer == :builtin }
|
|
1124
|
+
result.skills.each { |u| by_ext[u.ext_id]["skills"] << label.call(u) unless u.layer == :builtin }
|
|
1125
|
+
|
|
1126
|
+
out = {}
|
|
1127
|
+
result.agents.each do |u|
|
|
1128
|
+
next if u.layer == :builtin
|
|
1129
|
+
contrib = by_ext[u.ext_id]
|
|
1130
|
+
next if contrib["panels"].empty? && contrib["skills"].empty?
|
|
1131
|
+
out[u.id] = contrib
|
|
1132
|
+
end
|
|
1133
|
+
out
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
# panel id => list of agent names allowed to see it. Two sources merge:
|
|
1137
|
+
# 1. `agent.panels: [id]` — agent-side explicit reference (opt-in mount)
|
|
1138
|
+
# 2. `panel.attach: [...]` — panel-author's default suggestion
|
|
1139
|
+
# - `attach: [foo]` → visible to agent `foo`
|
|
1140
|
+
# - `attach: ["*"]` → visible to every known agent
|
|
1141
|
+
# - omitted → hidden unless (1) references it
|
|
1142
|
+
# A panel with no source stays hidden.
|
|
1075
1143
|
private def panel_agents_map
|
|
1076
1144
|
result = Hash.new { |h, k| h[k] = [] }
|
|
1145
|
+
add = ->(pid, agent) { result[pid] << agent unless result[pid].include?(agent) }
|
|
1146
|
+
|
|
1147
|
+
agent_data = agent_profile_data
|
|
1148
|
+
agent_data.each do |name, data|
|
|
1149
|
+
ext_id = data["_ext_id"]
|
|
1150
|
+
Array(data["panels"]).each do |panel|
|
|
1151
|
+
pid = panel.to_s
|
|
1152
|
+
full = pid.include?("/") ? pid : (ext_id ? "#{ext_id}/#{pid}" : pid)
|
|
1153
|
+
add.call(full, name)
|
|
1154
|
+
end
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
all_agent_ids = agent_data.keys
|
|
1158
|
+
Array(Clacky::ExtensionLoader.last_result&.panels).each do |unit|
|
|
1159
|
+
full_id = "#{unit.ext_id}/#{unit.id}"
|
|
1160
|
+
attach = Array(unit.spec["attach"])
|
|
1161
|
+
next if attach.empty?
|
|
1162
|
+
if attach.include?("*")
|
|
1163
|
+
all_agent_ids.each { |a| add.call(full_id, a) }
|
|
1164
|
+
else
|
|
1165
|
+
attach.each { |a| add.call(full_id, a) if all_agent_ids.include?(a) }
|
|
1166
|
+
end
|
|
1167
|
+
end
|
|
1168
|
+
result
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
# { agent_name => parsed_profile_data } merged across built-in, user, and
|
|
1172
|
+
# ext-contributed agents. Override order: ext < user (ext provides defaults
|
|
1173
|
+
# for ids not in user dirs; user dirs win on collision).
|
|
1174
|
+
private def agent_profile_data
|
|
1175
|
+
data = {}
|
|
1176
|
+
Array(Clacky::ExtensionLoader.last_result&.agents).each do |unit|
|
|
1177
|
+
data[unit.id] = {
|
|
1178
|
+
"name" => unit.id,
|
|
1179
|
+
"description" => unit.spec["description"],
|
|
1180
|
+
"panels" => unit.spec["panels"],
|
|
1181
|
+
"skills" => unit.spec["skills"],
|
|
1182
|
+
"_ext_id" => unit.ext_id,
|
|
1183
|
+
}
|
|
1184
|
+
end
|
|
1077
1185
|
agent_profile_dirs.each do |name, dir|
|
|
1078
1186
|
yml = File.join(dir, "profile.yml")
|
|
1079
1187
|
next unless File.file?(yml)
|
|
1080
1188
|
|
|
1081
|
-
|
|
1189
|
+
parsed = begin
|
|
1082
1190
|
YAML.safe_load(File.read(yml)) || {}
|
|
1083
1191
|
rescue StandardError
|
|
1084
1192
|
{}
|
|
1085
1193
|
end
|
|
1086
|
-
|
|
1194
|
+
data[name] = parsed
|
|
1087
1195
|
end
|
|
1088
|
-
|
|
1196
|
+
data
|
|
1089
1197
|
end
|
|
1090
1198
|
|
|
1091
|
-
# { agent_name => resolved_dir }
|
|
1092
|
-
#
|
|
1199
|
+
# { agent_name => resolved_dir } for user-side agent overrides only.
|
|
1200
|
+
# Extension-provided agents are served through the extension pipeline.
|
|
1093
1201
|
private def agent_profile_dirs
|
|
1094
1202
|
dirs = {}
|
|
1095
|
-
|
|
1096
|
-
|
|
1203
|
+
root = Clacky::AgentProfile::USER_AGENTS_DIR
|
|
1204
|
+
return dirs unless Dir.exist?(root)
|
|
1097
1205
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1206
|
+
Dir.children(root).each do |name|
|
|
1207
|
+
next if name.start_with?("_")
|
|
1208
|
+
full = File.join(root, name)
|
|
1209
|
+
next unless File.directory?(full) && File.file?(File.join(full, "profile.yml"))
|
|
1102
1210
|
|
|
1103
|
-
|
|
1104
|
-
end
|
|
1211
|
+
dirs[name] = full
|
|
1105
1212
|
end
|
|
1106
1213
|
dirs
|
|
1107
1214
|
end
|
|
1108
1215
|
|
|
1109
|
-
# Built-in extensions from default_extensions/*/*.js — loaded before user
|
|
1110
|
-
# extensions so user ~/.clacky/webui_ext/ can override by ext id.
|
|
1111
|
-
private def builtin_ext_script_tags
|
|
1112
|
-
return "" unless Dir.exist?(BUILTIN_EXT_ROOT)
|
|
1113
|
-
|
|
1114
|
-
Dir.glob(File.join(BUILTIN_EXT_ROOT, "*", "**", "*.js")).sort.map do |abs|
|
|
1115
|
-
rel = abs.delete_prefix(BUILTIN_EXT_ROOT + "/")
|
|
1116
|
-
ext_id = "builtin/" + rel.delete_suffix(".js")
|
|
1117
|
-
src = "/builtin_ext/#{rel}"
|
|
1118
|
-
"<script>Clacky.ext._extBegin(#{ext_id.to_json}, #{nil.to_json}, #{nil.to_json})</script>" \
|
|
1119
|
-
"<script src=#{src.to_json} data-ext-id=#{ext_id.to_json}></script>" \
|
|
1120
|
-
"<script>Clacky.ext._extEnd()</script>"
|
|
1121
|
-
end.join("\n")
|
|
1122
|
-
end
|
|
1123
|
-
|
|
1124
|
-
# Global extensions — visible for all agents (unchanged legacy behavior).
|
|
1125
|
-
private def global_ext_script_tags
|
|
1126
|
-
ext_script_block(WEBUI_EXT_ROOT, "/webui_ext")
|
|
1127
|
-
end
|
|
1128
|
-
|
|
1129
1216
|
# Agent-scoped UI: agents/<name>/webui/**/*.js. Each script is tagged with
|
|
1130
1217
|
# its owning agent so the client only mounts it for that profile.
|
|
1131
1218
|
private def agent_webui_script_tags
|
|
@@ -1137,21 +1224,6 @@ module Clacky
|
|
|
1137
1224
|
end.reject(&:empty?).join("\n")
|
|
1138
1225
|
end
|
|
1139
1226
|
|
|
1140
|
-
# Official panels: default_agents/_panels/<id>/**/*.js. Scope comes from
|
|
1141
|
-
# the panel→agents map (registerPanelAgents), so a panel only mounts for
|
|
1142
|
-
# agents whose profile.yml references it.
|
|
1143
|
-
private def official_panel_script_tags
|
|
1144
|
-
root = File.join(Clacky::AgentProfile::DEFAULT_AGENTS_DIR, "_panels")
|
|
1145
|
-
return "" unless Dir.exist?(root)
|
|
1146
|
-
|
|
1147
|
-
Dir.children(root).sort.filter_map do |panel|
|
|
1148
|
-
dir = File.join(root, panel)
|
|
1149
|
-
next unless File.directory?(dir)
|
|
1150
|
-
|
|
1151
|
-
ext_script_block(dir, "/panel_ui/#{panel}", id_prefix: "panel/#{panel}/", panel: panel)
|
|
1152
|
-
end.reject(&:empty?).join("\n")
|
|
1153
|
-
end
|
|
1154
|
-
|
|
1155
1227
|
# Emit begin/script/end triples for every *.js under `root`, served from
|
|
1156
1228
|
# `url_base`. `agents`/`panel` carry agent-scoping to _extBegin so the
|
|
1157
1229
|
# client can decide visibility. Returns "" when the dir is absent.
|
|
@@ -1173,39 +1245,49 @@ module Clacky
|
|
|
1173
1245
|
end.join("\n")
|
|
1174
1246
|
end
|
|
1175
1247
|
|
|
1176
|
-
#
|
|
1177
|
-
#
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1248
|
+
# Whole-program static checks over ext.yml — unknown keys, bad scopes,
|
|
1249
|
+
# dangling agent→panel refs, layer-shadow overrides. Uses the already-
|
|
1250
|
+
# loaded ExtensionLoader result so this is essentially free at boot.
|
|
1251
|
+
private def report_extension_issues
|
|
1252
|
+
result = Clacky::ExtensionLoader.load_all
|
|
1253
|
+
issues = Clacky::ExtensionVerifier.verify(result)
|
|
1254
|
+
return if issues.empty?
|
|
1255
|
+
|
|
1256
|
+
errors = issues.count { |i| i.level == :error }
|
|
1257
|
+
warns = issues.size - errors
|
|
1258
|
+
Clacky::Logger.info("[ExtensionVerifier] #{errors} error(s), #{warns} warning(s)")
|
|
1259
|
+
|
|
1260
|
+
issues.each do |issue|
|
|
1261
|
+
location = issue.file ? " [#{issue.file}]" : ""
|
|
1262
|
+
unit = issue.unit ? " #{issue.unit}" : ""
|
|
1263
|
+
message = "[ExtensionVerifier] #{issue.ext}#{unit} (#{issue.code}) — #{issue.message}#{location}"
|
|
1264
|
+
if issue.level == :error
|
|
1265
|
+
Clacky::Logger.error(message)
|
|
1266
|
+
else
|
|
1267
|
+
Clacky::Logger.warn(message)
|
|
1268
|
+
end
|
|
1269
|
+
end
|
|
1270
|
+
rescue StandardError => e
|
|
1271
|
+
Clacky::Logger.warn("[ExtensionVerifier] verify failed: #{e.message}")
|
|
1272
|
+
end
|
|
1273
|
+
|
|
1274
|
+
# Serve a file from a resolved extension container: /ext_ui/<ext_id>/<rel>.
|
|
1275
|
+
# The container dir is looked up via the loader (it may live in any layer),
|
|
1276
|
+
# then strict path containment prevents escaping it. JS/CSS only, no-store.
|
|
1277
|
+
private def serve_ext_ui(req, res)
|
|
1278
|
+
rel = req.path.delete_prefix("/ext_ui/")
|
|
1279
|
+
ext_id, _, sub = rel.partition("/")
|
|
1181
1280
|
|
|
1182
|
-
|
|
1281
|
+
unit = Clacky::ExtensionLoader.load_all.units.find { |u| u.ext_id == ext_id }
|
|
1282
|
+
unless unit
|
|
1183
1283
|
res.status = 404
|
|
1184
1284
|
res.body = "not found"
|
|
1185
1285
|
return
|
|
1186
1286
|
end
|
|
1187
1287
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
unless ctype
|
|
1192
|
-
res.status = 415
|
|
1193
|
-
res.body = "unsupported media type"
|
|
1194
|
-
return
|
|
1195
|
-
end
|
|
1196
|
-
|
|
1197
|
-
res.status = 200
|
|
1198
|
-
res["Content-Type"] = ctype
|
|
1199
|
-
res["Cache-Control"] = "no-store"
|
|
1200
|
-
res["Pragma"] = "no-cache"
|
|
1201
|
-
res.body = File.read(abs)
|
|
1202
|
-
end
|
|
1203
|
-
|
|
1204
|
-
private def serve_builtin_ext(req, res)
|
|
1205
|
-
rel = req.path.delete_prefix("/builtin_ext/")
|
|
1206
|
-
abs = File.expand_path(File.join(BUILTIN_EXT_ROOT, rel))
|
|
1207
|
-
|
|
1208
|
-
unless abs.start_with?(BUILTIN_EXT_ROOT + File::SEPARATOR) && File.file?(abs)
|
|
1288
|
+
root = File.expand_path(unit.dir)
|
|
1289
|
+
abs = File.expand_path(File.join(root, sub))
|
|
1290
|
+
unless abs.start_with?(root + File::SEPARATOR) && File.file?(abs)
|
|
1209
1291
|
res.status = 404
|
|
1210
1292
|
res.body = "not found"
|
|
1211
1293
|
return
|
|
@@ -1237,14 +1319,40 @@ module Clacky
|
|
|
1237
1319
|
serve_static_under(File.join(dir, "webui"), rel, res)
|
|
1238
1320
|
end
|
|
1239
1321
|
|
|
1240
|
-
#
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
return (res.status = 404; res.body = "not found") if
|
|
1322
|
+
# GET /agent_avatar/<id> — serve an agent's avatar image (extension
|
|
1323
|
+
# `avatar` file, or a user agent's avatar.png). Read-only, images only.
|
|
1324
|
+
private def serve_agent_avatar(req, res)
|
|
1325
|
+
id = req.path.delete_prefix("/agent_avatar/").split("/").first.to_s
|
|
1326
|
+
return (res.status = 404; res.body = "not found") if id.empty?
|
|
1245
1327
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1328
|
+
abs = agent_avatar_path(id)
|
|
1329
|
+
unless abs && File.file?(abs)
|
|
1330
|
+
res.status = 404
|
|
1331
|
+
res.body = "not found"
|
|
1332
|
+
return
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
ctype = { ".png" => "image/png", ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg",
|
|
1336
|
+
".webp" => "image/webp", ".svg" => "image/svg+xml", ".gif" => "image/gif" }[File.extname(abs).downcase]
|
|
1337
|
+
unless ctype
|
|
1338
|
+
res.status = 415
|
|
1339
|
+
res.body = "unsupported media type"
|
|
1340
|
+
return
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1343
|
+
res.status = 200
|
|
1344
|
+
res["Content-Type"] = ctype
|
|
1345
|
+
res["Cache-Control"] = "public, max-age=86400"
|
|
1346
|
+
res.body = File.binread(abs)
|
|
1347
|
+
end
|
|
1348
|
+
|
|
1349
|
+
private def agent_avatar_path(id)
|
|
1350
|
+
unit = Clacky::ExtensionLoader.last_result&.agents&.find { |u| u.id == id }
|
|
1351
|
+
avatar_abs = unit&.spec&.[]("avatar_abs").to_s
|
|
1352
|
+
return avatar_abs unless avatar_abs.empty?
|
|
1353
|
+
|
|
1354
|
+
user_png = File.expand_path("~/.clacky/agents/#{id}/avatar.png")
|
|
1355
|
+
File.file?(user_png) ? user_png : nil
|
|
1248
1356
|
end
|
|
1249
1357
|
|
|
1250
1358
|
# Read-only static serve of `rel` under `root`, JS/CSS/HTML only, with
|
|
@@ -1383,6 +1491,9 @@ module Clacky
|
|
|
1383
1491
|
output_dir = body["output_dir"].to_s
|
|
1384
1492
|
output_dir = @agent_config.default_working_dir || Dir.pwd if output_dir.empty?
|
|
1385
1493
|
|
|
1494
|
+
session_id = body["session_id"].to_s
|
|
1495
|
+
session_id = nil if session_id.empty?
|
|
1496
|
+
|
|
1386
1497
|
result = Clacky::Media::Generator.new(@agent_config).generate_image(
|
|
1387
1498
|
prompt: prompt,
|
|
1388
1499
|
aspect_ratio: aspect_ratio,
|
|
@@ -1391,7 +1502,7 @@ module Clacky
|
|
|
1391
1502
|
images: body["images"]
|
|
1392
1503
|
)
|
|
1393
1504
|
if result["success"]
|
|
1394
|
-
log_media_usage(result, prompt: prompt)
|
|
1505
|
+
log_media_usage(result, prompt: prompt, session_id: session_id)
|
|
1395
1506
|
end
|
|
1396
1507
|
status = result["success"] ? 200 : 422
|
|
1397
1508
|
json_response(res, status, result)
|
|
@@ -1415,6 +1526,9 @@ module Clacky
|
|
|
1415
1526
|
output_dir = body["output_dir"].to_s
|
|
1416
1527
|
output_dir = @agent_config.default_working_dir || Dir.pwd if output_dir.empty?
|
|
1417
1528
|
|
|
1529
|
+
session_id = body["session_id"].to_s
|
|
1530
|
+
session_id = nil if session_id.empty?
|
|
1531
|
+
|
|
1418
1532
|
result = Clacky::Media::Generator.new(@agent_config).generate_video(
|
|
1419
1533
|
prompt: prompt,
|
|
1420
1534
|
aspect_ratio: aspect_ratio,
|
|
@@ -1423,7 +1537,7 @@ module Clacky
|
|
|
1423
1537
|
image: image
|
|
1424
1538
|
)
|
|
1425
1539
|
if result["success"]
|
|
1426
|
-
log_media_usage(result, prompt: prompt)
|
|
1540
|
+
log_media_usage(result, prompt: prompt, session_id: session_id)
|
|
1427
1541
|
end
|
|
1428
1542
|
status = result["success"] ? 200 : 422
|
|
1429
1543
|
json_response(res, status, result)
|
|
@@ -1444,13 +1558,16 @@ module Clacky
|
|
|
1444
1558
|
output_dir = body["output_dir"].to_s
|
|
1445
1559
|
output_dir = @agent_config.default_working_dir || Dir.pwd if output_dir.empty?
|
|
1446
1560
|
|
|
1561
|
+
session_id = body["session_id"].to_s
|
|
1562
|
+
session_id = nil if session_id.empty?
|
|
1563
|
+
|
|
1447
1564
|
result = Clacky::Media::Generator.new(@agent_config).generate_speech(
|
|
1448
1565
|
input: input,
|
|
1449
1566
|
voice: voice,
|
|
1450
1567
|
output_dir: output_dir
|
|
1451
1568
|
)
|
|
1452
1569
|
if result["success"]
|
|
1453
|
-
log_media_usage(result, prompt: input)
|
|
1570
|
+
log_media_usage(result, prompt: input, session_id: session_id)
|
|
1454
1571
|
end
|
|
1455
1572
|
status = result["success"] ? 200 : 422
|
|
1456
1573
|
json_response(res, status, result)
|
|
@@ -1511,7 +1628,7 @@ module Clacky
|
|
|
1511
1628
|
json_response(res, 500, { error: e.message })
|
|
1512
1629
|
end
|
|
1513
1630
|
|
|
1514
|
-
private def log_media_usage(result, prompt:)
|
|
1631
|
+
private def log_media_usage(result, prompt:, session_id: nil)
|
|
1515
1632
|
usage = result["usage"]
|
|
1516
1633
|
cost = result["cost_usd"]
|
|
1517
1634
|
return if usage.nil? && cost.nil?
|
|
@@ -1532,6 +1649,20 @@ module Clacky
|
|
|
1532
1649
|
else "image"
|
|
1533
1650
|
end
|
|
1534
1651
|
Clacky::Logger.info("[Media] #{kind} generated #{parts.join(" ")}")
|
|
1652
|
+
|
|
1653
|
+
require_relative "../billing/billing_store"
|
|
1654
|
+
require_relative "../billing/billing_record"
|
|
1655
|
+
record = Clacky::Billing::BillingRecord.new(
|
|
1656
|
+
session_id: session_id,
|
|
1657
|
+
model: result["model"].to_s,
|
|
1658
|
+
prompt_tokens: usage.is_a?(Hash) ? usage["prompt_tokens"].to_i : 0,
|
|
1659
|
+
completion_tokens: usage.is_a?(Hash) ? usage["completion_tokens"].to_i : 0,
|
|
1660
|
+
cache_read_tokens: usage.is_a?(Hash) ? usage["cache_read_tokens"].to_i : 0,
|
|
1661
|
+
cache_write_tokens: usage.is_a?(Hash) ? usage["cache_write_tokens"].to_i : 0,
|
|
1662
|
+
cost_usd: cost.to_f,
|
|
1663
|
+
cost_source: :api
|
|
1664
|
+
)
|
|
1665
|
+
Clacky::Billing::BillingStore.new.append(record)
|
|
1535
1666
|
end
|
|
1536
1667
|
|
|
1537
1668
|
# GET /api/media/types
|
|
@@ -2189,6 +2320,124 @@ module Clacky
|
|
|
2189
2320
|
end
|
|
2190
2321
|
end
|
|
2191
2322
|
|
|
2323
|
+
# GET /api/store/extensions?q=&sort=
|
|
2324
|
+
#
|
|
2325
|
+
# Public extension marketplace catalog. Anonymous (no license needed).
|
|
2326
|
+
# Proxies BrandConfig#search_extensions! which hits the platform's public
|
|
2327
|
+
# /api/v1/extensions endpoint. Extension archives are NOT downloadable here
|
|
2328
|
+
# — they ship inside license-gated brand packages (path B distribution).
|
|
2329
|
+
def api_store_extensions(req, res)
|
|
2330
|
+
brand = Clacky::BrandConfig.load
|
|
2331
|
+
result = brand.search_extensions!(query: req.query["q"], sort: req.query["sort"])
|
|
2332
|
+
|
|
2333
|
+
if result[:success]
|
|
2334
|
+
installed = installed_extension_slugs
|
|
2335
|
+
extensions = Array(result[:extensions]).map do |ext|
|
|
2336
|
+
slug = ext["name"] || ext[:name] || ext["slug"] || ext[:slug]
|
|
2337
|
+
ext.merge("installed" => installed.include?(slug))
|
|
2338
|
+
end
|
|
2339
|
+
json_response(res, 200, { ok: true, extensions: extensions })
|
|
2340
|
+
else
|
|
2341
|
+
json_response(res, 200, {
|
|
2342
|
+
ok: true,
|
|
2343
|
+
extensions: [],
|
|
2344
|
+
warning: result[:error] || "Could not reach the extension store."
|
|
2345
|
+
})
|
|
2346
|
+
end
|
|
2347
|
+
end
|
|
2348
|
+
|
|
2349
|
+
# Slugs of every extension container currently loaded (any layer), used to
|
|
2350
|
+
# flag "installed" on the public marketplace catalog.
|
|
2351
|
+
def installed_extension_slugs
|
|
2352
|
+
result = Clacky::ExtensionLoader.load_all
|
|
2353
|
+
Array(result&.containers).map { |id, _c| id }.to_set
|
|
2354
|
+
rescue StandardError
|
|
2355
|
+
Set.new
|
|
2356
|
+
end
|
|
2357
|
+
|
|
2358
|
+
# GET /api/store/extension?id=<slug-or-id>
|
|
2359
|
+
#
|
|
2360
|
+
# Public detail for a single marketplace extension (contributes + version
|
|
2361
|
+
# history). Anonymous, proxies BrandConfig#extension_detail!.
|
|
2362
|
+
def api_store_extension_detail(req, res)
|
|
2363
|
+
id = req.query["id"].to_s
|
|
2364
|
+
if id.strip.empty?
|
|
2365
|
+
json_response(res, 400, { ok: false, error: "Missing id." })
|
|
2366
|
+
return
|
|
2367
|
+
end
|
|
2368
|
+
|
|
2369
|
+
brand = Clacky::BrandConfig.load
|
|
2370
|
+
result = brand.extension_detail!(id)
|
|
2371
|
+
|
|
2372
|
+
if result[:success] && result[:extension]
|
|
2373
|
+
ext = result[:extension]
|
|
2374
|
+
slug = ext["name"] || ext[:name] || ext["slug"] || ext[:slug]
|
|
2375
|
+
container = extension_container(slug)
|
|
2376
|
+
ext = ext.merge(
|
|
2377
|
+
"installed" => !container.nil?,
|
|
2378
|
+
"removable" => container && container[:layer] == :installed,
|
|
2379
|
+
"disabled" => container ? container[:disabled] == true : false
|
|
2380
|
+
)
|
|
2381
|
+
json_response(res, 200, { ok: true, extension: ext })
|
|
2382
|
+
else
|
|
2383
|
+
json_response(res, 404, { ok: false, error: result[:error] || "Not found" })
|
|
2384
|
+
end
|
|
2385
|
+
end
|
|
2386
|
+
|
|
2387
|
+
# Locate an installed container by slug/id (nil when not installed).
|
|
2388
|
+
def extension_container(slug)
|
|
2389
|
+
return nil if slug.to_s.strip.empty?
|
|
2390
|
+
|
|
2391
|
+
result = Clacky::ExtensionLoader.load_all
|
|
2392
|
+
Array(result&.containers).to_h[slug.to_s]
|
|
2393
|
+
rescue StandardError
|
|
2394
|
+
nil
|
|
2395
|
+
end
|
|
2396
|
+
|
|
2397
|
+
# POST /api/store/extension/disable body: { id: <slug> }
|
|
2398
|
+
def api_store_extension_disable(req, res)
|
|
2399
|
+
toggle_extension(req, res, :disable)
|
|
2400
|
+
end
|
|
2401
|
+
|
|
2402
|
+
# POST /api/store/extension/enable body: { id: <slug> }
|
|
2403
|
+
def api_store_extension_enable(req, res)
|
|
2404
|
+
toggle_extension(req, res, :enable)
|
|
2405
|
+
end
|
|
2406
|
+
|
|
2407
|
+
def toggle_extension(req, res, action)
|
|
2408
|
+
id = parse_json_body(req)["id"].to_s
|
|
2409
|
+
container = extension_container(id)
|
|
2410
|
+
if container.nil?
|
|
2411
|
+
json_response(res, 404, { ok: false, error: "Not installed." })
|
|
2412
|
+
return
|
|
2413
|
+
end
|
|
2414
|
+
|
|
2415
|
+
action == :disable ? Clacky::ExtensionLoader.disable!(id) : Clacky::ExtensionLoader.enable!(id)
|
|
2416
|
+
json_response(res, 200, { ok: true, id: id, disabled: action == :disable })
|
|
2417
|
+
rescue StandardError => e
|
|
2418
|
+
json_response(res, 500, { ok: false, error: e.message })
|
|
2419
|
+
end
|
|
2420
|
+
|
|
2421
|
+
# DELETE /api/store/extension body: { id: <slug> }
|
|
2422
|
+
def api_store_extension_uninstall(req, res)
|
|
2423
|
+
id = parse_json_body(req)["id"].to_s
|
|
2424
|
+
container = extension_container(id)
|
|
2425
|
+
if container.nil?
|
|
2426
|
+
json_response(res, 404, { ok: false, error: "Not installed." })
|
|
2427
|
+
return
|
|
2428
|
+
end
|
|
2429
|
+
unless container[:layer] == :installed
|
|
2430
|
+
json_response(res, 422, { ok: false, error: "Only marketplace-installed extensions can be removed." })
|
|
2431
|
+
return
|
|
2432
|
+
end
|
|
2433
|
+
|
|
2434
|
+
if Clacky::ExtensionLoader.uninstall!(id)
|
|
2435
|
+
json_response(res, 200, { ok: true, id: id })
|
|
2436
|
+
else
|
|
2437
|
+
json_response(res, 404, { ok: false, error: "Not installed." })
|
|
2438
|
+
end
|
|
2439
|
+
end
|
|
2440
|
+
|
|
2192
2441
|
# POST /api/store/skills/:slug/install
|
|
2193
2442
|
def api_brand_skills(res)
|
|
2194
2443
|
brand = Clacky::BrandConfig.load
|
|
@@ -3708,6 +3957,16 @@ module Clacky
|
|
|
3708
3957
|
json_response(res, 200, { skills: skills })
|
|
3709
3958
|
end
|
|
3710
3959
|
|
|
3960
|
+
# GET /api/agents — list all available agent profiles (default + user override + ext).
|
|
3961
|
+
# Each entry carries { id, title, title_zh, description, description_zh,
|
|
3962
|
+
# source, order, layer, author }. Extensions can supply title_zh /
|
|
3963
|
+
# description_zh / author in their ext.yml so the New Session cards read
|
|
3964
|
+
# naturally and credit their author.
|
|
3965
|
+
def api_list_agents(res)
|
|
3966
|
+
agents = Clacky::AgentProfile.all
|
|
3967
|
+
json_response(res, 200, { agents: agents })
|
|
3968
|
+
end
|
|
3969
|
+
|
|
3711
3970
|
# GET /api/sessions/:id/skills — list user-invocable skills for a session,
|
|
3712
3971
|
# filtered by the session's agent profile. Used by the frontend slash-command
|
|
3713
3972
|
# autocomplete so only skills valid for the current profile are suggested.
|
|
@@ -3753,7 +4012,36 @@ module Clacky
|
|
|
3753
4012
|
json_response(res, 200, { skills: skill_data })
|
|
3754
4013
|
end
|
|
3755
4014
|
|
|
3756
|
-
# GET /api/
|
|
4015
|
+
# GET /api/agents/:id/skills — like api_session_skills but keyed on an
|
|
4016
|
+
# agent id rather than a live session. Used by the New Session page so the
|
|
4017
|
+
# first-message composer can offer slash-command autocomplete before a
|
|
4018
|
+
# session exists.
|
|
4019
|
+
def api_agent_skills(agent_id, res)
|
|
4020
|
+
profile = begin
|
|
4021
|
+
Clacky::AgentProfile.load(agent_id)
|
|
4022
|
+
rescue StandardError
|
|
4023
|
+
nil
|
|
4024
|
+
end
|
|
4025
|
+
|
|
4026
|
+
@skill_loader.load_all
|
|
4027
|
+
skills = @skill_loader.user_invocable_skills
|
|
4028
|
+
skills = skills.select { |s| s.allowed_for_agent?(profile.name) } if profile
|
|
4029
|
+
|
|
4030
|
+
loaded_from = @skill_loader.loaded_from
|
|
4031
|
+
skill_data = skills.map do |skill|
|
|
4032
|
+
{
|
|
4033
|
+
name: skill.identifier,
|
|
4034
|
+
name_zh: skill.name_zh,
|
|
4035
|
+
description: skill.description || skill.context_description,
|
|
4036
|
+
description_zh: skill.description_zh,
|
|
4037
|
+
encrypted: skill.encrypted?,
|
|
4038
|
+
source_type: loaded_from[skill.identifier],
|
|
4039
|
+
always_show: skill.always_show
|
|
4040
|
+
}
|
|
4041
|
+
end
|
|
4042
|
+
|
|
4043
|
+
json_response(res, 200, { skills: skill_data })
|
|
4044
|
+
end
|
|
3757
4045
|
# working_dir. action ∈ status|diff|log|branches. diff accepts ?file=,
|
|
3758
4046
|
# log accepts ?limit=.
|
|
3759
4047
|
def api_session_git(session_id, action, req, res)
|
|
@@ -4112,14 +4400,12 @@ module Clacky
|
|
|
4112
4400
|
# Returns two separate groups:
|
|
4113
4401
|
# cloud_skills — published to the platform (with download_count)
|
|
4114
4402
|
# local_skills — local user skills not yet published, or published but with local changes
|
|
4115
|
-
#
|
|
4403
|
+
# Local skills are always returned. Cloud listing needs a user license;
|
|
4404
|
+
# when absent we return licensed:false so the UI can explain that becoming
|
|
4405
|
+
# a creator is required to upload.
|
|
4116
4406
|
private def api_creator_skills(res)
|
|
4117
|
-
brand
|
|
4118
|
-
|
|
4119
|
-
unless brand.user_licensed?
|
|
4120
|
-
json_response(res, 403, { ok: false, error: "User license required" })
|
|
4121
|
-
return
|
|
4122
|
-
end
|
|
4407
|
+
brand = Clacky::BrandConfig.load
|
|
4408
|
+
licensed = brand.user_licensed?
|
|
4123
4409
|
|
|
4124
4410
|
@skill_loader.load_all
|
|
4125
4411
|
upload_meta = Clacky::BrandConfig.load_upload_meta
|
|
@@ -4148,8 +4434,9 @@ module Clacky
|
|
|
4148
4434
|
}
|
|
4149
4435
|
end
|
|
4150
4436
|
|
|
4151
|
-
# Fetch platform skills (may fail — we
|
|
4152
|
-
|
|
4437
|
+
# Fetch platform skills only when licensed (may still fail — we always
|
|
4438
|
+
# return local skills regardless).
|
|
4439
|
+
platform_result = licensed ? brand.fetch_my_skills! : { success: false, skills: [] }
|
|
4153
4440
|
platform_skills = platform_result[:success] ? platform_result[:skills] : []
|
|
4154
4441
|
|
|
4155
4442
|
# cloud_skills: everything that has been published to the platform
|
|
@@ -4185,6 +4472,7 @@ module Clacky
|
|
|
4185
4472
|
|
|
4186
4473
|
json_response(res, 200, {
|
|
4187
4474
|
ok: true,
|
|
4475
|
+
licensed: licensed,
|
|
4188
4476
|
cloud_skills: cloud_skills,
|
|
4189
4477
|
local_skills: local_skills,
|
|
4190
4478
|
platform_fetch_error: platform_result[:success] ? nil : platform_result[:error]
|
|
@@ -4354,7 +4642,8 @@ module Clacky
|
|
|
4354
4642
|
total_tasks: s.dig(:stats, :total_tasks) || 0,
|
|
4355
4643
|
file_size: s[:file_size] || 0,
|
|
4356
4644
|
model: s[:model],
|
|
4357
|
-
working_dir: s[:working_dir]
|
|
4645
|
+
working_dir: s[:working_dir],
|
|
4646
|
+
source: s[:source]
|
|
4358
4647
|
}
|
|
4359
4648
|
end
|
|
4360
4649
|
|
|
@@ -4479,7 +4768,6 @@ module Clacky
|
|
|
4479
4768
|
# endpoints let the Web UI read and edit those files.
|
|
4480
4769
|
|
|
4481
4770
|
PROFILE_USER_AGENTS_DIR = File.expand_path("~/.clacky/agents").freeze
|
|
4482
|
-
PROFILE_DEFAULT_AGENTS_DIR = File.expand_path("../../default_agents", __dir__).freeze
|
|
4483
4771
|
PROFILE_MAX_BYTES = 50_000 # Hard limit; prevents runaway content.
|
|
4484
4772
|
|
|
4485
4773
|
# GET /api/profile
|
|
@@ -4531,31 +4819,30 @@ module Clacky
|
|
|
4531
4819
|
json_response(res, 500, { ok: false, error: e.message })
|
|
4532
4820
|
end
|
|
4533
4821
|
|
|
4534
|
-
# Read a profile file — user override if present, else built-in default.
|
|
4822
|
+
# Read a profile file — user override if present, else built-in default constant.
|
|
4535
4823
|
# Returns { path:, content:, is_default:, writable: }.
|
|
4536
4824
|
private def _profile_read_file(filename)
|
|
4537
|
-
user_path
|
|
4538
|
-
default_path = File.join(PROFILE_DEFAULT_AGENTS_DIR, filename)
|
|
4825
|
+
user_path = File.join(PROFILE_USER_AGENTS_DIR, filename)
|
|
4539
4826
|
|
|
4540
4827
|
if File.exist?(user_path) && !File.zero?(user_path)
|
|
4541
|
-
{
|
|
4828
|
+
return {
|
|
4542
4829
|
path: user_path,
|
|
4543
4830
|
content: File.read(user_path),
|
|
4544
4831
|
is_default: false
|
|
4545
4832
|
}
|
|
4546
|
-
elsif File.exist?(default_path)
|
|
4547
|
-
{
|
|
4548
|
-
path: default_path,
|
|
4549
|
-
content: File.read(default_path),
|
|
4550
|
-
is_default: true
|
|
4551
|
-
}
|
|
4552
|
-
else
|
|
4553
|
-
{
|
|
4554
|
-
path: user_path, # Where it WILL be written
|
|
4555
|
-
content: "",
|
|
4556
|
-
is_default: true
|
|
4557
|
-
}
|
|
4558
4833
|
end
|
|
4834
|
+
|
|
4835
|
+
default_content = case filename
|
|
4836
|
+
when "SOUL.md" then Clacky::AgentProfile::DEFAULT_SOUL
|
|
4837
|
+
when "USER.md" then Clacky::AgentProfile::DEFAULT_USER
|
|
4838
|
+
else ""
|
|
4839
|
+
end
|
|
4840
|
+
|
|
4841
|
+
{
|
|
4842
|
+
path: user_path,
|
|
4843
|
+
content: default_content,
|
|
4844
|
+
is_default: true
|
|
4845
|
+
}
|
|
4559
4846
|
rescue StandardError => e
|
|
4560
4847
|
{ path: "", content: "", is_default: true, error: e.message }
|
|
4561
4848
|
end
|
|
@@ -5386,7 +5673,7 @@ module Clacky
|
|
|
5386
5673
|
end
|
|
5387
5674
|
|
|
5388
5675
|
# PATCH /api/sessions/:id/reasoning_effort
|
|
5389
|
-
# Body: { "reasoning_effort": "off" | "low" | "medium" | "high" }
|
|
5676
|
+
# Body: { "reasoning_effort": "off" | "low" | "medium" | "high" | "xhigh" }
|
|
5390
5677
|
def api_switch_session_reasoning_effort(session_id, req, res)
|
|
5391
5678
|
body = parse_json_body(req)
|
|
5392
5679
|
raw = body["reasoning_effort"]
|
|
@@ -5918,9 +6205,15 @@ module Clacky
|
|
|
5918
6205
|
return unless @registry.exist?(session_id)
|
|
5919
6206
|
|
|
5920
6207
|
agent = nil
|
|
5921
|
-
|
|
6208
|
+
web_ui = nil
|
|
6209
|
+
@registry.with_session(session_id) do |s|
|
|
6210
|
+
agent = s[:agent]
|
|
6211
|
+
web_ui = s[:ui]
|
|
6212
|
+
end
|
|
5922
6213
|
return unless agent
|
|
5923
6214
|
|
|
6215
|
+
web_ui&.show_user_message(display_message, source: :web) if display_message
|
|
6216
|
+
|
|
5924
6217
|
run_agent_task(session_id, agent) { agent.run(prompt, display_text: display_message) }
|
|
5925
6218
|
end
|
|
5926
6219
|
|