openclacky 0.8.1 → 0.8.2
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/.clacky/skills/gem-release/SKILL.md +17 -1
- data/CHANGELOG.md +15 -3
- data/lib/clacky/agent/skill_manager.rb +23 -6
- data/lib/clacky/agent.rb +13 -11
- data/lib/clacky/agent_profile.rb +1 -7
- data/lib/clacky/cli.rb +5 -5
- data/lib/clacky/default_agents/coding/profile.yml +0 -10
- data/lib/clacky/default_agents/general/profile.yml +0 -5
- data/lib/clacky/default_skills/code-explorer/SKILL.md +1 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +1 -0
- data/lib/clacky/default_skills/new/SKILL.md +1 -0
- data/lib/clacky/server/http_server.rb +45 -10
- data/lib/clacky/skill.rb +24 -0
- data/lib/clacky/skill_loader.rb +21 -0
- data/lib/clacky/ui2/components/command_suggestions.rb +1 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.js +26 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 534d725368218baaf47a39be9b5ce86005b53ebec0f3a2c81d01d754232b56d6
|
|
4
|
+
data.tar.gz: 0d4cfa47f16a72ddc2cc35e4c7a714ef3bee1d961be5d135aa110804b692e9f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d4250858cbb68fd49f0e25cadde03352ffd40566f05957d7de23b386e0e4a13668e02ae00858a0039ce3346eeff69a6bf2cba40da9e8074f3f954c22cc564c8
|
|
7
|
+
data.tar.gz: 4a794d9b85a3a0a6f64e70e18e047aadc4d69b64780965f97a5c827403fff75536068937e7b7f824064f4bb0efbd6e793abcf4eb0975a1523d3e21fe27633624
|
|
@@ -126,7 +126,7 @@ To use this skill, simply say:
|
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
3. **Analyze and Categorize Commits**
|
|
129
|
-
- Review each commit message
|
|
129
|
+
- Review each commit message AND its diff (`git show <hash> --stat`) to understand the actual change
|
|
130
130
|
- Categorize into:
|
|
131
131
|
- **Major Features**: User-visible functionality additions
|
|
132
132
|
- **Improvements**: Performance, UX, architecture enhancements
|
|
@@ -134,6 +134,22 @@ To use this skill, simply say:
|
|
|
134
134
|
- **Changes**: Breaking changes or significant refactoring
|
|
135
135
|
- **Minor Details**: Small fixes, style changes, trivial updates
|
|
136
136
|
|
|
137
|
+
**⚠️ Critical: Do NOT over-merge commits on the same topic**
|
|
138
|
+
|
|
139
|
+
It is tempting to group multiple commits under one bullet because they share a theme (e.g., "all about memory"). Resist this — each commit with **independent user-facing value** deserves its own bullet.
|
|
140
|
+
|
|
141
|
+
Ask for every commit: *"Does this enable something the user couldn't do before, separate from other commits on this topic?"*
|
|
142
|
+
- YES → write a separate CHANGELOG bullet
|
|
143
|
+
- NO (pure refactor, stability fix, threshold tweak) → merge into a related bullet or put in "More"
|
|
144
|
+
|
|
145
|
+
**Example of the mistake to avoid:**
|
|
146
|
+
- `feat: add long-term memory update system` and `feat: skill template context and recall-memory meta injection` are both "about memory", but they describe distinct capabilities:
|
|
147
|
+
- First: agent writes memories after sessions
|
|
148
|
+
- Second: skills receive a pre-built index so agent can selectively load only relevant memories
|
|
149
|
+
- These must be two separate bullets, not one.
|
|
150
|
+
|
|
151
|
+
**Sanity check after writing:** Count your `### Added` bullets vs the number of `feat:` commits. If `feat` commits > bullets, you likely merged too aggressively — revisit.
|
|
152
|
+
|
|
137
153
|
4. **Write CHANGELOG Entries**
|
|
138
154
|
|
|
139
155
|
**Format for Significant Items:**
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.2] - 2026-03-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Skill count limits**: two-layer guard to keep context tokens bounded — at most 50 skills loaded from disk (`MAX_SKILLS`) and at most 30 injected into the system prompt (`MAX_CONTEXT_SKILLS`); excess skills are skipped and a warning is written to the file logger
|
|
14
|
+
|
|
15
|
+
### Improved
|
|
16
|
+
- Skill `agent` field is now self-declared in each `SKILL.md` instead of being listed in `profile.yml` — makes skill-to-profile assignment portable and removes the need to edit profile config when adding skills
|
|
17
|
+
- Slash command autocomplete in the web UI now filters by the active session's agent profile, so only relevant skills appear
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- CLI startup crash: `ui: nil` keyword argument now correctly passed to `Agent.new`
|
|
21
|
+
|
|
10
22
|
## [0.8.1] - 2026-03-09
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -14,14 +26,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
14
26
|
- **Skill autocomplete dropdown** in the web UI: type `/` in the chat input to see a filtered list of available skills
|
|
15
27
|
- **File-based logger** (`Clacky::Logger`): thread-safe structured logging to `~/.clacky/logs/` for debugging agent sessions
|
|
16
28
|
- **Session persistence on startup**: server now restores the most recent session for the working directory automatically on boot
|
|
17
|
-
- **Long-term memory update system**: agent automatically updates `~/.clacky/memories/` after sessions using a whitelist-driven approach
|
|
29
|
+
- **Long-term memory update system**: agent automatically updates `~/.clacky/memories/` after sessions using a whitelist-driven approach; memories persist across restarts and are injected into agent context on startup
|
|
30
|
+
- **recall-memory skill with smart meta injection**: the `recall-memory` skill now receives a pre-built index of all memory files (topic, description, last updated) so the agent can selectively load only relevant memories without reading every file
|
|
18
31
|
- **Compressed message archiving**: older messages are compressed and archived to chunk Markdown files to keep context window manageable
|
|
19
32
|
- **Network pre-flight check**: connection is verified before agent starts; helpful VPN/proxy suggestions shown on failure
|
|
20
33
|
- **Encrypted brand skills**: white-label brand skills can now be shipped as encrypted `.enc` files for privacy
|
|
21
34
|
|
|
22
35
|
### Improved
|
|
23
|
-
- Memory update logic tightened
|
|
24
|
-
- Memory update threshold raised and prompt made dynamic for more reliable triggering
|
|
36
|
+
- Memory update logic tightened: whitelist-driven approach, raised trigger threshold, and dynamic prompt — reduces false writes and improves reliability
|
|
25
37
|
- Slash commands in onboarding (`/create-task`, `/skill-add`) now use the pending-message pattern so they work correctly before WS connects
|
|
26
38
|
- Sidebar shows "No sessions yet" placeholder during onboarding
|
|
27
39
|
- Session delete is now optimistic — UI updates immediately without waiting for WS broadcast, and 404 ghost sessions are cleaned up automatically
|
|
@@ -31,8 +31,8 @@ module Clacky
|
|
|
31
31
|
# Check if user can invoke this skill
|
|
32
32
|
return nil unless skill.user_invocable?
|
|
33
33
|
|
|
34
|
-
# Check if this skill is allowed
|
|
35
|
-
return nil if @agent_profile &&
|
|
34
|
+
# Check if this skill is allowed for the current agent profile
|
|
35
|
+
return nil if @agent_profile && !skill.allowed_for_agent?(@agent_profile.name)
|
|
36
36
|
|
|
37
37
|
{ skill: skill, arguments: arguments }
|
|
38
38
|
else
|
|
@@ -64,6 +64,10 @@ module Clacky
|
|
|
64
64
|
expanded_content
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# Maximum number of skills injected into the system prompt.
|
|
68
|
+
# Keeps context tokens bounded regardless of how many skills are installed.
|
|
69
|
+
MAX_CONTEXT_SKILLS = 30
|
|
70
|
+
|
|
67
71
|
# Generate skill context - loads all auto-invocable skills allowed by the agent profile
|
|
68
72
|
# @return [String] Skill context to add to system prompt
|
|
69
73
|
def build_skill_context
|
|
@@ -72,6 +76,17 @@ module Clacky
|
|
|
72
76
|
all_skills = filter_skills_by_profile(all_skills)
|
|
73
77
|
auto_invocable = all_skills.select(&:model_invocation_allowed?)
|
|
74
78
|
|
|
79
|
+
# Enforce system prompt injection limit to control token usage
|
|
80
|
+
if auto_invocable.size > MAX_CONTEXT_SKILLS
|
|
81
|
+
dropped = auto_invocable.size - MAX_CONTEXT_SKILLS
|
|
82
|
+
Clacky::Logger.warn(
|
|
83
|
+
"Skill context limit: #{auto_invocable.size} auto-invocable skills found, " \
|
|
84
|
+
"only injecting first #{MAX_CONTEXT_SKILLS} (#{dropped} dropped). " \
|
|
85
|
+
"Remove unused skills to restore full visibility."
|
|
86
|
+
)
|
|
87
|
+
auto_invocable = auto_invocable.first(MAX_CONTEXT_SKILLS)
|
|
88
|
+
end
|
|
89
|
+
|
|
75
90
|
return "" if auto_invocable.empty?
|
|
76
91
|
|
|
77
92
|
plain_skills = auto_invocable.reject(&:encrypted?)
|
|
@@ -82,7 +97,7 @@ module Clacky
|
|
|
82
97
|
context += "=" * 80 + "\n\n"
|
|
83
98
|
context += "CRITICAL SKILL USAGE RULES:\n"
|
|
84
99
|
context += "- When user's request matches a skill description, you MUST use invoke_skill tool — invoke only the single BEST matching skill, do NOT call multiple skills for the same request\n"
|
|
85
|
-
context += "- Example: invoke_skill(skill_name: '
|
|
100
|
+
context += "- Example: invoke_skill(skill_name: 'xxx', task: 'xxx')\n"
|
|
86
101
|
context += "- SLASH COMMAND (HIGHEST PRIORITY): If user input starts with /skill_name, you MUST invoke_skill immediately as the first action with no exceptions.\n"
|
|
87
102
|
context += "\n"
|
|
88
103
|
context += "Available skills:\n\n"
|
|
@@ -115,14 +130,16 @@ module Clacky
|
|
|
115
130
|
|
|
116
131
|
private
|
|
117
132
|
|
|
118
|
-
# Filter skills by the agent profile's
|
|
119
|
-
#
|
|
133
|
+
# Filter skills by the agent profile name using the skill's own `agent:` field.
|
|
134
|
+
# Each skill declares which agents it supports via its frontmatter `agent:` field.
|
|
135
|
+
# If the skill has no `agent:` field (defaults to "all"), it is allowed everywhere.
|
|
136
|
+
# If no agent profile is set, all skills are allowed (backward-compatible).
|
|
120
137
|
# @param skills [Array<Skill>]
|
|
121
138
|
# @return [Array<Skill>]
|
|
122
139
|
def filter_skills_by_profile(skills)
|
|
123
140
|
return skills unless @agent_profile
|
|
124
141
|
|
|
125
|
-
skills.select { |skill|
|
|
142
|
+
skills.select { |skill| skill.allowed_for_agent?(@agent_profile.name) }
|
|
126
143
|
end
|
|
127
144
|
|
|
128
145
|
# Build template context for skill content expansion.
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -35,7 +35,7 @@ module Clacky
|
|
|
35
35
|
attr_reader :session_id, :messages, :iterations, :total_cost, :working_dir, :created_at, :total_tasks, :todos,
|
|
36
36
|
:cache_stats, :cost_source, :ui, :skill_loader, :agent_profile
|
|
37
37
|
|
|
38
|
-
def initialize(client, config
|
|
38
|
+
def initialize(client, config , working_dir: , ui: , profile: )
|
|
39
39
|
@client = client # Client for current model
|
|
40
40
|
@config = config.is_a?(AgentConfig) ? config : AgentConfig.new(config)
|
|
41
41
|
@agent_profile = AgentProfile.load(profile)
|
|
@@ -90,8 +90,9 @@ module Clacky
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
# Restore from a saved session
|
|
93
|
-
def self.from_session(client, config, session_data, ui: nil, profile:
|
|
94
|
-
|
|
93
|
+
def self.from_session(client, config, session_data, ui: nil, profile:)
|
|
94
|
+
working_dir = session_data[:working_dir] || session_data["working_dir"] || Dir.pwd
|
|
95
|
+
agent = new(client, config, working_dir: working_dir, ui: ui, profile: profile)
|
|
95
96
|
agent.restore_session(session_data)
|
|
96
97
|
agent
|
|
97
98
|
end
|
|
@@ -143,7 +144,7 @@ module Clacky
|
|
|
143
144
|
def run(user_input, images: [])
|
|
144
145
|
# Start new task for Time Machine
|
|
145
146
|
task_id = start_new_task
|
|
146
|
-
|
|
147
|
+
|
|
147
148
|
@start_time = Time.now
|
|
148
149
|
@task_cost_source = :estimated # Reset for new task
|
|
149
150
|
# Note: Do NOT reset @previous_total_tokens here - it should maintain the value from the last iteration
|
|
@@ -259,7 +260,7 @@ module Clacky
|
|
|
259
260
|
end
|
|
260
261
|
|
|
261
262
|
result = build_result(:success)
|
|
262
|
-
|
|
263
|
+
|
|
263
264
|
# Save snapshots of modified files for Time Machine
|
|
264
265
|
if @modified_files_in_task && !@modified_files_in_task.empty?
|
|
265
266
|
save_modified_files_snapshot(@modified_files_in_task)
|
|
@@ -488,16 +489,16 @@ module Clacky
|
|
|
488
489
|
progress_shown = false
|
|
489
490
|
progress_timer = nil
|
|
490
491
|
output_buffer = nil
|
|
491
|
-
|
|
492
|
+
|
|
492
493
|
if @ui
|
|
493
494
|
progress_message = build_tool_progress_message(call[:name], args)
|
|
494
|
-
|
|
495
|
+
|
|
495
496
|
# For shell commands, create shared output buffer
|
|
496
497
|
if call[:name] == "shell" || call[:name] == "safe_shell"
|
|
497
498
|
output_buffer = { content: "", timestamp: Time.now }
|
|
498
499
|
args[:output_buffer] = output_buffer
|
|
499
500
|
end
|
|
500
|
-
|
|
501
|
+
|
|
501
502
|
progress_timer = Thread.new do
|
|
502
503
|
sleep 2
|
|
503
504
|
@ui.show_progress(progress_message, prefix_newline: false, output_buffer: output_buffer)
|
|
@@ -700,12 +701,13 @@ module Clacky
|
|
|
700
701
|
anthropic_format: subagent_config.anthropic_format?
|
|
701
702
|
)
|
|
702
703
|
|
|
703
|
-
# Create subagent (reuses all tools from parent)
|
|
704
|
+
# Create subagent (reuses all tools from parent, inherits agent profile from parent)
|
|
704
705
|
subagent = self.class.new(
|
|
705
706
|
subagent_client,
|
|
706
707
|
subagent_config,
|
|
707
708
|
working_dir: @working_dir,
|
|
708
|
-
ui: @ui
|
|
709
|
+
ui: @ui,
|
|
710
|
+
profile: @agent_profile.name
|
|
709
711
|
)
|
|
710
712
|
|
|
711
713
|
# Deep clone messages to avoid cross-contamination
|
|
@@ -829,7 +831,7 @@ module Clacky
|
|
|
829
831
|
# @param args [Hash] Arguments passed to the tool
|
|
830
832
|
def track_modified_files(tool_name, args)
|
|
831
833
|
@modified_files_in_task ||= []
|
|
832
|
-
|
|
834
|
+
|
|
833
835
|
case tool_name
|
|
834
836
|
when "write", "edit"
|
|
835
837
|
file_path = args[:path]
|
data/lib/clacky/agent_profile.rb
CHANGED
|
@@ -21,13 +21,12 @@ module Clacky
|
|
|
21
21
|
DEFAULT_AGENTS_DIR = File.expand_path("../default_agents", __FILE__).freeze
|
|
22
22
|
USER_AGENTS_DIR = File.expand_path("~/.clacky/agents").freeze
|
|
23
23
|
|
|
24
|
-
attr_reader :name, :description
|
|
24
|
+
attr_reader :name, :description
|
|
25
25
|
|
|
26
26
|
def initialize(name)
|
|
27
27
|
@name = name.to_s
|
|
28
28
|
profile_data = load_profile_yml
|
|
29
29
|
@description = profile_data["description"] || ""
|
|
30
|
-
@skills = Array(profile_data["skills"])
|
|
31
30
|
@system_prompt_content = load_agent_file("system_prompt.md")
|
|
32
31
|
end
|
|
33
32
|
|
|
@@ -58,11 +57,6 @@ module Clacky
|
|
|
58
57
|
load_global_file("USER.md")
|
|
59
58
|
end
|
|
60
59
|
|
|
61
|
-
# @return [Boolean] whether a given skill name is allowed for this profile
|
|
62
|
-
def skill_allowed?(skill_name)
|
|
63
|
-
@skills.include?(skill_name.to_s)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
60
|
private def load_profile_yml
|
|
67
61
|
path = find_agent_file("profile.yml")
|
|
68
62
|
raise ArgumentError, "Agent profile '#{@name}' not found. " \
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -102,7 +102,7 @@ module Clacky
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
# Create new agent if no session loaded
|
|
105
|
-
agent ||= Clacky::Agent.new(client, agent_config, working_dir: working_dir, profile: agent_profile)
|
|
105
|
+
agent ||= Clacky::Agent.new(client, agent_config, working_dir: working_dir, ui: nil, profile: agent_profile)
|
|
106
106
|
|
|
107
107
|
# Change to working directory
|
|
108
108
|
original_dir = Dir.pwd
|
|
@@ -368,7 +368,7 @@ module Clacky
|
|
|
368
368
|
say ""
|
|
369
369
|
end
|
|
370
370
|
|
|
371
|
-
def load_latest_session(client, agent_config, session_manager, working_dir, profile:
|
|
371
|
+
def load_latest_session(client, agent_config, session_manager, working_dir, profile:)
|
|
372
372
|
session_data = session_manager.latest_for_directory(working_dir)
|
|
373
373
|
|
|
374
374
|
if session_data.nil?
|
|
@@ -380,7 +380,7 @@ module Clacky
|
|
|
380
380
|
Clacky::Agent.from_session(client, agent_config, session_data, profile: profile)
|
|
381
381
|
end
|
|
382
382
|
|
|
383
|
-
def load_session_by_number(client, agent_config, session_manager, working_dir, identifier, profile:
|
|
383
|
+
def load_session_by_number(client, agent_config, session_manager, working_dir, identifier, profile:)
|
|
384
384
|
# Get a larger list to search through (for ID prefix matching)
|
|
385
385
|
sessions = session_manager.list(current_dir: working_dir, limit: 100)
|
|
386
386
|
|
|
@@ -480,7 +480,7 @@ module Clacky
|
|
|
480
480
|
# {"type":"confirmation","id":"conf_1","result":"yes"} — answer to request_confirmation
|
|
481
481
|
#
|
|
482
482
|
# If a bare string line is received it is treated as a message content.
|
|
483
|
-
def run_agent_with_json(agent, working_dir, agent_config, session_manager, client, profile:
|
|
483
|
+
def run_agent_with_json(agent, working_dir, agent_config, session_manager, client, profile:)
|
|
484
484
|
json_ui = Clacky::JsonUIController.new
|
|
485
485
|
agent.instance_variable_set(:@ui, json_ui)
|
|
486
486
|
|
|
@@ -514,7 +514,7 @@ module Clacky
|
|
|
514
514
|
when "/exit", "/quit"
|
|
515
515
|
break
|
|
516
516
|
when "/clear"
|
|
517
|
-
agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir, profile: profile)
|
|
517
|
+
agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir, ui: nil, profile: profile)
|
|
518
518
|
agent.instance_variable_set(:@ui, json_ui)
|
|
519
519
|
json_ui.emit("info", message: "Session cleared. Starting fresh.")
|
|
520
520
|
next
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-explorer
|
|
3
3
|
description: Use this skill when exploring, analyzing, or understanding project/code structure. Required for tasks like "analyze project", "explore codebase", "understand how X works".
|
|
4
|
+
agent: coding
|
|
4
5
|
fork_agent: true
|
|
5
6
|
forbidden_tools:
|
|
6
7
|
- write
|
|
@@ -105,7 +105,7 @@ module Clacky
|
|
|
105
105
|
session_registry: @registry,
|
|
106
106
|
session_builder: method(:build_session)
|
|
107
107
|
)
|
|
108
|
-
@skill_loader = Clacky::SkillLoader.new
|
|
108
|
+
@skill_loader = Clacky::SkillLoader.new(nil, brand_config: Clacky::BrandConfig.load)
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
def start
|
|
@@ -210,7 +210,10 @@ module Clacky
|
|
|
210
210
|
when ["GET", "/api/brand/skills"] then api_brand_skills(res)
|
|
211
211
|
when ["GET", "/api/brand"] then api_brand_info(res)
|
|
212
212
|
else
|
|
213
|
-
if method == "GET" && path.match?(%r{^/api/sessions/[^/]+/
|
|
213
|
+
if method == "GET" && path.match?(%r{^/api/sessions/[^/]+/skills$})
|
|
214
|
+
session_id = path.sub("/api/sessions/", "").sub("/skills", "")
|
|
215
|
+
api_session_skills(session_id, res)
|
|
216
|
+
elsif method == "GET" && path.match?(%r{^/api/sessions/[^/]+/messages$})
|
|
214
217
|
session_id = path.sub("/api/sessions/", "").sub("/messages", "")
|
|
215
218
|
api_session_messages(session_id, req, res)
|
|
216
219
|
elsif method == "DELETE" && path.start_with?("/api/sessions/")
|
|
@@ -380,6 +383,9 @@ module Clacky
|
|
|
380
383
|
result = @brand_test ? brand.activate_mock!(key) : brand.activate!(key)
|
|
381
384
|
|
|
382
385
|
if result[:success]
|
|
386
|
+
# Refresh skill_loader with the now-activated brand config so brand
|
|
387
|
+
# skills are loadable from this point forward (e.g. after sync).
|
|
388
|
+
@skill_loader = Clacky::SkillLoader.new(nil, brand_config: brand)
|
|
383
389
|
json_response(res, 200, { ok: true, brand_name: result[:brand_name] || brand.brand_name })
|
|
384
390
|
else
|
|
385
391
|
json_response(res, 422, { ok: false, error: result[:message] })
|
|
@@ -446,8 +452,9 @@ module Clacky
|
|
|
446
452
|
result = @brand_test ? brand.install_mock_brand_skill!(skill_info) : brand.install_brand_skill!(skill_info)
|
|
447
453
|
|
|
448
454
|
if result[:success]
|
|
449
|
-
# Reload skills so the Agent can pick up the new skill immediately
|
|
450
|
-
|
|
455
|
+
# Reload skills so the Agent can pick up the new skill immediately.
|
|
456
|
+
# Re-create the loader with the current brand_config so brand skills are decryptable.
|
|
457
|
+
@skill_loader = Clacky::SkillLoader.new(nil, brand_config: brand)
|
|
451
458
|
json_response(res, 200, { ok: true, slug: result[:slug], version: result[:version] })
|
|
452
459
|
else
|
|
453
460
|
json_response(res, 422, { ok: false, error: result[:error] })
|
|
@@ -655,6 +662,38 @@ module Clacky
|
|
|
655
662
|
json_response(res, 200, { skills: skills })
|
|
656
663
|
end
|
|
657
664
|
|
|
665
|
+
# GET /api/sessions/:id/skills — list user-invocable skills for a session,
|
|
666
|
+
# filtered by the session's agent profile. Used by the frontend slash-command
|
|
667
|
+
# autocomplete so only skills valid for the current profile are suggested.
|
|
668
|
+
def api_session_skills(session_id, res)
|
|
669
|
+
session = @registry.get(session_id)
|
|
670
|
+
unless session
|
|
671
|
+
json_response(res, 404, { error: "Session not found" })
|
|
672
|
+
return
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
agent = session[:agent]
|
|
676
|
+
unless agent
|
|
677
|
+
json_response(res, 404, { error: "Agent not found" })
|
|
678
|
+
return
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
agent.skill_loader.load_all
|
|
682
|
+
profile = agent.agent_profile
|
|
683
|
+
|
|
684
|
+
skills = agent.skill_loader.user_invocable_skills
|
|
685
|
+
skills = skills.select { |s| s.allowed_for_agent?(profile.name) } if profile
|
|
686
|
+
|
|
687
|
+
skill_data = skills.map do |skill|
|
|
688
|
+
{
|
|
689
|
+
name: skill.identifier,
|
|
690
|
+
description: skill.description || skill.context_description
|
|
691
|
+
}
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
json_response(res, 200, { skills: skill_data })
|
|
695
|
+
end
|
|
696
|
+
|
|
658
697
|
# PATCH /api/skills/:name/toggle — enable or disable a skill
|
|
659
698
|
# Body: { enabled: true/false }
|
|
660
699
|
def api_toggle_skill(name, req, res)
|
|
@@ -1078,11 +1117,9 @@ module Clacky
|
|
|
1078
1117
|
client = @client_factory.call
|
|
1079
1118
|
config = @agent_config.dup
|
|
1080
1119
|
config.permission_mode = permission_mode
|
|
1081
|
-
agent = Clacky::Agent.new(client, config, working_dir: working_dir, profile: profile)
|
|
1082
|
-
|
|
1083
1120
|
broadcaster = method(:broadcast)
|
|
1084
1121
|
ui = WebUIController.new(session_id, broadcaster)
|
|
1085
|
-
agent.
|
|
1122
|
+
agent = Clacky::Agent.new(client, config, working_dir: working_dir, ui: ui, profile: profile)
|
|
1086
1123
|
|
|
1087
1124
|
@registry.with_session(session_id) do |s|
|
|
1088
1125
|
s[:agent] = agent
|
|
@@ -1106,11 +1143,9 @@ module Clacky
|
|
|
1106
1143
|
|
|
1107
1144
|
client = @client_factory.call
|
|
1108
1145
|
config = @agent_config.dup
|
|
1109
|
-
agent = Clacky::Agent.from_session(client, config, session_data, profile: "general")
|
|
1110
|
-
|
|
1111
1146
|
broadcaster = method(:broadcast)
|
|
1112
1147
|
ui = WebUIController.new(session_id, broadcaster)
|
|
1113
|
-
agent.
|
|
1148
|
+
agent = Clacky::Agent.from_session(client, config, session_data, ui: ui, profile: "general")
|
|
1114
1149
|
|
|
1115
1150
|
@registry.with_session(session_id) do |s|
|
|
1116
1151
|
s[:agent] = agent
|
data/lib/clacky/skill.rb
CHANGED
|
@@ -95,6 +95,30 @@ module Clacky
|
|
|
95
95
|
@auto_summarize != false
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
# Get the agent scope for this skill.
|
|
99
|
+
# Parsed from the `agent:` frontmatter field.
|
|
100
|
+
# Returns an array of agent names, or ["all"] if not specified.
|
|
101
|
+
# @return [Array<String>]
|
|
102
|
+
def agents_scope
|
|
103
|
+
return ["all"] if @agent_type.nil?
|
|
104
|
+
|
|
105
|
+
case @agent_type
|
|
106
|
+
when "all" then ["all"]
|
|
107
|
+
when Array then @agent_type.map(&:to_s)
|
|
108
|
+
else [@agent_type.to_s]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Check if this skill is allowed for the given agent profile name.
|
|
113
|
+
# Returns true when the skill's `agent:` field is "all" (default) or
|
|
114
|
+
# includes the given profile name.
|
|
115
|
+
# @param profile_name [String] e.g. "coding", "general"
|
|
116
|
+
# @return [Boolean]
|
|
117
|
+
def allowed_for_agent?(profile_name)
|
|
118
|
+
scope = agents_scope
|
|
119
|
+
scope.include?("all") || scope.include?(profile_name.to_s)
|
|
120
|
+
end
|
|
121
|
+
|
|
98
122
|
# Get the slash command for this skill
|
|
99
123
|
# @return [String] e.g., "/explain-code"
|
|
100
124
|
def slash_command
|
data/lib/clacky/skill_loader.rb
CHANGED
|
@@ -18,6 +18,11 @@ module Clacky
|
|
|
18
18
|
:brand # ~/.clacky/brand_skills/ (encrypted, license-gated)
|
|
19
19
|
].freeze
|
|
20
20
|
|
|
21
|
+
# Maximum number of skills that can be loaded in total.
|
|
22
|
+
# When exceeded, a warning is recorded in @errors and extra skills are dropped.
|
|
23
|
+
# This prevents runaway memory usage and excessively long system prompts.
|
|
24
|
+
MAX_SKILLS = 50
|
|
25
|
+
|
|
21
26
|
# Initialize the skill loader and automatically load all skills
|
|
22
27
|
# @param working_dir [String] Current working directory for project-level discovery
|
|
23
28
|
# @param brand_config [Clacky::BrandConfig, nil] Optional brand config used to
|
|
@@ -334,6 +339,14 @@ module Clacky
|
|
|
334
339
|
end
|
|
335
340
|
end
|
|
336
341
|
|
|
342
|
+
# Enforce skill count limit
|
|
343
|
+
if @skills.size >= MAX_SKILLS
|
|
344
|
+
msg = "Skill limit reached (max #{MAX_SKILLS}): skipping brand skill '#{skill.identifier}' from #{skill_dir}"
|
|
345
|
+
@errors << msg
|
|
346
|
+
Clacky::Logger.warn(msg)
|
|
347
|
+
return nil
|
|
348
|
+
end
|
|
349
|
+
|
|
337
350
|
@skills[skill.identifier] = skill
|
|
338
351
|
@skills_by_command[skill.slash_command] = skill
|
|
339
352
|
@loaded_from[skill.identifier] = :brand
|
|
@@ -368,6 +381,14 @@ module Clacky
|
|
|
368
381
|
end
|
|
369
382
|
end
|
|
370
383
|
|
|
384
|
+
# Enforce skill count limit
|
|
385
|
+
if @skills.size >= MAX_SKILLS
|
|
386
|
+
msg = "Skill limit reached (max #{MAX_SKILLS}): skipping '#{skill.identifier}' from #{skill_dir}"
|
|
387
|
+
@errors << msg
|
|
388
|
+
Clacky::Logger.warn(msg)
|
|
389
|
+
return nil
|
|
390
|
+
end
|
|
391
|
+
|
|
371
392
|
# Register skill
|
|
372
393
|
@skills[skill.identifier] = skill
|
|
373
394
|
@skills_by_command[skill.slash_command] = skill
|
|
@@ -46,7 +46,7 @@ module Clacky
|
|
|
46
46
|
return unless skill_loader
|
|
47
47
|
|
|
48
48
|
skills = skill_loader.user_invocable_skills
|
|
49
|
-
skills = skills.select { |s|
|
|
49
|
+
skills = skills.select { |s| s.allowed_for_agent?(agent_profile.name) } if agent_profile
|
|
50
50
|
|
|
51
51
|
@skill_commands = skills.map do |skill|
|
|
52
52
|
{
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.js
CHANGED
|
@@ -136,6 +136,9 @@ const Router = (() => {
|
|
|
136
136
|
Sessions.renderList();
|
|
137
137
|
$("user-input").focus();
|
|
138
138
|
|
|
139
|
+
// Load session-scoped skill list (filtered by agent profile) for slash autocomplete
|
|
140
|
+
SkillAC.loadForSession(id);
|
|
141
|
+
|
|
139
142
|
// Always reload history on every switch (cache is not used)
|
|
140
143
|
Sessions.loadHistory(id);
|
|
141
144
|
break;
|
|
@@ -553,9 +556,24 @@ $("user-input").addEventListener("blur", () => {
|
|
|
553
556
|
|
|
554
557
|
// ── Skill autocomplete ────────────────────────────────────────────────────
|
|
555
558
|
const SkillAC = (() => {
|
|
556
|
-
let _visible
|
|
557
|
-
let _activeIndex
|
|
558
|
-
let _items
|
|
559
|
+
let _visible = false;
|
|
560
|
+
let _activeIndex = -1;
|
|
561
|
+
let _items = []; // filtered [{ name, description }]
|
|
562
|
+
let _sessionSkills = []; // skills allowed for the active session's profile
|
|
563
|
+
|
|
564
|
+
/** Fetch session-specific skill list from the server and cache it.
|
|
565
|
+
* Called whenever the active session changes. */
|
|
566
|
+
async function _loadForSession(sessionId) {
|
|
567
|
+
if (!sessionId) { _sessionSkills = []; return; }
|
|
568
|
+
try {
|
|
569
|
+
const res = await fetch(`/api/sessions/${sessionId}/skills`);
|
|
570
|
+
const data = await res.json();
|
|
571
|
+
_sessionSkills = data.skills || [];
|
|
572
|
+
} catch (e) {
|
|
573
|
+
console.error("[SkillAC] loadForSession failed", e);
|
|
574
|
+
_sessionSkills = [];
|
|
575
|
+
}
|
|
576
|
+
}
|
|
559
577
|
|
|
560
578
|
/** Return the /xxx prefix if the entire input is a slash command, else null. */
|
|
561
579
|
function _getSlashQuery(value) {
|
|
@@ -568,7 +586,8 @@ const SkillAC = (() => {
|
|
|
568
586
|
}
|
|
569
587
|
|
|
570
588
|
function _render(query) {
|
|
571
|
-
|
|
589
|
+
// Use session-scoped skill list when available; fall back to global Skills list
|
|
590
|
+
const all = _sessionSkills.length > 0 ? _sessionSkills : Skills.all;
|
|
572
591
|
_items = all.filter(s => s.name.toLowerCase().startsWith(query));
|
|
573
592
|
|
|
574
593
|
if (_items.length === 0) { _hide(); return; }
|
|
@@ -665,6 +684,9 @@ const SkillAC = (() => {
|
|
|
665
684
|
/** Open dropdown with all skills (triggered by / button). */
|
|
666
685
|
openAll: _openAll,
|
|
667
686
|
|
|
687
|
+
/** Reload session-scoped skill list when the active session changes. */
|
|
688
|
+
loadForSession: _loadForSession,
|
|
689
|
+
|
|
668
690
|
/** Handle keyboard nav inside the dropdown. Returns true if event was consumed. */
|
|
669
691
|
handleKey(e) {
|
|
670
692
|
if (!_visible) return false;
|