openclacky 1.3.5 → 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 +42 -0
- data/lib/clacky/agent/message_compressor.rb +32 -8
- data/lib/clacky/agent/message_compressor_helper.rb +113 -12
- data/lib/clacky/agent.rb +8 -4
- data/lib/clacky/agent_profile.rb +121 -53
- data/lib/clacky/brand_config.rb +309 -6
- data/lib/clacky/cli.rb +4 -58
- 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 +435 -263
- data/lib/clacky/server/session_registry.rb +59 -3
- data/lib/clacky/session_manager.rb +35 -4
- data/lib/clacky/skill_loader.rb +28 -1
- data/lib/clacky/ui2/layout_manager.rb +0 -5
- data/lib/clacky/ui2/progress_handle.rb +0 -3
- data/lib/clacky/ui2/ui_controller.rb +9 -15
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +782 -554
- 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 +2 -8
- data/lib/clacky/web/core/ext.js +316 -79
- data/lib/clacky/web/features/backup/store.js +24 -0
- data/lib/clacky/web/features/backup/view.js +49 -23
- 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 +88 -27
- 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 +140 -108
- data/lib/clacky/web/index.html +190 -214
- data/lib/clacky/web/sessions.js +99 -304
- 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 +2 -1
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48e94ccf83645b5e18be8d5d33582dc128b06824ab226dd314540658b9cc7e4a
|
|
4
|
+
data.tar.gz: 1112ed443505eca99e745838377ebbf4b6f1e96a7c7f83c7bf1cff844b5ddea9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b0e4b2664bb7990ec91dd7c5598cfb561ce6f931b1f006a962b6cbf5516ee424b3a6f605fd70c3996ad4d0e0bf1a20dfd626d6466020393cc8cf18832008a68a
|
|
7
|
+
data.tar.gz: 2b74eea0a552e5d26545779da2f0a2b1f88812063398ffbf15be913e396d6efe7131d8205bff766939ef252c4ca3b8075ce8e86c855de59774226e3e03bdbfde
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,48 @@ 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.7] - 2026-07-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Extension ecosystem: install, manage, and publish extensions with Creator Studio
|
|
12
|
+
- Brand extension and brand badge support for extension agents
|
|
13
|
+
- Hook/profile extension type for intercepting and customizing agent behavior
|
|
14
|
+
- Extension upload CLI for publishing extensions from the command line
|
|
15
|
+
- New session page with improved layout and discoverability
|
|
16
|
+
- Session auto-cleanup hint, skeleton loading, and type filter in trash panel
|
|
17
|
+
- Renew button on brand warning bar for expiring subscriptions
|
|
18
|
+
- i18n tooltips for header theme toggle and trash toolbar buttons
|
|
19
|
+
- Refresh button in header to reload sessions
|
|
20
|
+
- xhigh reasoning effort level for supported models
|
|
21
|
+
- i18n file upload error messages
|
|
22
|
+
|
|
23
|
+
### Improved
|
|
24
|
+
- Merge Creator Center into ext-studio as a unified extension hub
|
|
25
|
+
- Extension developer experience: scaffold template improvements, badge and author metadata support
|
|
26
|
+
- Share modal UI polish — theme dots on hover, close button hover effect, layout refinement
|
|
27
|
+
- Unify Clacky.* JS namespace and single-segment extension architecture
|
|
28
|
+
- Migrate default agents to default extensions with unified panel visibility
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Ctrl+C now works properly in TUI auto-answer mode
|
|
32
|
+
- Show-system-skills toggle persists correctly and uses theme accent color
|
|
33
|
+
- Cron display handles every-N-hours with weekdays pattern correctly
|
|
34
|
+
- New-session dropdown arrow button responds to both click and keyboard
|
|
35
|
+
- New cron virtual entry inserts at the correct position relative to pinned sessions
|
|
36
|
+
- File upload error messages use i18n consistently
|
|
37
|
+
|
|
38
|
+
## [1.3.6] - 2026-06-30
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- Cron task session isolation with independent pagination and stable task count
|
|
42
|
+
- Backup/restore UI in settings with WebSocket reconnect refresh and WSL open-folder support
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- Rewrite human-readable cron parser to handle comma-separated lists, hour ranges, and `*/1` normalization
|
|
46
|
+
- Setup submit button hover color follows accent theme variable
|
|
47
|
+
- Settings button no longer navigates away when already on settings page
|
|
48
|
+
- Use Unix epoch for old thread timestamps to avoid date parsing issues
|
|
49
|
+
|
|
8
50
|
## [1.3.5] - 2026-06-29
|
|
9
51
|
|
|
10
52
|
### Added
|
|
@@ -39,10 +39,14 @@ module Clacky
|
|
|
39
39
|
|
|
40
40
|
REQUIRED RESPONSE FORMAT:
|
|
41
41
|
First output a <topics> line listing 3-6 key topic phrases (comma-separated, concise).
|
|
42
|
+
Then output a <continues_previous> line: "true" if this conversation is a direct
|
|
43
|
+
continuation of the SAME task/topic as the PREVIOUS chunk shown below, "false" if it
|
|
44
|
+
has moved on to a different task or topic. If there is no previous chunk, output "false".
|
|
42
45
|
Then output the full summary wrapped in <summary> tags.
|
|
43
46
|
|
|
44
47
|
Example format:
|
|
45
48
|
<topics>Rails setup, database config, deploy pipeline, Tailwind CSS</topics>
|
|
49
|
+
<continues_previous>false</continues_previous>
|
|
46
50
|
<summary>
|
|
47
51
|
...full summary text...
|
|
48
52
|
</summary>
|
|
@@ -54,7 +58,8 @@ module Clacky
|
|
|
54
58
|
- Errors encountered and fixes applied
|
|
55
59
|
- Current work status and pending tasks
|
|
56
60
|
|
|
57
|
-
Begin your response NOW. Remember: PURE TEXT only, starting with <topics> then
|
|
61
|
+
Begin your response NOW. Remember: PURE TEXT only, starting with <topics> then
|
|
62
|
+
<continues_previous> then <summary>.
|
|
58
63
|
PROMPT
|
|
59
64
|
|
|
60
65
|
def initialize(client, model: nil)
|
|
@@ -72,18 +77,25 @@ module Clacky
|
|
|
72
77
|
#
|
|
73
78
|
# @param messages [Array<Hash>] Original conversation messages
|
|
74
79
|
# @param recent_messages [Array<Hash>] Recent messages to keep uncompressed (optional)
|
|
80
|
+
# @param previous_topics [String, nil] Topics of the most recent chunk on disk,
|
|
81
|
+
# shown to the LLM so it can decide whether the current conversation is a
|
|
82
|
+
# continuation (drives the <continues_previous> output for chunk merging).
|
|
75
83
|
# @return [Hash] Compression instruction message to insert, or nil if nothing to compress
|
|
76
|
-
def build_compression_message(messages, recent_messages: [])
|
|
84
|
+
def build_compression_message(messages, recent_messages: [], previous_topics: nil)
|
|
77
85
|
# Get messages to compress (exclude system message and recent messages)
|
|
78
86
|
messages_to_compress = messages.reject { |m| m[:role] == "system" || recent_messages.include?(m) }
|
|
79
87
|
|
|
80
88
|
# If nothing to compress, return nil
|
|
81
89
|
return nil if messages_to_compress.empty?
|
|
82
90
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
content = COMPRESSION_PROMPT
|
|
92
|
+
if previous_topics && !previous_topics.strip.empty?
|
|
93
|
+
content = "#{COMPRESSION_PROMPT}\n\nPREVIOUS CHUNK TOPICS (for <continues_previous> judgement): #{previous_topics}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
role: "user",
|
|
98
|
+
content: content,
|
|
87
99
|
system_injected: true
|
|
88
100
|
}
|
|
89
101
|
end
|
|
@@ -142,6 +154,15 @@ module Clacky
|
|
|
142
154
|
m ? m[1].strip : nil
|
|
143
155
|
end
|
|
144
156
|
|
|
157
|
+
# Parse the <continues_previous> tag. Returns true only when the LLM
|
|
158
|
+
# explicitly says "true"; missing tag or any other value → false.
|
|
159
|
+
# This conservative default ensures we never merge unless the model is sure.
|
|
160
|
+
def parse_continues_previous(content)
|
|
161
|
+
return false if content.nil? || content.to_s.empty?
|
|
162
|
+
m = content.to_s.match(/<continues_previous>(.*?)<\/continues_previous>/m)
|
|
163
|
+
m ? m[1].strip.downcase == "true" : false
|
|
164
|
+
end
|
|
165
|
+
|
|
145
166
|
def parse_compressed_result(result, chunk_path: nil, topics: nil, previous_chunks: [])
|
|
146
167
|
# Return the compressed result as a single user message (role: "user").
|
|
147
168
|
#
|
|
@@ -171,8 +192,11 @@ module Clacky
|
|
|
171
192
|
if content.empty?
|
|
172
193
|
[]
|
|
173
194
|
else
|
|
174
|
-
# Strip out the <topics>
|
|
175
|
-
|
|
195
|
+
# Strip out the <topics> and <continues_previous> blocks — they're
|
|
196
|
+
# metadata for chunk handling, not for AI context.
|
|
197
|
+
content_without_topics = content.gsub(/<topics>.*?<\/topics>\n*/m, "")
|
|
198
|
+
.gsub(/<continues_previous>.*?<\/continues_previous>\n*/m, "")
|
|
199
|
+
.strip
|
|
176
200
|
|
|
177
201
|
# Build previous chunks index section — links to older chunk files so the AI
|
|
178
202
|
# can find earlier conversations without keeping all prior compressed_summary
|
|
@@ -193,8 +193,18 @@ module Clacky
|
|
|
193
193
|
recent_messages = get_recent_messages_with_tool_pairs(all_messages, target_recent_count)
|
|
194
194
|
recent_messages = [] if recent_messages.nil?
|
|
195
195
|
|
|
196
|
+
# Surface the most recent chunk's topics so the compression LLM can judge
|
|
197
|
+
# whether this conversation continues the same task (drives chunk merging).
|
|
198
|
+
previous_topics = nil
|
|
199
|
+
if @session_id && @created_at
|
|
200
|
+
latest = session_manager.chunks_for_current(@session_id, @created_at).last
|
|
201
|
+
previous_topics = latest && latest[:topics]
|
|
202
|
+
end
|
|
203
|
+
|
|
196
204
|
# Build compression instruction message (to be inserted into conversation)
|
|
197
|
-
compression_message = @message_compressor.build_compression_message(
|
|
205
|
+
compression_message = @message_compressor.build_compression_message(
|
|
206
|
+
all_messages, recent_messages: recent_messages, previous_topics: previous_topics
|
|
207
|
+
)
|
|
198
208
|
|
|
199
209
|
return nil if compression_message.nil?
|
|
200
210
|
|
|
@@ -242,25 +252,52 @@ module Clacky
|
|
|
242
252
|
# all chunk file I/O (naming, writing, discovery) — we just ask it.
|
|
243
253
|
sm = session_manager
|
|
244
254
|
existing_chunks = sm.chunks_for_current(@session_id, @created_at)
|
|
245
|
-
chunk_index = sm.next_chunk_index(@session_id, @created_at)
|
|
246
255
|
|
|
247
256
|
# Extract topics from the LLM response to store in both the chunk MD front
|
|
248
257
|
# matter and the compressed_summary message hash (for future chunk indexing).
|
|
249
258
|
topics = @message_compressor.parse_topics(compressed_content)
|
|
250
259
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
)
|
|
260
|
+
# Decide whether to MERGE into the previous chunk or create a NEW one.
|
|
261
|
+
# The LLM judges (via <continues_previous>) whether this conversation is a
|
|
262
|
+
# direct continuation of the previous chunk's task. Merging avoids tiny
|
|
263
|
+
# fragmented chunks (e.g. a long task compressed mid-flight into 2-message
|
|
264
|
+
# chunks) that pollute the topics index and degrade recall.
|
|
265
|
+
latest_chunk = existing_chunks.last
|
|
266
|
+
continues = latest_chunk && @message_compressor.parse_continues_previous(compressed_content)
|
|
267
|
+
|
|
268
|
+
if continues
|
|
269
|
+
chunk_path = merge_into_previous_chunk(
|
|
270
|
+
latest_chunk,
|
|
271
|
+
original_messages,
|
|
272
|
+
compression_context[:recent_messages],
|
|
273
|
+
compression_level: compression_context[:compression_level],
|
|
274
|
+
topics: topics
|
|
275
|
+
)
|
|
276
|
+
# Fallback to new chunk if the merge could not be performed.
|
|
277
|
+
chunk_path ||= save_compressed_chunk(
|
|
278
|
+
original_messages, compression_context[:recent_messages],
|
|
279
|
+
chunk_index: sm.next_chunk_index(@session_id, @created_at),
|
|
280
|
+
compression_level: compression_context[:compression_level], topics: topics
|
|
281
|
+
)
|
|
282
|
+
# The merged chunk is the current chunk — exclude it from previous_chunks.
|
|
283
|
+
index_chunks = existing_chunks.reject { |c| c[:index] == latest_chunk[:index] }
|
|
284
|
+
else
|
|
285
|
+
chunk_index = sm.next_chunk_index(@session_id, @created_at)
|
|
286
|
+
chunk_path = save_compressed_chunk(
|
|
287
|
+
original_messages,
|
|
288
|
+
compression_context[:recent_messages],
|
|
289
|
+
chunk_index: chunk_index,
|
|
290
|
+
compression_level: compression_context[:compression_level],
|
|
291
|
+
topics: topics
|
|
292
|
+
)
|
|
293
|
+
index_chunks = existing_chunks
|
|
294
|
+
end
|
|
258
295
|
|
|
259
296
|
# Build previous_chunks index from the disk-discovered chunks (already
|
|
260
297
|
# sorted by index ascending). This gives the new summary a complete
|
|
261
298
|
# chronological index of all older archives so the AI can recall any
|
|
262
299
|
# past chunk via file_reader, not just the most recent one.
|
|
263
|
-
previous_chunks =
|
|
300
|
+
previous_chunks = index_chunks.map do |c|
|
|
264
301
|
{ basename: c[:basename], path: c[:path], topics: c[:topics] }
|
|
265
302
|
end
|
|
266
303
|
|
|
@@ -483,6 +520,62 @@ module Clacky
|
|
|
483
520
|
nil
|
|
484
521
|
end
|
|
485
522
|
|
|
523
|
+
# Merge the current batch of compressed messages INTO an existing chunk
|
|
524
|
+
# (overwrite-in-place, same chunk index). Used when the LLM judged this
|
|
525
|
+
# conversation as a continuation of the previous chunk's task. Keeps the
|
|
526
|
+
# archive on a single, growing, well-formed chunk instead of fragmenting
|
|
527
|
+
# into tiny standalone files that pollute the topics index.
|
|
528
|
+
#
|
|
529
|
+
# Every write hits disk immediately, so a crash never loses archived
|
|
530
|
+
# messages — there is no in-memory buffering.
|
|
531
|
+
#
|
|
532
|
+
# @param prev_chunk [Hash] disk-discovered chunk hash ({ index:, path:, topics: })
|
|
533
|
+
# @return [String, nil] the chunk path on success, nil if merge not possible
|
|
534
|
+
def merge_into_previous_chunk(prev_chunk, original_messages, recent_messages, compression_level:, topics: nil)
|
|
535
|
+
return nil unless @session_id && @created_at
|
|
536
|
+
|
|
537
|
+
recent_set = recent_messages.to_a
|
|
538
|
+
messages_to_archive = original_messages.reject do |m|
|
|
539
|
+
m[:role] == "system" || m[:system_injected] || m[:compressed_summary] || recent_set.include?(m)
|
|
540
|
+
end
|
|
541
|
+
return nil if messages_to_archive.empty?
|
|
542
|
+
|
|
543
|
+
sm = session_manager
|
|
544
|
+
raw = sm.read_chunk(prev_chunk[:path])
|
|
545
|
+
return nil unless raw
|
|
546
|
+
|
|
547
|
+
fm, body = sm.split_chunk_md(raw)
|
|
548
|
+
return nil unless fm
|
|
549
|
+
|
|
550
|
+
new_sections = render_message_sections(messages_to_archive)
|
|
551
|
+
|
|
552
|
+
fm["compression_level"] = compression_level.to_s
|
|
553
|
+
fm["archived_at"] = Time.now.iso8601
|
|
554
|
+
fm["message_count"] = (fm["message_count"].to_i + messages_to_archive.size).to_s
|
|
555
|
+
fm["merged_count"] = (fm.fetch("merged_count", "1").to_i + 1).to_s
|
|
556
|
+
fm["topics"] = merge_topics(fm["topics"], topics)
|
|
557
|
+
|
|
558
|
+
lines = ["---"]
|
|
559
|
+
fm.each { |k, v| lines << "#{k}: #{v}" }
|
|
560
|
+
lines << "---"
|
|
561
|
+
lines << body.rstrip
|
|
562
|
+
lines << ""
|
|
563
|
+
lines.concat(new_sections)
|
|
564
|
+
|
|
565
|
+
sm.write_chunk(@session_id, @created_at, prev_chunk[:index], lines.join("\n"))
|
|
566
|
+
rescue => e
|
|
567
|
+
@ui&.log("Failed to merge chunk MD: #{e.message}", level: :warn)
|
|
568
|
+
nil
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# Union two comma-separated topic strings, preserving order, dropping dups.
|
|
572
|
+
private def merge_topics(existing, incoming)
|
|
573
|
+
a = (existing || "").split(/\s*,\s*/).map(&:strip).reject(&:empty?)
|
|
574
|
+
b = (incoming || "").split(/\s*,\s*/).map(&:strip).reject(&:empty?)
|
|
575
|
+
merged = (a + b).uniq
|
|
576
|
+
merged.empty? ? nil : merged.join(", ")
|
|
577
|
+
end
|
|
578
|
+
|
|
486
579
|
# Build markdown content from a list of messages
|
|
487
580
|
# @param messages [Array<Hash>] Messages to render
|
|
488
581
|
# @param chunk_index [Integer] Chunk number for metadata
|
|
@@ -508,6 +601,15 @@ module Clacky
|
|
|
508
601
|
lines << "> Use `file_reader` to recall specific details from this conversation."
|
|
509
602
|
lines << ""
|
|
510
603
|
|
|
604
|
+
lines.concat(render_message_sections(messages))
|
|
605
|
+
|
|
606
|
+
lines.join("\n")
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# Render messages into chunk MD body sections (no front matter / header).
|
|
610
|
+
# Shared by build_chunk_md and the chunk-merge path.
|
|
611
|
+
def render_message_sections(messages)
|
|
612
|
+
lines = []
|
|
511
613
|
messages.each do |msg|
|
|
512
614
|
role = msg[:role]
|
|
513
615
|
content = msg[:content]
|
|
@@ -560,8 +662,7 @@ module Clacky
|
|
|
560
662
|
lines << ""
|
|
561
663
|
end
|
|
562
664
|
end
|
|
563
|
-
|
|
564
|
-
lines.join("\n")
|
|
665
|
+
lines
|
|
565
666
|
end
|
|
566
667
|
|
|
567
668
|
# Format message content (handles string or array of content blocks)
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -51,7 +51,7 @@ module Clacky
|
|
|
51
51
|
attr_accessor :pinned
|
|
52
52
|
attr_accessor :channel_info
|
|
53
53
|
|
|
54
|
-
REASONING_EFFORTS = %w[low medium high].freeze
|
|
54
|
+
REASONING_EFFORTS = %w[low medium high xhigh].freeze
|
|
55
55
|
|
|
56
56
|
def permission_mode
|
|
57
57
|
@config&.permission_mode&.to_s || ""
|
|
@@ -136,6 +136,8 @@ module Clacky
|
|
|
136
136
|
# Free-mode counterpart: branded but not activated → fetch unencrypted skills
|
|
137
137
|
# via the public endpoint so users get a working install with no serial number.
|
|
138
138
|
@brand_config.sync_free_skills_async!
|
|
139
|
+
# Brand extensions bundled into the activated license's distribution.
|
|
140
|
+
@brand_config.sync_brand_extensions_async!
|
|
139
141
|
|
|
140
142
|
# Initialize Time Machine
|
|
141
143
|
init_time_machine
|
|
@@ -146,6 +148,11 @@ module Clacky
|
|
|
146
148
|
# Load declarative shell hooks from ~/.clacky/hooks.yml
|
|
147
149
|
ShellHookLoader.load_into(@hooks)
|
|
148
150
|
|
|
151
|
+
# Copy ext.yml-contributed hook callbacks (contributes.hooks) onto this
|
|
152
|
+
# agent's hook manager. The callbacks were registered process-wide at
|
|
153
|
+
# boot via ExtensionHookLoader.
|
|
154
|
+
ExtensionHookRegistry.apply_to(@hooks)
|
|
155
|
+
|
|
149
156
|
# Ensure user-space parsers are in place (~/.clacky/parsers/)
|
|
150
157
|
Utils::ParserManager.setup!
|
|
151
158
|
|
|
@@ -683,9 +690,6 @@ module Clacky
|
|
|
683
690
|
raise
|
|
684
691
|
ensure
|
|
685
692
|
# Safety net: ensure any lingering progress spinner is stopped.
|
|
686
|
-
# Normal paths close their own spinners; this guards against exceptions
|
|
687
|
-
# raised between a progress slot's active/done pair.
|
|
688
|
-
Clacky::Logger.warn("[ph_debug] agent_run_ensure")
|
|
689
693
|
@ui&.show_progress(phase: "done")
|
|
690
694
|
|
|
691
695
|
# Fire-and-forget telemetry after every agent run.
|
data/lib/clacky/agent_profile.rb
CHANGED
|
@@ -6,107 +6,175 @@ module Clacky
|
|
|
6
6
|
# Loads and represents an agent profile (system prompt + skill whitelist).
|
|
7
7
|
#
|
|
8
8
|
# Lookup order for a profile named "coding":
|
|
9
|
-
# 1. ~/.clacky/agents/coding/
|
|
10
|
-
# 2.
|
|
9
|
+
# 1. ~/.clacky/agents/coding/ (user override, physical dir)
|
|
10
|
+
# 2. extension agent unit with id == "coding" (ext.yml contributes.agents)
|
|
11
11
|
#
|
|
12
|
-
# Each profile directory
|
|
12
|
+
# Each user profile directory (opt-in override) contains:
|
|
13
13
|
# - profile.yml — name, description, skills whitelist
|
|
14
14
|
# - system_prompt.md — agent-specific system prompt content
|
|
15
15
|
#
|
|
16
|
-
# Global files (shared across all agents)
|
|
17
|
-
# - SOUL.md — agent personality/values
|
|
18
|
-
# - USER.md — user profile
|
|
19
|
-
#
|
|
16
|
+
# Global files (shared across all agents) are user-only overrides:
|
|
17
|
+
# - ~/.clacky/agents/SOUL.md — agent personality/values (else DEFAULT_SOUL)
|
|
18
|
+
# - ~/.clacky/agents/USER.md — user profile info (else DEFAULT_USER)
|
|
19
|
+
# The universal behavioural rules (todo manager, tool usage, response style,
|
|
20
|
+
# etc.) live in a bundled resource file at lib/clacky/prompts/base.md.
|
|
20
21
|
class AgentProfile
|
|
21
|
-
DEFAULT_AGENTS_DIR = File.expand_path("../default_agents", __FILE__).freeze
|
|
22
22
|
USER_AGENTS_DIR = File.expand_path("~/.clacky/agents").freeze
|
|
23
|
+
BASE_PROMPT_PATH = File.expand_path("../prompts/base.md", __FILE__).freeze
|
|
24
|
+
|
|
25
|
+
DEFAULT_SOUL = <<~MD.freeze
|
|
26
|
+
You are calm, precise, and helpful. You communicate clearly and concisely.
|
|
27
|
+
You are honest about uncertainty and ask for clarification when needed.
|
|
28
|
+
You take initiative but respect the user's preferences and decisions.
|
|
29
|
+
MD
|
|
30
|
+
|
|
31
|
+
DEFAULT_USER = "(No user profile configured yet. To personalize, create ~/.clacky/agents/USER.md)"
|
|
23
32
|
|
|
24
33
|
attr_reader :name, :description
|
|
25
34
|
|
|
26
35
|
def initialize(name)
|
|
27
36
|
@name = name.to_s
|
|
37
|
+
result = ExtensionLoader.last_result
|
|
38
|
+
@ext_unit = result&.agents&.find { |u| u.id == @name }
|
|
39
|
+
if @ext_unit.nil?
|
|
40
|
+
result = ExtensionLoader.load_all(force: true)
|
|
41
|
+
@ext_unit = result&.agents&.find { |u| u.id == @name }
|
|
42
|
+
end
|
|
28
43
|
profile_data = load_profile_yml
|
|
29
44
|
@description = profile_data["description"] || ""
|
|
30
45
|
@system_prompt_content = load_agent_file("system_prompt.md")
|
|
31
46
|
end
|
|
32
47
|
|
|
33
|
-
# Load a named profile. Raises ArgumentError if profile directory not found.
|
|
34
48
|
# @param name [String, Symbol] profile name (e.g. "coding", "general")
|
|
35
49
|
# @return [AgentProfile]
|
|
36
50
|
def self.load(name)
|
|
37
51
|
new(name)
|
|
38
52
|
end
|
|
39
53
|
|
|
54
|
+
# List all available agent profiles across user + extension layers.
|
|
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: }
|
|
57
|
+
def self.all
|
|
58
|
+
out = {}
|
|
59
|
+
|
|
60
|
+
add = lambda do |id, title, title_zh, description, description_zh, source, order, layer, author|
|
|
61
|
+
next if id.nil? || id.empty?
|
|
62
|
+
out[id] = {
|
|
63
|
+
id: id,
|
|
64
|
+
title: title,
|
|
65
|
+
title_zh: title_zh,
|
|
66
|
+
description: description,
|
|
67
|
+
description_zh: description_zh,
|
|
68
|
+
source: source,
|
|
69
|
+
order: order,
|
|
70
|
+
layer: layer,
|
|
71
|
+
author: author,
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
ext_result = ExtensionLoader.last_result || ExtensionLoader.load_all
|
|
76
|
+
ext_result&.agents&.each do |unit|
|
|
77
|
+
spec = unit.spec || {}
|
|
78
|
+
title = spec["title"].to_s
|
|
79
|
+
title = unit.id if title.empty?
|
|
80
|
+
add.call(
|
|
81
|
+
unit.id, title, spec["title_zh"].to_s,
|
|
82
|
+
spec["description"].to_s, spec["description_zh"].to_s,
|
|
83
|
+
"extension", spec["order"], unit.layer.to_s,
|
|
84
|
+
spec["author"].to_s
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Dir.glob(File.join(USER_AGENTS_DIR, "*")).sort.each do |path|
|
|
89
|
+
next unless File.directory?(path)
|
|
90
|
+
id = File.basename(path)
|
|
91
|
+
next if id.start_with?("_")
|
|
92
|
+
next unless File.file?(File.join(path, "profile.yml"))
|
|
93
|
+
meta = read_profile_yml(File.join(path, "profile.yml"))
|
|
94
|
+
add.call(
|
|
95
|
+
id, meta["title"] || meta["name"] || id, meta["title_zh"].to_s,
|
|
96
|
+
meta["description"].to_s, meta["description_zh"].to_s,
|
|
97
|
+
"user", meta["order"], "user",
|
|
98
|
+
meta["author"].to_s.empty? ? "You" : meta["author"].to_s
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
source_rank = { "user" => 0, "extension" => 1 }
|
|
103
|
+
out.values.sort_by { |a| [source_rank[a[:source]] || 9, a[:order] || 999, a[:id]] }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private_class_method def self.read_profile_yml(path)
|
|
107
|
+
return {} unless File.file?(path)
|
|
108
|
+
YAML.safe_load(File.read(path)) || {}
|
|
109
|
+
rescue StandardError
|
|
110
|
+
{}
|
|
111
|
+
end
|
|
112
|
+
|
|
40
113
|
# @return [String] agent-specific system prompt content
|
|
41
114
|
def system_prompt
|
|
42
115
|
@system_prompt_content
|
|
43
116
|
end
|
|
44
117
|
|
|
45
|
-
# @return [String] base prompt shared by all agents
|
|
118
|
+
# @return [String] base prompt shared by all agents (bundled resource)
|
|
46
119
|
def base_prompt
|
|
47
|
-
|
|
120
|
+
return "" unless File.file?(BASE_PROMPT_PATH)
|
|
121
|
+
File.read(BASE_PROMPT_PATH).strip
|
|
48
122
|
end
|
|
49
123
|
|
|
50
|
-
# @return [String] soul content (user override
|
|
124
|
+
# @return [String] soul content (user override, else default)
|
|
51
125
|
def soul
|
|
52
|
-
|
|
126
|
+
user_path = File.join(USER_AGENTS_DIR, "SOUL.md")
|
|
127
|
+
if File.exist?(user_path) && !File.zero?(user_path)
|
|
128
|
+
File.read(user_path).strip
|
|
129
|
+
else
|
|
130
|
+
DEFAULT_SOUL.strip
|
|
131
|
+
end
|
|
53
132
|
end
|
|
54
133
|
|
|
55
|
-
# @return [String] user profile content (user override
|
|
134
|
+
# @return [String] user profile content (user override, else default)
|
|
56
135
|
def user_profile
|
|
57
|
-
|
|
136
|
+
user_path = File.join(USER_AGENTS_DIR, "USER.md")
|
|
137
|
+
if File.exist?(user_path) && !File.zero?(user_path)
|
|
138
|
+
File.read(user_path).strip
|
|
139
|
+
else
|
|
140
|
+
DEFAULT_USER
|
|
141
|
+
end
|
|
58
142
|
end
|
|
59
143
|
|
|
60
144
|
private def load_profile_yml
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
YAML.safe_load(File.read(path)) || {}
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Load a file from the agent-specific directory (user override → built-in)
|
|
69
|
-
private def load_agent_file(filename)
|
|
70
|
-
path = find_agent_file(filename)
|
|
71
|
-
return "" unless path
|
|
72
|
-
|
|
73
|
-
File.read(path).strip
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Load a global file shared across all agents (user override → built-in)
|
|
77
|
-
private def load_global_file(filename)
|
|
78
|
-
user_path = File.join(USER_AGENTS_DIR, filename)
|
|
79
|
-
default_path = File.join(DEFAULT_AGENTS_DIR, filename)
|
|
80
|
-
|
|
81
|
-
path = if File.exist?(user_path) && !File.zero?(user_path)
|
|
82
|
-
user_path
|
|
83
|
-
elsif File.exist?(default_path)
|
|
84
|
-
default_path
|
|
85
|
-
end
|
|
145
|
+
user_yml = File.join(user_agent_dir, "profile.yml")
|
|
146
|
+
if File.file?(user_yml)
|
|
147
|
+
return YAML.safe_load(File.read(user_yml)) || {}
|
|
148
|
+
end
|
|
86
149
|
|
|
87
|
-
|
|
150
|
+
if @ext_unit
|
|
151
|
+
return {
|
|
152
|
+
"name" => @name,
|
|
153
|
+
"description" => @ext_unit.spec["description"],
|
|
154
|
+
"panels" => @ext_unit.spec["panels"],
|
|
155
|
+
"skills" => @ext_unit.spec["skills"],
|
|
156
|
+
}
|
|
157
|
+
end
|
|
88
158
|
|
|
89
|
-
|
|
159
|
+
raise ArgumentError, "Agent profile '#{@name}' not found. " \
|
|
160
|
+
"Looked in #{user_agent_dir} and extension registry."
|
|
90
161
|
end
|
|
91
162
|
|
|
92
|
-
#
|
|
93
|
-
private def
|
|
163
|
+
# Agent-specific file lookup: user override → extension prompt (system_prompt.md only).
|
|
164
|
+
private def load_agent_file(filename)
|
|
94
165
|
user_path = File.join(user_agent_dir, filename)
|
|
95
|
-
|
|
166
|
+
return File.read(user_path).strip if File.exist?(user_path) && !File.zero?(user_path)
|
|
96
167
|
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
default_path
|
|
168
|
+
if @ext_unit && filename == "system_prompt.md"
|
|
169
|
+
prompt_abs = @ext_unit.spec["prompt_abs"]
|
|
170
|
+
return File.read(prompt_abs).strip if prompt_abs && File.file?(prompt_abs)
|
|
101
171
|
end
|
|
172
|
+
|
|
173
|
+
""
|
|
102
174
|
end
|
|
103
175
|
|
|
104
176
|
private def user_agent_dir
|
|
105
177
|
File.join(USER_AGENTS_DIR, @name)
|
|
106
178
|
end
|
|
107
|
-
|
|
108
|
-
private def default_agent_dir
|
|
109
|
-
File.join(DEFAULT_AGENTS_DIR, @name)
|
|
110
|
-
end
|
|
111
179
|
end
|
|
112
180
|
end
|