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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/lib/clacky/agent/message_compressor.rb +32 -8
  4. data/lib/clacky/agent/message_compressor_helper.rb +113 -12
  5. data/lib/clacky/agent.rb +8 -4
  6. data/lib/clacky/agent_profile.rb +121 -53
  7. data/lib/clacky/brand_config.rb +309 -6
  8. data/lib/clacky/cli.rb +4 -58
  9. data/lib/clacky/default_extensions/coding/ext.yml +18 -0
  10. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
  11. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
  12. data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
  13. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
  14. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
  15. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
  16. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
  17. data/lib/clacky/default_extensions/general/ext.yml +18 -0
  18. data/lib/clacky/default_extensions/git/ext.yml +12 -0
  19. data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
  20. data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
  21. data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
  22. data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
  23. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
  24. data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
  25. data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
  26. data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
  27. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
  28. data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
  29. data/lib/clacky/extension/api_loader.rb +136 -0
  30. data/lib/clacky/extension/cli_commands.rb +226 -0
  31. data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
  32. data/lib/clacky/extension/hook_loader.rb +77 -0
  33. data/lib/clacky/extension/loader.rb +483 -0
  34. data/lib/clacky/extension/packager.rb +226 -0
  35. data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
  36. data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
  37. data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
  38. data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
  39. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  40. data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
  41. data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
  42. data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
  43. data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
  44. data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
  45. data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
  46. data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
  47. data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
  48. data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
  49. data/lib/clacky/extension/scaffold.rb +55 -0
  50. data/lib/clacky/extension/verifier.rb +196 -0
  51. data/lib/clacky/identity.rb +70 -0
  52. data/lib/clacky/locales/en.rb +19 -1
  53. data/lib/clacky/locales/zh.rb +19 -1
  54. data/lib/clacky/platform_http_client.rb +18 -15
  55. data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
  56. data/lib/clacky/server/channel.rb +6 -0
  57. data/lib/clacky/server/http_server.rb +435 -263
  58. data/lib/clacky/server/session_registry.rb +59 -3
  59. data/lib/clacky/session_manager.rb +35 -4
  60. data/lib/clacky/skill_loader.rb +28 -1
  61. data/lib/clacky/ui2/layout_manager.rb +0 -5
  62. data/lib/clacky/ui2/progress_handle.rb +0 -3
  63. data/lib/clacky/ui2/ui_controller.rb +9 -15
  64. data/lib/clacky/version.rb +1 -1
  65. data/lib/clacky/web/app.css +782 -554
  66. data/lib/clacky/web/app.js +63 -15
  67. data/lib/clacky/web/auth.js +2 -0
  68. data/lib/clacky/web/components/notify.js +2 -0
  69. data/lib/clacky/web/components/sidebar.js +2 -8
  70. data/lib/clacky/web/core/ext.js +316 -79
  71. data/lib/clacky/web/features/backup/store.js +24 -0
  72. data/lib/clacky/web/features/backup/view.js +49 -23
  73. data/lib/clacky/web/features/brand/view.js +13 -5
  74. data/lib/clacky/web/features/extensions/store.js +184 -0
  75. data/lib/clacky/web/features/extensions/view.js +389 -0
  76. data/lib/clacky/web/features/new-session/store.js +186 -0
  77. data/lib/clacky/web/features/new-session/view.js +319 -0
  78. data/lib/clacky/web/features/share/view.js +63 -14
  79. data/lib/clacky/web/features/skills/store.js +3 -1
  80. data/lib/clacky/web/features/skills/view.js +1 -1
  81. data/lib/clacky/web/features/tasks/view.js +88 -27
  82. data/lib/clacky/web/features/trash/store.js +24 -6
  83. data/lib/clacky/web/features/trash/view.js +46 -9
  84. data/lib/clacky/web/features/workspace/store.js +2 -0
  85. data/lib/clacky/web/features/workspace/view.js +0 -1
  86. data/lib/clacky/web/i18n.js +140 -108
  87. data/lib/clacky/web/index.html +190 -214
  88. data/lib/clacky/web/sessions.js +99 -304
  89. data/lib/clacky/web/settings.js +1 -1
  90. data/lib/clacky/web/skills.js +2 -0
  91. data/lib/clacky/web/theme.js +2 -0
  92. data/lib/clacky/web/ws-dispatcher.js +2 -1
  93. data/lib/clacky/web/ws.js +2 -0
  94. data/lib/clacky.rb +21 -10
  95. metadata +55 -24
  96. data/benchmark/runner.rb +0 -441
  97. data/lib/clacky/api_extension_loader.rb +0 -168
  98. data/lib/clacky/default_agents/SOUL.md +0 -3
  99. data/lib/clacky/default_agents/USER.md +0 -1
  100. data/lib/clacky/default_agents/coding/profile.yml +0 -5
  101. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  102. data/lib/clacky/default_agents/general/profile.yml +0 -2
  103. data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
  104. data/lib/clacky/web/features/creator/store.js +0 -81
  105. data/lib/clacky/web/features/creator/view.js +0 -380
  106. /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
  107. /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
  108. /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
@@ -49,6 +49,7 @@ module Clacky
49
49
  agent: nil,
50
50
  ui: nil,
51
51
  thread: nil,
52
+ epoch: 0,
52
53
  idle_timer: nil,
53
54
  pending_task: nil,
54
55
  pending_working_dir: nil
@@ -116,8 +117,15 @@ module Clacky
116
117
  src = (session_data[:source] || "manual").to_s
117
118
  next if counts[src] >= n
118
119
  next if exist?(session_data[:session_id])
119
- @session_restorer.call(session_data)
120
- counts[src] += 1
120
+ begin
121
+ @session_restorer.call(session_data)
122
+ counts[src] += 1
123
+ rescue => e
124
+ Clacky::Logger.warn(
125
+ "[SessionRegistry] skip session #{session_data[:session_id]}: " \
126
+ "#{e.class}: #{e.message}"
127
+ )
128
+ end
121
129
  end
122
130
  end
123
131
 
@@ -138,6 +146,40 @@ module Clacky
138
146
  end
139
147
  end
140
148
 
149
+ # Atomically bump the session's task epoch and return the new value.
150
+ # Each user message that starts a task claims a fresh epoch; a stale task
151
+ # thread (interrupted but not yet dead) compares its epoch against the
152
+ # current one and discards any side effects (status writes, broadcasts)
153
+ # once it has been superseded.
154
+ def claim_epoch(session_id)
155
+ @mutex.synchronize do
156
+ session = @sessions[session_id]
157
+ return nil unless session
158
+
159
+ session[:epoch] = session[:epoch].to_i + 1
160
+ end
161
+ end
162
+
163
+ # Current task epoch for a session (0 if none / unknown).
164
+ def current_epoch(session_id)
165
+ @mutex.synchronize { @sessions[session_id]&.fetch(:epoch, 0).to_i }
166
+ end
167
+
168
+ # Update fields only if the caller still owns the current epoch. Returns
169
+ # true if the update was applied, false if the epoch was stale (a newer
170
+ # task has taken over) or the session is gone.
171
+ def update_if_epoch(session_id, epoch, **fields)
172
+ @mutex.synchronize do
173
+ session = @sessions[session_id]
174
+ return false unless session
175
+ return false unless session[:epoch].to_i == epoch.to_i
176
+
177
+ fields[:updated_at] = Time.now
178
+ session.merge!(fields)
179
+ true
180
+ end
181
+ end
182
+
141
183
  # Return a session list from disk enriched with live registry status.
142
184
  # Sorted by created_at descending (newest first).
143
185
  #
@@ -161,7 +203,7 @@ module Clacky
161
203
  # [ ...all_pinned_matching (newest-first), ...non_pinned (newest-first, limited) ]
162
204
  #
163
205
  # source and profile are orthogonal — either can be nil independently.
164
- def list(limit: nil, before: nil, q: nil, q_scope: "name", date: nil, type: nil, include_pinned: true)
206
+ def list(limit: nil, before: nil, q: nil, q_scope: "name", date: nil, type: nil, exclude_type: nil, include_pinned: true)
165
207
  return [] unless @session_manager
166
208
 
167
209
  live = @mutex.synchronize do
@@ -196,6 +238,12 @@ module Clacky
196
238
  end
197
239
  end
198
240
 
241
+ # ── exclude_type filter ───────────────────────────────────────────────
242
+ if exclude_type
243
+ excluded = Array(exclude_type)
244
+ all = all.reject { |s| excluded.include?(s_source(s)) }
245
+ end
246
+
199
247
  # ── date filter (YYYY-MM-DD, matches created_at prefix) ──────────────
200
248
  all = all.select { |s| s[:created_at].to_s.start_with?(date) } if date
201
249
 
@@ -349,6 +397,14 @@ module Clacky
349
397
  @session_manager.all_sessions.count { |s| s_source(s) == "cron" }
350
398
  end
351
399
 
400
+ # Count + latest updated_at for cron sessions (used by sidebar virtual entry).
401
+ def cron_stats
402
+ return { count: 0, latest_updated_at: nil } unless @session_manager
403
+ cron_all = @session_manager.all_sessions.select { |s| s_source(s) == "cron" }
404
+ latest = cron_all.map { |s| s[:updated_at] || s[:created_at] }.compact.max
405
+ { count: cron_all.size, latest_updated_at: latest }
406
+ end
407
+
352
408
  # Delete a session from registry (and interrupt its thread).
353
409
  def delete(session_id)
354
410
  @mutex.synchronize do
@@ -35,7 +35,7 @@ module Clacky
35
35
 
36
36
  # Keep only the most recent 200 sessions (best-effort, never block save)
37
37
  begin
38
- cleanup_by_count(keep: 200)
38
+ cleanup_by_count(keep: 200, keep_cron: 200)
39
39
  rescue Exception # rubocop:disable Lint/RescueException
40
40
  # Cleanup is non-critical; swallow all errors (including AgentInterrupted)
41
41
  end
@@ -163,6 +163,33 @@ module Clacky
163
163
  chunk_path
164
164
  end
165
165
 
166
+ # Read the raw markdown of a chunk file. Returns nil if missing.
167
+ def read_chunk(chunk_path)
168
+ return nil unless chunk_path && File.exist?(chunk_path)
169
+ File.read(chunk_path)
170
+ end
171
+
172
+ # Split raw chunk markdown into [front_matter_hash, body_string].
173
+ # front_matter_hash preserves insertion order; body is everything after
174
+ # the closing "---". Returns [nil, raw] when there is no leading block.
175
+ def split_chunk_md(raw)
176
+ return [nil, raw.to_s] unless raw.to_s.start_with?("---")
177
+
178
+ fm_end = raw.index("\n---\n", 4)
179
+ return [nil, raw] unless fm_end
180
+
181
+ fm_text = raw[4...fm_end]
182
+ body = raw[(fm_end + 5)..] || ""
183
+
184
+ fm = {}
185
+ fm_text.each_line do |line|
186
+ k, _, v = line.chomp.partition(":")
187
+ next if k.strip.empty?
188
+ fm[k.strip] = v.strip
189
+ end
190
+ [fm, body]
191
+ end
192
+
166
193
  # All sessions from disk, newest-first (sorted by last activity / updated_at,
167
194
  # falling back to created_at for legacy sessions without updated_at).
168
195
  # Optional filters:
@@ -305,11 +332,15 @@ module Clacky
305
332
  # are soft-deleted (moved to the session trash, recoverable). Pinned
306
333
  # sessions are never deleted and do not count toward the cap.
307
334
  # Returns count of soft-deleted sessions.
308
- def cleanup_by_count(keep:)
335
+ def cleanup_by_count(keep:, keep_cron: 200)
309
336
  non_pinned = all_sessions.reject { |s| s[:pinned] } # already sorted newest-first
310
- return 0 if non_pinned.size <= keep
311
337
 
312
- victims = non_pinned[keep..]
338
+ cron, regular = non_pinned.partition { |s| s[:source].to_s == "cron" }
339
+
340
+ victims = []
341
+ victims += regular[keep..] if regular.size > keep
342
+ victims += cron[keep_cron..] if cron.size > keep_cron
343
+
313
344
  victims.each { |session| soft_delete(session[:session_id]) }
314
345
  victims.size
315
346
  end
@@ -12,6 +12,7 @@ module Clacky
12
12
  # Skill discovery locations (in priority order: lower index = lower priority)
13
13
  LOCATIONS = [
14
14
  :default, # gem's built-in default skills (lowest priority)
15
+ :extension, # contributes.skills declared in ext.yml containers
15
16
  :global_clacky, # ~/.clacky/skills/
16
17
  :project_clacky, # .clacky/skills/ (highest priority among plain skills)
17
18
  :brand # ~/.clacky/brand_skills/ (encrypted, license-gated)
@@ -62,6 +63,7 @@ module Clacky
62
63
  clear
63
64
 
64
65
  load_default_skills
66
+ load_extension_skills
65
67
  load_global_clacky_skills
66
68
 
67
69
  # Only load project-level skills when working_dir is explicitly provided.
@@ -401,7 +403,7 @@ module Clacky
401
403
  # @return [Skill, nil] nil when the skill was rejected (duplicate/limit)
402
404
  private def register_skill(skill, source:)
403
405
  id = skill.identifier
404
- priority_order = %i[default global_clacky project_clacky brand]
406
+ priority_order = %i[default extension global_clacky project_clacky brand]
405
407
 
406
408
  # --- duplicate check ---
407
409
  if (existing = @skills[id])
@@ -472,5 +474,30 @@ module Clacky
472
474
  end
473
475
  end
474
476
  end
477
+
478
+ # Load skills declared via `contributes.skills` in ext.yml containers.
479
+ # Protected (encrypted) units are skipped here — they are listed by
480
+ # ExtensionLoader for display/management but their decryption still flows
481
+ # through the legacy brand_skill chain (see load_brand_skills).
482
+ private def load_extension_skills
483
+ result = Clacky::ExtensionLoader.last_result
484
+ return unless result&.skills
485
+
486
+ result.skills.each do |unit|
487
+ next if unit.spec["protected"] || unit.spec["encrypted"]
488
+
489
+ skill_dir_abs = unit.spec["skill_dir_abs"]
490
+ next unless File.directory?(skill_dir_abs) && File.file?(File.join(skill_dir_abs, "SKILL.md"))
491
+
492
+ begin
493
+ skill = Skill.new(Pathname.new(skill_dir_abs))
494
+ register_skill(skill, source: :extension)
495
+ rescue Clacky::AgentError => e
496
+ @errors << "Error loading extension skill '#{unit.id}' from #{skill_dir_abs}: #{e.message}"
497
+ rescue StandardError => e
498
+ @errors << "Unexpected error loading extension skill '#{unit.id}' from #{skill_dir_abs}: #{e.message}"
499
+ end
500
+ end
501
+ end
475
502
  end
476
503
  end
@@ -120,28 +120,23 @@ module Clacky
120
120
  @render_mutex.synchronize do
121
121
  entry = @buffer.entry_by_id(id)
122
122
  if entry.nil?
123
- Clacky::Logger.warn("[ph_debug] replace_entry_nil", id: id, content: content.to_s[0, 120])
124
123
  return
125
124
  end
126
125
  if entry.committed
127
- Clacky::Logger.warn("[ph_debug] replace_entry_committed", id: id, content: content.to_s[0, 120])
128
126
  return
129
127
  end
130
128
  if (entry.committed_line_offset || 0) > 0
131
- Clacky::Logger.warn("[ph_debug] replace_entry_partial", id: id, offset: entry.committed_line_offset, content: content.to_s[0, 120])
132
129
  return
133
130
  end
134
131
 
135
132
  old_lines = entry.lines.dup
136
133
  new_lines = wrap_content_to_lines(content)
137
134
  if old_lines == new_lines
138
- Clacky::Logger.warn("[ph_debug] replace_entry_same", id: id)
139
135
  screen.flush
140
136
  return
141
137
  end
142
138
  @buffer.replace(id, new_lines)
143
139
  is_tail = @buffer.live_entries.last&.id == id
144
- Clacky::Logger.warn("[ph_debug] replace_entry_paint", id: id, is_tail: is_tail, old_n: old_lines.length, new_n: new_lines.length, content: content.to_s[0, 120])
145
140
 
146
141
  unless @fullscreen_mode
147
142
  # repaint_entry_in_place relies on the entry being the tail of
@@ -182,7 +182,6 @@ module Clacky
182
182
  # @param final_message [String, nil] Optional override for the last
183
183
  # frame. If nil, the handle composes "<message>… (<elapsed>s)".
184
184
  def finish(final_message: nil)
185
- Clacky::Logger.warn("[ph_debug] finish_entry", oid: object_id, state: @state, unreg: @unregistered, msg: @message, eid: @entry_id)
186
185
  snapshot = @monitor.synchronize do
187
186
  return if @unregistered
188
187
  first_close = @state == :running
@@ -201,10 +200,8 @@ module Clacky
201
200
  else
202
201
  compose_final_frame(snapshot[:message], snapshot[:elapsed])
203
202
  end
204
- Clacky::Logger.warn("[ph_debug] finish_unregister", oid: object_id, eid: @entry_id, first_close: snapshot[:first_close], final_frame: final_frame.to_s[0, 200])
205
203
  @owner.unregister_progress(self, final_frame: final_frame)
206
204
  @monitor.synchronize { @unregistered = true }
207
- Clacky::Logger.warn("[ph_debug] finish_done", oid: object_id)
208
205
  end
209
206
  alias_method :cancel, :finish
210
207
 
@@ -664,22 +664,13 @@ module Clacky
664
664
 
665
665
  # Called by ProgressHandle#finish.
666
666
  def unregister_progress(handle, final_frame:)
667
- Clacky::Logger.warn("[ph_debug] unreg_entry", oid: handle.object_id, eid: handle.entry_id, top: @progress_stack.last == handle, stack_size: @progress_stack.size, ff: final_frame.to_s[0, 200])
668
667
  @progress_mutex.synchronize do
669
- # If this handle still holds its entry (it's currently top), we
670
- # render one last frame there and release the id. If it was
671
- # previously detached (someone above is still active), its entry
672
- # is already gone and the final_frame is simply dropped.
673
668
  if handle.entry_id
674
669
  if final_frame && !final_frame.to_s.strip.empty?
675
- Clacky::Logger.warn("[ph_debug] unreg_update_entry", oid: handle.object_id, eid: handle.entry_id)
676
670
  update_entry(handle.entry_id, @renderer.render_progress(final_frame))
677
671
  else
678
- Clacky::Logger.warn("[ph_debug] unreg_remove_entry", oid: handle.object_id, eid: handle.entry_id)
679
672
  remove_entry(handle.entry_id)
680
673
  end
681
- else
682
- Clacky::Logger.warn("[ph_debug] unreg_no_entry_id", oid: handle.object_id)
683
674
  end
684
675
 
685
676
  @progress_stack.delete(handle)
@@ -1094,12 +1085,15 @@ module Clacky
1094
1085
  queue.push(:timeout) unless session[:intervened]
1095
1086
  end
1096
1087
 
1097
- result = queue.pop
1098
- session[:watchdog].kill if session[:watchdog]&.alive?
1099
- @feedback_countdown = nil
1100
- @layout.remove_entry(entry_id) if entry_id
1101
- @layout.recalculate_layout
1102
- @layout.render_all
1088
+ begin
1089
+ result = queue.pop
1090
+ ensure
1091
+ session[:watchdog].kill if session[:watchdog]&.alive?
1092
+ @feedback_countdown = nil
1093
+ @layout.remove_entry(entry_id) if entry_id
1094
+ @layout.recalculate_layout
1095
+ @layout.render_all
1096
+ end
1103
1097
 
1104
1098
  if result == :timeout
1105
1099
  append_output(theme.format_text(" No response — continuing automatically.", :thinking))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.3.5"
4
+ VERSION = "1.3.7"
5
5
  end