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
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Clacky
|
|
7
|
+
# Discovers extension containers across three source layers and resolves them
|
|
8
|
+
# into a flat list of capability units (panels, api, skills, agents) for the
|
|
9
|
+
# rest of the system to mount.
|
|
10
|
+
#
|
|
11
|
+
# A container is a directory holding an `ext.yml` manifest:
|
|
12
|
+
#
|
|
13
|
+
# id: canvas-suite
|
|
14
|
+
# name: Canvas Suite
|
|
15
|
+
# version: "1.0.0"
|
|
16
|
+
# author: Jane Doe # display credit (optional)
|
|
17
|
+
# homepage: https://example.com # optional
|
|
18
|
+
# license: MIT # optional, SPDX identifier
|
|
19
|
+
# origin: self # self | marketplace | enterprise
|
|
20
|
+
# contributes:
|
|
21
|
+
# panels:
|
|
22
|
+
# - id: canvas
|
|
23
|
+
# view: panels/canvas/view.js
|
|
24
|
+
# attach: [designer] # optional: panel author's default
|
|
25
|
+
# # ("*" = all agents; omit = hidden
|
|
26
|
+
# # unless an agent references it)
|
|
27
|
+
# api: api/handler.rb # single backend for the whole ext
|
|
28
|
+
#
|
|
29
|
+
# Source layers (ascending priority — same id, higher layer wins):
|
|
30
|
+
# builtin gem default_extensions/<id>/
|
|
31
|
+
# installed ~/.clacky/ext/installed/<id>/
|
|
32
|
+
# local ~/.clacky/ext/local/<id>/
|
|
33
|
+
#
|
|
34
|
+
# This class only *discovers and resolves*; actual mounting is done by the
|
|
35
|
+
# existing call sites:
|
|
36
|
+
# - api unit → ApiExtensionLoader.load_one(...) (server boot / reload)
|
|
37
|
+
# - panel units → http_server ext_script_block(...) (rendered per request)
|
|
38
|
+
# - skill units → SkillLoader#load_extension_skills (reads last_result on load_all)
|
|
39
|
+
# - agent units → AgentProfile + http_server agent_profile_data (lookup by id)
|
|
40
|
+
# - channel units → channel.rb extension_adapter_loader (require + register at boot)
|
|
41
|
+
# - patch units → PatchLoader.load_extension_patches (require at boot)
|
|
42
|
+
# - hook units → ShellHookLoader.load_extension_hooks (register at boot)
|
|
43
|
+
module ExtensionLoader
|
|
44
|
+
BUILTIN_DIR = File.expand_path("../default_extensions", __dir__)
|
|
45
|
+
INSTALLED_DIR = File.expand_path("~/.clacky/ext/installed")
|
|
46
|
+
LOCAL_DIR = File.expand_path("~/.clacky/ext/local")
|
|
47
|
+
DISABLED_FILE = File.expand_path("~/.clacky/ext/disabled.json")
|
|
48
|
+
MANIFEST = "ext.yml"
|
|
49
|
+
|
|
50
|
+
# Layers in ascending priority; later entries override earlier ones by id.
|
|
51
|
+
LAYERS = %i[builtin installed local].freeze
|
|
52
|
+
ORIGINS = %w[self marketplace enterprise].freeze
|
|
53
|
+
|
|
54
|
+
# One resolved capability unit. `kind` is :panel or :api.
|
|
55
|
+
Unit = Struct.new(
|
|
56
|
+
:kind, :id, :ext_id, :layer, :origin, :dir, :spec,
|
|
57
|
+
keyword_init: true
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# One resolution error, structured so an AI author can locate and fix it.
|
|
61
|
+
Error = Struct.new(:ext_id, :layer, :unit, :message, :file, keyword_init: true)
|
|
62
|
+
|
|
63
|
+
Result = Struct.new(:panels, :api, :skills, :agents, :channels, :patches, :hooks, :errors, :overridden, :containers, keyword_init: true) do
|
|
64
|
+
def units
|
|
65
|
+
panels + api + skills + agents + channels + patches + hooks
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class << self
|
|
70
|
+
def dir_for(layer)
|
|
71
|
+
case layer
|
|
72
|
+
when :builtin then BUILTIN_DIR
|
|
73
|
+
when :installed then INSTALLED_DIR
|
|
74
|
+
when :local then LOCAL_DIR
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Scan all layers, resolve overrides, return a structured Result.
|
|
79
|
+
# `layers` maps layer name => root dir; defaults to the real three-layer
|
|
80
|
+
# dirs. Tests inject temp dirs through it.
|
|
81
|
+
#
|
|
82
|
+
# Cached by a fingerprint of every container's `ext.yml` mtime — a hot
|
|
83
|
+
# path (per web request, per API call) hits the cache; the moment any
|
|
84
|
+
# manifest changes, we invalidate and rescan. Pass `force: true` to
|
|
85
|
+
# bypass the cache (used by CLI / tests).
|
|
86
|
+
def load_all(layers: default_layers, force: false)
|
|
87
|
+
fingerprint = [fingerprint_layers(layers), disabled_fingerprint]
|
|
88
|
+
if !force && @last_result && @last_fingerprint == fingerprint
|
|
89
|
+
return @last_result
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
by_id = {} # ext_id => resolved container (winning layer)
|
|
93
|
+
overridden = [] # [ext_id, losing_layer, winning_layer]
|
|
94
|
+
errors = []
|
|
95
|
+
|
|
96
|
+
layers.each do |layer, root|
|
|
97
|
+
next unless root && Dir.exist?(root)
|
|
98
|
+
|
|
99
|
+
Dir.children(root).sort.each do |ext_id|
|
|
100
|
+
next if ext_id.start_with?("_", ".")
|
|
101
|
+
dir = File.join(root, ext_id)
|
|
102
|
+
next unless File.directory?(dir)
|
|
103
|
+
|
|
104
|
+
manifest = File.join(dir, MANIFEST)
|
|
105
|
+
next unless File.file?(manifest)
|
|
106
|
+
|
|
107
|
+
container = read_container(ext_id, layer, dir, manifest, errors)
|
|
108
|
+
next unless container
|
|
109
|
+
|
|
110
|
+
if (prev = by_id[ext_id])
|
|
111
|
+
overridden << [ext_id, prev[:layer], layer]
|
|
112
|
+
end
|
|
113
|
+
by_id[ext_id] = container
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
result = Result.new(panels: [], api: [], skills: [], agents: [], channels: [], patches: [], hooks: [], errors: errors, overridden: overridden, containers: by_id)
|
|
118
|
+
disabled = disabled_ids
|
|
119
|
+
by_id.each_value do |container|
|
|
120
|
+
container[:disabled] = disabled.include?(container[:ext_id])
|
|
121
|
+
resolve_units(container, result) unless container[:disabled]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
@last_result = result
|
|
125
|
+
@last_fingerprint = fingerprint
|
|
126
|
+
result
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def default_layers
|
|
130
|
+
LAYERS.each_with_object({}) { |layer, h| h[layer] = dir_for(layer) }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def last_result
|
|
134
|
+
@last_result || load_all
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Discard the mtime cache so the next `load_all` rescans from disk.
|
|
138
|
+
# Used by CLI commands that mutate the ext tree (new / pack).
|
|
139
|
+
def invalidate_cache!
|
|
140
|
+
@last_fingerprint = nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# ── Disabled state ────────────────────────────────────────────────
|
|
144
|
+
# A disabled extension stays discoverable (still listed as installed)
|
|
145
|
+
# but contributes no units until re-enabled. Persisted as a JSON array
|
|
146
|
+
# of ext ids at ~/.clacky/ext/disabled.json.
|
|
147
|
+
|
|
148
|
+
def disabled_ids
|
|
149
|
+
return Set.new unless File.file?(DISABLED_FILE)
|
|
150
|
+
|
|
151
|
+
data = JSON.parse(File.read(DISABLED_FILE))
|
|
152
|
+
data.is_a?(Array) ? data.map(&:to_s).to_set : Set.new
|
|
153
|
+
rescue StandardError
|
|
154
|
+
Set.new
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def disabled?(id)
|
|
158
|
+
disabled_ids.include?(id.to_s)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def disable!(id)
|
|
162
|
+
ids = disabled_ids
|
|
163
|
+
ids << id.to_s
|
|
164
|
+
write_disabled(ids)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def enable!(id)
|
|
168
|
+
ids = disabled_ids
|
|
169
|
+
ids.delete(id.to_s)
|
|
170
|
+
write_disabled(ids)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Remove an installed extension by deleting its installed-layer dir.
|
|
174
|
+
# Only the installed layer is removable (builtin ships with the gem;
|
|
175
|
+
# local is the author's own working copy). Returns true on removal.
|
|
176
|
+
def uninstall!(id)
|
|
177
|
+
dir = File.join(INSTALLED_DIR, id.to_s)
|
|
178
|
+
return false unless File.directory?(dir)
|
|
179
|
+
|
|
180
|
+
FileUtils.rm_rf(dir)
|
|
181
|
+
enable!(id) # clear any stale disabled flag
|
|
182
|
+
invalidate_cache!
|
|
183
|
+
true
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private def write_disabled(ids)
|
|
187
|
+
FileUtils.mkdir_p(File.dirname(DISABLED_FILE))
|
|
188
|
+
File.write(DISABLED_FILE, JSON.generate(ids.to_a.sort))
|
|
189
|
+
invalidate_cache!
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private def disabled_fingerprint
|
|
193
|
+
File.file?(DISABLED_FILE) ? File.mtime(DISABLED_FILE).to_f : 0.0
|
|
194
|
+
rescue StandardError
|
|
195
|
+
Object.new
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# A layer's fingerprint is the sorted list of "<ext_id>|<mtime>" for
|
|
200
|
+
# every container manifest present. Cheap to compute (one stat per
|
|
201
|
+
# manifest); the whole map compared as a Hash so a container appearing
|
|
202
|
+
# or disappearing also invalidates.
|
|
203
|
+
private def fingerprint_layers(layers)
|
|
204
|
+
layers.each_with_object({}) do |(layer, root), acc|
|
|
205
|
+
next unless root && Dir.exist?(root)
|
|
206
|
+
|
|
207
|
+
entries = []
|
|
208
|
+
Dir.children(root).sort.each do |ext_id|
|
|
209
|
+
next if ext_id.start_with?("_", ".")
|
|
210
|
+
manifest = File.join(root, ext_id, MANIFEST)
|
|
211
|
+
next unless File.file?(manifest)
|
|
212
|
+
|
|
213
|
+
entries << [ext_id, File.mtime(manifest).to_f]
|
|
214
|
+
end
|
|
215
|
+
acc[layer] = entries
|
|
216
|
+
end
|
|
217
|
+
rescue StandardError
|
|
218
|
+
# On any stat error just return a unique object so we always rescan.
|
|
219
|
+
Object.new
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private def read_container(ext_id, layer, dir, manifest, errors)
|
|
223
|
+
data = YAMLCompat.load_file(manifest) || {}
|
|
224
|
+
unless data.is_a?(Hash)
|
|
225
|
+
errors << Error.new(ext_id: ext_id, layer: layer.to_s,
|
|
226
|
+
message: "ext.yml must be a mapping", file: manifest)
|
|
227
|
+
return nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
origin = (data["origin"] || "self").to_s
|
|
231
|
+
unless ORIGINS.include?(origin)
|
|
232
|
+
errors << Error.new(ext_id: ext_id, layer: layer.to_s,
|
|
233
|
+
message: "invalid origin #{origin.inspect} (expected one of #{ORIGINS.join('/')})",
|
|
234
|
+
file: manifest)
|
|
235
|
+
return nil
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
{ ext_id: ext_id, layer: layer, dir: dir, origin: origin,
|
|
239
|
+
public: data["public"] == true,
|
|
240
|
+
name: (data["name"] || ext_id).to_s,
|
|
241
|
+
version: data["version"].to_s,
|
|
242
|
+
author: data["author"].to_s,
|
|
243
|
+
homepage: data["homepage"].to_s,
|
|
244
|
+
license: data["license"].to_s,
|
|
245
|
+
contributes: data["contributes"] || {},
|
|
246
|
+
raw: data }
|
|
247
|
+
rescue StandardError => e
|
|
248
|
+
errors << Error.new(ext_id: ext_id, layer: layer.to_s,
|
|
249
|
+
message: "failed to parse ext.yml: #{e.message}", file: manifest)
|
|
250
|
+
nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
private def resolve_units(container, result)
|
|
254
|
+
contributes = container[:contributes]
|
|
255
|
+
return unless contributes.is_a?(Hash)
|
|
256
|
+
|
|
257
|
+
Array(contributes["panels"]).each do |spec|
|
|
258
|
+
unit = build_panel_unit(container, spec, result.errors)
|
|
259
|
+
result.panels << unit if unit
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
if (api_spec = contributes["api"])
|
|
263
|
+
unit = build_api_unit(container, api_spec, result.errors)
|
|
264
|
+
result.api << unit if unit
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
Array(contributes["skills"]).each do |spec|
|
|
268
|
+
unit = build_skill_unit(container, spec, result.errors)
|
|
269
|
+
result.skills << unit if unit
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
Array(contributes["agents"]).each do |spec|
|
|
273
|
+
unit = build_agent_unit(container, spec, result.errors)
|
|
274
|
+
result.agents << unit if unit
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
Array(contributes["channels"]).each do |spec|
|
|
278
|
+
unit = build_channel_unit(container, spec, result.errors)
|
|
279
|
+
result.channels << unit if unit
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
Array(contributes["patches"]).each do |spec|
|
|
283
|
+
unit = build_patch_unit(container, spec, result.errors)
|
|
284
|
+
result.patches << unit if unit
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
Array(contributes["hooks"]).each do |spec|
|
|
288
|
+
unit = build_hook_unit(container, spec, result.errors)
|
|
289
|
+
result.hooks << unit if unit
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
private def build_panel_unit(container, spec, errors)
|
|
294
|
+
ext_id = container[:ext_id]
|
|
295
|
+
unless spec.is_a?(Hash) && spec["id"] && spec["view"]
|
|
296
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "panel",
|
|
297
|
+
message: "panel needs both `id` and `view`")
|
|
298
|
+
return nil
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
view_abs = File.join(container[:dir], spec["view"])
|
|
302
|
+
unless File.file?(view_abs)
|
|
303
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "panel/#{spec['id']}",
|
|
304
|
+
message: "view file not found: #{spec['view']}", file: view_abs)
|
|
305
|
+
return nil
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
Unit.new(kind: :panel, id: spec["id"].to_s, ext_id: ext_id,
|
|
309
|
+
layer: container[:layer], origin: container[:origin],
|
|
310
|
+
dir: container[:dir],
|
|
311
|
+
spec: {
|
|
312
|
+
"view" => spec["view"],
|
|
313
|
+
"title" => spec["title"],
|
|
314
|
+
"title_zh" => spec["title_zh"],
|
|
315
|
+
"description" => spec["description"],
|
|
316
|
+
"description_zh" => spec["description_zh"],
|
|
317
|
+
"order" => spec["order"],
|
|
318
|
+
"attach" => Array(spec["attach"]).map(&:to_s),
|
|
319
|
+
})
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
private def build_api_unit(container, spec, errors)
|
|
323
|
+
ext_id = container[:ext_id]
|
|
324
|
+
handler_rel = spec.is_a?(String) ? spec : nil
|
|
325
|
+
unless handler_rel && !handler_rel.empty?
|
|
326
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "api",
|
|
327
|
+
message: "api must be a string path to the handler file (e.g. `api: api/handler.rb`)",
|
|
328
|
+
file: File.join(container[:dir], "ext.yml"))
|
|
329
|
+
return nil
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
handler_abs = File.join(container[:dir], handler_rel)
|
|
333
|
+
unless File.file?(handler_abs)
|
|
334
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "api",
|
|
335
|
+
message: "handler file not found: #{handler_rel}", file: handler_abs)
|
|
336
|
+
return nil
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
Unit.new(kind: :api, id: ext_id, ext_id: ext_id,
|
|
340
|
+
layer: container[:layer], origin: container[:origin],
|
|
341
|
+
dir: container[:dir],
|
|
342
|
+
spec: { "handler" => handler_rel, "handler_abs" => handler_abs })
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
private def build_skill_unit(container, spec, errors)
|
|
346
|
+
ext_id = container[:ext_id]
|
|
347
|
+
unless spec.is_a?(Hash) && spec["id"]
|
|
348
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "skill",
|
|
349
|
+
message: "skill needs `id`")
|
|
350
|
+
return nil
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
rel_dir = spec["dir"] || "skills/#{spec['id']}"
|
|
354
|
+
skill_dir = File.join(container[:dir], rel_dir)
|
|
355
|
+
unless File.directory?(skill_dir)
|
|
356
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "skill/#{spec['id']}",
|
|
357
|
+
message: "skill dir not found: #{rel_dir}", file: skill_dir)
|
|
358
|
+
return nil
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
encrypted = File.file?(File.join(skill_dir, "SKILL.md.enc"))
|
|
362
|
+
plain = File.file?(File.join(skill_dir, "SKILL.md"))
|
|
363
|
+
unless encrypted || plain
|
|
364
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "skill/#{spec['id']}",
|
|
365
|
+
message: "skill dir missing SKILL.md or SKILL.md.enc", file: skill_dir)
|
|
366
|
+
return nil
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
protected_default = container[:origin] == "marketplace"
|
|
370
|
+
protected_flag = spec.key?("protected") ? !!spec["protected"] : protected_default
|
|
371
|
+
|
|
372
|
+
Unit.new(kind: :skill, id: spec["id"].to_s, ext_id: ext_id,
|
|
373
|
+
layer: container[:layer], origin: container[:origin],
|
|
374
|
+
dir: container[:dir],
|
|
375
|
+
spec: { "dir" => rel_dir, "skill_dir_abs" => skill_dir,
|
|
376
|
+
"protected" => protected_flag, "encrypted" => encrypted })
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
private def build_agent_unit(container, spec, errors)
|
|
380
|
+
ext_id = container[:ext_id]
|
|
381
|
+
unless spec.is_a?(Hash) && spec["id"] && spec["prompt"]
|
|
382
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "agent",
|
|
383
|
+
message: "agent needs both `id` and `prompt`")
|
|
384
|
+
return nil
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
prompt_abs = File.join(container[:dir], spec["prompt"])
|
|
388
|
+
unless File.file?(prompt_abs)
|
|
389
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "agent/#{spec['id']}",
|
|
390
|
+
message: "prompt file not found: #{spec['prompt']}", file: prompt_abs)
|
|
391
|
+
return nil
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
Unit.new(kind: :agent, id: spec["id"].to_s, ext_id: ext_id,
|
|
395
|
+
layer: container[:layer], origin: container[:origin],
|
|
396
|
+
dir: container[:dir],
|
|
397
|
+
spec: {
|
|
398
|
+
"title" => spec["title"].to_s,
|
|
399
|
+
"title_zh" => spec["title_zh"].to_s,
|
|
400
|
+
"prompt" => spec["prompt"],
|
|
401
|
+
"prompt_abs" => prompt_abs,
|
|
402
|
+
"description" => spec["description"].to_s,
|
|
403
|
+
"description_zh" => spec["description_zh"].to_s,
|
|
404
|
+
"order" => spec["order"],
|
|
405
|
+
"author" => container[:author],
|
|
406
|
+
"homepage" => container[:homepage],
|
|
407
|
+
"license" => container[:license],
|
|
408
|
+
"panels" => Array(spec["panels"]).map(&:to_s),
|
|
409
|
+
"skills" => Array(spec["skills"]).map(&:to_s),
|
|
410
|
+
})
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
private def build_channel_unit(container, spec, errors)
|
|
414
|
+
ext_id = container[:ext_id]
|
|
415
|
+
unless spec.is_a?(Hash) && spec["id"] && spec["adapter"]
|
|
416
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "channel",
|
|
417
|
+
message: "channel needs both `id` and `adapter`")
|
|
418
|
+
return nil
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
adapter_abs = File.join(container[:dir], spec["adapter"])
|
|
422
|
+
unless File.file?(adapter_abs)
|
|
423
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "channel/#{spec['id']}",
|
|
424
|
+
message: "adapter file not found: #{spec['adapter']}", file: adapter_abs)
|
|
425
|
+
return nil
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
Unit.new(kind: :channel, id: spec["id"].to_s, ext_id: ext_id,
|
|
429
|
+
layer: container[:layer], origin: container[:origin],
|
|
430
|
+
dir: container[:dir],
|
|
431
|
+
spec: { "adapter" => spec["adapter"], "adapter_abs" => adapter_abs })
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
private def build_patch_unit(container, spec, errors)
|
|
435
|
+
ext_id = container[:ext_id]
|
|
436
|
+
unless spec.is_a?(Hash) && spec["target"] && spec["file"]
|
|
437
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "patch",
|
|
438
|
+
message: "patch needs both `target` and `file`")
|
|
439
|
+
return nil
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
file_abs = File.join(container[:dir], spec["file"])
|
|
443
|
+
unless File.file?(file_abs)
|
|
444
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "patch/#{spec['target']}",
|
|
445
|
+
message: "patch file not found: #{spec['file']}", file: file_abs)
|
|
446
|
+
return nil
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
Unit.new(kind: :patch, id: spec["target"].to_s, ext_id: ext_id,
|
|
450
|
+
layer: container[:layer], origin: container[:origin],
|
|
451
|
+
dir: container[:dir],
|
|
452
|
+
spec: {
|
|
453
|
+
"target" => spec["target"].to_s,
|
|
454
|
+
"file" => spec["file"],
|
|
455
|
+
"file_abs" => file_abs,
|
|
456
|
+
"fingerprint" => spec["fingerprint"].to_s,
|
|
457
|
+
"on_mismatch" => (spec["on_mismatch"] || "disable").to_s,
|
|
458
|
+
})
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
private def build_hook_unit(container, spec, errors)
|
|
462
|
+
ext_id = container[:ext_id]
|
|
463
|
+
unless spec.is_a?(Hash) && spec["event"] && spec["file"]
|
|
464
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "hook",
|
|
465
|
+
message: "hook needs both `event` and `file`")
|
|
466
|
+
return nil
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
file_abs = File.join(container[:dir], spec["file"])
|
|
470
|
+
unless File.file?(file_abs)
|
|
471
|
+
errors << Error.new(ext_id: ext_id, layer: container[:layer].to_s, unit: "hook/#{spec['event']}",
|
|
472
|
+
message: "hook file not found: #{spec['file']}", file: file_abs)
|
|
473
|
+
return nil
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
Unit.new(kind: :hook, id: "#{spec['event']}/#{File.basename(spec['file'], '.rb')}", ext_id: ext_id,
|
|
477
|
+
layer: container[:layer], origin: container[:origin],
|
|
478
|
+
dir: container[:dir],
|
|
479
|
+
spec: { "event" => spec["event"].to_s, "file" => spec["file"], "file_abs" => file_abs })
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|