openclacky 1.3.6 → 1.3.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/CHANGELOG.md +30 -0
- data/lib/clacky/agent.rb +8 -1
- data/lib/clacky/agent_profile.rb +121 -53
- data/lib/clacky/brand_config.rb +309 -6
- data/lib/clacky/cli.rb +4 -57
- data/lib/clacky/default_extensions/coding/ext.yml +18 -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 +32 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -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/ext.yml +18 -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} +1 -2
- 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 -89
- 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 +483 -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 +313 -144
- data/lib/clacky/server/session_registry.rb +9 -2
- data/lib/clacky/skill_loader.rb +28 -1
- data/lib/clacky/ui2/ui_controller.rb +9 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +748 -573
- 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 +316 -79
- 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 +389 -0
- data/lib/clacky/web/features/new-session/store.js +186 -0
- data/lib/clacky/web/features/new-session/view.js +319 -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 +46 -9
- 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 +118 -106
- data/lib/clacky/web/index.html +138 -188
- data/lib/clacky/web/sessions.js +64 -269
- data/lib/clacky/web/settings.js +1 -1
- data/lib/clacky/web/skills.js +2 -0
- data/lib/clacky/web/theme.js +2 -0
- data/lib/clacky/web/ws-dispatcher.js +1 -0
- data/lib/clacky/web/ws.js +2 -0
- data/lib/clacky.rb +21 -10
- metadata +55 -24
- 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,10 @@ 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?("/ext_ui/")
|
|
373
|
+
self.send(:serve_ext_ui, req, res)
|
|
378
374
|
else
|
|
379
375
|
file_handler.service(req, res)
|
|
380
376
|
res["Cache-Control"] = "no-store"
|
|
@@ -385,12 +381,18 @@ module Clacky
|
|
|
385
381
|
# Auto-create a default session on startup
|
|
386
382
|
create_default_session
|
|
387
383
|
|
|
388
|
-
# Load
|
|
389
|
-
#
|
|
390
|
-
#
|
|
391
|
-
#
|
|
384
|
+
# Load api backends contributed by ext.yml containers into the shared
|
|
385
|
+
# ApiExtension registry. Each unit mounts at /api/ext/<ext_id>/. Done
|
|
386
|
+
# here (not at gem load) so handlers can resolve session_manager and
|
|
387
|
+
# other host helpers as soon as they are wired up.
|
|
392
388
|
Clacky::ApiExtensionLoader.load_all
|
|
393
389
|
|
|
390
|
+
# Static verification of every ext.yml container: unknown keys, bad
|
|
391
|
+
# scopes, dangling agent→panel references, layer-shadow warnings.
|
|
392
|
+
# Errors from the loader itself are already surfaced elsewhere; this
|
|
393
|
+
# pass covers the whole-program checks an author needs to see at boot.
|
|
394
|
+
report_extension_issues
|
|
395
|
+
|
|
394
396
|
# Start the background scheduler
|
|
395
397
|
@scheduler.start
|
|
396
398
|
puts " Scheduler: #{@scheduler.schedules.size} task(s) loaded"
|
|
@@ -490,9 +492,9 @@ module Clacky
|
|
|
490
492
|
path = req.path
|
|
491
493
|
method = req.request_method
|
|
492
494
|
|
|
493
|
-
#
|
|
494
|
-
#
|
|
495
|
-
#
|
|
495
|
+
# HTTP API extensions declared in an ext.yml container mount at
|
|
496
|
+
# /api/ext/<ext_id>/... Routed through a separate dispatcher so the
|
|
497
|
+
# host's giant case table stays focused on built-in endpoints.
|
|
496
498
|
if path.start_with?(Clacky::Server::ApiExtensionDispatcher::MOUNT_PREFIX)
|
|
497
499
|
return if Clacky::Server::ApiExtensionDispatcher.handle(req, res, http_server: self)
|
|
498
500
|
end
|
|
@@ -503,6 +505,7 @@ module Clacky
|
|
|
503
505
|
when ["GET", "/api/cron-tasks"] then api_list_cron_tasks(res)
|
|
504
506
|
when ["POST", "/api/cron-tasks"] then api_create_cron_task(req, res)
|
|
505
507
|
when ["GET", "/api/skills"] then api_list_skills(res)
|
|
508
|
+
when ["GET", "/api/agents"] then api_list_agents(res)
|
|
506
509
|
when ["GET", "/api/config"] then api_get_config(req, res)
|
|
507
510
|
when ["GET", "/api/config/settings"] then api_get_settings(res)
|
|
508
511
|
when ["GET", "/api/exchange-rate"] then api_exchange_rate(req, res)
|
|
@@ -533,6 +536,11 @@ module Clacky
|
|
|
533
536
|
when ["POST", "/api/onboard/complete"] then api_onboard_complete(req, res)
|
|
534
537
|
when ["POST", "/api/onboard/skip-soul"] then api_onboard_skip_soul(req, res)
|
|
535
538
|
when ["GET", "/api/store/skills"] then api_store_skills(res)
|
|
539
|
+
when ["GET", "/api/store/extensions"] then api_store_extensions(req, res)
|
|
540
|
+
when ["GET", "/api/store/extension"] then api_store_extension_detail(req, res)
|
|
541
|
+
when ["POST", "/api/store/extension/disable"] then api_store_extension_disable(req, res)
|
|
542
|
+
when ["POST", "/api/store/extension/enable"] then api_store_extension_enable(req, res)
|
|
543
|
+
when ["DELETE", "/api/store/extension"] then api_store_extension_uninstall(req, res)
|
|
536
544
|
when ["GET", "/api/brand/status"] then api_brand_status(res)
|
|
537
545
|
when ["POST", "/api/brand/activate"] then api_brand_activate(req, res)
|
|
538
546
|
when ["DELETE", "/api/brand/license"] then api_brand_deactivate(res)
|
|
@@ -997,7 +1005,13 @@ module Clacky
|
|
|
997
1005
|
api_key: data["api_key"],
|
|
998
1006
|
base_url: data["base_url"],
|
|
999
1007
|
model: data["default_model"]
|
|
1000
|
-
)
|
|
1008
|
+
) if data["api_key"]
|
|
1009
|
+
if data["device_token"]
|
|
1010
|
+
Clacky::Identity.load.bind!(
|
|
1011
|
+
device_token: data["device_token"],
|
|
1012
|
+
user_id: data["user_id"]
|
|
1013
|
+
)
|
|
1014
|
+
end
|
|
1001
1015
|
json_response(res, 200, {
|
|
1002
1016
|
ok: true,
|
|
1003
1017
|
status: "approved",
|
|
@@ -1039,28 +1053,41 @@ module Clacky
|
|
|
1039
1053
|
@agent_config.save
|
|
1040
1054
|
end
|
|
1041
1055
|
|
|
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
1056
|
# 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.
|
|
1057
|
+
# 1. panel→agents registry (which agent profiles reference each panel)
|
|
1058
|
+
# 2. agent-scoped UI — user ~/.clacky/agents/<name>/webui/**/*.js (data-agent)
|
|
1059
|
+
# 3. ext.yml containers — panel view.js served from /ext_ui/
|
|
1060
|
+
# Never raises.
|
|
1054
1061
|
private def webui_ext_script_tags
|
|
1055
1062
|
[
|
|
1056
1063
|
panel_agents_script,
|
|
1057
|
-
builtin_ext_script_tags,
|
|
1058
|
-
global_ext_script_tags,
|
|
1059
1064
|
agent_webui_script_tags,
|
|
1060
|
-
|
|
1065
|
+
container_ext_script_tags,
|
|
1061
1066
|
].reject(&:empty?).join("\n")
|
|
1062
1067
|
end
|
|
1063
1068
|
|
|
1069
|
+
# Container extensions (ext.yml): inject each declared panel's view.js.
|
|
1070
|
+
# Visibility is entirely driven by `agent.panels: [id]` references in
|
|
1071
|
+
# profile.yml / ext.yml agents (see `panel_agents_map`). A panel that no
|
|
1072
|
+
# agent references stays hidden — panels never mount themselves.
|
|
1073
|
+
# Served from /ext_ui/<ext_id>/<rel> with no-store so edits go live on
|
|
1074
|
+
# page refresh. Never raises.
|
|
1075
|
+
private def container_ext_script_tags
|
|
1076
|
+
result = Clacky::ExtensionLoader.load_all
|
|
1077
|
+
result.panels.map do |unit|
|
|
1078
|
+
rel = unit.spec["view"]
|
|
1079
|
+
ext_id = "ext/#{unit.ext_id}/#{unit.id}"
|
|
1080
|
+
panel_id = "#{unit.ext_id}/#{unit.id}"
|
|
1081
|
+
src = "/ext_ui/#{unit.ext_id}/#{rel}"
|
|
1082
|
+
"<script>Clacky.ext._extBegin(#{ext_id.to_json}, #{nil.to_json}, #{panel_id.to_json})</script>" \
|
|
1083
|
+
"<script src=#{src.to_json} data-ext-id=#{ext_id.to_json}></script>" \
|
|
1084
|
+
"<script>Clacky.ext._extEnd()</script>"
|
|
1085
|
+
end.join("\n")
|
|
1086
|
+
rescue StandardError => e
|
|
1087
|
+
Clacky::Logger.warn("[ExtensionLoader] panel injection failed: #{e.message}")
|
|
1088
|
+
""
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1064
1091
|
# Inline script registering the panel→agents map (which agent profiles
|
|
1065
1092
|
# reference each official panel, from their profile.yml `panels:`).
|
|
1066
1093
|
private def panel_agents_script
|
|
@@ -1070,62 +1097,86 @@ module Clacky
|
|
|
1070
1097
|
"<script>Clacky.ext.registerPanelAgents(#{map.to_json})</script>"
|
|
1071
1098
|
end
|
|
1072
1099
|
|
|
1073
|
-
#
|
|
1074
|
-
# `panels
|
|
1100
|
+
# panel id => list of agent names allowed to see it. Two sources merge:
|
|
1101
|
+
# 1. `agent.panels: [id]` — agent-side explicit reference (opt-in mount)
|
|
1102
|
+
# 2. `panel.attach: [...]` — panel-author's default suggestion
|
|
1103
|
+
# - `attach: [foo]` → visible to agent `foo`
|
|
1104
|
+
# - `attach: ["*"]` → visible to every known agent
|
|
1105
|
+
# - omitted → hidden unless (1) references it
|
|
1106
|
+
# A panel with no source stays hidden.
|
|
1075
1107
|
private def panel_agents_map
|
|
1076
1108
|
result = Hash.new { |h, k| h[k] = [] }
|
|
1109
|
+
add = ->(pid, agent) { result[pid] << agent unless result[pid].include?(agent) }
|
|
1110
|
+
|
|
1111
|
+
agent_data = agent_profile_data
|
|
1112
|
+
agent_data.each do |name, data|
|
|
1113
|
+
ext_id = data["_ext_id"]
|
|
1114
|
+
Array(data["panels"]).each do |panel|
|
|
1115
|
+
pid = panel.to_s
|
|
1116
|
+
full = pid.include?("/") ? pid : (ext_id ? "#{ext_id}/#{pid}" : pid)
|
|
1117
|
+
add.call(full, name)
|
|
1118
|
+
end
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
all_agent_ids = agent_data.keys
|
|
1122
|
+
Array(Clacky::ExtensionLoader.last_result&.panels).each do |unit|
|
|
1123
|
+
full_id = "#{unit.ext_id}/#{unit.id}"
|
|
1124
|
+
attach = Array(unit.spec["attach"])
|
|
1125
|
+
next if attach.empty?
|
|
1126
|
+
if attach.include?("*")
|
|
1127
|
+
all_agent_ids.each { |a| add.call(full_id, a) }
|
|
1128
|
+
else
|
|
1129
|
+
attach.each { |a| add.call(full_id, a) if all_agent_ids.include?(a) }
|
|
1130
|
+
end
|
|
1131
|
+
end
|
|
1132
|
+
result
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
# { agent_name => parsed_profile_data } merged across built-in, user, and
|
|
1136
|
+
# ext-contributed agents. Override order: ext < user (ext provides defaults
|
|
1137
|
+
# for ids not in user dirs; user dirs win on collision).
|
|
1138
|
+
private def agent_profile_data
|
|
1139
|
+
data = {}
|
|
1140
|
+
Array(Clacky::ExtensionLoader.last_result&.agents).each do |unit|
|
|
1141
|
+
data[unit.id] = {
|
|
1142
|
+
"name" => unit.id,
|
|
1143
|
+
"description" => unit.spec["description"],
|
|
1144
|
+
"panels" => unit.spec["panels"],
|
|
1145
|
+
"skills" => unit.spec["skills"],
|
|
1146
|
+
"_ext_id" => unit.ext_id,
|
|
1147
|
+
}
|
|
1148
|
+
end
|
|
1077
1149
|
agent_profile_dirs.each do |name, dir|
|
|
1078
1150
|
yml = File.join(dir, "profile.yml")
|
|
1079
1151
|
next unless File.file?(yml)
|
|
1080
1152
|
|
|
1081
|
-
|
|
1153
|
+
parsed = begin
|
|
1082
1154
|
YAML.safe_load(File.read(yml)) || {}
|
|
1083
1155
|
rescue StandardError
|
|
1084
1156
|
{}
|
|
1085
1157
|
end
|
|
1086
|
-
|
|
1158
|
+
data[name] = parsed
|
|
1087
1159
|
end
|
|
1088
|
-
|
|
1160
|
+
data
|
|
1089
1161
|
end
|
|
1090
1162
|
|
|
1091
|
-
# { agent_name => resolved_dir }
|
|
1092
|
-
#
|
|
1163
|
+
# { agent_name => resolved_dir } for user-side agent overrides only.
|
|
1164
|
+
# Extension-provided agents are served through the extension pipeline.
|
|
1093
1165
|
private def agent_profile_dirs
|
|
1094
1166
|
dirs = {}
|
|
1095
|
-
|
|
1096
|
-
|
|
1167
|
+
root = Clacky::AgentProfile::USER_AGENTS_DIR
|
|
1168
|
+
return dirs unless Dir.exist?(root)
|
|
1097
1169
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1170
|
+
Dir.children(root).each do |name|
|
|
1171
|
+
next if name.start_with?("_")
|
|
1172
|
+
full = File.join(root, name)
|
|
1173
|
+
next unless File.directory?(full) && File.file?(File.join(full, "profile.yml"))
|
|
1102
1174
|
|
|
1103
|
-
|
|
1104
|
-
end
|
|
1175
|
+
dirs[name] = full
|
|
1105
1176
|
end
|
|
1106
1177
|
dirs
|
|
1107
1178
|
end
|
|
1108
1179
|
|
|
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
1180
|
# Agent-scoped UI: agents/<name>/webui/**/*.js. Each script is tagged with
|
|
1130
1181
|
# its owning agent so the client only mounts it for that profile.
|
|
1131
1182
|
private def agent_webui_script_tags
|
|
@@ -1137,21 +1188,6 @@ module Clacky
|
|
|
1137
1188
|
end.reject(&:empty?).join("\n")
|
|
1138
1189
|
end
|
|
1139
1190
|
|
|
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
1191
|
# Emit begin/script/end triples for every *.js under `root`, served from
|
|
1156
1192
|
# `url_base`. `agents`/`panel` carry agent-scoping to _extBegin so the
|
|
1157
1193
|
# client can decide visibility. Returns "" when the dir is absent.
|
|
@@ -1173,39 +1209,49 @@ module Clacky
|
|
|
1173
1209
|
end.join("\n")
|
|
1174
1210
|
end
|
|
1175
1211
|
|
|
1176
|
-
#
|
|
1177
|
-
#
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1212
|
+
# Whole-program static checks over ext.yml — unknown keys, bad scopes,
|
|
1213
|
+
# dangling agent→panel refs, layer-shadow overrides. Uses the already-
|
|
1214
|
+
# loaded ExtensionLoader result so this is essentially free at boot.
|
|
1215
|
+
private def report_extension_issues
|
|
1216
|
+
result = Clacky::ExtensionLoader.load_all
|
|
1217
|
+
issues = Clacky::ExtensionVerifier.verify(result)
|
|
1218
|
+
return if issues.empty?
|
|
1219
|
+
|
|
1220
|
+
errors = issues.count { |i| i.level == :error }
|
|
1221
|
+
warns = issues.size - errors
|
|
1222
|
+
Clacky::Logger.info("[ExtensionVerifier] #{errors} error(s), #{warns} warning(s)")
|
|
1223
|
+
|
|
1224
|
+
issues.each do |issue|
|
|
1225
|
+
location = issue.file ? " [#{issue.file}]" : ""
|
|
1226
|
+
unit = issue.unit ? " #{issue.unit}" : ""
|
|
1227
|
+
message = "[ExtensionVerifier] #{issue.ext}#{unit} (#{issue.code}) — #{issue.message}#{location}"
|
|
1228
|
+
if issue.level == :error
|
|
1229
|
+
Clacky::Logger.error(message)
|
|
1230
|
+
else
|
|
1231
|
+
Clacky::Logger.warn(message)
|
|
1232
|
+
end
|
|
1233
|
+
end
|
|
1234
|
+
rescue StandardError => e
|
|
1235
|
+
Clacky::Logger.warn("[ExtensionVerifier] verify failed: #{e.message}")
|
|
1236
|
+
end
|
|
1181
1237
|
|
|
1182
|
-
|
|
1238
|
+
# Serve a file from a resolved extension container: /ext_ui/<ext_id>/<rel>.
|
|
1239
|
+
# The container dir is looked up via the loader (it may live in any layer),
|
|
1240
|
+
# then strict path containment prevents escaping it. JS/CSS only, no-store.
|
|
1241
|
+
private def serve_ext_ui(req, res)
|
|
1242
|
+
rel = req.path.delete_prefix("/ext_ui/")
|
|
1243
|
+
ext_id, _, sub = rel.partition("/")
|
|
1244
|
+
|
|
1245
|
+
unit = Clacky::ExtensionLoader.load_all.units.find { |u| u.ext_id == ext_id }
|
|
1246
|
+
unless unit
|
|
1183
1247
|
res.status = 404
|
|
1184
1248
|
res.body = "not found"
|
|
1185
1249
|
return
|
|
1186
1250
|
end
|
|
1187
1251
|
|
|
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)
|
|
1252
|
+
root = File.expand_path(unit.dir)
|
|
1253
|
+
abs = File.expand_path(File.join(root, sub))
|
|
1254
|
+
unless abs.start_with?(root + File::SEPARATOR) && File.file?(abs)
|
|
1209
1255
|
res.status = 404
|
|
1210
1256
|
res.body = "not found"
|
|
1211
1257
|
return
|
|
@@ -1237,16 +1283,6 @@ module Clacky
|
|
|
1237
1283
|
serve_static_under(File.join(dir, "webui"), rel, res)
|
|
1238
1284
|
end
|
|
1239
1285
|
|
|
1240
|
-
# Serve official panel assets: /panel_ui/<panel>/<rel>.
|
|
1241
|
-
private def serve_panel_ui(req, res)
|
|
1242
|
-
rest = req.path.delete_prefix("/panel_ui/")
|
|
1243
|
-
panel, _, rel = rest.partition("/")
|
|
1244
|
-
return (res.status = 404; res.body = "not found") if panel.empty? || rel.empty?
|
|
1245
|
-
|
|
1246
|
-
root = File.join(Clacky::AgentProfile::DEFAULT_AGENTS_DIR, "_panels", panel)
|
|
1247
|
-
serve_static_under(root, rel, res)
|
|
1248
|
-
end
|
|
1249
|
-
|
|
1250
1286
|
# Read-only static serve of `rel` under `root`, JS/CSS/HTML only, with
|
|
1251
1287
|
# strict path containment so a crafted rel cannot escape `root`.
|
|
1252
1288
|
private def serve_static_under(root, rel, res)
|
|
@@ -2189,6 +2225,124 @@ module Clacky
|
|
|
2189
2225
|
end
|
|
2190
2226
|
end
|
|
2191
2227
|
|
|
2228
|
+
# GET /api/store/extensions?q=&sort=
|
|
2229
|
+
#
|
|
2230
|
+
# Public extension marketplace catalog. Anonymous (no license needed).
|
|
2231
|
+
# Proxies BrandConfig#search_extensions! which hits the platform's public
|
|
2232
|
+
# /api/v1/extensions endpoint. Extension archives are NOT downloadable here
|
|
2233
|
+
# — they ship inside license-gated brand packages (path B distribution).
|
|
2234
|
+
def api_store_extensions(req, res)
|
|
2235
|
+
brand = Clacky::BrandConfig.load
|
|
2236
|
+
result = brand.search_extensions!(query: req.query["q"], sort: req.query["sort"])
|
|
2237
|
+
|
|
2238
|
+
if result[:success]
|
|
2239
|
+
installed = installed_extension_slugs
|
|
2240
|
+
extensions = Array(result[:extensions]).map do |ext|
|
|
2241
|
+
slug = ext["name"] || ext[:name] || ext["slug"] || ext[:slug]
|
|
2242
|
+
ext.merge("installed" => installed.include?(slug))
|
|
2243
|
+
end
|
|
2244
|
+
json_response(res, 200, { ok: true, extensions: extensions })
|
|
2245
|
+
else
|
|
2246
|
+
json_response(res, 200, {
|
|
2247
|
+
ok: true,
|
|
2248
|
+
extensions: [],
|
|
2249
|
+
warning: result[:error] || "Could not reach the extension store."
|
|
2250
|
+
})
|
|
2251
|
+
end
|
|
2252
|
+
end
|
|
2253
|
+
|
|
2254
|
+
# Slugs of every extension container currently loaded (any layer), used to
|
|
2255
|
+
# flag "installed" on the public marketplace catalog.
|
|
2256
|
+
def installed_extension_slugs
|
|
2257
|
+
result = Clacky::ExtensionLoader.load_all
|
|
2258
|
+
Array(result&.containers).map { |id, _c| id }.to_set
|
|
2259
|
+
rescue StandardError
|
|
2260
|
+
Set.new
|
|
2261
|
+
end
|
|
2262
|
+
|
|
2263
|
+
# GET /api/store/extension?id=<slug-or-id>
|
|
2264
|
+
#
|
|
2265
|
+
# Public detail for a single marketplace extension (contributes + version
|
|
2266
|
+
# history). Anonymous, proxies BrandConfig#extension_detail!.
|
|
2267
|
+
def api_store_extension_detail(req, res)
|
|
2268
|
+
id = req.query["id"].to_s
|
|
2269
|
+
if id.strip.empty?
|
|
2270
|
+
json_response(res, 400, { ok: false, error: "Missing id." })
|
|
2271
|
+
return
|
|
2272
|
+
end
|
|
2273
|
+
|
|
2274
|
+
brand = Clacky::BrandConfig.load
|
|
2275
|
+
result = brand.extension_detail!(id)
|
|
2276
|
+
|
|
2277
|
+
if result[:success] && result[:extension]
|
|
2278
|
+
ext = result[:extension]
|
|
2279
|
+
slug = ext["name"] || ext[:name] || ext["slug"] || ext[:slug]
|
|
2280
|
+
container = extension_container(slug)
|
|
2281
|
+
ext = ext.merge(
|
|
2282
|
+
"installed" => !container.nil?,
|
|
2283
|
+
"removable" => container && container[:layer] == :installed,
|
|
2284
|
+
"disabled" => container ? container[:disabled] == true : false
|
|
2285
|
+
)
|
|
2286
|
+
json_response(res, 200, { ok: true, extension: ext })
|
|
2287
|
+
else
|
|
2288
|
+
json_response(res, 404, { ok: false, error: result[:error] || "Not found" })
|
|
2289
|
+
end
|
|
2290
|
+
end
|
|
2291
|
+
|
|
2292
|
+
# Locate an installed container by slug/id (nil when not installed).
|
|
2293
|
+
def extension_container(slug)
|
|
2294
|
+
return nil if slug.to_s.strip.empty?
|
|
2295
|
+
|
|
2296
|
+
result = Clacky::ExtensionLoader.load_all
|
|
2297
|
+
Array(result&.containers).to_h[slug.to_s]
|
|
2298
|
+
rescue StandardError
|
|
2299
|
+
nil
|
|
2300
|
+
end
|
|
2301
|
+
|
|
2302
|
+
# POST /api/store/extension/disable body: { id: <slug> }
|
|
2303
|
+
def api_store_extension_disable(req, res)
|
|
2304
|
+
toggle_extension(req, res, :disable)
|
|
2305
|
+
end
|
|
2306
|
+
|
|
2307
|
+
# POST /api/store/extension/enable body: { id: <slug> }
|
|
2308
|
+
def api_store_extension_enable(req, res)
|
|
2309
|
+
toggle_extension(req, res, :enable)
|
|
2310
|
+
end
|
|
2311
|
+
|
|
2312
|
+
def toggle_extension(req, res, action)
|
|
2313
|
+
id = parse_json_body(req)["id"].to_s
|
|
2314
|
+
container = extension_container(id)
|
|
2315
|
+
if container.nil?
|
|
2316
|
+
json_response(res, 404, { ok: false, error: "Not installed." })
|
|
2317
|
+
return
|
|
2318
|
+
end
|
|
2319
|
+
|
|
2320
|
+
action == :disable ? Clacky::ExtensionLoader.disable!(id) : Clacky::ExtensionLoader.enable!(id)
|
|
2321
|
+
json_response(res, 200, { ok: true, id: id, disabled: action == :disable })
|
|
2322
|
+
rescue StandardError => e
|
|
2323
|
+
json_response(res, 500, { ok: false, error: e.message })
|
|
2324
|
+
end
|
|
2325
|
+
|
|
2326
|
+
# DELETE /api/store/extension body: { id: <slug> }
|
|
2327
|
+
def api_store_extension_uninstall(req, res)
|
|
2328
|
+
id = parse_json_body(req)["id"].to_s
|
|
2329
|
+
container = extension_container(id)
|
|
2330
|
+
if container.nil?
|
|
2331
|
+
json_response(res, 404, { ok: false, error: "Not installed." })
|
|
2332
|
+
return
|
|
2333
|
+
end
|
|
2334
|
+
unless container[:layer] == :installed
|
|
2335
|
+
json_response(res, 422, { ok: false, error: "Only marketplace-installed extensions can be removed." })
|
|
2336
|
+
return
|
|
2337
|
+
end
|
|
2338
|
+
|
|
2339
|
+
if Clacky::ExtensionLoader.uninstall!(id)
|
|
2340
|
+
json_response(res, 200, { ok: true, id: id })
|
|
2341
|
+
else
|
|
2342
|
+
json_response(res, 404, { ok: false, error: "Not installed." })
|
|
2343
|
+
end
|
|
2344
|
+
end
|
|
2345
|
+
|
|
2192
2346
|
# POST /api/store/skills/:slug/install
|
|
2193
2347
|
def api_brand_skills(res)
|
|
2194
2348
|
brand = Clacky::BrandConfig.load
|
|
@@ -3708,6 +3862,16 @@ module Clacky
|
|
|
3708
3862
|
json_response(res, 200, { skills: skills })
|
|
3709
3863
|
end
|
|
3710
3864
|
|
|
3865
|
+
# GET /api/agents — list all available agent profiles (default + user override + ext).
|
|
3866
|
+
# Each entry carries { id, title, title_zh, description, description_zh,
|
|
3867
|
+
# source, order, layer, author }. Extensions can supply title_zh /
|
|
3868
|
+
# description_zh / author in their ext.yml so the New Session cards read
|
|
3869
|
+
# naturally and credit their author.
|
|
3870
|
+
def api_list_agents(res)
|
|
3871
|
+
agents = Clacky::AgentProfile.all
|
|
3872
|
+
json_response(res, 200, { agents: agents })
|
|
3873
|
+
end
|
|
3874
|
+
|
|
3711
3875
|
# GET /api/sessions/:id/skills — list user-invocable skills for a session,
|
|
3712
3876
|
# filtered by the session's agent profile. Used by the frontend slash-command
|
|
3713
3877
|
# autocomplete so only skills valid for the current profile are suggested.
|
|
@@ -4112,14 +4276,12 @@ module Clacky
|
|
|
4112
4276
|
# Returns two separate groups:
|
|
4113
4277
|
# cloud_skills — published to the platform (with download_count)
|
|
4114
4278
|
# local_skills — local user skills not yet published, or published but with local changes
|
|
4115
|
-
#
|
|
4279
|
+
# Local skills are always returned. Cloud listing needs a user license;
|
|
4280
|
+
# when absent we return licensed:false so the UI can explain that becoming
|
|
4281
|
+
# a creator is required to upload.
|
|
4116
4282
|
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
|
|
4283
|
+
brand = Clacky::BrandConfig.load
|
|
4284
|
+
licensed = brand.user_licensed?
|
|
4123
4285
|
|
|
4124
4286
|
@skill_loader.load_all
|
|
4125
4287
|
upload_meta = Clacky::BrandConfig.load_upload_meta
|
|
@@ -4148,8 +4310,9 @@ module Clacky
|
|
|
4148
4310
|
}
|
|
4149
4311
|
end
|
|
4150
4312
|
|
|
4151
|
-
# Fetch platform skills (may fail — we
|
|
4152
|
-
|
|
4313
|
+
# Fetch platform skills only when licensed (may still fail — we always
|
|
4314
|
+
# return local skills regardless).
|
|
4315
|
+
platform_result = licensed ? brand.fetch_my_skills! : { success: false, skills: [] }
|
|
4153
4316
|
platform_skills = platform_result[:success] ? platform_result[:skills] : []
|
|
4154
4317
|
|
|
4155
4318
|
# cloud_skills: everything that has been published to the platform
|
|
@@ -4185,6 +4348,7 @@ module Clacky
|
|
|
4185
4348
|
|
|
4186
4349
|
json_response(res, 200, {
|
|
4187
4350
|
ok: true,
|
|
4351
|
+
licensed: licensed,
|
|
4188
4352
|
cloud_skills: cloud_skills,
|
|
4189
4353
|
local_skills: local_skills,
|
|
4190
4354
|
platform_fetch_error: platform_result[:success] ? nil : platform_result[:error]
|
|
@@ -4354,7 +4518,8 @@ module Clacky
|
|
|
4354
4518
|
total_tasks: s.dig(:stats, :total_tasks) || 0,
|
|
4355
4519
|
file_size: s[:file_size] || 0,
|
|
4356
4520
|
model: s[:model],
|
|
4357
|
-
working_dir: s[:working_dir]
|
|
4521
|
+
working_dir: s[:working_dir],
|
|
4522
|
+
source: s[:source]
|
|
4358
4523
|
}
|
|
4359
4524
|
end
|
|
4360
4525
|
|
|
@@ -4479,7 +4644,6 @@ module Clacky
|
|
|
4479
4644
|
# endpoints let the Web UI read and edit those files.
|
|
4480
4645
|
|
|
4481
4646
|
PROFILE_USER_AGENTS_DIR = File.expand_path("~/.clacky/agents").freeze
|
|
4482
|
-
PROFILE_DEFAULT_AGENTS_DIR = File.expand_path("../../default_agents", __dir__).freeze
|
|
4483
4647
|
PROFILE_MAX_BYTES = 50_000 # Hard limit; prevents runaway content.
|
|
4484
4648
|
|
|
4485
4649
|
# GET /api/profile
|
|
@@ -4531,31 +4695,30 @@ module Clacky
|
|
|
4531
4695
|
json_response(res, 500, { ok: false, error: e.message })
|
|
4532
4696
|
end
|
|
4533
4697
|
|
|
4534
|
-
# Read a profile file — user override if present, else built-in default.
|
|
4698
|
+
# Read a profile file — user override if present, else built-in default constant.
|
|
4535
4699
|
# Returns { path:, content:, is_default:, writable: }.
|
|
4536
4700
|
private def _profile_read_file(filename)
|
|
4537
|
-
user_path
|
|
4538
|
-
default_path = File.join(PROFILE_DEFAULT_AGENTS_DIR, filename)
|
|
4701
|
+
user_path = File.join(PROFILE_USER_AGENTS_DIR, filename)
|
|
4539
4702
|
|
|
4540
4703
|
if File.exist?(user_path) && !File.zero?(user_path)
|
|
4541
|
-
{
|
|
4704
|
+
return {
|
|
4542
4705
|
path: user_path,
|
|
4543
4706
|
content: File.read(user_path),
|
|
4544
4707
|
is_default: false
|
|
4545
4708
|
}
|
|
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
4709
|
end
|
|
4710
|
+
|
|
4711
|
+
default_content = case filename
|
|
4712
|
+
when "SOUL.md" then Clacky::AgentProfile::DEFAULT_SOUL
|
|
4713
|
+
when "USER.md" then Clacky::AgentProfile::DEFAULT_USER
|
|
4714
|
+
else ""
|
|
4715
|
+
end
|
|
4716
|
+
|
|
4717
|
+
{
|
|
4718
|
+
path: user_path,
|
|
4719
|
+
content: default_content,
|
|
4720
|
+
is_default: true
|
|
4721
|
+
}
|
|
4559
4722
|
rescue StandardError => e
|
|
4560
4723
|
{ path: "", content: "", is_default: true, error: e.message }
|
|
4561
4724
|
end
|
|
@@ -5386,7 +5549,7 @@ module Clacky
|
|
|
5386
5549
|
end
|
|
5387
5550
|
|
|
5388
5551
|
# PATCH /api/sessions/:id/reasoning_effort
|
|
5389
|
-
# Body: { "reasoning_effort": "off" | "low" | "medium" | "high" }
|
|
5552
|
+
# Body: { "reasoning_effort": "off" | "low" | "medium" | "high" | "xhigh" }
|
|
5390
5553
|
def api_switch_session_reasoning_effort(session_id, req, res)
|
|
5391
5554
|
body = parse_json_body(req)
|
|
5392
5555
|
raw = body["reasoning_effort"]
|
|
@@ -5918,9 +6081,15 @@ module Clacky
|
|
|
5918
6081
|
return unless @registry.exist?(session_id)
|
|
5919
6082
|
|
|
5920
6083
|
agent = nil
|
|
5921
|
-
|
|
6084
|
+
web_ui = nil
|
|
6085
|
+
@registry.with_session(session_id) do |s|
|
|
6086
|
+
agent = s[:agent]
|
|
6087
|
+
web_ui = s[:ui]
|
|
6088
|
+
end
|
|
5922
6089
|
return unless agent
|
|
5923
6090
|
|
|
6091
|
+
web_ui&.show_user_message(display_message, source: :web) if display_message
|
|
6092
|
+
|
|
5924
6093
|
run_agent_task(session_id, agent) { agent.run(prompt, display_text: display_message) }
|
|
5925
6094
|
end
|
|
5926
6095
|
|
|
@@ -117,8 +117,15 @@ module Clacky
|
|
|
117
117
|
src = (session_data[:source] || "manual").to_s
|
|
118
118
|
next if counts[src] >= n
|
|
119
119
|
next if exist?(session_data[:session_id])
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
begin
|
|
121
|
+
@session_restorer.call(session_data)
|
|
122
|
+
counts[src] += 1
|
|
123
|
+
rescue => e
|
|
124
|
+
Clacky::Logger.warn(
|
|
125
|
+
"[SessionRegistry] skip session #{session_data[:session_id]}: " \
|
|
126
|
+
"#{e.class}: #{e.message}"
|
|
127
|
+
)
|
|
128
|
+
end
|
|
122
129
|
end
|
|
123
130
|
end
|
|
124
131
|
|