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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de09b43a446d08c592e1cd49deb914a3ff8c70375cf653f130d2aef9a1cd460e
4
- data.tar.gz: 8030ce355abbe973c9b8294995744b5d4a96e3e38bfc494cb081621172619085
3
+ metadata.gz: 29e2af05d27f902fc645a1399eb16d16d41681ec03312ee861fdd594292368c2
4
+ data.tar.gz: bbb62ec065f07867a2085b6dc0f81d5702c34cf0ff4e10e0b88513daa5fb4541
5
5
  SHA512:
6
- metadata.gz: bd57f31e2d97f1a98b10a775017e2c42ce6354e3fab4a87f9392e884ecca3a2ca3c3ec19d30e8adcb7d8498330446b3d80be787c3594e26b63d07a3e2349f9a7
7
- data.tar.gz: 4b18ae8839c6bd9ab319c7077dca1bc61ea836d38152438215c61a524099c7e17213b0b76f37f8157621a7b168325042fe3137bf7d0b5e257a3609c58ca7c8fc
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
@@ -452,6 +452,7 @@ module Clacky
452
452
  # @return [Hash<String, Proc>]
453
453
  def build_template_context
454
454
  {
455
+ "session_id" => -> { @session_id },
455
456
  "memories_meta" => -> { load_memories_meta },
456
457
  "all_skills_meta" => -> { load_all_skills_meta }
457
458
  }
@@ -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
- File.write(BRAND_FILE, to_yaml)
165
- FileUtils.chmod(0o600, BRAND_FILE)
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: result.units.select { |u| u.ext_id == ext_id }.map { |u| serialize_unit(u) },
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,