openclacky 1.3.10 → 1.4.0
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 +48 -0
- data/lib/clacky/agent/skill_manager.rb +1 -0
- data/lib/clacky/brand_config.rb +32 -6
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +78 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +390 -50
- data/lib/clacky/default_skills/media-gen/SKILL.md +182 -7
- data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +5 -3
- data/lib/clacky/extension/packager.rb +13 -3
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/media/generator.rb +41 -0
- data/lib/clacky/media/volcengine.rb +394 -0
- data/lib/clacky/server/http_server.rb +107 -45
- data/lib/clacky/telemetry.rb +9 -0
- data/lib/clacky/tools/terminal.rb +24 -8
- data/lib/clacky/ui2/layout_manager.rb +4 -0
- data/lib/clacky/ui2/screen_buffer.rb +31 -3
- data/lib/clacky/utils/file_processor.rb +13 -8
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +189 -24
- data/lib/clacky/web/features/extensions/store.js +14 -7
- data/lib/clacky/web/features/extensions/view.js +77 -13
- data/lib/clacky/web/features/skills/view.js +61 -1
- data/lib/clacky/web/i18n.js +38 -6
- data/lib/clacky/web/index.html +6 -11
- data/lib/clacky/web/sessions.js +12 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29e2af05d27f902fc645a1399eb16d16d41681ec03312ee861fdd594292368c2
|
|
4
|
+
data.tar.gz: bbb62ec065f07867a2085b6dc0f81d5702c34cf0ff4e10e0b88513daa5fb4541
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf7f4ad60821686404ee3b43607e59c51cb0d38420171b94eaea2c6d2877d71c03052f9147b56f61aeb5fc01155127d4aab15a8f0b6ecc0b73d6b294fb0bbe3c
|
|
7
|
+
data.tar.gz: ba3a05a10e80fd8b48f10f0778388c5417ce8fa79e0f3936cb6ef734117609bc85f56868227e174675e4132d0c619a8b385619c7aaf62e75039fc3f4895d15f8
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,54 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
|
|
7
|
+
## [1.4.0] - 2026-07-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Volcengine Ark Seedance video generation provider (async submit + poll)
|
|
11
|
+
- Entry points config and extension info editing in ext-studio
|
|
12
|
+
- Search box in skills panel tab bar
|
|
13
|
+
- Default letter gradient icon for extensions without a custom icon
|
|
14
|
+
- Skeleton loading for published extensions list in ext-studio
|
|
15
|
+
|
|
16
|
+
### Improved
|
|
17
|
+
- Extensions market renders `display_name` instead of raw slug
|
|
18
|
+
- ext-studio extension picker and cards show display name; unit tags aggregated by type
|
|
19
|
+
- Session list "Today / Yesterday" labels localized
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Terminal Pass 1 echo-strip no longer swallows real output (C-5701)
|
|
23
|
+
- OS metadata files (`.DS_Store`, `__MACOSX`) excluded from packed extension zips (C-5728)
|
|
24
|
+
- TUI exit now restores original terminal settings via `stty` (C-5724)
|
|
25
|
+
- Local image cache busted with mtime version param when file is overwritten (C-5703)
|
|
26
|
+
- Generated media usage correctly associated with sessions (C-5739)
|
|
27
|
+
- Mobile header search action alignment (C-5733)
|
|
28
|
+
- Session search shortcut hint adapted for web (C-5734)
|
|
29
|
+
- Memory card expand button height aligned with other action buttons
|
|
30
|
+
- Billing model filter select styling improved
|
|
31
|
+
- Modal overlay click-to-close disabled (prevents accidental dismissal)
|
|
32
|
+
- Version number / date separator added in extension version history
|
|
33
|
+
- All users can now publish extensions (permission gate removed)
|
|
34
|
+
- Invalid extension version format shows inline error in publish modal
|
|
35
|
+
- Brand update button hover color uses correct CSS variable
|
|
36
|
+
|
|
37
|
+
## [1.3.11] - 2026-07-12
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- Delete button for unpublished local extensions in ext-studio
|
|
41
|
+
- Extension install count shown in marketplace
|
|
42
|
+
- Update badge and installed version shown in marketplace list
|
|
43
|
+
|
|
44
|
+
### Improved
|
|
45
|
+
- Extension version history styled with markdown rendering
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- Preserve user data on extension update (overwrite instead of replace)
|
|
49
|
+
- Extension install timeout increased to 300s (was too short for large extensions)
|
|
50
|
+
- Installed tab search and loading state in extension store
|
|
51
|
+
- Prevent license loss from non-atomic brand.yml writes
|
|
52
|
+
- Extension detail page content no longer cut off or unscrollable
|
|
53
|
+
- Extension author label position corrected to end of title row
|
|
54
|
+
|
|
7
55
|
## [1.3.10] - 2026-07-09
|
|
8
56
|
|
|
9
57
|
### Added
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -92,17 +92,25 @@ module Clacky
|
|
|
92
92
|
instance = new(data)
|
|
93
93
|
instance.ensure_device_id!
|
|
94
94
|
instance
|
|
95
|
-
rescue StandardError
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
# A read/parse failure here is almost always a transient corruption (e.g. a
|
|
97
|
+
# concurrent write caught mid-truncate). Do NOT touch brand.yml — overwriting
|
|
98
|
+
# it with an empty config would permanently wipe the license, and even moving
|
|
99
|
+
# it aside could lose a file that self-heals a few milliseconds later. Leave
|
|
100
|
+
# the on-disk file untouched and return an in-memory-only config (device_id
|
|
101
|
+
# generated but never saved); the next load usually succeeds.
|
|
102
|
+
Clacky::Logger.error("[Brand] load failed, using in-memory fallback WITHOUT touching brand.yml: #{e.class}: #{e.message}")
|
|
96
103
|
instance = new({})
|
|
97
|
-
instance.ensure_device_id!
|
|
104
|
+
instance.ensure_device_id!(persist: false)
|
|
98
105
|
instance
|
|
99
106
|
end
|
|
100
107
|
|
|
101
|
-
def ensure_device_id!
|
|
108
|
+
def ensure_device_id!(persist: true)
|
|
102
109
|
return if @device_id && !@device_id.strip.empty?
|
|
103
110
|
|
|
111
|
+
Clacky::Logger.warn("[Brand] regenerating device_id (previous was blank; brand.yml may have been reset or corrupted)")
|
|
104
112
|
@device_id = generate_device_id
|
|
105
|
-
save
|
|
113
|
+
save if persist
|
|
106
114
|
end
|
|
107
115
|
|
|
108
116
|
# Returns true when this installation has a product name configured.
|
|
@@ -159,10 +167,28 @@ module Clacky
|
|
|
159
167
|
end
|
|
160
168
|
|
|
161
169
|
# Save current state to ~/.clacky/brand.yml
|
|
170
|
+
#
|
|
171
|
+
# Writes atomically: content goes to a per-process temp file that is fsynced
|
|
172
|
+
# and then renamed over the target. rename is atomic within a filesystem, so
|
|
173
|
+
# concurrent processes (server + CLI, or multiple servers) never observe a
|
|
174
|
+
# truncated or half-written file — which previously caused load to fall back
|
|
175
|
+
# to an empty config and wipe the license.
|
|
162
176
|
def save
|
|
163
177
|
FileUtils.mkdir_p(CONFIG_DIR)
|
|
164
|
-
|
|
165
|
-
|
|
178
|
+
Clacky::Logger.debug("[Brand] save: product_name=#{@product_name.inspect} license_key?=#{!@license_key.nil? && !@license_key.to_s.empty?} device_id?=#{!@device_id.nil? && !@device_id.to_s.empty?}")
|
|
179
|
+
|
|
180
|
+
tmp_path = "#{BRAND_FILE}.#{Process.pid}.#{SecureRandom.hex(4)}.tmp"
|
|
181
|
+
begin
|
|
182
|
+
File.open(tmp_path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |f|
|
|
183
|
+
f.write(to_yaml)
|
|
184
|
+
f.flush
|
|
185
|
+
f.fsync
|
|
186
|
+
end
|
|
187
|
+
File.rename(tmp_path, BRAND_FILE)
|
|
188
|
+
rescue StandardError
|
|
189
|
+
File.delete(tmp_path) if File.exist?(tmp_path)
|
|
190
|
+
raise
|
|
191
|
+
end
|
|
166
192
|
end
|
|
167
193
|
|
|
168
194
|
# Remove the local license binding and wipe all brand-related fields from disk.
|
|
@@ -111,7 +111,7 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
111
111
|
exts = Array(result[:extensions]).map do |ext|
|
|
112
112
|
{
|
|
113
113
|
id: ext["name"] || ext["slug"] || ext["id"],
|
|
114
|
-
name: ext["name"],
|
|
114
|
+
name: ext["display_name"] || ext["name"],
|
|
115
115
|
version: (ext["latest_version"] || {})["version"] || ext["version"],
|
|
116
116
|
status: ext["status"],
|
|
117
117
|
units: ext["units"] || {}
|
|
@@ -120,6 +120,26 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
120
120
|
json(extensions: exts)
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# DELETE /api/ext/ext-studio/local
|
|
124
|
+
# body: { ext_id }
|
|
125
|
+
# Permanently removes a local extension directory. Only allowed for
|
|
126
|
+
# unpublished extensions (caller must check; server enforces dir existence).
|
|
127
|
+
delete "/local" do
|
|
128
|
+
ext_id = require_ext_id!
|
|
129
|
+
|
|
130
|
+
result = Clacky::ExtensionLoader.load_all(force: false)
|
|
131
|
+
container = Array(result.containers).find { |id, _| id == ext_id }&.last
|
|
132
|
+
error!("extension not found: #{ext_id}", status: 404) unless container
|
|
133
|
+
error!("not a local extension", status: 422) unless container[:layer] == :local
|
|
134
|
+
|
|
135
|
+
dir = container[:dir]
|
|
136
|
+
error!("extension directory not found", status: 404) unless Dir.exist?(dir)
|
|
137
|
+
|
|
138
|
+
FileUtils.rm_rf(dir)
|
|
139
|
+
Clacky::ExtensionLoader.load_all(force: true)
|
|
140
|
+
json(ok: true, ext_id: ext_id)
|
|
141
|
+
end
|
|
142
|
+
|
|
123
143
|
# POST /api/ext/ext-studio/unpublish
|
|
124
144
|
# body: { ext_id }
|
|
125
145
|
post "/unpublish" do
|
|
@@ -129,6 +149,47 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
129
149
|
json(ok: true, ext_id: ext_id)
|
|
130
150
|
end
|
|
131
151
|
|
|
152
|
+
# POST /api/ext/ext-studio/set_meta
|
|
153
|
+
# body: { ext_id, name?, description?, entry_points? }
|
|
154
|
+
# entry_points: [{ unit_id, slot }] — stored under contributes.panels[id].entry_points
|
|
155
|
+
# Writes display metadata back to the local ext.yml without touching other fields.
|
|
156
|
+
post "/set_meta" do
|
|
157
|
+
ext_id = require_ext_id!
|
|
158
|
+
|
|
159
|
+
result = Clacky::ExtensionLoader.load_all(force: false)
|
|
160
|
+
container = Array(result.containers).find { |id, _| id == ext_id }&.last
|
|
161
|
+
error!("extension not found: #{ext_id}", status: 404) unless container
|
|
162
|
+
|
|
163
|
+
yml_path = File.join(container[:dir], "ext.yml")
|
|
164
|
+
error!("ext.yml not found", status: 404) unless File.exist?(yml_path)
|
|
165
|
+
|
|
166
|
+
manifest = Psych.safe_load(File.read(yml_path), permitted_classes: [], aliases: true) || {}
|
|
167
|
+
|
|
168
|
+
manifest["name"] = presence(json_body["name"]) if json_body.key?("name")
|
|
169
|
+
manifest["description"] = presence(json_body["description"]) if json_body.key?("description")
|
|
170
|
+
|
|
171
|
+
if json_body.key?("entry_points")
|
|
172
|
+
eps = json_body["entry_points"]
|
|
173
|
+
by_panel = Hash.new { |h, k| h[k] = [] }
|
|
174
|
+
Array(eps).each { |ep| by_panel[ep["unit_id"].to_s] << { "slot" => ep["slot"] } if ep["slot"] }
|
|
175
|
+
panels = Array((manifest["contributes"] || {})["panels"])
|
|
176
|
+
panels.each do |panel|
|
|
177
|
+
pid = panel["id"].to_s
|
|
178
|
+
slots = by_panel[pid]
|
|
179
|
+
if slots.any?
|
|
180
|
+
panel["entry_points"] = slots
|
|
181
|
+
else
|
|
182
|
+
panel.delete("entry_points")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
File.write(yml_path, Psych.dump(manifest))
|
|
188
|
+
Clacky::ExtensionLoader.load_all(force: true)
|
|
189
|
+
|
|
190
|
+
json(ok: true, ext_id: ext_id)
|
|
191
|
+
end
|
|
192
|
+
|
|
132
193
|
# POST /api/ext/ext-studio/set_version
|
|
133
194
|
# body: { ext_id, version }
|
|
134
195
|
# Writes the new version string back to the local ext.yml.
|
|
@@ -230,6 +291,11 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
230
291
|
raw = container[:raw] || {}
|
|
231
292
|
ext_issues = issues.select { |i| i.ext == ext_id }
|
|
232
293
|
dir = container[:dir]
|
|
294
|
+
ext_units = result.units.select { |u| u.ext_id == ext_id }
|
|
295
|
+
panels = Array((raw["contributes"] || {})["panels"])
|
|
296
|
+
entry_points = panels.flat_map do |p|
|
|
297
|
+
Array(p["entry_points"]).map { |ep| { panel_id: p["id"], slot: ep["slot"] } }
|
|
298
|
+
end
|
|
233
299
|
{
|
|
234
300
|
id: ext_id,
|
|
235
301
|
name: raw["name"] || ext_id,
|
|
@@ -239,7 +305,10 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
239
305
|
layer: container[:layer].to_s,
|
|
240
306
|
dir: dir,
|
|
241
307
|
mtime: File.mtime(File.join(dir, "ext.yml")).to_i,
|
|
242
|
-
units:
|
|
308
|
+
units: ext_units.map { |u| serialize_unit(u) },
|
|
309
|
+
unit_counts: unit_counts(ext_units),
|
|
310
|
+
contributes: raw["contributes"] || {},
|
|
311
|
+
entry_points: entry_points,
|
|
243
312
|
error_count: ext_issues.count { |i| i.level == :error },
|
|
244
313
|
warning_count: ext_issues.count { |i| i.level == :warning }
|
|
245
314
|
}
|
|
@@ -249,6 +318,13 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
249
318
|
{ kind: unit.kind.to_s, id: unit.id, layer: unit.layer.to_s }
|
|
250
319
|
end
|
|
251
320
|
|
|
321
|
+
private def unit_counts(units)
|
|
322
|
+
units.each_with_object({}) do |unit, counts|
|
|
323
|
+
kind = unit.kind.to_s
|
|
324
|
+
counts[kind] = (counts[kind] || 0) + 1
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
252
328
|
private def serialize_issue(issue)
|
|
253
329
|
{
|
|
254
330
|
ext: issue.ext,
|