openclacky 1.3.6 → 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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/lib/clacky/agent/hook_manager.rb +10 -1
  4. data/lib/clacky/agent/system_prompt_builder.rb +1 -1
  5. data/lib/clacky/agent.rb +17 -3
  6. data/lib/clacky/agent_profile.rb +124 -53
  7. data/lib/clacky/billing/billing_store.rb +3 -2
  8. data/lib/clacky/brand_config.rb +309 -6
  9. data/lib/clacky/cli.rb +4 -57
  10. data/lib/clacky/default_extensions/coding/agents/coding/avatar.png +0 -0
  11. data/lib/clacky/default_extensions/coding/ext.yml +19 -0
  12. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/avatar.png +0 -0
  13. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
  14. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
  15. data/lib/clacky/default_extensions/ext-studio/ext.yml +33 -0
  16. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1288 -0
  17. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
  18. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
  19. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
  20. data/lib/clacky/default_extensions/general/agents/general/avatar.png +0 -0
  21. data/lib/clacky/default_extensions/general/ext.yml +19 -0
  22. data/lib/clacky/default_extensions/git/ext.yml +12 -0
  23. data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +4 -5
  24. data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
  25. data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
  26. data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -90
  27. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
  28. data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
  29. data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
  30. data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
  31. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
  32. data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
  33. data/lib/clacky/extension/api_loader.rb +136 -0
  34. data/lib/clacky/extension/cli_commands.rb +226 -0
  35. data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
  36. data/lib/clacky/extension/hook_loader.rb +77 -0
  37. data/lib/clacky/extension/loader.rb +489 -0
  38. data/lib/clacky/extension/packager.rb +226 -0
  39. data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
  40. data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
  41. data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
  42. data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
  43. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  44. data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
  45. data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
  46. data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
  47. data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
  48. data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
  49. data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
  50. data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
  51. data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
  52. data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
  53. data/lib/clacky/extension/scaffold.rb +55 -0
  54. data/lib/clacky/extension/verifier.rb +196 -0
  55. data/lib/clacky/identity.rb +70 -0
  56. data/lib/clacky/locales/en.rb +19 -1
  57. data/lib/clacky/locales/zh.rb +19 -1
  58. data/lib/clacky/platform_http_client.rb +18 -15
  59. data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
  60. data/lib/clacky/server/channel.rb +6 -0
  61. data/lib/clacky/server/http_server.rb +439 -146
  62. data/lib/clacky/server/session_registry.rb +9 -2
  63. data/lib/clacky/shell_hook_loader.rb +266 -22
  64. data/lib/clacky/skill_loader.rb +28 -1
  65. data/lib/clacky/ui2/components/welcome_banner.rb +1 -1
  66. data/lib/clacky/ui2/ui_controller.rb +9 -6
  67. data/lib/clacky/utils/workspace_rules.rb +2 -2
  68. data/lib/clacky/version.rb +1 -1
  69. data/lib/clacky/web/app.css +1034 -675
  70. data/lib/clacky/web/app.js +63 -15
  71. data/lib/clacky/web/auth.js +2 -0
  72. data/lib/clacky/web/components/notify.js +2 -0
  73. data/lib/clacky/web/components/sidebar.js +1 -5
  74. data/lib/clacky/web/core/ext.js +432 -89
  75. data/lib/clacky/web/features/backup/store.js +1 -0
  76. data/lib/clacky/web/features/backup/view.js +0 -1
  77. data/lib/clacky/web/features/brand/view.js +13 -5
  78. data/lib/clacky/web/features/extensions/store.js +184 -0
  79. data/lib/clacky/web/features/extensions/view.js +390 -0
  80. data/lib/clacky/web/features/new-session/store.js +199 -0
  81. data/lib/clacky/web/features/new-session/view.js +619 -0
  82. data/lib/clacky/web/features/share/view.js +63 -14
  83. data/lib/clacky/web/features/skills/store.js +3 -1
  84. data/lib/clacky/web/features/skills/view.js +1 -1
  85. data/lib/clacky/web/features/tasks/view.js +12 -0
  86. data/lib/clacky/web/features/trash/store.js +24 -6
  87. data/lib/clacky/web/features/trash/view.js +48 -12
  88. data/lib/clacky/web/features/workspace/store.js +2 -0
  89. data/lib/clacky/web/features/workspace/view.js +0 -1
  90. data/lib/clacky/web/i18n.js +152 -106
  91. data/lib/clacky/web/index.html +172 -190
  92. data/lib/clacky/web/sessions.js +67 -272
  93. data/lib/clacky/web/settings.js +1 -1
  94. data/lib/clacky/web/skills.js +88 -48
  95. data/lib/clacky/web/theme.js +5 -0
  96. data/lib/clacky/web/ws-dispatcher.js +12 -2
  97. data/lib/clacky/web/ws.js +2 -0
  98. data/lib/clacky.rb +21 -10
  99. metadata +52 -21
  100. data/benchmark/runner.rb +0 -441
  101. data/lib/clacky/api_extension_loader.rb +0 -168
  102. data/lib/clacky/default_agents/SOUL.md +0 -3
  103. data/lib/clacky/default_agents/USER.md +0 -1
  104. data/lib/clacky/default_agents/coding/profile.yml +0 -5
  105. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  106. data/lib/clacky/default_agents/general/profile.yml +0 -2
  107. data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
  108. data/lib/clacky/web/features/creator/store.js +0 -81
  109. data/lib/clacky/web/features/creator/view.js +0 -380
  110. /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
  111. /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
  112. /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: 45a64960de249e34ee18e67f0ce38888b510a886988d545c58aca558d942bbdb
4
- data.tar.gz: '008d7b7bec8fd7edf43cb9848949637d23783d2ad3a8a2b6199010f9b8ed660d'
3
+ metadata.gz: 589d52edb2af8ec7ebea8d596a4b342791230009666a8757f4391b95df22d667
4
+ data.tar.gz: 07c80f79e8a9df57b4232a1317b1a6f8b4557cf136289f7ea1b7a9e93caba5aa
5
5
  SHA512:
6
- metadata.gz: 2f0e9c3fb21691cf9bbfdd6820faaaed86086bebb820278b6104356f92fec4c0e53584a8c3ca40bfd42fb7f82e1f3a484ed404c2b693a47467e9c43d517ed9e8
7
- data.tar.gz: 83cb1fa9ac5a3ae4834bf5c02c344da9db09a5863a4970857f2d233656c66d9b20fde131f0af4b04d23e204a09a978968ae7d2310ae1c3c3ceb8bcb6d721e3c2
6
+ metadata.gz: e474dff0d682c03bbd9756a37e9c64a29d13f51c35b73a76440e62eeac4b92d5ce6251e7357493531ce0669a770607a37a17ceb8715eadf5f849a7b04a6f373a
7
+ data.tar.gz: 8863d9a310224162b3fc0dad4513c933ebafcc74be5d2157d890d7a4f2dd667f66c9f658e61d289c078fdf39c5961bb5c0895317553c0f25b5054142f2c127b9
data/CHANGELOG.md CHANGED
@@ -5,6 +5,60 @@ 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
+
32
+ ## [1.3.7] - 2026-07-06
33
+
34
+ ### Added
35
+ - Extension ecosystem: install, manage, and publish extensions with Creator Studio
36
+ - Brand extension and brand badge support for extension agents
37
+ - Hook/profile extension type for intercepting and customizing agent behavior
38
+ - Extension upload CLI for publishing extensions from the command line
39
+ - New session page with improved layout and discoverability
40
+ - Session auto-cleanup hint, skeleton loading, and type filter in trash panel
41
+ - Renew button on brand warning bar for expiring subscriptions
42
+ - i18n tooltips for header theme toggle and trash toolbar buttons
43
+ - Refresh button in header to reload sessions
44
+ - xhigh reasoning effort level for supported models
45
+ - i18n file upload error messages
46
+
47
+ ### Improved
48
+ - Merge Creator Center into ext-studio as a unified extension hub
49
+ - Extension developer experience: scaffold template improvements, badge and author metadata support
50
+ - Share modal UI polish — theme dots on hover, close button hover effect, layout refinement
51
+ - Unify Clacky.* JS namespace and single-segment extension architecture
52
+ - Migrate default agents to default extensions with unified panel visibility
53
+
54
+ ### Fixed
55
+ - Ctrl+C now works properly in TUI auto-answer mode
56
+ - Show-system-skills toggle persists correctly and uses theme accent color
57
+ - Cron display handles every-N-hours with weekdays pattern correctly
58
+ - New-session dropdown arrow button responds to both click and keyboard
59
+ - New cron virtual entry inserts at the correct position relative to pinned sessions
60
+ - File upload error messages use i18n consistently
61
+
8
62
  ## [1.3.6] - 2026-06-30
9
63
 
10
64
  ### 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
- result.merge!(hook_result) if hook_result.is_a?(Hash)
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
@@ -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
@@ -143,8 +145,20 @@ module Clacky
143
145
  # Register built-in tools
144
146
  register_builtin_tools
145
147
 
146
- # Load declarative shell hooks from ~/.clacky/hooks.yml
147
- ShellHookLoader.load_into(@hooks)
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
+ )
157
+
158
+ # Copy ext.yml-contributed hook callbacks (contributes.hooks) onto this
159
+ # agent's hook manager. The callbacks were registered process-wide at
160
+ # boot via ExtensionHookLoader.
161
+ ExtensionHookRegistry.apply_to(@hooks)
148
162
 
149
163
  # Ensure user-space parsers are in place (~/.clacky/parsers/)
150
164
  Utils::ParserManager.setup!
@@ -6,107 +6,178 @@ 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/ (user override)
10
- # 2. <gem>/lib/clacky/default_agents/coding/ (built-in default)
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 must contain:
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), also with user-override support:
17
- # - SOUL.md — agent personality/values
18
- # - USER.md — user profile information
19
- # - base_prompt.md universal behavioral rules (todo manager, tool usage, etc.)
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:, avatar: }
57
+ def self.all
58
+ out = {}
59
+
60
+ add = lambda do |id, title, title_zh, description, description_zh, source, order, layer, author, avatar|
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
+ avatar: avatar,
73
+ }
74
+ end
75
+
76
+ ext_result = ExtensionLoader.last_result || ExtensionLoader.load_all
77
+ ext_result&.agents&.each do |unit|
78
+ spec = unit.spec || {}
79
+ title = spec["title"].to_s
80
+ title = unit.id if title.empty?
81
+ avatar = spec["avatar_abs"].to_s.empty? ? nil : "/agent_avatar/#{unit.id}"
82
+ add.call(
83
+ unit.id, title, spec["title_zh"].to_s,
84
+ spec["description"].to_s, spec["description_zh"].to_s,
85
+ "extension", spec["order"], unit.layer.to_s,
86
+ spec["author"].to_s, avatar
87
+ )
88
+ end
89
+
90
+ Dir.glob(File.join(USER_AGENTS_DIR, "*")).sort.each do |path|
91
+ next unless File.directory?(path)
92
+ id = File.basename(path)
93
+ next if id.start_with?("_")
94
+ next unless File.file?(File.join(path, "profile.yml"))
95
+ meta = read_profile_yml(File.join(path, "profile.yml"))
96
+ user_avatar = File.file?(File.join(path, "avatar.png")) ? "/agent_avatar/#{id}" : nil
97
+ add.call(
98
+ id, meta["title"] || meta["name"] || id, meta["title_zh"].to_s,
99
+ meta["description"].to_s, meta["description_zh"].to_s,
100
+ "user", meta["order"], "user",
101
+ meta["author"].to_s.empty? ? "You" : meta["author"].to_s, user_avatar
102
+ )
103
+ end
104
+
105
+ source_rank = { "user" => 0, "extension" => 1 }
106
+ out.values.sort_by { |a| [source_rank[a[:source]] || 9, a[:order] || 999, a[:id]] }
107
+ end
108
+
109
+ private_class_method def self.read_profile_yml(path)
110
+ return {} unless File.file?(path)
111
+ YAML.safe_load(File.read(path)) || {}
112
+ rescue StandardError
113
+ {}
114
+ end
115
+
40
116
  # @return [String] agent-specific system prompt content
41
117
  def system_prompt
42
118
  @system_prompt_content
43
119
  end
44
120
 
45
- # @return [String] base prompt shared by all agents
121
+ # @return [String] base prompt shared by all agents (bundled resource)
46
122
  def base_prompt
47
- load_global_file("base_prompt.md")
123
+ return "" unless File.file?(BASE_PROMPT_PATH)
124
+ File.read(BASE_PROMPT_PATH).strip
48
125
  end
49
126
 
50
- # @return [String] soul content (user override built-in default)
127
+ # @return [String] soul content (user override, else default)
51
128
  def soul
52
- load_global_file("SOUL.md")
129
+ user_path = File.join(USER_AGENTS_DIR, "SOUL.md")
130
+ if File.exist?(user_path) && !File.zero?(user_path)
131
+ File.read(user_path).strip
132
+ else
133
+ DEFAULT_SOUL.strip
134
+ end
53
135
  end
54
136
 
55
- # @return [String] user profile content (user override built-in default)
137
+ # @return [String] user profile content (user override, else default)
56
138
  def user_profile
57
- load_global_file("USER.md")
139
+ user_path = File.join(USER_AGENTS_DIR, "USER.md")
140
+ if File.exist?(user_path) && !File.zero?(user_path)
141
+ File.read(user_path).strip
142
+ else
143
+ DEFAULT_USER
144
+ end
58
145
  end
59
146
 
60
147
  private def load_profile_yml
61
- path = find_agent_file("profile.yml")
62
- raise ArgumentError, "Agent profile '#{@name}' not found. " \
63
- "Looked in #{user_agent_dir} and #{default_agent_dir}" unless path
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
148
+ user_yml = File.join(user_agent_dir, "profile.yml")
149
+ if File.file?(user_yml)
150
+ return YAML.safe_load(File.read(user_yml)) || {}
151
+ end
86
152
 
87
- return "" unless path
153
+ if @ext_unit
154
+ return {
155
+ "name" => @name,
156
+ "description" => @ext_unit.spec["description"],
157
+ "panels" => @ext_unit.spec["panels"],
158
+ "skills" => @ext_unit.spec["skills"],
159
+ }
160
+ end
88
161
 
89
- File.read(path).strip
162
+ raise ArgumentError, "Agent profile '#{@name}' not found. " \
163
+ "Looked in #{user_agent_dir} and extension registry."
90
164
  end
91
165
 
92
- # Find a file in user override dir first, then built-in default dir
93
- private def find_agent_file(filename)
166
+ # Agent-specific file lookup: user override extension prompt (system_prompt.md only).
167
+ private def load_agent_file(filename)
94
168
  user_path = File.join(user_agent_dir, filename)
95
- default_path = File.join(default_agent_dir, filename)
169
+ return File.read(user_path).strip if File.exist?(user_path) && !File.zero?(user_path)
96
170
 
97
- if File.exist?(user_path) && !File.zero?(user_path)
98
- user_path
99
- elsif File.exist?(default_path)
100
- default_path
171
+ if @ext_unit && filename == "system_prompt.md"
172
+ prompt_abs = @ext_unit.spec["prompt_abs"]
173
+ return File.read(prompt_abs).strip if prompt_abs && File.file?(prompt_abs)
101
174
  end
175
+
176
+ ""
102
177
  end
103
178
 
104
179
  private def user_agent_dir
105
180
  File.join(USER_AGENTS_DIR, @name)
106
181
  end
107
-
108
- private def default_agent_dir
109
- File.join(DEFAULT_AGENTS_DIR, @name)
110
- end
111
182
  end
112
183
  end
@@ -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 - (7 * 24 * 60 * 60)
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