openclacky 1.3.7 → 1.3.8
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 +24 -0
- data/lib/clacky/agent/hook_manager.rb +10 -1
- data/lib/clacky/agent/system_prompt_builder.rb +1 -1
- data/lib/clacky/agent.rb +9 -2
- data/lib/clacky/agent_profile.rb +7 -4
- data/lib/clacky/billing/billing_store.rb +3 -2
- data/lib/clacky/default_extensions/coding/agents/coding/avatar.png +0 -0
- data/lib/clacky/default_extensions/coding/ext.yml +4 -3
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/avatar.png +0 -0
- data/lib/clacky/default_extensions/ext-studio/ext.yml +1 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +24 -5
- data/lib/clacky/default_extensions/general/agents/general/avatar.png +0 -0
- data/lib/clacky/default_extensions/general/ext.yml +4 -3
- data/lib/clacky/default_extensions/git/panels/git/view.js +3 -3
- data/lib/clacky/default_extensions/meeting/panels/meeting/view.js +1 -2
- data/lib/clacky/extension/loader.rb +6 -0
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/server/http_server.rb +129 -5
- data/lib/clacky/shell_hook_loader.rb +266 -22
- data/lib/clacky/ui2/components/welcome_banner.rb +1 -1
- data/lib/clacky/utils/workspace_rules.rb +2 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +230 -46
- data/lib/clacky/web/core/ext.js +130 -24
- data/lib/clacky/web/features/extensions/view.js +4 -3
- data/lib/clacky/web/features/new-session/store.js +13 -0
- data/lib/clacky/web/features/new-session/view.js +314 -14
- data/lib/clacky/web/features/trash/view.js +4 -5
- data/lib/clacky/web/i18n.js +34 -0
- data/lib/clacky/web/index.html +43 -11
- data/lib/clacky/web/sessions.js +3 -3
- data/lib/clacky/web/skills.js +86 -48
- data/lib/clacky/web/theme.js +3 -0
- data/lib/clacky/web/ws-dispatcher.js +11 -2
- metadata +7 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 589d52edb2af8ec7ebea8d596a4b342791230009666a8757f4391b95df22d667
|
|
4
|
+
data.tar.gz: 07c80f79e8a9df57b4232a1317b1a6f8b4557cf136289f7ea1b7a9e93caba5aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e474dff0d682c03bbd9756a37e9c64a29d13f51c35b73a76440e62eeac4b92d5ce6251e7357493531ce0669a770607a37a17ceb8715eadf5f849a7b04a6f373a
|
|
7
|
+
data.tar.gz: 8863d9a310224162b3fc0dad4513c933ebafcc74be5d2157d890d7a4f2dd667f66c9f658e61d289c078fdf39c5961bb5c0895317553c0f25b5054142f2c127b9
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.8] - 2026-07-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Shell hook `type:rewrite` protocol with `updatedInput` support — hooks can now rewrite the command before execution
|
|
12
|
+
- Support for `AGENTS.md` workspace rules — agent reads project-level rules from AGENTS.md automatically
|
|
13
|
+
- Avatar support in Web UI
|
|
14
|
+
- Open first tab automatically on session start
|
|
15
|
+
|
|
16
|
+
### Improved
|
|
17
|
+
- New-session page UI humanized with improved layout and discoverability
|
|
18
|
+
- Session bar slot support for extension customization
|
|
19
|
+
- Extension description metadata in ext-studio
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Mobile new-session page layout uses fixed positioning correctly
|
|
23
|
+
- Weekly stats period now starts from Monday instead of a rolling 7-day window
|
|
24
|
+
- i18n translations updated and UI refreshes immediately on language change
|
|
25
|
+
- Extension market toolbar layout and back button styling
|
|
26
|
+
- Skeleton screen rendering in ext-studio and extension market
|
|
27
|
+
- Trash skeleton screen restored on loading state
|
|
28
|
+
- Media usage logging now passes session_id correctly for image/video generation
|
|
29
|
+
- Billing writes in tests isolated via `CLACKY_BILLING_DIR` env var
|
|
30
|
+
- Meeting panel no longer overrides aside-panel container class
|
|
31
|
+
|
|
8
32
|
## [1.3.7] - 2026-07-06
|
|
9
33
|
|
|
10
34
|
### Added
|
|
@@ -28,7 +28,16 @@ module Clacky
|
|
|
28
28
|
@hooks[event].each do |hook|
|
|
29
29
|
begin
|
|
30
30
|
hook_result = hook.call(*args)
|
|
31
|
-
|
|
31
|
+
next unless hook_result.is_a?(Hash)
|
|
32
|
+
# First deny wins and stops the chain: a weaker later verdict must
|
|
33
|
+
# never clobber a stronger earlier one, and the first deny's reason
|
|
34
|
+
# is the one that reaches the agent. Rewrite hooks mutate `call` in
|
|
35
|
+
# place (chained rewrite), so for non-deny results there's nothing to
|
|
36
|
+
# merge — we just keep going.
|
|
37
|
+
if hook_result[:action] == :deny
|
|
38
|
+
result = hook_result
|
|
39
|
+
break
|
|
40
|
+
end
|
|
32
41
|
rescue StandardError => e
|
|
33
42
|
# Log error but don't fail
|
|
34
43
|
Clacky::Logger.error("Hook error", event: event, error: e)
|
|
@@ -8,7 +8,7 @@ module Clacky
|
|
|
8
8
|
# Builds system prompt by composing layers:
|
|
9
9
|
# 1. Agent-specific system_prompt.md (role & responsibilities)
|
|
10
10
|
# 2. base_prompt.md (universal rules: todo manager, tool usage, etc.)
|
|
11
|
-
# 3. Project rules (.clackyrules / .cursorrules / CLAUDE.md)
|
|
11
|
+
# 3. Project rules (.clackyrules / AGENTS.md / .cursorrules / CLAUDE.md)
|
|
12
12
|
# 4. SOUL.md (agent personality — user override or built-in default)
|
|
13
13
|
# 5. USER.md (user profile — user override or built-in default)
|
|
14
14
|
# 6. Skills context (available skills list)
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -145,8 +145,15 @@ module Clacky
|
|
|
145
145
|
# Register built-in tools
|
|
146
146
|
register_builtin_tools
|
|
147
147
|
|
|
148
|
-
# Load declarative shell hooks from ~/.clacky/hooks.yml
|
|
149
|
-
|
|
148
|
+
# Load declarative shell hooks from ~/.clacky/hooks.yml. Entries with
|
|
149
|
+
# `type: rewrite` use the rich JSON protocol (updatedInput rewrite);
|
|
150
|
+
# entries without `type` use the simple exit-code protocol.
|
|
151
|
+
ShellHookLoader.load_into(
|
|
152
|
+
@hooks,
|
|
153
|
+
session_id_fn: -> { @session_id },
|
|
154
|
+
cwd_fn: -> { @working_dir },
|
|
155
|
+
permission_mode_fn: -> { @config.permission_mode.to_s }
|
|
156
|
+
)
|
|
150
157
|
|
|
151
158
|
# Copy ext.yml-contributed hook callbacks (contributes.hooks) onto this
|
|
152
159
|
# agent's hook manager. The callbacks were registered process-wide at
|
data/lib/clacky/agent_profile.rb
CHANGED
|
@@ -53,11 +53,11 @@ module Clacky
|
|
|
53
53
|
|
|
54
54
|
# List all available agent profiles across user + extension layers.
|
|
55
55
|
# Precedence on id collision: user override → extension unit.
|
|
56
|
-
# @return [Array<Hash>] each: { id:, title:, title_zh:, description:, description_zh:, source:, order:, layer:, author: }
|
|
56
|
+
# @return [Array<Hash>] each: { id:, title:, title_zh:, description:, description_zh:, source:, order:, layer:, author:, avatar: }
|
|
57
57
|
def self.all
|
|
58
58
|
out = {}
|
|
59
59
|
|
|
60
|
-
add = lambda do |id, title, title_zh, description, description_zh, source, order, layer, author|
|
|
60
|
+
add = lambda do |id, title, title_zh, description, description_zh, source, order, layer, author, avatar|
|
|
61
61
|
next if id.nil? || id.empty?
|
|
62
62
|
out[id] = {
|
|
63
63
|
id: id,
|
|
@@ -69,6 +69,7 @@ module Clacky
|
|
|
69
69
|
order: order,
|
|
70
70
|
layer: layer,
|
|
71
71
|
author: author,
|
|
72
|
+
avatar: avatar,
|
|
72
73
|
}
|
|
73
74
|
end
|
|
74
75
|
|
|
@@ -77,11 +78,12 @@ module Clacky
|
|
|
77
78
|
spec = unit.spec || {}
|
|
78
79
|
title = spec["title"].to_s
|
|
79
80
|
title = unit.id if title.empty?
|
|
81
|
+
avatar = spec["avatar_abs"].to_s.empty? ? nil : "/agent_avatar/#{unit.id}"
|
|
80
82
|
add.call(
|
|
81
83
|
unit.id, title, spec["title_zh"].to_s,
|
|
82
84
|
spec["description"].to_s, spec["description_zh"].to_s,
|
|
83
85
|
"extension", spec["order"], unit.layer.to_s,
|
|
84
|
-
spec["author"].to_s
|
|
86
|
+
spec["author"].to_s, avatar
|
|
85
87
|
)
|
|
86
88
|
end
|
|
87
89
|
|
|
@@ -91,11 +93,12 @@ module Clacky
|
|
|
91
93
|
next if id.start_with?("_")
|
|
92
94
|
next unless File.file?(File.join(path, "profile.yml"))
|
|
93
95
|
meta = read_profile_yml(File.join(path, "profile.yml"))
|
|
96
|
+
user_avatar = File.file?(File.join(path, "avatar.png")) ? "/agent_avatar/#{id}" : nil
|
|
94
97
|
add.call(
|
|
95
98
|
id, meta["title"] || meta["name"] || id, meta["title_zh"].to_s,
|
|
96
99
|
meta["description"].to_s, meta["description_zh"].to_s,
|
|
97
100
|
"user", meta["order"], "user",
|
|
98
|
-
meta["author"].to_s.empty? ? "You" : meta["author"].to_s
|
|
101
|
+
meta["author"].to_s.empty? ? "You" : meta["author"].to_s, user_avatar
|
|
99
102
|
)
|
|
100
103
|
end
|
|
101
104
|
|
|
@@ -13,7 +13,7 @@ module Clacky
|
|
|
13
13
|
BILLING_DIR = File.join(Dir.home, ".clacky", "billing")
|
|
14
14
|
|
|
15
15
|
def initialize(billing_dir: nil)
|
|
16
|
-
@billing_dir = billing_dir || BILLING_DIR
|
|
16
|
+
@billing_dir = billing_dir || ENV["CLACKY_BILLING_DIR"] || BILLING_DIR
|
|
17
17
|
ensure_billing_dir
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -339,7 +339,8 @@ module Clacky
|
|
|
339
339
|
when :day
|
|
340
340
|
Time.new(now.year, now.month, now.day)
|
|
341
341
|
when :week
|
|
342
|
-
now
|
|
342
|
+
days_since_monday = now.wday == 0 ? 6 : now.wday - 1
|
|
343
|
+
Time.new(now.year, now.month, now.day) - (days_since_monday * 24 * 60 * 60)
|
|
343
344
|
when :month
|
|
344
345
|
Time.new(now.year, now.month, 1)
|
|
345
346
|
when :year
|
|
Binary file
|
|
@@ -10,9 +10,10 @@ contributes:
|
|
|
10
10
|
agents:
|
|
11
11
|
- id: coding
|
|
12
12
|
title: Coding
|
|
13
|
-
title_zh:
|
|
14
|
-
description:
|
|
15
|
-
description_zh:
|
|
13
|
+
title_zh: 编程开发
|
|
14
|
+
description: Claude Code-grade coding capability, your technical co-founder
|
|
15
|
+
description_zh: 对齐 Claude Code 的编程能力,你的技术合伙人
|
|
16
16
|
order: 2
|
|
17
17
|
prompt: agents/coding/system_prompt.md
|
|
18
|
+
avatar: agents/coding/avatar.png
|
|
18
19
|
panels: [git/git, time_machine/time_machine]
|
|
@@ -787,6 +787,23 @@
|
|
|
787
787
|
};
|
|
788
788
|
}
|
|
789
789
|
|
|
790
|
+
function _skeletonHtml() {
|
|
791
|
+
return Array.from({ length: 3 }).map(() => `
|
|
792
|
+
<div class="studio-skill-card" style="pointer-events:none">
|
|
793
|
+
<div class="studio-skill-head">
|
|
794
|
+
<span class="skel" style="display:inline-block;height:1rem;width:40%"></span>
|
|
795
|
+
<span class="skel" style="display:inline-block;height:1.2rem;width:3.5rem;border-radius:4px;"></span>
|
|
796
|
+
</div>
|
|
797
|
+
<div class="studio-skill-meta" style="margin-top:6px">
|
|
798
|
+
<span class="skel" style="display:inline-block;height:0.85rem;width:20%"></span>
|
|
799
|
+
</div>
|
|
800
|
+
<div class="studio-actions">
|
|
801
|
+
<span class="skel" style="display:inline-block;height:1.75rem;width:5rem;border-radius:6px;"></span>
|
|
802
|
+
<span class="skel" style="display:inline-block;height:1.75rem;width:4rem;border-radius:6px;"></span>
|
|
803
|
+
</div>
|
|
804
|
+
</div>`).join("");
|
|
805
|
+
}
|
|
806
|
+
|
|
790
807
|
// ── Extensions tab (full-page): cloud + local extension cards ──────────────
|
|
791
808
|
// Reuses backend endpoints GET /published (cloud), the shared `store` (local,
|
|
792
809
|
// GET /extensions), POST /publish|/pack|/unpublish, and POST /develop to open
|
|
@@ -926,7 +943,7 @@
|
|
|
926
943
|
async attach(root) {
|
|
927
944
|
container = el("div", { class: "studio-panel" });
|
|
928
945
|
root.appendChild(container);
|
|
929
|
-
container.
|
|
946
|
+
container.innerHTML = _skeletonHtml();
|
|
930
947
|
try { await reload(); rebuild(); }
|
|
931
948
|
catch (e) { container.innerHTML = ""; container.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) })); }
|
|
932
949
|
},
|
|
@@ -1090,7 +1107,7 @@
|
|
|
1090
1107
|
async attach(root) {
|
|
1091
1108
|
container = el("div", { class: "studio-panel" });
|
|
1092
1109
|
root.appendChild(container);
|
|
1093
|
-
container.
|
|
1110
|
+
container.innerHTML = _skeletonHtml();
|
|
1094
1111
|
try { await reload(); rebuild(); }
|
|
1095
1112
|
catch (e) { container.innerHTML = ""; container.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) })); }
|
|
1096
1113
|
},
|
|
@@ -1146,7 +1163,7 @@
|
|
|
1146
1163
|
createSkillsPanel().attach(root);
|
|
1147
1164
|
}
|
|
1148
1165
|
|
|
1149
|
-
function navRow(
|
|
1166
|
+
function navRow(labelKey, onClick) {
|
|
1150
1167
|
const item = el("div", { class: "task-item task-item-summary" });
|
|
1151
1168
|
item.innerHTML =
|
|
1152
1169
|
'<div class="task-row">' +
|
|
@@ -1159,7 +1176,9 @@
|
|
|
1159
1176
|
'</svg>' +
|
|
1160
1177
|
'<div class="task-info"><span class="task-name"></span></div>' +
|
|
1161
1178
|
'</div>';
|
|
1162
|
-
item.querySelector(".task-name")
|
|
1179
|
+
const nameEl = item.querySelector(".task-name");
|
|
1180
|
+
nameEl.textContent = t(labelKey);
|
|
1181
|
+
document.addEventListener("langchange", () => { nameEl.textContent = t(labelKey); });
|
|
1163
1182
|
item.addEventListener("click", onClick);
|
|
1164
1183
|
return item;
|
|
1165
1184
|
}
|
|
@@ -1170,7 +1189,7 @@
|
|
|
1170
1189
|
});
|
|
1171
1190
|
|
|
1172
1191
|
Clacky.ext.ui.mount("sidebar.nav.bottom", function () {
|
|
1173
|
-
return navRow(
|
|
1192
|
+
return navRow("nav.entry", function () { Clacky.ext.ui.openWorkspace(WS_ID); });
|
|
1174
1193
|
}, { workspace: WS_ID });
|
|
1175
1194
|
|
|
1176
1195
|
// Extension-developer session tools: debug + publish, as aside tabs. These
|
|
Binary file
|
|
@@ -10,9 +10,10 @@ contributes:
|
|
|
10
10
|
agents:
|
|
11
11
|
- id: general
|
|
12
12
|
title: General
|
|
13
|
-
title_zh:
|
|
14
|
-
description:
|
|
15
|
-
description_zh:
|
|
13
|
+
title_zh: 日常工作
|
|
14
|
+
description: Handles your everyday work
|
|
15
|
+
description_zh: 帮你打理日常工作,查资料、写东西、整理归纳都行
|
|
16
16
|
order: 1
|
|
17
17
|
prompt: agents/general/system_prompt.md
|
|
18
|
+
avatar: agents/general/avatar.png
|
|
18
19
|
panels: [meeting/meeting]
|
|
@@ -187,12 +187,12 @@
|
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
Clacky.ext.ui.mount("session.aside", (ctx) => {
|
|
191
|
-
if (!ctx || !ctx.sessionId) return
|
|
190
|
+
Clacky.ext.ui.mount("session.aside", (container, ctx) => {
|
|
191
|
+
if (!ctx || !ctx.sessionId) return;
|
|
192
192
|
const body = el("div", { class: "changes-panel" });
|
|
193
193
|
const root = el("div", { class: "changes-root", "data-panel": "changes" }, body);
|
|
194
|
+
container.appendChild(root);
|
|
194
195
|
refresh(ctx.sessionId, root, body, ctx);
|
|
195
|
-
return root;
|
|
196
196
|
}, {
|
|
197
197
|
order: 10,
|
|
198
198
|
tab: { id: "changes", label: () => t("changes.tab") },
|
|
@@ -893,7 +893,6 @@
|
|
|
893
893
|
// Every tab activation gets a fresh empty container; wire it into state
|
|
894
894
|
// and paint the current state onto it.
|
|
895
895
|
attach(container) {
|
|
896
|
-
container.className = "meeting-container";
|
|
897
896
|
state.container = container;
|
|
898
897
|
renderUI(container);
|
|
899
898
|
},
|
|
@@ -923,7 +922,7 @@
|
|
|
923
922
|
// Inject minimal styles
|
|
924
923
|
const style = document.createElement("style");
|
|
925
924
|
style.textContent = `
|
|
926
|
-
.meeting-
|
|
925
|
+
.meeting-panel { padding: 16px; font-size: 13px; color: var(--color-text-secondary); }
|
|
927
926
|
.meeting-btn { padding: 7px 14px; border-radius: var(--radius-sm); border: 1px solid transparent; cursor: pointer; font-size: 13px; font-weight: 500; transition: background .15s, border-color .15s; }
|
|
928
927
|
.meeting-btn-start { background: var(--color-button-primary); color: var(--color-button-primary-text); }
|
|
929
928
|
.meeting-btn-start:hover { background: var(--color-button-primary-hover); }
|
|
@@ -391,6 +391,10 @@ module Clacky
|
|
|
391
391
|
return nil
|
|
392
392
|
end
|
|
393
393
|
|
|
394
|
+
avatar_rel = spec["avatar"].to_s
|
|
395
|
+
avatar_abs = avatar_rel.empty? ? "" : File.join(container[:dir], avatar_rel)
|
|
396
|
+
avatar_abs = "" unless !avatar_abs.empty? && File.file?(avatar_abs)
|
|
397
|
+
|
|
394
398
|
Unit.new(kind: :agent, id: spec["id"].to_s, ext_id: ext_id,
|
|
395
399
|
layer: container[:layer], origin: container[:origin],
|
|
396
400
|
dir: container[:dir],
|
|
@@ -399,6 +403,8 @@ module Clacky
|
|
|
399
403
|
"title_zh" => spec["title_zh"].to_s,
|
|
400
404
|
"prompt" => spec["prompt"],
|
|
401
405
|
"prompt_abs" => prompt_abs,
|
|
406
|
+
"avatar" => avatar_rel,
|
|
407
|
+
"avatar_abs" => avatar_abs,
|
|
402
408
|
"description" => spec["description"].to_s,
|
|
403
409
|
"description_zh" => spec["description_zh"].to_s,
|
|
404
410
|
"order" => spec["order"],
|
|
@@ -22,7 +22,7 @@ module Clacky
|
|
|
22
22
|
PANEL_KEYS = %w[id title title_zh description description_zh view order attach].freeze
|
|
23
23
|
API_KEYS = %w[id handler].freeze
|
|
24
24
|
SKILL_KEYS = %w[id dir protected].freeze
|
|
25
|
-
AGENT_KEYS = %w[id title title_zh description description_zh order prompt panels skills].freeze
|
|
25
|
+
AGENT_KEYS = %w[id title title_zh description description_zh order prompt panels skills avatar].freeze
|
|
26
26
|
CHANNEL_KEYS = %w[id platform adapter].freeze
|
|
27
27
|
PATCH_KEYS = %w[target file fingerprint on_mismatch].freeze
|
|
28
28
|
HOOK_KEYS = %w[event file].freeze
|
|
@@ -369,6 +369,8 @@ module Clacky
|
|
|
369
369
|
res.body = html
|
|
370
370
|
elsif req.path.start_with?("/agent_ui/")
|
|
371
371
|
self.send(:serve_agent_ui, req, res)
|
|
372
|
+
elsif req.path.start_with?("/agent_avatar/")
|
|
373
|
+
self.send(:serve_agent_avatar, req, res)
|
|
372
374
|
elsif req.path.start_with?("/ext_ui/")
|
|
373
375
|
self.send(:serve_ext_ui, req, res)
|
|
374
376
|
else
|
|
@@ -623,6 +625,9 @@ module Clacky
|
|
|
623
625
|
elsif method == "GET" && path.match?(%r{^/api/sessions/[^/]+/skills$})
|
|
624
626
|
session_id = path.sub("/api/sessions/", "").sub("/skills", "")
|
|
625
627
|
api_session_skills(session_id, res)
|
|
628
|
+
elsif method == "GET" && path.match?(%r{^/api/agents/[^/]+/skills$})
|
|
629
|
+
agent_id = path[%r{^/api/agents/([^/]+)/skills$}, 1]
|
|
630
|
+
api_agent_skills(agent_id, res)
|
|
626
631
|
elsif method == "GET" && path.match?(%r{^/api/sessions/[^/]+/git/[a-z]+$})
|
|
627
632
|
session_id = path[%r{^/api/sessions/([^/]+)/git/}, 1]
|
|
628
633
|
action = path[%r{/git/([a-z]+)$}, 1]
|
|
@@ -1061,6 +1066,7 @@ module Clacky
|
|
|
1061
1066
|
private def webui_ext_script_tags
|
|
1062
1067
|
[
|
|
1063
1068
|
panel_agents_script,
|
|
1069
|
+
ext_contributions_script,
|
|
1064
1070
|
agent_webui_script_tags,
|
|
1065
1071
|
container_ext_script_tags,
|
|
1066
1072
|
].reject(&:empty?).join("\n")
|
|
@@ -1097,6 +1103,36 @@ module Clacky
|
|
|
1097
1103
|
"<script>Clacky.ext.registerPanelAgents(#{map.to_json})</script>"
|
|
1098
1104
|
end
|
|
1099
1105
|
|
|
1106
|
+
# Inline script mapping agent id => the panels & skills its extension
|
|
1107
|
+
# contributes (straight from ext.yml). Built-in extensions are excluded so
|
|
1108
|
+
# the new-session page only advertises what a third-party agent adds.
|
|
1109
|
+
private def ext_contributions_script
|
|
1110
|
+
map = ext_contributions_map
|
|
1111
|
+
return "" if map.empty?
|
|
1112
|
+
|
|
1113
|
+
"<script>Clacky.ext.registerAgentContributions(#{map.to_json})</script>"
|
|
1114
|
+
end
|
|
1115
|
+
|
|
1116
|
+
private def ext_contributions_map
|
|
1117
|
+
result = Clacky::ExtensionLoader.load_all
|
|
1118
|
+
by_ext = Hash.new { |h, k| h[k] = { "panels" => [], "skills" => [] } }
|
|
1119
|
+
label = lambda do |unit|
|
|
1120
|
+
{ "id" => unit.id, "title" => unit.spec["title"] || unit.id, "title_zh" => unit.spec["title_zh"] }
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
result.panels.each { |u| by_ext[u.ext_id]["panels"] << label.call(u) unless u.layer == :builtin }
|
|
1124
|
+
result.skills.each { |u| by_ext[u.ext_id]["skills"] << label.call(u) unless u.layer == :builtin }
|
|
1125
|
+
|
|
1126
|
+
out = {}
|
|
1127
|
+
result.agents.each do |u|
|
|
1128
|
+
next if u.layer == :builtin
|
|
1129
|
+
contrib = by_ext[u.ext_id]
|
|
1130
|
+
next if contrib["panels"].empty? && contrib["skills"].empty?
|
|
1131
|
+
out[u.id] = contrib
|
|
1132
|
+
end
|
|
1133
|
+
out
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1100
1136
|
# panel id => list of agent names allowed to see it. Two sources merge:
|
|
1101
1137
|
# 1. `agent.panels: [id]` — agent-side explicit reference (opt-in mount)
|
|
1102
1138
|
# 2. `panel.attach: [...]` — panel-author's default suggestion
|
|
@@ -1283,6 +1319,42 @@ module Clacky
|
|
|
1283
1319
|
serve_static_under(File.join(dir, "webui"), rel, res)
|
|
1284
1320
|
end
|
|
1285
1321
|
|
|
1322
|
+
# GET /agent_avatar/<id> — serve an agent's avatar image (extension
|
|
1323
|
+
# `avatar` file, or a user agent's avatar.png). Read-only, images only.
|
|
1324
|
+
private def serve_agent_avatar(req, res)
|
|
1325
|
+
id = req.path.delete_prefix("/agent_avatar/").split("/").first.to_s
|
|
1326
|
+
return (res.status = 404; res.body = "not found") if id.empty?
|
|
1327
|
+
|
|
1328
|
+
abs = agent_avatar_path(id)
|
|
1329
|
+
unless abs && File.file?(abs)
|
|
1330
|
+
res.status = 404
|
|
1331
|
+
res.body = "not found"
|
|
1332
|
+
return
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
ctype = { ".png" => "image/png", ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg",
|
|
1336
|
+
".webp" => "image/webp", ".svg" => "image/svg+xml", ".gif" => "image/gif" }[File.extname(abs).downcase]
|
|
1337
|
+
unless ctype
|
|
1338
|
+
res.status = 415
|
|
1339
|
+
res.body = "unsupported media type"
|
|
1340
|
+
return
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1343
|
+
res.status = 200
|
|
1344
|
+
res["Content-Type"] = ctype
|
|
1345
|
+
res["Cache-Control"] = "public, max-age=86400"
|
|
1346
|
+
res.body = File.binread(abs)
|
|
1347
|
+
end
|
|
1348
|
+
|
|
1349
|
+
private def agent_avatar_path(id)
|
|
1350
|
+
unit = Clacky::ExtensionLoader.last_result&.agents&.find { |u| u.id == id }
|
|
1351
|
+
avatar_abs = unit&.spec&.[]("avatar_abs").to_s
|
|
1352
|
+
return avatar_abs unless avatar_abs.empty?
|
|
1353
|
+
|
|
1354
|
+
user_png = File.expand_path("~/.clacky/agents/#{id}/avatar.png")
|
|
1355
|
+
File.file?(user_png) ? user_png : nil
|
|
1356
|
+
end
|
|
1357
|
+
|
|
1286
1358
|
# Read-only static serve of `rel` under `root`, JS/CSS/HTML only, with
|
|
1287
1359
|
# strict path containment so a crafted rel cannot escape `root`.
|
|
1288
1360
|
private def serve_static_under(root, rel, res)
|
|
@@ -1419,6 +1491,9 @@ module Clacky
|
|
|
1419
1491
|
output_dir = body["output_dir"].to_s
|
|
1420
1492
|
output_dir = @agent_config.default_working_dir || Dir.pwd if output_dir.empty?
|
|
1421
1493
|
|
|
1494
|
+
session_id = body["session_id"].to_s
|
|
1495
|
+
session_id = nil if session_id.empty?
|
|
1496
|
+
|
|
1422
1497
|
result = Clacky::Media::Generator.new(@agent_config).generate_image(
|
|
1423
1498
|
prompt: prompt,
|
|
1424
1499
|
aspect_ratio: aspect_ratio,
|
|
@@ -1427,7 +1502,7 @@ module Clacky
|
|
|
1427
1502
|
images: body["images"]
|
|
1428
1503
|
)
|
|
1429
1504
|
if result["success"]
|
|
1430
|
-
log_media_usage(result, prompt: prompt)
|
|
1505
|
+
log_media_usage(result, prompt: prompt, session_id: session_id)
|
|
1431
1506
|
end
|
|
1432
1507
|
status = result["success"] ? 200 : 422
|
|
1433
1508
|
json_response(res, status, result)
|
|
@@ -1451,6 +1526,9 @@ module Clacky
|
|
|
1451
1526
|
output_dir = body["output_dir"].to_s
|
|
1452
1527
|
output_dir = @agent_config.default_working_dir || Dir.pwd if output_dir.empty?
|
|
1453
1528
|
|
|
1529
|
+
session_id = body["session_id"].to_s
|
|
1530
|
+
session_id = nil if session_id.empty?
|
|
1531
|
+
|
|
1454
1532
|
result = Clacky::Media::Generator.new(@agent_config).generate_video(
|
|
1455
1533
|
prompt: prompt,
|
|
1456
1534
|
aspect_ratio: aspect_ratio,
|
|
@@ -1459,7 +1537,7 @@ module Clacky
|
|
|
1459
1537
|
image: image
|
|
1460
1538
|
)
|
|
1461
1539
|
if result["success"]
|
|
1462
|
-
log_media_usage(result, prompt: prompt)
|
|
1540
|
+
log_media_usage(result, prompt: prompt, session_id: session_id)
|
|
1463
1541
|
end
|
|
1464
1542
|
status = result["success"] ? 200 : 422
|
|
1465
1543
|
json_response(res, status, result)
|
|
@@ -1480,13 +1558,16 @@ module Clacky
|
|
|
1480
1558
|
output_dir = body["output_dir"].to_s
|
|
1481
1559
|
output_dir = @agent_config.default_working_dir || Dir.pwd if output_dir.empty?
|
|
1482
1560
|
|
|
1561
|
+
session_id = body["session_id"].to_s
|
|
1562
|
+
session_id = nil if session_id.empty?
|
|
1563
|
+
|
|
1483
1564
|
result = Clacky::Media::Generator.new(@agent_config).generate_speech(
|
|
1484
1565
|
input: input,
|
|
1485
1566
|
voice: voice,
|
|
1486
1567
|
output_dir: output_dir
|
|
1487
1568
|
)
|
|
1488
1569
|
if result["success"]
|
|
1489
|
-
log_media_usage(result, prompt: input)
|
|
1570
|
+
log_media_usage(result, prompt: input, session_id: session_id)
|
|
1490
1571
|
end
|
|
1491
1572
|
status = result["success"] ? 200 : 422
|
|
1492
1573
|
json_response(res, status, result)
|
|
@@ -1547,7 +1628,7 @@ module Clacky
|
|
|
1547
1628
|
json_response(res, 500, { error: e.message })
|
|
1548
1629
|
end
|
|
1549
1630
|
|
|
1550
|
-
private def log_media_usage(result, prompt:)
|
|
1631
|
+
private def log_media_usage(result, prompt:, session_id: nil)
|
|
1551
1632
|
usage = result["usage"]
|
|
1552
1633
|
cost = result["cost_usd"]
|
|
1553
1634
|
return if usage.nil? && cost.nil?
|
|
@@ -1568,6 +1649,20 @@ module Clacky
|
|
|
1568
1649
|
else "image"
|
|
1569
1650
|
end
|
|
1570
1651
|
Clacky::Logger.info("[Media] #{kind} generated #{parts.join(" ")}")
|
|
1652
|
+
|
|
1653
|
+
require_relative "../billing/billing_store"
|
|
1654
|
+
require_relative "../billing/billing_record"
|
|
1655
|
+
record = Clacky::Billing::BillingRecord.new(
|
|
1656
|
+
session_id: session_id,
|
|
1657
|
+
model: result["model"].to_s,
|
|
1658
|
+
prompt_tokens: usage.is_a?(Hash) ? usage["prompt_tokens"].to_i : 0,
|
|
1659
|
+
completion_tokens: usage.is_a?(Hash) ? usage["completion_tokens"].to_i : 0,
|
|
1660
|
+
cache_read_tokens: usage.is_a?(Hash) ? usage["cache_read_tokens"].to_i : 0,
|
|
1661
|
+
cache_write_tokens: usage.is_a?(Hash) ? usage["cache_write_tokens"].to_i : 0,
|
|
1662
|
+
cost_usd: cost.to_f,
|
|
1663
|
+
cost_source: :api
|
|
1664
|
+
)
|
|
1665
|
+
Clacky::Billing::BillingStore.new.append(record)
|
|
1571
1666
|
end
|
|
1572
1667
|
|
|
1573
1668
|
# GET /api/media/types
|
|
@@ -3917,7 +4012,36 @@ module Clacky
|
|
|
3917
4012
|
json_response(res, 200, { skills: skill_data })
|
|
3918
4013
|
end
|
|
3919
4014
|
|
|
3920
|
-
# GET /api/
|
|
4015
|
+
# GET /api/agents/:id/skills — like api_session_skills but keyed on an
|
|
4016
|
+
# agent id rather than a live session. Used by the New Session page so the
|
|
4017
|
+
# first-message composer can offer slash-command autocomplete before a
|
|
4018
|
+
# session exists.
|
|
4019
|
+
def api_agent_skills(agent_id, res)
|
|
4020
|
+
profile = begin
|
|
4021
|
+
Clacky::AgentProfile.load(agent_id)
|
|
4022
|
+
rescue StandardError
|
|
4023
|
+
nil
|
|
4024
|
+
end
|
|
4025
|
+
|
|
4026
|
+
@skill_loader.load_all
|
|
4027
|
+
skills = @skill_loader.user_invocable_skills
|
|
4028
|
+
skills = skills.select { |s| s.allowed_for_agent?(profile.name) } if profile
|
|
4029
|
+
|
|
4030
|
+
loaded_from = @skill_loader.loaded_from
|
|
4031
|
+
skill_data = skills.map do |skill|
|
|
4032
|
+
{
|
|
4033
|
+
name: skill.identifier,
|
|
4034
|
+
name_zh: skill.name_zh,
|
|
4035
|
+
description: skill.description || skill.context_description,
|
|
4036
|
+
description_zh: skill.description_zh,
|
|
4037
|
+
encrypted: skill.encrypted?,
|
|
4038
|
+
source_type: loaded_from[skill.identifier],
|
|
4039
|
+
always_show: skill.always_show
|
|
4040
|
+
}
|
|
4041
|
+
end
|
|
4042
|
+
|
|
4043
|
+
json_response(res, 200, { skills: skill_data })
|
|
4044
|
+
end
|
|
3921
4045
|
# working_dir. action ∈ status|diff|log|branches. diff accepts ?file=,
|
|
3922
4046
|
# log accepts ?limit=.
|
|
3923
4047
|
def api_session_git(session_id, action, req, res)
|