openclacky 1.4.0 → 1.4.1
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 +29 -0
- data/lib/clacky/agent/session_serializer.rb +11 -4
- data/lib/clacky/agent.rb +4 -5
- data/lib/clacky/brand_config.rb +53 -3
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +144 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +412 -173
- data/lib/clacky/extension/packager.rb +10 -1
- data/lib/clacky/platform_http_client.rb +9 -7
- data/lib/clacky/providers.rb +4 -4
- data/lib/clacky/server/http_server.rb +19 -18
- data/lib/clacky/tools/grep.rb +6 -1
- data/lib/clacky/utils/environment_detector.rb +16 -9
- data/lib/clacky/utils/file_processor.rb +21 -24
- data/lib/clacky/utils/model_pricing.rb +49 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +121 -17
- data/lib/clacky/web/components/onboard.js +14 -4
- data/lib/clacky/web/features/backup/view.js +4 -3
- data/lib/clacky/web/features/extensions/store.js +2 -2
- data/lib/clacky/web/features/extensions/view.js +14 -19
- data/lib/clacky/web/features/mcp/view.js +5 -2
- data/lib/clacky/web/features/workspace/store.js +11 -0
- data/lib/clacky/web/features/workspace/view.js +10 -4
- data/lib/clacky/web/i18n.js +6 -18
- data/lib/clacky/web/index.html +23 -2
- data/lib/clacky/web/sessions.js +33 -5
- data/scripts/uninstall.sh +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c8311a2bd1bd4cd050fce3e806479c580afb4da476201e59a041c2293347b5f
|
|
4
|
+
data.tar.gz: 520ba1c8c1821aede0b4766be3f321b3c121a91d3d9fe9e5dada0034479a3d4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e2d171e71ae9eb9cbecfc79bbf6ac1f3977268baf5a4026a57caffc378501ca6ca1bad8426237bcd949564ac1986d1b5b4bfe4f549f108d35f3e216edee75bdc
|
|
7
|
+
data.tar.gz: 8ba4d9fe3a50aacd4a247aae6877f391c73297c4e63b8c428b99a09f9c2d9ab642104ff5d6370c04556b679b2e3d041785f1833727605b3205638e7c7ea01126
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ 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.1] - 2026-07-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Extension README edit/validate flow and screenshot upload in ext-studio publish
|
|
11
|
+
- Kimi k3 and k2.7-code models added to Kimi preset
|
|
12
|
+
- New session split-button with improved styling
|
|
13
|
+
- Rename-on-name-conflict flow in ext-studio publish
|
|
14
|
+
- Skip OpenClacky AI Keys card for branded onboarding
|
|
15
|
+
|
|
16
|
+
### Improved
|
|
17
|
+
- Secondary API host added with reduced failover latency
|
|
18
|
+
- Extensions default sort changed to most-installed (downloads first)
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- WSL file panel "Copy Path" now copies Windows-accessible UNC path via `wslpath` (C-5693)
|
|
22
|
+
- Edit button hidden on compressed user messages archived to chunk (C-5723)
|
|
23
|
+
- Message-edit Enter key blocked during IME composition (C-5742)
|
|
24
|
+
- Markdown images with `file://~/` tilde paths now render correctly (C-5747)
|
|
25
|
+
- Local `file://` path resolution unified for Windows drive letters (C-5746, C-5708)
|
|
26
|
+
- Brand extension installs under its real ID, not as unpacked (C-5735)
|
|
27
|
+
- Backup success status no longer stays displayed forever (C-5711)
|
|
28
|
+
- `grep` tool defaults to recursive behavior
|
|
29
|
+
- `display_name` fields included in installed extensions API response
|
|
30
|
+
- Published list reloads after setup entry point and publish
|
|
31
|
+
- Unpublish button size matches iterate button in ext-studio
|
|
32
|
+
- `device_token` correctly passed in screenshot upload multipart fields
|
|
33
|
+
- MCP tool list rendering fixed and display styling improved
|
|
34
|
+
- Uninstall script now removes all openclacky gem versions
|
|
35
|
+
|
|
7
36
|
## [1.4.0] - 2026-07-14
|
|
8
37
|
|
|
9
38
|
### Added
|
|
@@ -257,8 +257,10 @@ module Clacky
|
|
|
257
257
|
end
|
|
258
258
|
|
|
259
259
|
if is_real_user_msg
|
|
260
|
-
# Start a new round at each real user message
|
|
261
|
-
|
|
260
|
+
# Start a new round at each real user message.
|
|
261
|
+
# editable: true — this message still lives in the active in-memory
|
|
262
|
+
# @history, so truncate_from_created_at can locate and truncate it.
|
|
263
|
+
current_round = { user_msg: msg, events: [], editable: true }
|
|
262
264
|
rounds << current_round
|
|
263
265
|
elsif current_round
|
|
264
266
|
current_round[:events] << msg
|
|
@@ -325,7 +327,8 @@ module Clacky
|
|
|
325
327
|
preview_path: f[:preview_path] || f["preview_path"] }
|
|
326
328
|
}
|
|
327
329
|
all_files = image_files + disk_files
|
|
328
|
-
ui.show_user_message(raw_text, created_at: msg[:created_at], files: all_files
|
|
330
|
+
ui.show_user_message(raw_text, created_at: msg[:created_at], files: all_files,
|
|
331
|
+
editable: round[:editable] != false)
|
|
329
332
|
|
|
330
333
|
round[:events].each do |ev|
|
|
331
334
|
# Skip system-injected messages (e.g. synthetic skill content, memory prompts)
|
|
@@ -480,7 +483,11 @@ module Clacky
|
|
|
480
483
|
created_at: synthetic_ts,
|
|
481
484
|
_from_chunk: true
|
|
482
485
|
},
|
|
483
|
-
events: []
|
|
486
|
+
events: [],
|
|
487
|
+
# editable: false — this message was archived into a chunk MD and no
|
|
488
|
+
# longer exists in the active in-memory @history, so it cannot be
|
|
489
|
+
# truncated/edited (truncate_from_created_at would silently no-op).
|
|
490
|
+
editable: false
|
|
484
491
|
}
|
|
485
492
|
rounds << current_round
|
|
486
493
|
elsif current_round
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -1862,11 +1862,10 @@ module Clacky
|
|
|
1862
1862
|
files = []
|
|
1863
1863
|
content.scan(/(!?)\[([^\]]*)\]\(file:\/\/([^)]+)\)/) do
|
|
1864
1864
|
inline = $1 == "!"
|
|
1865
|
-
#
|
|
1866
|
-
|
|
1867
|
-
name = File.basename(
|
|
1868
|
-
path
|
|
1869
|
-
Clacky::Logger.info("[parse_file_links] raw=#{$3.inspect} expanded=#{path.inspect} exist=#{File.exist?(path)}")
|
|
1865
|
+
# Resolve the AI-emitted path: decode, WSL drive-letter normalize, expand.
|
|
1866
|
+
path = Clacky::Utils::EnvironmentDetector.resolve_local_path($3)
|
|
1867
|
+
name = File.basename(path)
|
|
1868
|
+
Clacky::Logger.info("[parse_file_links] raw=#{$3.inspect} resolved=#{path.inspect} exist=#{File.exist?(path)}")
|
|
1870
1869
|
files << { name: name, path: path, inline: inline }
|
|
1871
1870
|
end
|
|
1872
1871
|
{ text: content, files: files }
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -622,7 +622,7 @@ module Clacky
|
|
|
622
622
|
#
|
|
623
623
|
# Returns { success: true, extension: {...} } or
|
|
624
624
|
# { success: false, error: "...", already_exists: Boolean }.
|
|
625
|
-
def upload_extension!(ext_id, zip_data, force: false, status: nil, changelog: nil, origin: 'marketplace')
|
|
625
|
+
def upload_extension!(ext_id, zip_data, force: false, status: nil, changelog: nil, readme: nil, origin: 'marketplace')
|
|
626
626
|
identity = Clacky::Identity.load
|
|
627
627
|
return { success: false, error: "Device not bound to a platform account" } unless identity.bound?
|
|
628
628
|
|
|
@@ -636,6 +636,7 @@ module Clacky
|
|
|
636
636
|
fields["origin"] = origin.to_s if origin
|
|
637
637
|
fields["status"] = status.to_s if status
|
|
638
638
|
fields["changelog"] = changelog.to_s if changelog
|
|
639
|
+
fields["readme"] = readme.to_s if readme
|
|
639
640
|
|
|
640
641
|
body_bytes, boundary = build_multipart(fields, "extension_zip", "#{ext_id}.zip", zip_data)
|
|
641
642
|
|
|
@@ -700,6 +701,55 @@ module Clacky
|
|
|
700
701
|
end
|
|
701
702
|
end
|
|
702
703
|
|
|
704
|
+
# Update an extension's readme text without publishing a new version.
|
|
705
|
+
# Uses PATCH /api/v1/client/extensions/:id.
|
|
706
|
+
# Returns { success:, extension:, error: }
|
|
707
|
+
def update_extension_readme!(ext_id, readme)
|
|
708
|
+
identity = Clacky::Identity.load
|
|
709
|
+
return { success: false, error: "Device not bound to a platform account" } unless identity.bound?
|
|
710
|
+
|
|
711
|
+
path = "/api/v1/client/extensions/#{URI.encode_www_form_component(ext_id)}"
|
|
712
|
+
payload = { "device_token" => identity.device_token, "readme" => readme.to_s }
|
|
713
|
+
result = platform_client.patch(path, payload)
|
|
714
|
+
|
|
715
|
+
if result[:success]
|
|
716
|
+
{ success: true, extension: result[:data]["extension"] }
|
|
717
|
+
else
|
|
718
|
+
body = result[:data] || {}
|
|
719
|
+
{ success: false, error: result[:error] || body["code"] || "Update readme failed" }
|
|
720
|
+
end
|
|
721
|
+
rescue StandardError => e
|
|
722
|
+
{ success: false, error: "Network error: #{e.message}" }
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# Upload any file to the platform's model-agnostic upload endpoint.
|
|
726
|
+
# Returns an orphan blob URL — no extension record required.
|
|
727
|
+
# Uses POST /api/v1/client/uploads.
|
|
728
|
+
# Parameters:
|
|
729
|
+
# data (bytes) - raw file bytes
|
|
730
|
+
# filename (string) - filename including extension
|
|
731
|
+
# content_type (string) - MIME type
|
|
732
|
+
# Returns { success:, url:, filename:, blob_key:, error: }
|
|
733
|
+
def upload_file!(data, filename:, content_type: "image/png")
|
|
734
|
+
identity = Clacky::Identity.load
|
|
735
|
+
return { success: false, error: "Device not bound to a platform account" } unless identity.bound?
|
|
736
|
+
|
|
737
|
+
path = "/api/v1/client/uploads"
|
|
738
|
+
fields = { "device_token" => identity.device_token }
|
|
739
|
+
body_bytes, boundary = build_multipart(fields, "file", filename, data, content_type: content_type)
|
|
740
|
+
result = platform_client.multipart_post(path, body_bytes, boundary, read_timeout: 60)
|
|
741
|
+
|
|
742
|
+
if result[:success]
|
|
743
|
+
d = result[:data] || {}
|
|
744
|
+
{ success: true, url: d["url"], filename: d["filename"], blob_key: d["blob_key"] }
|
|
745
|
+
else
|
|
746
|
+
body = result[:data] || {}
|
|
747
|
+
{ success: false, error: result[:error] || body["code"] || "File upload failed" }
|
|
748
|
+
end
|
|
749
|
+
rescue StandardError => e
|
|
750
|
+
{ success: false, error: "Network error: #{e.message}" }
|
|
751
|
+
end
|
|
752
|
+
|
|
703
753
|
# Search the public extension marketplace. Anonymous — no license required.
|
|
704
754
|
# Uses GET /api/v1/extensions. Returns { success:, extensions: [], error: }.
|
|
705
755
|
def search_extensions!(query: nil, sort: nil)
|
|
@@ -1579,7 +1629,7 @@ module Clacky
|
|
|
1579
1629
|
|
|
1580
1630
|
# Assemble a binary multipart/form-data body: text fields + one file part.
|
|
1581
1631
|
# Kept binary-safe so null bytes in the ZIP survive. Returns [body, boundary].
|
|
1582
|
-
private def build_multipart(fields, file_field, filename, file_bytes)
|
|
1632
|
+
private def build_multipart(fields, file_field, filename, file_bytes, content_type: "application/zip")
|
|
1583
1633
|
boundary = "----ClackyMultipart#{SecureRandom.hex(8)}"
|
|
1584
1634
|
crlf = "\r\n"
|
|
1585
1635
|
parts = []
|
|
@@ -1591,7 +1641,7 @@ module Clacky
|
|
|
1591
1641
|
end
|
|
1592
1642
|
parts << "--#{boundary}#{crlf}"
|
|
1593
1643
|
parts << "Content-Disposition: form-data; name=\"#{file_field}\"; filename=\"#{filename}\"#{crlf}"
|
|
1594
|
-
parts << "Content-Type:
|
|
1644
|
+
parts << "Content-Type: #{content_type}#{crlf}#{crlf}"
|
|
1595
1645
|
parts << file_bytes.b
|
|
1596
1646
|
parts << "#{crlf}--#{boundary}--#{crlf}"
|
|
1597
1647
|
[parts.map(&:b).join, boundary]
|
|
@@ -79,11 +79,24 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
79
79
|
res = Clacky::ExtensionPackager.pack(ext_id, out_dir: tmp)
|
|
80
80
|
zip_data = File.binread(res.path)
|
|
81
81
|
|
|
82
|
+
# Read readme from local README.md if present, so first-publish carries it.
|
|
83
|
+
local_readme = nil
|
|
84
|
+
result_load = Clacky::ExtensionLoader.load_all(force: false)
|
|
85
|
+
container = Array(result_load.containers).find { |id, _| id == res.ext_id }&.last
|
|
86
|
+
if container
|
|
87
|
+
readme_path = File.join(container[:dir], "README.md")
|
|
88
|
+
if File.exist?(readme_path)
|
|
89
|
+
local_readme = File.read(readme_path, encoding: "utf-8").strip
|
|
90
|
+
local_readme = nil if local_readme.empty?
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
82
94
|
result = brand.upload_extension!(
|
|
83
95
|
res.ext_id, zip_data,
|
|
84
96
|
force: json_body["force"] == true,
|
|
85
97
|
status: presence(json_body["status"]),
|
|
86
|
-
changelog: presence(json_body["changelog"])
|
|
98
|
+
changelog: presence(json_body["changelog"]),
|
|
99
|
+
readme: local_readme
|
|
87
100
|
)
|
|
88
101
|
|
|
89
102
|
if result[:success]
|
|
@@ -91,8 +104,15 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
91
104
|
ver = (ext["latest_version"] || {})["version"]
|
|
92
105
|
json(ok: true, ext_id: res.ext_id, version: ver, status: ext["status"])
|
|
93
106
|
elsif result[:already_exists]
|
|
107
|
+
# The same user has already published this extension — they need to use
|
|
108
|
+
# force: true to publish a new version.
|
|
94
109
|
json(ok: false, already_exists: true,
|
|
95
110
|
error: "#{res.ext_id} already published. Publish a new version with force.")
|
|
111
|
+
elsif result[:error].to_s.include?("already taken")
|
|
112
|
+
# Another user has claimed this name globally — the creator must rename
|
|
113
|
+
# their extension before publishing.
|
|
114
|
+
json(ok: false, name_taken: true,
|
|
115
|
+
error: result[:error])
|
|
96
116
|
else
|
|
97
117
|
error!(result[:error] || "publish failed", status: 502)
|
|
98
118
|
end
|
|
@@ -149,6 +169,81 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
149
169
|
json(ok: true, ext_id: ext_id)
|
|
150
170
|
end
|
|
151
171
|
|
|
172
|
+
# PATCH /api/ext/ext-studio/readme
|
|
173
|
+
# body: { ext_id, readme }
|
|
174
|
+
# Two modes:
|
|
175
|
+
# - Extension already published → save to platform via API
|
|
176
|
+
# - Extension not yet published → write readme field to local ext.yml
|
|
177
|
+
patch "/readme" do
|
|
178
|
+
ext_id = require_ext_id!
|
|
179
|
+
readme = json_body["readme"].to_s
|
|
180
|
+
|
|
181
|
+
# Check whether this extension has been published on the platform.
|
|
182
|
+
brand = Clacky::BrandConfig.load
|
|
183
|
+
identity = Clacky::Identity.load
|
|
184
|
+
published = false
|
|
185
|
+
|
|
186
|
+
if identity.bound?
|
|
187
|
+
my_result = brand.fetch_my_extensions!
|
|
188
|
+
if my_result[:success]
|
|
189
|
+
published = Array(my_result[:extensions]).any? { |e| (e["name"] || e["slug"]) == ext_id }
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
if published
|
|
194
|
+
# Already on the platform — update readme there.
|
|
195
|
+
result = brand.update_extension_readme!(ext_id, readme)
|
|
196
|
+
error!(result[:error] || "readme update failed", status: 502) unless result[:success]
|
|
197
|
+
else
|
|
198
|
+
# Not yet published — persist readme to local README.md so it's included on first publish.
|
|
199
|
+
result = Clacky::ExtensionLoader.load_all(force: false)
|
|
200
|
+
container = Array(result.containers).find { |id, _| id == ext_id }&.last
|
|
201
|
+
error!("extension not found: #{ext_id}", status: 404) unless container
|
|
202
|
+
|
|
203
|
+
readme_path = File.join(container[:dir], "README.md")
|
|
204
|
+
if readme.strip.empty?
|
|
205
|
+
File.delete(readme_path) if File.exist?(readme_path)
|
|
206
|
+
else
|
|
207
|
+
File.write(readme_path, readme, encoding: "utf-8")
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
json(ok: true, ext_id: ext_id, saved_to: published ? "platform" : "local")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# POST /api/ext/ext-studio/screenshot
|
|
215
|
+
# Multipart body: { ext_id (field), file (image file) }
|
|
216
|
+
# Uploads a screenshot image via the platform's model-agnostic upload API
|
|
217
|
+
# and returns the CDN URL. Does NOT require the extension to be published yet.
|
|
218
|
+
post "/screenshot" do
|
|
219
|
+
q = req.query
|
|
220
|
+
ext_id = q["ext_id"].to_s.strip
|
|
221
|
+
error!("ext_id required", status: 422) if ext_id.empty?
|
|
222
|
+
|
|
223
|
+
uploaded = q["file"]
|
|
224
|
+
error!("missing file", status: 422) unless uploaded
|
|
225
|
+
|
|
226
|
+
# WEBrick::HTTPUtils::FormData inherits from String — .to_s is the file bytes,
|
|
227
|
+
# .filename is the original filename, .["content-type"] gives the MIME type.
|
|
228
|
+
filename = uploaded.respond_to?(:filename) ? (uploaded.filename || "screenshot.png") : "screenshot.png"
|
|
229
|
+
content_type = (uploaded.respond_to?(:[]) ? uploaded["content-type"].to_s : "")
|
|
230
|
+
content_type = "image/png" if content_type.empty?
|
|
231
|
+
data = uploaded.to_s
|
|
232
|
+
error!("missing file", status: 422) if data.empty?
|
|
233
|
+
|
|
234
|
+
unless Clacky::Identity.load.bound?
|
|
235
|
+
error!("device not bound to a platform account", status: 428, needs_binding: true)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
result = Clacky::BrandConfig.load.upload_file!(
|
|
239
|
+
data,
|
|
240
|
+
filename: filename,
|
|
241
|
+
content_type: content_type
|
|
242
|
+
)
|
|
243
|
+
error!(result[:error] || "screenshot upload failed", status: 502) unless result[:success]
|
|
244
|
+
json(ok: true, url: result[:url], filename: result[:filename])
|
|
245
|
+
end
|
|
246
|
+
|
|
152
247
|
# POST /api/ext/ext-studio/set_meta
|
|
153
248
|
# body: { ext_id, name?, description?, entry_points? }
|
|
154
249
|
# entry_points: [{ unit_id, slot }] — stored under contributes.panels[id].entry_points
|
|
@@ -217,6 +312,50 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
217
312
|
end
|
|
218
313
|
|
|
219
314
|
|
|
315
|
+
# POST /api/ext/ext-studio/set_id
|
|
316
|
+
# body: { ext_id, new_id }
|
|
317
|
+
# Renames an extension: updates the `id` field in ext.yml and renames the
|
|
318
|
+
# local container directory. Used when the user wants to republish under a
|
|
319
|
+
# different name (e.g. after a name-taken conflict on the marketplace).
|
|
320
|
+
post "/set_id" do
|
|
321
|
+
ext_id = require_ext_id!
|
|
322
|
+
new_id = presence(json_body["new_id"])
|
|
323
|
+
error!("new_id required", status: 422) unless new_id
|
|
324
|
+
|
|
325
|
+
slug_format = /\A[a-z0-9][a-z0-9\-]*[a-z0-9]\z/
|
|
326
|
+
unless new_id.length <= 64 && new_id.match?(slug_format)
|
|
327
|
+
error!("new_id must be lowercase letters, digits and hyphens (e.g. my-ext)", status: 422)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
result = Clacky::ExtensionLoader.load_all(force: false)
|
|
331
|
+
container = Array(result.containers).find { |id, _| id == ext_id }&.last
|
|
332
|
+
error!("extension not found: #{ext_id}", status: 404) unless container
|
|
333
|
+
|
|
334
|
+
old_dir = container[:dir]
|
|
335
|
+
new_dir = File.join(File.dirname(old_dir), new_id)
|
|
336
|
+
error!("An extension named '#{new_id}' already exists locally", status: 409) if File.exist?(new_dir)
|
|
337
|
+
|
|
338
|
+
yml_path = File.join(old_dir, "ext.yml")
|
|
339
|
+
error!("ext.yml not found", status: 404) unless File.exist?(yml_path)
|
|
340
|
+
|
|
341
|
+
# Update the id field in ext.yml, preserving all other content.
|
|
342
|
+
content = File.read(yml_path)
|
|
343
|
+
if content =~ /^id:/
|
|
344
|
+
content = content.sub(/^id:.*$/, "id: #{new_id}")
|
|
345
|
+
else
|
|
346
|
+
content = "id: #{new_id}\n" + content
|
|
347
|
+
end
|
|
348
|
+
File.write(yml_path, content)
|
|
349
|
+
|
|
350
|
+
# Rename the container directory.
|
|
351
|
+
File.rename(old_dir, new_dir)
|
|
352
|
+
|
|
353
|
+
# Reload so the new id is visible immediately.
|
|
354
|
+
Clacky::ExtensionLoader.load_all(force: true)
|
|
355
|
+
|
|
356
|
+
json(ok: true, old_id: ext_id, new_id: new_id)
|
|
357
|
+
end
|
|
358
|
+
|
|
220
359
|
# body: { idea? }
|
|
221
360
|
# Spawns a session bound to the ext-developer agent, optionally seeded with
|
|
222
361
|
# the user's idea as the first task — the "let AI build it for me" entry.
|
|
@@ -296,6 +435,9 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
296
435
|
entry_points = panels.flat_map do |p|
|
|
297
436
|
Array(p["entry_points"]).map { |ep| { panel_id: p["id"], slot: ep["slot"] } }
|
|
298
437
|
end
|
|
438
|
+
readme_path = File.join(dir, "README.md")
|
|
439
|
+
readme_content = File.exist?(readme_path) ? File.read(readme_path, encoding: "utf-8").strip : nil
|
|
440
|
+
readme_content = nil if readme_content&.empty?
|
|
299
441
|
{
|
|
300
442
|
id: ext_id,
|
|
301
443
|
name: raw["name"] || ext_id,
|
|
@@ -309,6 +451,7 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
309
451
|
unit_counts: unit_counts(ext_units),
|
|
310
452
|
contributes: raw["contributes"] || {},
|
|
311
453
|
entry_points: entry_points,
|
|
454
|
+
readme: readme_content,
|
|
312
455
|
error_count: ext_issues.count { |i| i.level == :error },
|
|
313
456
|
warning_count: ext_issues.count { |i| i.level == :warning }
|
|
314
457
|
}
|