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
data/lib/clacky/cli.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "ui2"
7
7
  require_relative "json_ui_controller"
8
8
  require_relative "plain_ui_controller"
9
9
  require_relative "brand_config"
10
+ require_relative "extension/cli_commands"
10
11
 
11
12
  module Clacky
12
13
  class CLI < Thor
@@ -1121,7 +1122,7 @@ module Clacky
1121
1122
  LONGDESC
1122
1123
  option :desc, type: :string, aliases: "-d", default: "", desc: "Short description"
1123
1124
  def patch_new(id, target)
1124
- require_relative "patch_loader"
1125
+ require_relative "extension/patch_loader"
1125
1126
  path = Clacky::PatchLoader.scaffold(id, target, description: options[:desc])
1126
1127
  puts "Created patch: #{path}"
1127
1128
  puts "Edit patch.rb, then run: clacky patch_verify"
@@ -1213,62 +1214,8 @@ module Clacky
1213
1214
  exit 1 if result.skipped.any?
1214
1215
  end
1215
1216
 
1216
- desc "api_ext_new NAME", "Scaffold a custom HTTP API extension at ~/.clacky/api_ext/NAME/"
1217
- long_desc <<-LONGDESC
1218
- Generate a ready-to-edit HTTP API extension skeleton. The skeleton mounts
1219
- a sample route under /api/ext/NAME/ — fill in your routes, then verify with
1220
- `clacky api_ext_verify`.
1221
-
1222
- Examples:
1223
- $ clacky api_ext_new my-dashboard
1224
- LONGDESC
1225
- def api_ext_new(name)
1226
- path = Clacky::ApiExtensionLoader.scaffold(name)
1227
- puts "Created api extension: #{path}"
1228
- puts "Edit the routes, then run: clacky api_ext_verify"
1229
- rescue ArgumentError => e
1230
- warn "Error: #{e.message}"
1231
- exit 1
1232
- end
1233
-
1234
- desc "api_ext_verify", "Load user API extensions and report which are valid"
1235
- def api_ext_verify
1236
- result = Clacky::ApiExtensionLoader.load_all
1237
-
1238
- if result.loaded.empty? && result.skipped.empty?
1239
- puts "No api extensions found in ~/.clacky/api_ext/"
1240
- return
1241
- end
1242
-
1243
- result.loaded.each do |id|
1244
- klass = Clacky::ApiExtension.registry[id]
1245
- public_count = klass.public_paths.size
1246
- suffix = public_count > 0 ? " (#{public_count} public)" : ""
1247
- puts "[OK] #{id} — #{klass.routes.size} route(s)#{suffix}"
1248
- end
1249
- result.skipped.each { |(n, reason)| puts "[SKIP] #{n} — #{reason}" }
1250
- exit 1 if result.skipped.any?
1251
- end
1252
-
1253
- desc "api_ext_list", "List loaded API extensions and their routes"
1254
- def api_ext_list
1255
- Clacky::ApiExtensionLoader.load_all if Clacky::ApiExtension.registry.empty?
1256
-
1257
- if Clacky::ApiExtension.registry.empty?
1258
- puts "No api extensions loaded."
1259
- return
1260
- end
1261
-
1262
- Clacky::ApiExtension.registry.each do |id, klass|
1263
- public_tag = klass.public_paths.any? ? " (public)" : ""
1264
- puts "#{id}#{public_tag}"
1265
- klass.routes.each do |route|
1266
- full_path = "/api/ext/#{id}#{route.pattern}".chomp("/")
1267
- full_path = "/api/ext/#{id}/" if full_path == "/api/ext/#{id}"
1268
- puts " #{route.method.to_s.upcase.ljust(6)} #{full_path}"
1269
- end
1270
- end
1271
- end
1217
+ desc "ext SUBCOMMAND", "Manage extension containers (new, verify, list, pack, install, publish, search)"
1218
+ subcommand "ext", Clacky::CliExtensionCommands
1272
1219
 
1273
1220
  desc "billing", "Show billing summary and usage statistics"
1274
1221
  long_desc <<-LONGDESC
@@ -0,0 +1,18 @@
1
+ id: coding
2
+ name: Coding
3
+ description: AI coding assistant agent
4
+ version: "0.1.0"
5
+ author: OpenClacky
6
+ homepage: https://www.openclacky.com
7
+ license: MIT
8
+ origin: self
9
+ contributes:
10
+ agents:
11
+ - id: coding
12
+ title: Coding
13
+ title_zh: 编程
14
+ description: AI coding assistant and technical co-founder
15
+ description_zh: AI 编程助手,你的技术合伙人
16
+ order: 2
17
+ prompt: agents/coding/system_prompt.md
18
+ panels: [git/git, time_machine/time_machine]
@@ -0,0 +1,95 @@
1
+ You are Extension Developer, an AI expert who helps users build, debug, and publish
2
+ OpenClacky extensions through conversation. You know the OpenClacky extension system
3
+ inside out and drive the whole workflow — scaffold, edit, verify, reload, publish —
4
+ so the user never has to memorize commands or file layouts.
5
+
6
+ Your role is to:
7
+ - Turn a plain-language idea ("I want an extension that shows the weather") into a
8
+ working extension by scaffolding it, wiring the right contributes, and iterating.
9
+ - Read and edit extension files directly, then verify and hot-reload to confirm.
10
+ - Debug using structured verify errors, fixing manifest and file issues.
11
+ - Guide the user through packing and publishing to the marketplace.
12
+
13
+ ## The Extension Model (ground truth)
14
+
15
+ An extension is one directory containing an `ext.yml` manifest that declares
16
+ `contributes:`. Nothing is nested — units reference each other by id.
17
+
18
+ Three layers, override precedence `local > installed > builtin`:
19
+ - `builtin` — bundled in the gem (`default_extensions/`)
20
+ - `installed` — `~/.clacky/ext/installed/<id>/` (from `ext install`)
21
+ - `local` — `~/.clacky/ext/local/<id>/` (where users develop; `ext new` lands here)
22
+
23
+ Seven contributes types (each is a self-describing unit):
24
+ - `panels` — WebUI panels (a `view.js`, no build step, no React, no iframe)
25
+ - `api` — one backend file `api/handler.rb`, mounted at `/api/ext/<id>/`
26
+ - `skills` — a `SKILL.md` under `skills/<id>/` (prompt-only capability)
27
+ - `agents` — a `system_prompt.md`; can reference `panels: [id]` and `skills: [id]`
28
+ - `channels` — an IM adapter
29
+ - `patches` — monkey-patch a real class (advanced, supply-chain risk)
30
+ - `hooks` — lifecycle hooks like `before_tool_use` (advanced)
31
+
32
+ Hot reload is per-request: after editing `view.js`, `handler.rb`, or a `SKILL.md`,
33
+ the user just reloads the WebUI page — no server restart. Editing `ext.yml` also
34
+ takes effect on the next load.
35
+
36
+ ## ext.yml shape (memorize this)
37
+
38
+ ```yaml
39
+ id: my-ext
40
+ name: My Extension
41
+ description: what it does
42
+ version: "0.1.0"
43
+ origin: self
44
+ contributes:
45
+ api: api/handler.rb
46
+ panels:
47
+ - id: dashboard
48
+ view: panels/dashboard/view.js
49
+ attach: ["*"] # panels/agents to attach to; "*" = all
50
+ skills:
51
+ - id: my-ext-skill # SKILL.md lives at skills/my-ext-skill/SKILL.md
52
+ agents:
53
+ - id: helper
54
+ title: Helper
55
+ prompt: agents/helper/system_prompt.md
56
+ panels: [dashboard]
57
+ skills: [my-ext-skill]
58
+ ```
59
+
60
+ Panel `view.js` mounts via `Clacky.ext.ui.mount(slot, spec, opts)`. Backend classes
61
+ subclass `Clacky::ApiExtension` and define routes relative to `/api/ext/<id>/`.
62
+
63
+ ## Working process
64
+
65
+ You have three companion skills — they fire automatically when the situation matches,
66
+ but you own the flow and decide when to lean on each:
67
+ - **ext-scaffold** — when the user wants to start a new extension.
68
+ - **ext-debug** — when something is broken, verify reports errors, or a panel/api won't load.
69
+ - **ext-publish** — when the extension is ready to ship to the marketplace.
70
+
71
+ Typical loop:
72
+ 1. Clarify the idea in one question if it's ambiguous (what should it DO, and where —
73
+ a panel, a skill, an agent, a backend?). Then map it to the smallest set of
74
+ contributes types. Don't over-scope — most extensions are one panel + one handler,
75
+ or one skill.
76
+ 2. Scaffold with `clacky ext new <id>` (add `--full` only if they truly need the
77
+ kitchen-sink reference). Read the generated files so you know the starting point.
78
+ 3. Edit the files to match the idea — real code, not placeholders. Follow the panel
79
+ styling convention: reuse host classes (`btn-primary`, `btn-secondary`,
80
+ `form-input`, `form-textarea`, `form-label`) so the extension inherits the theme.
81
+ 4. Run `clacky ext verify` and read the output. Each `[ERR]`/`[WARN]` is structured
82
+ (`{ext, unit, code, message, file, hint}`) — the `hint` tells you how to fix it.
83
+ Fix, re-verify, until clean.
84
+ 5. Tell the user to reload the WebUI page to see panels/api changes live.
85
+ 6. When they're happy, hand off to publishing: `clacky ext pack <id>` then
86
+ `clacky ext publish <id>` (requires an activated user license).
87
+
88
+ ## Guidance
89
+
90
+ - Prefer editing real files over describing what to do. You are hands-on.
91
+ - Keep extensions minimal — add only the contributes types the idea needs.
92
+ - Never scaffold `patches` or `hooks` unless the user explicitly asks; they run
93
+ arbitrary Ruby and carry supply-chain risk.
94
+ - Explain results in plain terms — the user may not be an extension expert.
95
+ - Verify before you claim something works. "It should work" is not "it works."
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "tmpdir"
5
+ require "socket"
6
+ require "digest"
7
+
8
+ # Extension Studio — backend for the debug/publish panels. Mounted at
9
+ # /api/ext/ext-studio/. Reads the resolved extension tree, runs the verifier,
10
+ # packs and publishes local containers to the marketplace.
11
+ class ExtStudioExt < Clacky::ApiExtension
12
+ timeout 60
13
+
14
+ # GET /api/ext/ext-studio/extensions
15
+ # List every local-layer container with a units summary and verify status,
16
+ # so the debug panel can populate its picker and detail view.
17
+ get "/extensions" do
18
+ result = Clacky::ExtensionLoader.load_all(force: true)
19
+ issues = Clacky::ExtensionVerifier.verify(result)
20
+
21
+ exts = local_containers(result).map do |ext_id, container|
22
+ serialize_container(ext_id, container, result, issues)
23
+ end
24
+ exts.sort_by! { |e| -e[:mtime] }
25
+
26
+ json(extensions: exts)
27
+ end
28
+
29
+ # POST /api/ext/ext-studio/verify
30
+ # body: { ext_id? } — omit ext_id to verify the whole tree.
31
+ # Returns structured issues (same shape the CLI prints) plus resolved units.
32
+ post "/verify" do
33
+ ext_id = json_body["ext_id"].to_s.strip
34
+ result = Clacky::ExtensionLoader.load_all(force: true)
35
+ issues = Clacky::ExtensionVerifier.verify(result)
36
+
37
+ scoped = ext_id.empty? ? issues : issues.select { |i| i.ext == ext_id }
38
+ units = result.units
39
+ units = units.select { |u| u.ext_id == ext_id } unless ext_id.empty?
40
+
41
+ json(
42
+ ext_id: ext_id.empty? ? nil : ext_id,
43
+ units: units.map { |u| serialize_unit(u) },
44
+ issues: scoped.map { |i| serialize_issue(i) },
45
+ ok: scoped.none? { |i| i.level == :error }
46
+ )
47
+ end
48
+
49
+ # POST /api/ext/ext-studio/pack
50
+ # body: { ext_id }
51
+ # Packs a local container into a zip (into a temp dir) and reports its path.
52
+ post "/pack" do
53
+ ext_id = require_ext_id!
54
+ Dir.mktmpdir("clacky-ext-studio-pack") do |tmp|
55
+ res = Clacky::ExtensionPackager.pack(ext_id, out_dir: tmp)
56
+ json(ok: true, ext_id: res.ext_id, path: res.path)
57
+ end
58
+ rescue Clacky::ExtensionPackager::Error => e
59
+ error!(e.message, status: 422)
60
+ end
61
+
62
+ # POST /api/ext/ext-studio/publish
63
+ # body: { ext_id, force?, status?, changelog? }
64
+ # Packs then uploads to the marketplace. Requires the device to be bound to a
65
+ # platform account (device token). When unbound, returns a 428 with a hint so
66
+ # the UI can trigger the on-demand device-authorization flow.
67
+ post "/publish" do
68
+ ext_id = require_ext_id!
69
+
70
+ unless Clacky::Identity.load.bound?
71
+ error!("device not bound to a platform account; authorize this device to publish",
72
+ status: 428, needs_binding: true)
73
+ end
74
+
75
+ brand = Clacky::BrandConfig.load
76
+
77
+ Dir.mktmpdir("clacky-ext-studio-publish") do |tmp|
78
+ res = Clacky::ExtensionPackager.pack(ext_id, out_dir: tmp)
79
+ zip_data = File.binread(res.path)
80
+
81
+ result = brand.upload_extension!(
82
+ res.ext_id, zip_data,
83
+ force: json_body["force"] == true,
84
+ status: presence(json_body["status"]),
85
+ changelog: presence(json_body["changelog"])
86
+ )
87
+
88
+ if result[:success]
89
+ ext = result[:extension] || {}
90
+ ver = (ext["latest_version"] || {})["version"]
91
+ json(ok: true, ext_id: res.ext_id, version: ver, status: ext["status"])
92
+ elsif result[:already_exists]
93
+ json(ok: false, already_exists: true,
94
+ error: "#{res.ext_id} already published. Publish a new version with force.")
95
+ else
96
+ error!(result[:error] || "publish failed", status: 502)
97
+ end
98
+ end
99
+ rescue Clacky::ExtensionPackager::Error => e
100
+ error!(e.message, status: 422)
101
+ end
102
+
103
+ # GET /api/ext/ext-studio/published
104
+ # List the current user's published extensions from the marketplace.
105
+ get "/published" do
106
+ brand = Clacky::BrandConfig.load
107
+ result = brand.fetch_my_extensions!
108
+ error!(result[:error] || "failed to fetch published extensions", status: 502) unless result[:success]
109
+
110
+ exts = Array(result[:extensions]).map do |ext|
111
+ {
112
+ id: ext["name"] || ext["slug"] || ext["id"],
113
+ name: ext["name"],
114
+ version: (ext["latest_version"] || {})["version"] || ext["version"],
115
+ status: ext["status"],
116
+ units: ext["units"] || {}
117
+ }
118
+ end
119
+ json(extensions: exts)
120
+ end
121
+
122
+ # POST /api/ext/ext-studio/unpublish
123
+ # body: { ext_id }
124
+ post "/unpublish" do
125
+ ext_id = require_ext_id!
126
+ result = Clacky::BrandConfig.load.delete_extension!(ext_id)
127
+ error!(result[:error] || "unpublish failed", status: 502) unless result[:success]
128
+ json(ok: true, ext_id: ext_id)
129
+ end
130
+
131
+ # POST /api/ext/ext-studio/develop
132
+ # body: { idea? }
133
+ # Spawns a session bound to the ext-developer agent, optionally seeded with
134
+ # the user's idea as the first task — the "let AI build it for me" entry.
135
+ post "/develop" do
136
+ idea = presence(json_body["idea"])
137
+ name = idea ? "扩展开发: #{idea[0, 40]}" : "扩展开发"
138
+ sid = create_session(name: name, prompt: idea, profile: "ext-developer", source: :setup)
139
+ json(ok: true, session_id: sid)
140
+ end
141
+
142
+ # GET /api/ext/ext-studio/binding
143
+ # Reports whether this device is bound to a platform account, so the publish
144
+ # panel can decide up-front whether to run the binding flow.
145
+ get "/binding" do
146
+ identity = Clacky::Identity.load
147
+ json(bound: identity.bound?, user_id: identity.user_id)
148
+ end
149
+
150
+ # POST /api/ext/ext-studio/binding/start
151
+ # Kicks off an RFC 8628 device-authorization flow against the platform and
152
+ # returns the verification URL the panel opens plus the device_code to poll.
153
+ post "/binding/start" do
154
+ client = Clacky::PlatformHttpClient.new
155
+ result = client.post("/api/v1/device/authorize", {
156
+ device_id: binding_device_id,
157
+ device_info: { os: RUBY_PLATFORM, hostname: Socket.gethostname, app_version: Clacky::VERSION }
158
+ })
159
+
160
+ error!(result[:error] || "could not start authorization", status: 502) unless result[:success]
161
+
162
+ data = result[:data]
163
+ json(
164
+ ok: true,
165
+ device_code: data["device_code"],
166
+ user_code: data["user_code"],
167
+ verification_uri: data["verification_uri"],
168
+ verification_uri_complete: data["verification_uri_complete"],
169
+ interval: data["interval"] || 5
170
+ )
171
+ end
172
+
173
+ # POST /api/ext/ext-studio/binding/poll { device_code }
174
+ # Polls the platform once. On approval, binds the issued device token to the
175
+ # local Identity so subsequent publishes authenticate as the platform account.
176
+ post "/binding/poll" do
177
+ device_code = presence(json_body["device_code"])
178
+ error!("device_code required", status: 422) unless device_code
179
+
180
+ client = Clacky::PlatformHttpClient.new
181
+ result = client.post("/api/v1/device/token", { device_code: device_code })
182
+ data = result[:data] || {}
183
+ status = data["status"]
184
+
185
+ if result[:success] && status == "approved"
186
+ Clacky::Identity.load.bind!(
187
+ device_token: data["device_token"],
188
+ user_id: data["user_id"]
189
+ ) if data["device_token"]
190
+ json(ok: true, status: "approved")
191
+ elsif status == "pending"
192
+ json(ok: true, status: "pending")
193
+ else
194
+ json(ok: false, status: status || "error", error: result[:error])
195
+ end
196
+ end
197
+
198
+ private def local_containers(result)
199
+ Array(result.containers).select { |_id, c| c[:layer] == :local }
200
+ end
201
+
202
+ private def serialize_container(ext_id, container, result, issues)
203
+ raw = container[:raw] || {}
204
+ ext_issues = issues.select { |i| i.ext == ext_id }
205
+ dir = container[:dir]
206
+ {
207
+ id: ext_id,
208
+ name: raw["name"] || ext_id,
209
+ description: raw["description"],
210
+ version: raw["version"],
211
+ origin: container[:origin],
212
+ layer: container[:layer].to_s,
213
+ dir: dir,
214
+ mtime: File.mtime(File.join(dir, "ext.yml")).to_i,
215
+ units: result.units.select { |u| u.ext_id == ext_id }.map { |u| serialize_unit(u) },
216
+ error_count: ext_issues.count { |i| i.level == :error },
217
+ warning_count: ext_issues.count { |i| i.level == :warning }
218
+ }
219
+ end
220
+
221
+ private def serialize_unit(unit)
222
+ { kind: unit.kind.to_s, id: unit.id, layer: unit.layer.to_s }
223
+ end
224
+
225
+ private def serialize_issue(issue)
226
+ {
227
+ ext: issue.ext,
228
+ unit: issue.unit,
229
+ level: issue.level.to_s,
230
+ code: issue.code,
231
+ message: issue.message,
232
+ file: issue.file,
233
+ hint: issue.hint
234
+ }
235
+ end
236
+
237
+ private def require_ext_id!
238
+ id = json_body["ext_id"].to_s.strip
239
+ error!("ext_id required", status: 422) if id.empty?
240
+ id
241
+ end
242
+
243
+ private def presence(value)
244
+ str = value.to_s.strip
245
+ str.empty? ? nil : str
246
+ end
247
+
248
+ # Stable per-machine id for the device-authorization flow. Matches the
249
+ # onboarding device_id so binding reuses the same device row on the platform.
250
+ private def binding_device_id
251
+ components = [Socket.gethostname, ENV["USER"] || ENV["USERNAME"] || "", RUBY_PLATFORM]
252
+ Digest::SHA256.hexdigest(components.join(":"))
253
+ end
254
+ end
@@ -0,0 +1,32 @@
1
+ id: ext-studio
2
+ name: Extension Studio
3
+ description: AI-driven workspace for building, debugging, and publishing OpenClacky extensions
4
+ version: "0.1.0"
5
+ author: OpenClacky
6
+ homepage: https://www.openclacky.com
7
+ license: MIT
8
+ origin: self
9
+ contributes:
10
+ api: api/handler.rb
11
+ panels:
12
+ - id: studio
13
+ title: Extension & Creation
14
+ title_zh: 扩展与创作
15
+ description: Build, debug and publish extensions and skills
16
+ description_zh: 开发、调试、发布扩展与 skill
17
+ view: panels/studio/view.js
18
+ order: 210
19
+ agents:
20
+ - id: ext-developer
21
+ title: Extension Developer
22
+ title_zh: 扩展开发
23
+ description: AI expert that helps you build, debug, and publish OpenClacky extensions
24
+ description_zh: 帮你开发、调试、发布 OpenClacky 扩展的 AI 专家
25
+ order: 5
26
+ prompt: agents/ext-developer/system_prompt.md
27
+ panels: [studio]
28
+ skills: [ext-scaffold, ext-debug, ext-publish]
29
+ skills:
30
+ - id: ext-scaffold
31
+ - id: ext-debug
32
+ - id: ext-publish