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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/lib/clacky/agent.rb +8 -1
  4. data/lib/clacky/agent_profile.rb +121 -53
  5. data/lib/clacky/brand_config.rb +309 -6
  6. data/lib/clacky/cli.rb +4 -57
  7. data/lib/clacky/default_extensions/coding/ext.yml +18 -0
  8. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
  9. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
  10. data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
  11. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
  12. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
  13. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
  14. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
  15. data/lib/clacky/default_extensions/general/ext.yml +18 -0
  16. data/lib/clacky/default_extensions/git/ext.yml +12 -0
  17. data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
  18. data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
  19. data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
  20. data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
  21. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
  22. data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
  23. data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
  24. data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
  25. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
  26. data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
  27. data/lib/clacky/extension/api_loader.rb +136 -0
  28. data/lib/clacky/extension/cli_commands.rb +226 -0
  29. data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
  30. data/lib/clacky/extension/hook_loader.rb +77 -0
  31. data/lib/clacky/extension/loader.rb +483 -0
  32. data/lib/clacky/extension/packager.rb +226 -0
  33. data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
  34. data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
  35. data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
  36. data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
  37. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  38. data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
  39. data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
  40. data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
  41. data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
  42. data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
  43. data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
  44. data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
  45. data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
  46. data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
  47. data/lib/clacky/extension/scaffold.rb +55 -0
  48. data/lib/clacky/extension/verifier.rb +196 -0
  49. data/lib/clacky/identity.rb +70 -0
  50. data/lib/clacky/locales/en.rb +19 -1
  51. data/lib/clacky/locales/zh.rb +19 -1
  52. data/lib/clacky/platform_http_client.rb +18 -15
  53. data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
  54. data/lib/clacky/server/channel.rb +6 -0
  55. data/lib/clacky/server/http_server.rb +313 -144
  56. data/lib/clacky/server/session_registry.rb +9 -2
  57. data/lib/clacky/skill_loader.rb +28 -1
  58. data/lib/clacky/ui2/ui_controller.rb +9 -6
  59. data/lib/clacky/version.rb +1 -1
  60. data/lib/clacky/web/app.css +748 -573
  61. data/lib/clacky/web/app.js +63 -15
  62. data/lib/clacky/web/auth.js +2 -0
  63. data/lib/clacky/web/components/notify.js +2 -0
  64. data/lib/clacky/web/components/sidebar.js +1 -5
  65. data/lib/clacky/web/core/ext.js +316 -79
  66. data/lib/clacky/web/features/backup/store.js +1 -0
  67. data/lib/clacky/web/features/backup/view.js +0 -1
  68. data/lib/clacky/web/features/brand/view.js +13 -5
  69. data/lib/clacky/web/features/extensions/store.js +184 -0
  70. data/lib/clacky/web/features/extensions/view.js +389 -0
  71. data/lib/clacky/web/features/new-session/store.js +186 -0
  72. data/lib/clacky/web/features/new-session/view.js +319 -0
  73. data/lib/clacky/web/features/share/view.js +63 -14
  74. data/lib/clacky/web/features/skills/store.js +3 -1
  75. data/lib/clacky/web/features/skills/view.js +1 -1
  76. data/lib/clacky/web/features/tasks/view.js +12 -0
  77. data/lib/clacky/web/features/trash/store.js +24 -6
  78. data/lib/clacky/web/features/trash/view.js +46 -9
  79. data/lib/clacky/web/features/workspace/store.js +2 -0
  80. data/lib/clacky/web/features/workspace/view.js +0 -1
  81. data/lib/clacky/web/i18n.js +118 -106
  82. data/lib/clacky/web/index.html +138 -188
  83. data/lib/clacky/web/sessions.js +64 -269
  84. data/lib/clacky/web/settings.js +1 -1
  85. data/lib/clacky/web/skills.js +2 -0
  86. data/lib/clacky/web/theme.js +2 -0
  87. data/lib/clacky/web/ws-dispatcher.js +1 -0
  88. data/lib/clacky/web/ws.js +2 -0
  89. data/lib/clacky.rb +21 -10
  90. metadata +55 -24
  91. data/benchmark/runner.rb +0 -441
  92. data/lib/clacky/api_extension_loader.rb +0 -168
  93. data/lib/clacky/default_agents/SOUL.md +0 -3
  94. data/lib/clacky/default_agents/USER.md +0 -1
  95. data/lib/clacky/default_agents/coding/profile.yml +0 -5
  96. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  97. data/lib/clacky/default_agents/general/profile.yml +0 -2
  98. data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
  99. data/lib/clacky/web/features/creator/store.js +0 -81
  100. data/lib/clacky/web/features/creator/view.js +0 -380
  101. /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
  102. /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
  103. /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "tmpdir"
5
+
6
+ module Clacky
7
+ # `clacky ext <subcommand>` — manage extension containers (ext.yml).
8
+ #
9
+ # Containers contribute panels (WebUI) and api (HTTP backends). This command
10
+ # group scaffolds, verifies, lists and hot-reloads them.
11
+ class CliExtensionCommands < Thor
12
+ def self.exit_on_failure?
13
+ true
14
+ end
15
+
16
+ desc "new ID", "Scaffold a runnable hello-panel container at ~/.clacky/ext/local/ID/"
17
+ method_option :full, type: :boolean, default: false,
18
+ desc: "Generate a kitchen-sink container exercising all 7 contributes types"
19
+ def new(id)
20
+ path = Clacky::ExtensionScaffold.new_container(id, full: options[:full])
21
+ puts "Created extension container: #{path}"
22
+ if options[:full]
23
+ puts "It contributes panels/api/skills/agents/channels/patches/hooks. Read its README.md, run `clacky ext verify`, then reload the WebUI."
24
+ else
25
+ puts "Reload the WebUI page to see the panel. Edits to view.js and handler.rb take effect on the next request — no restart needed."
26
+ end
27
+ rescue ArgumentError => e
28
+ warn "Error: #{e.message}"
29
+ exit 1
30
+ end
31
+
32
+ desc "verify", "Resolve all containers across layers and report units + structured issues"
33
+ def verify
34
+ result = Clacky::ExtensionLoader.load_all
35
+
36
+ if result.units.empty? && result.errors.empty?
37
+ puts "No extension containers found."
38
+ return
39
+ end
40
+
41
+ result.units.each do |u|
42
+ case u.kind
43
+ when :panel
44
+ attach = Array(u.spec["attach"])
45
+ suffix = attach.empty? ? "" : ", attach=#{attach.inspect}"
46
+ puts "[OK] #{u.ext_id}/#{u.id} (panel#{suffix}, #{u.layer})"
47
+ when :api
48
+ puts "[OK] #{u.ext_id} (api → /api/ext/#{u.ext_id}/, #{u.layer})"
49
+ else
50
+ puts "[OK] #{u.ext_id}/#{u.id} (#{u.kind}, #{u.layer})"
51
+ end
52
+ end
53
+
54
+ issues = Clacky::ExtensionVerifier.verify(result)
55
+ issues.each do |issue|
56
+ tag = issue.level == :error ? "[ERR]" : "[WARN]"
57
+ unit = issue.unit ? " #{issue.unit}" : ""
58
+ loc = issue.file ? " [#{issue.file}]" : ""
59
+ hint = issue.hint ? "\n hint: #{issue.hint}" : ""
60
+ puts "#{tag} #{issue.ext}#{unit} (#{issue.code}) — #{issue.message}#{loc}#{hint}"
61
+ end
62
+
63
+ exit 1 if issues.any? { |i| i.level == :error }
64
+ end
65
+
66
+ desc "list", "List resolved containers and their contributed units"
67
+ def list
68
+ result = Clacky::ExtensionLoader.load_all
69
+
70
+ if result.units.empty?
71
+ puts "No extension units resolved."
72
+ return
73
+ end
74
+
75
+ grouped = result.units.group_by(&:ext_id)
76
+ grouped.each do |ext_id, units|
77
+ layer = units.first.layer
78
+ origin = units.first.origin
79
+ puts "#{ext_id} (#{layer}, origin=#{origin})"
80
+ units.each do |u|
81
+ case u.kind
82
+ when :panel
83
+ attach = Array(u.spec["attach"])
84
+ suffix = attach.empty? ? "" : " attach=#{attach.inspect}"
85
+ puts " panel #{u.id}#{suffix}"
86
+ when :api
87
+ puts " api → /api/ext/#{ext_id}/"
88
+ else
89
+ puts " #{u.kind.to_s.ljust(6)} #{u.id}"
90
+ end
91
+ end
92
+ end
93
+
94
+ result.errors.each do |e|
95
+ puts "[ERR] #{e.ext_id} #{e.unit} — #{e.message}"
96
+ end
97
+ end
98
+
99
+ desc "pack ID", "Package a local container into a distributable zip"
100
+ method_option :out, type: :string, desc: "Output directory for the zip (default: current dir)"
101
+ def pack(id)
102
+ out = options[:out] || Dir.pwd
103
+ res = Clacky::ExtensionPackager.pack(id, out_dir: out)
104
+ puts "Packed #{res.ext_id} → #{res.path}"
105
+ rescue Clacky::ExtensionPackager::Error => e
106
+ warn "Error: #{e.message}"
107
+ exit 1
108
+ end
109
+
110
+ desc "install SOURCE", "Install a packed container (local zip path or http(s) URL) into the installed layer"
111
+ method_option :force, type: :boolean, default: false, desc: "Overwrite an already-installed extension of the same id"
112
+ def install(source)
113
+ res = Clacky::ExtensionPackager.install(source, force: options[:force])
114
+ puts "Installed #{res.ext_id} → #{res.path}"
115
+ puts "Reload the WebUI page to see any panels. No restart needed."
116
+ rescue Clacky::ExtensionPackager::Error => e
117
+ warn "Error: #{e.message}"
118
+ exit 1
119
+ end
120
+
121
+ desc "publish ID", "Pack a local container and publish it to the OpenClacky marketplace"
122
+ method_option :force, type: :boolean, default: false, desc: "Publish a new version of an already-published extension"
123
+ method_option :status, type: :string, desc: "Publish status: draft or published"
124
+ method_option :changelog, type: :string, desc: "Release notes for this version"
125
+ def publish(id)
126
+ unless Clacky::Identity.load.bound?
127
+ warn "Error: this device is not bound to a platform account. Authorize it first to publish."
128
+ exit 1
129
+ end
130
+
131
+ brand = Clacky::BrandConfig.load
132
+ Dir.mktmpdir("clacky-ext-publish") do |tmp|
133
+ res = Clacky::ExtensionPackager.pack(id, out_dir: tmp)
134
+ zip_data = File.binread(res.path)
135
+
136
+ result = brand.upload_extension!(
137
+ res.ext_id, zip_data,
138
+ force: options[:force],
139
+ status: options[:status],
140
+ changelog: options[:changelog]
141
+ )
142
+
143
+ if result[:success]
144
+ ext = result[:extension] || {}
145
+ ver = (ext["latest_version"] || {})["version"]
146
+ puts "Published #{res.ext_id}#{ver ? " v#{ver}" : ""} → status=#{ext["status"]}"
147
+ elsif result[:already_exists]
148
+ warn "Error: #{res.ext_id} already published. Re-run with --force to publish a new version."
149
+ exit 1
150
+ else
151
+ warn "Error: #{result[:error]}"
152
+ exit 1
153
+ end
154
+ end
155
+ rescue Clacky::ExtensionPackager::Error => e
156
+ warn "Error: #{e.message}"
157
+ exit 1
158
+ end
159
+
160
+ desc "published", "List extensions you have published to the marketplace"
161
+ def published
162
+ brand = Clacky::BrandConfig.load
163
+ result = brand.fetch_my_extensions!
164
+
165
+ unless result[:success]
166
+ warn "Error: #{result[:error]}"
167
+ exit 1
168
+ end
169
+
170
+ extensions = result[:extensions]
171
+ if extensions.empty?
172
+ puts "You have not published any extensions yet."
173
+ return
174
+ end
175
+
176
+ extensions.each do |ext|
177
+ ver = (ext["latest_version"] || {})["version"] || ext["version"]
178
+ units = (ext["units"] || {}).map { |k, v| "#{v} #{k}" }.join(", ")
179
+ puts "#{ext["name"]} v#{ver} [#{ext["status"]}]#{units.empty? ? "" : " (#{units})"}"
180
+ end
181
+ end
182
+
183
+ desc "unpublish ID", "Remove one of your published extensions from the marketplace"
184
+ def unpublish(id)
185
+ brand = Clacky::BrandConfig.load
186
+ result = brand.delete_extension!(id)
187
+
188
+ if result[:success]
189
+ puts "Unpublished #{id}."
190
+ else
191
+ warn "Error: #{result[:error]}"
192
+ exit 1
193
+ end
194
+ end
195
+
196
+ desc "search [QUERY]", "Search the public extension marketplace"
197
+ method_option :sort, type: :string, default: "newest",
198
+ desc: "Sort order: newest, updated, downloads"
199
+ def search(query = nil)
200
+ brand = Clacky::BrandConfig.load
201
+ result = brand.search_extensions!(query: query, sort: options[:sort])
202
+
203
+ unless result[:success]
204
+ warn "Error: #{result[:error]}"
205
+ exit 1
206
+ end
207
+
208
+ extensions = result[:extensions]
209
+ if extensions.empty?
210
+ puts query ? "No extensions found for #{query.inspect}." : "No extensions available yet."
211
+ return
212
+ end
213
+
214
+ extensions.each do |ext|
215
+ emoji = ext["emoji"] || "🧩"
216
+ ver = ext["version"]
217
+ units = (ext["units"] || {}).map { |k, v| "#{v} #{k}" }.join(", ")
218
+ name = ext["name_zh"] || ext["name"]
219
+ puts "#{emoji} #{name}#{ver ? " v#{ver}" : ""}#{units.empty? ? "" : " (#{units})"}"
220
+ desc = ext["description_zh"] || ext["description"]
221
+ puts " #{desc}" if desc && !desc.to_s.strip.empty?
222
+ end
223
+ end
224
+
225
+ end
226
+ end
@@ -17,11 +17,15 @@ module Clacky
17
17
  # Public entry called from HttpServer#_dispatch_rest.
18
18
  # Returns true to indicate the request was handled (200/4xx/5xx).
19
19
  def handle(req, res, http_server:)
20
- ext_id, sub_path = parse_path(req.path)
21
- return not_found(res, "extension id missing") unless ext_id
20
+ mount_id, sub_path = parse_path(req.path)
21
+ return not_found(res, "extension mount id missing") unless mount_id
22
22
 
23
- klass = Clacky::ApiExtension.registry[ext_id]
24
- return not_found(res, "extension '#{ext_id}' not found") unless klass
23
+ # Lazily (re)load the handler on each request so edits to handler.rb take
24
+ # effect with no restart cheap for a local app with a handful of exts.
25
+ Clacky::ApiExtensionLoader.ensure_fresh(mount_id)
26
+
27
+ klass = Clacky::ApiExtension.registry[mount_id]
28
+ return not_found(res, "extension '#{mount_id}' not found") unless klass
25
29
 
26
30
  method = req.request_method.to_s.downcase.to_sym
27
31
  route, params = find_route(klass, method, sub_path)
@@ -38,10 +42,10 @@ module Clacky
38
42
  # Tells HttpServer whether a given /api/ext/... path can skip access-key
39
43
  # auth, so the host can keep its single-source-of-truth auth logic.
40
44
  def public_path?(path, method)
41
- ext_id, sub_path = parse_path(path)
42
- return false unless ext_id
45
+ mount_id, sub_path = parse_path(path)
46
+ return false unless mount_id
43
47
 
44
- klass = Clacky::ApiExtension.registry[ext_id]
48
+ klass = Clacky::ApiExtension.registry[mount_id]
45
49
  return false unless klass
46
50
  return false if klass.public_paths.empty?
47
51
 
@@ -51,16 +55,25 @@ module Clacky
51
55
  klass.public_paths.include?(route.pattern)
52
56
  end
53
57
 
58
+ # Local-app convenience: see ApiExtensionLoader.ensure_fresh — that is
59
+ # the single source of truth for per-request hot reload.
60
+
61
+ # /api/ext/<ext_id>/<sub-path> → [ext_id, "/<sub-path>"]
54
62
  private def parse_path(path)
55
63
  return [nil, nil] unless path.to_s.start_with?(MOUNT_PREFIX)
56
64
 
57
65
  tail = path[MOUNT_PREFIX.length..]
58
66
  slash = tail.index("/")
59
67
  if slash
60
- [tail[0...slash], tail[slash..]]
68
+ ext_id = tail[0...slash]
69
+ sub = tail[slash..]
61
70
  else
62
- [tail, "/"]
71
+ ext_id = tail
72
+ sub = "/"
63
73
  end
74
+ return [nil, nil] if ext_id.empty?
75
+
76
+ [ext_id, sub]
64
77
  end
65
78
 
66
79
  private def find_route(klass, method, sub_path)
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ # Loads Ruby hook files contributed by ext.yml containers
5
+ # (`contributes.hooks: [{ event, file }]`). Each file is required once at
6
+ # process start; it registers callbacks via `Clacky::ExtensionHookRegistry.add`.
7
+ # Every agent then copies those callbacks onto its own `HookManager` during
8
+ # init, so each agent gets its own isolated hook chain.
9
+ module ExtensionHookLoader
10
+ Result = Struct.new(:registered, :skipped, keyword_init: true)
11
+
12
+ def self.load_all
13
+ result = Result.new(registered: [], skipped: [])
14
+ units = Array(Clacky::ExtensionLoader.last_result&.hooks)
15
+ units.each do |unit|
16
+ event = unit.spec["event"].to_sym
17
+ unless Clacky::HookManager::HOOK_EVENTS.include?(event)
18
+ msg = "unknown event: #{event}"
19
+ result.skipped << [unit.id, msg]
20
+ Clacky::Logger.warn("[ExtensionHookLoader] #{unit.ext_id}/#{unit.id}: #{msg}")
21
+ next
22
+ end
23
+
24
+ Clacky::ExtensionHookRegistry.current_event = event
25
+ require unit.spec["file_abs"]
26
+ result.registered << [unit.ext_id, unit.id, event]
27
+ rescue StandardError, ScriptError => e
28
+ result.skipped << [unit.id, e.message]
29
+ Clacky::Logger.warn("[ExtensionHookLoader] #{unit.ext_id}/#{unit.id}: #{e.message}")
30
+ ensure
31
+ Clacky::ExtensionHookRegistry.current_event = nil
32
+ end
33
+ @last_result = result
34
+ result
35
+ end
36
+
37
+ def self.last_result
38
+ @last_result || load_all
39
+ end
40
+ end
41
+
42
+ # Process-wide registry for ext-contributed hook callbacks. Callbacks are
43
+ # registered once at file load time; each new agent copies them onto its own
44
+ # HookManager during init via `apply_to`.
45
+ module ExtensionHookRegistry
46
+ @callbacks = Hash.new { |h, k| h[k] = [] }
47
+ @current_event = nil
48
+
49
+ class << self
50
+ attr_accessor :current_event
51
+
52
+ # Register a callback. `event` falls back to the loader-set context so
53
+ # ext hook files can call `add { ... }` without repeating the event name
54
+ # already declared in ext.yml.
55
+ def add(event = nil, &block)
56
+ ev = (event || @current_event)
57
+ raise ArgumentError, "ExtensionHookRegistry.add called outside a hook file" unless ev
58
+
59
+ @callbacks[ev.to_sym] << block
60
+ end
61
+
62
+ def callbacks
63
+ @callbacks
64
+ end
65
+
66
+ def apply_to(hook_manager)
67
+ @callbacks.each do |event, blocks|
68
+ blocks.each { |b| hook_manager.add(event, &b) }
69
+ end
70
+ end
71
+
72
+ def clear!
73
+ @callbacks.clear
74
+ end
75
+ end
76
+ end
77
+ end