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
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 "
|
|
1217
|
-
|
|
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
|