kollabor 0.4.9__py3-none-any.whl → 0.4.15__py3-none-any.whl
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.
- agents/__init__.py +2 -0
- agents/coder/__init__.py +0 -0
- agents/coder/agent.json +4 -0
- agents/coder/api-integration.md +2150 -0
- agents/coder/cli-pretty.md +765 -0
- agents/coder/code-review.md +1092 -0
- agents/coder/database-design.md +1525 -0
- agents/coder/debugging.md +1102 -0
- agents/coder/dependency-management.md +1397 -0
- agents/coder/git-workflow.md +1099 -0
- agents/coder/refactoring.md +1454 -0
- agents/coder/security-hardening.md +1732 -0
- agents/coder/system_prompt.md +1448 -0
- agents/coder/tdd.md +1367 -0
- agents/creative-writer/__init__.py +0 -0
- agents/creative-writer/agent.json +4 -0
- agents/creative-writer/character-development.md +1852 -0
- agents/creative-writer/dialogue-craft.md +1122 -0
- agents/creative-writer/plot-structure.md +1073 -0
- agents/creative-writer/revision-editing.md +1484 -0
- agents/creative-writer/system_prompt.md +690 -0
- agents/creative-writer/worldbuilding.md +2049 -0
- agents/data-analyst/__init__.py +30 -0
- agents/data-analyst/agent.json +4 -0
- agents/data-analyst/data-visualization.md +992 -0
- agents/data-analyst/exploratory-data-analysis.md +1110 -0
- agents/data-analyst/pandas-data-manipulation.md +1081 -0
- agents/data-analyst/sql-query-optimization.md +881 -0
- agents/data-analyst/statistical-analysis.md +1118 -0
- agents/data-analyst/system_prompt.md +928 -0
- agents/default/__init__.py +0 -0
- agents/default/agent.json +4 -0
- agents/default/dead-code.md +794 -0
- agents/default/explore-agent-system.md +585 -0
- agents/default/system_prompt.md +1448 -0
- agents/kollabor/__init__.py +0 -0
- agents/kollabor/analyze-plugin-lifecycle.md +175 -0
- agents/kollabor/analyze-terminal-rendering.md +388 -0
- agents/kollabor/code-review.md +1092 -0
- agents/kollabor/debug-mcp-integration.md +521 -0
- agents/kollabor/debug-plugin-hooks.md +547 -0
- agents/kollabor/debugging.md +1102 -0
- agents/kollabor/dependency-management.md +1397 -0
- agents/kollabor/git-workflow.md +1099 -0
- agents/kollabor/inspect-llm-conversation.md +148 -0
- agents/kollabor/monitor-event-bus.md +558 -0
- agents/kollabor/profile-performance.md +576 -0
- agents/kollabor/refactoring.md +1454 -0
- agents/kollabor/system_prompt copy.md +1448 -0
- agents/kollabor/system_prompt.md +757 -0
- agents/kollabor/trace-command-execution.md +178 -0
- agents/kollabor/validate-config.md +879 -0
- agents/research/__init__.py +0 -0
- agents/research/agent.json +4 -0
- agents/research/architecture-mapping.md +1099 -0
- agents/research/codebase-analysis.md +1077 -0
- agents/research/dependency-audit.md +1027 -0
- agents/research/performance-profiling.md +1047 -0
- agents/research/security-review.md +1359 -0
- agents/research/system_prompt.md +492 -0
- agents/technical-writer/__init__.py +0 -0
- agents/technical-writer/agent.json +4 -0
- agents/technical-writer/api-documentation.md +2328 -0
- agents/technical-writer/changelog-management.md +1181 -0
- agents/technical-writer/readme-writing.md +1360 -0
- agents/technical-writer/style-guide.md +1410 -0
- agents/technical-writer/system_prompt.md +653 -0
- agents/technical-writer/tutorial-creation.md +1448 -0
- core/__init__.py +0 -2
- core/application.py +343 -88
- core/cli.py +229 -10
- core/commands/menu_renderer.py +463 -59
- core/commands/registry.py +14 -9
- core/commands/system_commands.py +2461 -14
- core/config/loader.py +151 -37
- core/config/service.py +18 -6
- core/events/bus.py +29 -9
- core/events/executor.py +205 -75
- core/events/models.py +27 -8
- core/fullscreen/command_integration.py +20 -24
- core/fullscreen/components/__init__.py +10 -1
- core/fullscreen/components/matrix_components.py +1 -2
- core/fullscreen/components/space_shooter_components.py +654 -0
- core/fullscreen/plugin.py +5 -0
- core/fullscreen/renderer.py +52 -13
- core/fullscreen/session.py +52 -15
- core/io/__init__.py +29 -5
- core/io/buffer_manager.py +6 -1
- core/io/config_status_view.py +7 -29
- core/io/core_status_views.py +267 -347
- core/io/input/__init__.py +25 -0
- core/io/input/command_mode_handler.py +711 -0
- core/io/input/display_controller.py +128 -0
- core/io/input/hook_registrar.py +286 -0
- core/io/input/input_loop_manager.py +421 -0
- core/io/input/key_press_handler.py +502 -0
- core/io/input/modal_controller.py +1011 -0
- core/io/input/paste_processor.py +339 -0
- core/io/input/status_modal_renderer.py +184 -0
- core/io/input_errors.py +5 -1
- core/io/input_handler.py +211 -2452
- core/io/key_parser.py +7 -0
- core/io/layout.py +15 -3
- core/io/message_coordinator.py +111 -2
- core/io/message_renderer.py +129 -4
- core/io/status_renderer.py +147 -607
- core/io/terminal_renderer.py +97 -51
- core/io/terminal_state.py +21 -4
- core/io/visual_effects.py +816 -165
- core/llm/agent_manager.py +1063 -0
- core/llm/api_adapters/__init__.py +44 -0
- core/llm/api_adapters/anthropic_adapter.py +432 -0
- core/llm/api_adapters/base.py +241 -0
- core/llm/api_adapters/openai_adapter.py +326 -0
- core/llm/api_communication_service.py +167 -113
- core/llm/conversation_logger.py +322 -16
- core/llm/conversation_manager.py +556 -30
- core/llm/file_operations_executor.py +84 -32
- core/llm/llm_service.py +934 -103
- core/llm/mcp_integration.py +541 -57
- core/llm/message_display_service.py +135 -18
- core/llm/plugin_sdk.py +1 -2
- core/llm/profile_manager.py +1183 -0
- core/llm/response_parser.py +274 -56
- core/llm/response_processor.py +16 -3
- core/llm/tool_executor.py +6 -1
- core/logging/__init__.py +2 -0
- core/logging/setup.py +34 -6
- core/models/resume.py +54 -0
- core/plugins/__init__.py +4 -2
- core/plugins/base.py +127 -0
- core/plugins/collector.py +23 -161
- core/plugins/discovery.py +37 -3
- core/plugins/factory.py +6 -12
- core/plugins/registry.py +5 -17
- core/ui/config_widgets.py +128 -28
- core/ui/live_modal_renderer.py +2 -1
- core/ui/modal_actions.py +5 -0
- core/ui/modal_overlay_renderer.py +0 -60
- core/ui/modal_renderer.py +268 -7
- core/ui/modal_state_manager.py +29 -4
- core/ui/widgets/base_widget.py +7 -0
- core/updates/__init__.py +10 -0
- core/updates/version_check_service.py +348 -0
- core/updates/version_comparator.py +103 -0
- core/utils/config_utils.py +685 -526
- core/utils/plugin_utils.py +1 -1
- core/utils/session_naming.py +111 -0
- fonts/LICENSE +21 -0
- fonts/README.md +46 -0
- fonts/SymbolsNerdFont-Regular.ttf +0 -0
- fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
- fonts/__init__.py +44 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
- kollabor-0.4.15.dist-info/RECORD +228 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
- plugins/agent_orchestrator/__init__.py +39 -0
- plugins/agent_orchestrator/activity_monitor.py +181 -0
- plugins/agent_orchestrator/file_attacher.py +77 -0
- plugins/agent_orchestrator/message_injector.py +135 -0
- plugins/agent_orchestrator/models.py +48 -0
- plugins/agent_orchestrator/orchestrator.py +403 -0
- plugins/agent_orchestrator/plugin.py +976 -0
- plugins/agent_orchestrator/xml_parser.py +191 -0
- plugins/agent_orchestrator_plugin.py +9 -0
- plugins/enhanced_input/box_styles.py +1 -0
- plugins/enhanced_input/color_engine.py +19 -4
- plugins/enhanced_input/config.py +2 -2
- plugins/enhanced_input_plugin.py +61 -11
- plugins/fullscreen/__init__.py +6 -2
- plugins/fullscreen/example_plugin.py +1035 -222
- plugins/fullscreen/setup_wizard_plugin.py +592 -0
- plugins/fullscreen/space_shooter_plugin.py +131 -0
- plugins/hook_monitoring_plugin.py +436 -78
- plugins/query_enhancer_plugin.py +66 -30
- plugins/resume_conversation_plugin.py +1494 -0
- plugins/save_conversation_plugin.py +98 -32
- plugins/system_commands_plugin.py +70 -56
- plugins/tmux_plugin.py +154 -78
- plugins/workflow_enforcement_plugin.py +94 -92
- system_prompt/default.md +952 -886
- core/io/input_mode_manager.py +0 -402
- core/io/modal_interaction_handler.py +0 -315
- core/io/raw_input_processor.py +0 -946
- core/storage/__init__.py +0 -5
- core/storage/state_manager.py +0 -84
- core/ui/widget_integration.py +0 -222
- core/utils/key_reader.py +0 -171
- kollabor-0.4.9.dist-info/RECORD +0 -128
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1063 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent and Skill Manager.
|
|
3
|
+
|
|
4
|
+
Manages agents defined in .kollabor-cli/agents/ directories:
|
|
5
|
+
- Each agent has a system_prompt.md and optional skill files
|
|
6
|
+
- Skills are loaded dynamically and appended to system prompt
|
|
7
|
+
- Supports both local (project) and global (user) agent directories
|
|
8
|
+
|
|
9
|
+
Directory structure:
|
|
10
|
+
.kollabor-cli/agents/
|
|
11
|
+
default/
|
|
12
|
+
system_prompt.md
|
|
13
|
+
lint-editor/
|
|
14
|
+
system_prompt.md
|
|
15
|
+
agent.json # Optional config
|
|
16
|
+
create-tasks.md # Skill file
|
|
17
|
+
fix-file.md # Another skill
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Dict, List, Optional
|
|
25
|
+
|
|
26
|
+
from core.utils.config_utils import get_global_agents_dir, get_local_agents_dir, get_local_agents_path
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Skill:
|
|
33
|
+
"""
|
|
34
|
+
A skill that can be loaded into an agent's context.
|
|
35
|
+
|
|
36
|
+
Skills are markdown files containing instructions or context
|
|
37
|
+
that can be dynamically loaded during a session.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
name: Skill identifier (filename without extension)
|
|
41
|
+
content: Full content of the skill file
|
|
42
|
+
file_path: Path to the skill file
|
|
43
|
+
description: Optional description extracted from file header
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
content: str
|
|
48
|
+
file_path: Path
|
|
49
|
+
description: str = ""
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_file(cls, file_path: Path) -> Optional["Skill"]:
|
|
53
|
+
"""
|
|
54
|
+
Load skill from a markdown file.
|
|
55
|
+
|
|
56
|
+
Extracts description from HTML comment at start of file:
|
|
57
|
+
<!-- Description text here -->
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
file_path: Path to the .md file
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Skill instance or None on error
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
content = file_path.read_text(encoding="utf-8")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Failed to read skill file {file_path}: {e}")
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
# Extract description from HTML comment at start
|
|
72
|
+
description = ""
|
|
73
|
+
lines = content.split("\n")
|
|
74
|
+
if lines and lines[0].strip().startswith("<!--"):
|
|
75
|
+
comment_lines = []
|
|
76
|
+
for line in lines:
|
|
77
|
+
comment_lines.append(line)
|
|
78
|
+
if "-->" in line:
|
|
79
|
+
break
|
|
80
|
+
comment_text = "\n".join(comment_lines)
|
|
81
|
+
description = (
|
|
82
|
+
comment_text.replace("<!--", "")
|
|
83
|
+
.replace("-->", "")
|
|
84
|
+
.strip()
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return cls(
|
|
88
|
+
name=file_path.stem,
|
|
89
|
+
content=content,
|
|
90
|
+
file_path=file_path,
|
|
91
|
+
description=description,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
95
|
+
"""Convert skill to dictionary representation."""
|
|
96
|
+
return {
|
|
97
|
+
"name": self.name,
|
|
98
|
+
"description": self.description,
|
|
99
|
+
"file_path": str(self.file_path),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class Agent:
|
|
105
|
+
"""
|
|
106
|
+
An agent configuration with system prompt and available skills.
|
|
107
|
+
|
|
108
|
+
Agents are loaded from directories containing:
|
|
109
|
+
- system_prompt.md (required)
|
|
110
|
+
- agent.json (optional config)
|
|
111
|
+
- *.md files (skills)
|
|
112
|
+
|
|
113
|
+
Attributes:
|
|
114
|
+
name: Agent identifier (directory name)
|
|
115
|
+
directory: Path to agent directory
|
|
116
|
+
system_prompt: Base system prompt content
|
|
117
|
+
skills: Available skills (name -> Skill)
|
|
118
|
+
active_skills: Currently loaded skill names
|
|
119
|
+
profile: Optional preferred LLM profile
|
|
120
|
+
description: Human-readable description
|
|
121
|
+
default_skills: Skills to auto-load when agent is activated
|
|
122
|
+
source: 'local' or 'global' - where the agent was loaded from
|
|
123
|
+
overrides_global: True if local agent overrides a global agent with same name
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
name: str
|
|
127
|
+
directory: Path
|
|
128
|
+
system_prompt: str
|
|
129
|
+
skills: Dict[str, Skill] = field(default_factory=dict)
|
|
130
|
+
active_skills: List[str] = field(default_factory=list)
|
|
131
|
+
profile: Optional[str] = None
|
|
132
|
+
description: str = ""
|
|
133
|
+
default_skills: List[str] = field(default_factory=list)
|
|
134
|
+
source: str = "global"
|
|
135
|
+
overrides_global: bool = False
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_directory(
|
|
139
|
+
cls,
|
|
140
|
+
agent_dir: Path,
|
|
141
|
+
source: str = "global",
|
|
142
|
+
overrides_global: bool = False,
|
|
143
|
+
) -> Optional["Agent"]:
|
|
144
|
+
"""
|
|
145
|
+
Load agent from a directory.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
agent_dir: Path to agent directory
|
|
149
|
+
source: 'local' or 'global' - where the agent was loaded from
|
|
150
|
+
overrides_global: True if local agent overrides a global agent
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Agent instance or None if invalid
|
|
154
|
+
"""
|
|
155
|
+
if not agent_dir.is_dir():
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
# Load system prompt (required)
|
|
159
|
+
system_prompt_file = agent_dir / "system_prompt.md"
|
|
160
|
+
if not system_prompt_file.exists():
|
|
161
|
+
logger.warning(f"Agent {agent_dir.name} missing system_prompt.md")
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
system_prompt = system_prompt_file.read_text(encoding="utf-8")
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.error(f"Failed to read system prompt for {agent_dir.name}: {e}")
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
# Load skills (all .md files except system_prompt.md)
|
|
171
|
+
skills: Dict[str, Skill] = {}
|
|
172
|
+
for md_file in agent_dir.glob("*.md"):
|
|
173
|
+
if md_file.name != "system_prompt.md":
|
|
174
|
+
skill = Skill.from_file(md_file)
|
|
175
|
+
if skill:
|
|
176
|
+
skills[skill.name] = skill
|
|
177
|
+
|
|
178
|
+
# Load optional config
|
|
179
|
+
profile = None
|
|
180
|
+
description = ""
|
|
181
|
+
default_skills: List[str] = []
|
|
182
|
+
config_file = agent_dir / "agent.json"
|
|
183
|
+
if config_file.exists():
|
|
184
|
+
try:
|
|
185
|
+
config = json.loads(config_file.read_text(encoding="utf-8"))
|
|
186
|
+
profile = config.get("profile")
|
|
187
|
+
description = config.get("description", "")
|
|
188
|
+
default_skills = config.get("default_skills", [])
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.warning(f"Failed to load agent config for {agent_dir.name}: {e}")
|
|
191
|
+
|
|
192
|
+
return cls(
|
|
193
|
+
name=agent_dir.name,
|
|
194
|
+
directory=agent_dir,
|
|
195
|
+
system_prompt=system_prompt,
|
|
196
|
+
skills=skills,
|
|
197
|
+
profile=profile,
|
|
198
|
+
description=description,
|
|
199
|
+
default_skills=default_skills,
|
|
200
|
+
source=source,
|
|
201
|
+
overrides_global=overrides_global,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def get_full_system_prompt(self) -> str:
|
|
205
|
+
"""
|
|
206
|
+
Get system prompt with active skills appended.
|
|
207
|
+
|
|
208
|
+
Skills are added under "## Skill: {name}" headers.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Combined system prompt string
|
|
212
|
+
"""
|
|
213
|
+
parts = [self.system_prompt]
|
|
214
|
+
|
|
215
|
+
for skill_name in self.active_skills:
|
|
216
|
+
if skill_name in self.skills:
|
|
217
|
+
skill = self.skills[skill_name]
|
|
218
|
+
parts.append(f"\n\n## Skill: {skill_name}\n\n{skill.content}")
|
|
219
|
+
|
|
220
|
+
return "\n".join(parts)
|
|
221
|
+
|
|
222
|
+
def load_skill(self, skill_name: str) -> bool:
|
|
223
|
+
"""
|
|
224
|
+
Load a skill into active context.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
skill_name: Name of skill to load
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
True if loaded, False if not found
|
|
231
|
+
"""
|
|
232
|
+
if skill_name not in self.skills:
|
|
233
|
+
logger.error(f"Skill not found: {skill_name}")
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
if skill_name not in self.active_skills:
|
|
237
|
+
self.active_skills.append(skill_name)
|
|
238
|
+
logger.info(f"Loaded skill: {skill_name}")
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
def unload_skill(self, skill_name: str) -> bool:
|
|
242
|
+
"""
|
|
243
|
+
Unload a skill from active context.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
skill_name: Name of skill to unload
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
True if unloaded, False if not loaded
|
|
250
|
+
"""
|
|
251
|
+
if skill_name in self.active_skills:
|
|
252
|
+
self.active_skills.remove(skill_name)
|
|
253
|
+
logger.info(f"Unloaded skill: {skill_name}")
|
|
254
|
+
return True
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
def list_skills(self) -> List[Skill]:
|
|
258
|
+
"""Get list of available skills."""
|
|
259
|
+
return list(self.skills.values())
|
|
260
|
+
|
|
261
|
+
def get_skill(self, name: str) -> Optional[Skill]:
|
|
262
|
+
"""Get a specific skill by name."""
|
|
263
|
+
return self.skills.get(name)
|
|
264
|
+
|
|
265
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
266
|
+
"""Convert agent to dictionary representation."""
|
|
267
|
+
return {
|
|
268
|
+
"name": self.name,
|
|
269
|
+
"directory": str(self.directory),
|
|
270
|
+
"description": self.description,
|
|
271
|
+
"profile": self.profile,
|
|
272
|
+
"skills": [s.to_dict() for s in self.skills.values()],
|
|
273
|
+
"active_skills": self.active_skills,
|
|
274
|
+
"source": self.source,
|
|
275
|
+
"overrides_global": self.overrides_global,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class AgentManager:
|
|
280
|
+
"""
|
|
281
|
+
Manages agent discovery, loading, and skill management.
|
|
282
|
+
|
|
283
|
+
Searches for agents in:
|
|
284
|
+
1. Local: .kollabor-cli/agents/ (project-specific, higher priority)
|
|
285
|
+
2. Global: ~/.kollabor-cli/agents/ (user defaults)
|
|
286
|
+
|
|
287
|
+
Local agents override global agents with the same name.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
def __init__(self, config=None):
|
|
291
|
+
"""
|
|
292
|
+
Initialize agent manager.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
config: Configuration object (optional)
|
|
296
|
+
"""
|
|
297
|
+
self.config = config
|
|
298
|
+
self._agents: Dict[str, Agent] = {}
|
|
299
|
+
self._active_agent_name: Optional[str] = None
|
|
300
|
+
|
|
301
|
+
# Agent directories (in discovery order, lowest to highest priority)
|
|
302
|
+
# 1. Global: ~/.kollabor-cli/agents/ (user defaults)
|
|
303
|
+
# 2. Local: .kollabor-cli/agents/ (project-specific, where agents are created)
|
|
304
|
+
self.global_agents_dir = get_global_agents_dir()
|
|
305
|
+
self.local_agents_dir = get_local_agents_dir()
|
|
306
|
+
|
|
307
|
+
self._discover_agents()
|
|
308
|
+
|
|
309
|
+
def _discover_agents(self) -> None:
|
|
310
|
+
"""Discover all available agents from directories."""
|
|
311
|
+
# Skip these directory names during discovery
|
|
312
|
+
skip_dirs = {"__pycache__", ".git", ".svn", "node_modules"}
|
|
313
|
+
|
|
314
|
+
# Load from global first (lowest priority)
|
|
315
|
+
if self.global_agents_dir:
|
|
316
|
+
for agent_dir in self.global_agents_dir.iterdir():
|
|
317
|
+
if agent_dir.is_dir() and agent_dir.name not in skip_dirs and not agent_dir.name.startswith("."):
|
|
318
|
+
agent = Agent.from_directory(agent_dir, source="global", overrides_global=False)
|
|
319
|
+
if agent:
|
|
320
|
+
self._agents[agent.name] = agent
|
|
321
|
+
logger.debug(f"Discovered global agent: {agent.name}")
|
|
322
|
+
|
|
323
|
+
# Load from local (higher priority, overrides global)
|
|
324
|
+
if self.local_agents_dir:
|
|
325
|
+
for agent_dir in self.local_agents_dir.iterdir():
|
|
326
|
+
if agent_dir.is_dir() and agent_dir.name not in skip_dirs and not agent_dir.name.startswith("."):
|
|
327
|
+
# Check if this local agent overrides a global one
|
|
328
|
+
overrides = agent_dir.name in self._agents
|
|
329
|
+
agent = Agent.from_directory(agent_dir, source="local", overrides_global=overrides)
|
|
330
|
+
if agent:
|
|
331
|
+
self._agents[agent.name] = agent
|
|
332
|
+
override_msg = " (overrides global)" if overrides else ""
|
|
333
|
+
logger.debug(f"Discovered local agent: {agent.name}{override_msg}")
|
|
334
|
+
|
|
335
|
+
logger.info(f"Discovered {len(self._agents)} agents")
|
|
336
|
+
|
|
337
|
+
def get_agent(self, name: str) -> Optional[Agent]:
|
|
338
|
+
"""
|
|
339
|
+
Get agent by name.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
name: Agent name
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Agent instance or None if not found
|
|
346
|
+
"""
|
|
347
|
+
return self._agents.get(name)
|
|
348
|
+
|
|
349
|
+
def get_active_agent(self) -> Optional[Agent]:
|
|
350
|
+
"""
|
|
351
|
+
Get the currently active agent.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Active Agent or "default" agent or None
|
|
355
|
+
"""
|
|
356
|
+
if self._active_agent_name:
|
|
357
|
+
agent = self._agents.get(self._active_agent_name)
|
|
358
|
+
if agent:
|
|
359
|
+
return agent
|
|
360
|
+
|
|
361
|
+
# Fall back to "default" agent
|
|
362
|
+
return self._agents.get("default")
|
|
363
|
+
|
|
364
|
+
def set_active_agent(self, name: str, load_defaults: bool = True) -> bool:
|
|
365
|
+
"""
|
|
366
|
+
Set the active agent.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
name: Agent name to activate
|
|
370
|
+
load_defaults: If True, auto-load the agent's default skills
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
True if successful, False if agent not found
|
|
374
|
+
"""
|
|
375
|
+
if name not in self._agents:
|
|
376
|
+
logger.error(f"Agent not found: {name}")
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
old_agent = self._active_agent_name
|
|
380
|
+
self._active_agent_name = name
|
|
381
|
+
|
|
382
|
+
# Auto-load default skills if configured
|
|
383
|
+
agent = self._agents[name]
|
|
384
|
+
if load_defaults and agent.default_skills:
|
|
385
|
+
for skill_name in agent.default_skills:
|
|
386
|
+
if skill_name in agent.skills and skill_name not in agent.active_skills:
|
|
387
|
+
agent.load_skill(skill_name)
|
|
388
|
+
logger.debug(f"Auto-loaded default skill: {skill_name}")
|
|
389
|
+
|
|
390
|
+
logger.info(f"Activated agent: {old_agent} -> {name}")
|
|
391
|
+
return True
|
|
392
|
+
|
|
393
|
+
def clear_active_agent(self) -> None:
|
|
394
|
+
"""Clear the active agent (use default or no agent)."""
|
|
395
|
+
self._active_agent_name = None
|
|
396
|
+
logger.info("Cleared active agent")
|
|
397
|
+
|
|
398
|
+
def list_agents(self) -> List[Agent]:
|
|
399
|
+
"""
|
|
400
|
+
List all available agents.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
List of Agent instances
|
|
404
|
+
"""
|
|
405
|
+
return list(self._agents.values())
|
|
406
|
+
|
|
407
|
+
def get_agent_names(self) -> List[str]:
|
|
408
|
+
"""
|
|
409
|
+
Get list of agent names.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
List of agent name strings
|
|
413
|
+
"""
|
|
414
|
+
return list(self._agents.keys())
|
|
415
|
+
|
|
416
|
+
def has_agent(self, name: str) -> bool:
|
|
417
|
+
"""Check if an agent exists."""
|
|
418
|
+
return name in self._agents
|
|
419
|
+
|
|
420
|
+
def list_skills(self, agent_name: Optional[str] = None) -> List[Skill]:
|
|
421
|
+
"""
|
|
422
|
+
List skills for an agent.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
agent_name: Agent name (default: active agent)
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
List of Skill instances
|
|
429
|
+
"""
|
|
430
|
+
agent = self._agents.get(agent_name) if agent_name else self.get_active_agent()
|
|
431
|
+
if not agent:
|
|
432
|
+
return []
|
|
433
|
+
return agent.list_skills()
|
|
434
|
+
|
|
435
|
+
def load_skill(
|
|
436
|
+
self, skill_name: str, agent_name: Optional[str] = None
|
|
437
|
+
) -> bool:
|
|
438
|
+
"""
|
|
439
|
+
Load a skill into an agent's active context.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
skill_name: Name of skill to load
|
|
443
|
+
agent_name: Agent name (default: active agent)
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
True if loaded, False otherwise
|
|
447
|
+
"""
|
|
448
|
+
agent = self._agents.get(agent_name) if agent_name else self.get_active_agent()
|
|
449
|
+
if not agent:
|
|
450
|
+
logger.error("No agent available to load skill")
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
return agent.load_skill(skill_name)
|
|
454
|
+
|
|
455
|
+
def unload_skill(
|
|
456
|
+
self, skill_name: str, agent_name: Optional[str] = None
|
|
457
|
+
) -> bool:
|
|
458
|
+
"""
|
|
459
|
+
Unload a skill from an agent's active context.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
skill_name: Name of skill to unload
|
|
463
|
+
agent_name: Agent name (default: active agent)
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
True if unloaded, False otherwise
|
|
467
|
+
"""
|
|
468
|
+
agent = self._agents.get(agent_name) if agent_name else self.get_active_agent()
|
|
469
|
+
if not agent:
|
|
470
|
+
return False
|
|
471
|
+
|
|
472
|
+
return agent.unload_skill(skill_name)
|
|
473
|
+
|
|
474
|
+
def toggle_default_skill(
|
|
475
|
+
self, skill_name: str, agent_name: Optional[str] = None, scope: str = "project"
|
|
476
|
+
) -> tuple[bool, bool]:
|
|
477
|
+
"""
|
|
478
|
+
Toggle a skill as default (auto-loaded when agent is activated).
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
skill_name: Name of skill to toggle
|
|
482
|
+
agent_name: Agent name (default: active agent)
|
|
483
|
+
scope: "project" for .kollabor-cli or "global" for ~/.kollabor-cli
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Tuple of (success, is_now_default)
|
|
487
|
+
"""
|
|
488
|
+
agent = self._agents.get(agent_name) if agent_name else self.get_active_agent()
|
|
489
|
+
if not agent:
|
|
490
|
+
return (False, False)
|
|
491
|
+
|
|
492
|
+
# Check if skill exists
|
|
493
|
+
if skill_name not in agent.skills:
|
|
494
|
+
logger.error(f"Skill not found: {skill_name}")
|
|
495
|
+
return (False, False)
|
|
496
|
+
|
|
497
|
+
# Determine target directory based on scope
|
|
498
|
+
if scope == "global":
|
|
499
|
+
target_dir = self.global_agents_dir / agent.name
|
|
500
|
+
else:
|
|
501
|
+
# Use get_local_agents_path() for creation (creates dir if needed)
|
|
502
|
+
target_dir = get_local_agents_path() / agent.name
|
|
503
|
+
|
|
504
|
+
# Ensure directory exists
|
|
505
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
506
|
+
|
|
507
|
+
# Load existing config from target scope
|
|
508
|
+
config_file = target_dir / "agent.json"
|
|
509
|
+
current_defaults = []
|
|
510
|
+
if config_file.exists():
|
|
511
|
+
try:
|
|
512
|
+
config_data = json.loads(config_file.read_text(encoding="utf-8"))
|
|
513
|
+
current_defaults = config_data.get("default_skills", [])
|
|
514
|
+
except Exception as e:
|
|
515
|
+
logger.error(f"Failed to read {scope} agent.json: {e}")
|
|
516
|
+
|
|
517
|
+
# Toggle default status
|
|
518
|
+
if skill_name in current_defaults:
|
|
519
|
+
current_defaults.remove(skill_name)
|
|
520
|
+
is_default = False
|
|
521
|
+
logger.info(f"Removed skill from {scope} defaults: {skill_name}")
|
|
522
|
+
else:
|
|
523
|
+
current_defaults.append(skill_name)
|
|
524
|
+
is_default = True
|
|
525
|
+
logger.info(f"Added skill to {scope} defaults: {skill_name}")
|
|
526
|
+
|
|
527
|
+
# Save to target scope
|
|
528
|
+
self._save_agent_config_to_path(target_dir, current_defaults, agent)
|
|
529
|
+
|
|
530
|
+
# Reload agent to reflect changes
|
|
531
|
+
self._reload_agent(agent.name)
|
|
532
|
+
|
|
533
|
+
return (True, is_default)
|
|
534
|
+
|
|
535
|
+
def _save_agent_config(self, agent: Agent) -> bool:
|
|
536
|
+
"""
|
|
537
|
+
Save agent configuration to agent.json.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
agent: Agent to save config for
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
True if saved, False otherwise
|
|
544
|
+
"""
|
|
545
|
+
try:
|
|
546
|
+
config_file = agent.directory / "agent.json"
|
|
547
|
+
|
|
548
|
+
# Build config dict
|
|
549
|
+
agent_json: Dict[str, Any] = {}
|
|
550
|
+
if agent.description:
|
|
551
|
+
agent_json["description"] = agent.description
|
|
552
|
+
if agent.profile:
|
|
553
|
+
agent_json["profile"] = agent.profile
|
|
554
|
+
if agent.default_skills:
|
|
555
|
+
agent_json["default_skills"] = agent.default_skills
|
|
556
|
+
|
|
557
|
+
if agent_json:
|
|
558
|
+
config_file.write_text(
|
|
559
|
+
json.dumps(agent_json, indent=4, ensure_ascii=False),
|
|
560
|
+
encoding="utf-8"
|
|
561
|
+
)
|
|
562
|
+
elif config_file.exists():
|
|
563
|
+
# Remove agent.json if empty
|
|
564
|
+
config_file.unlink()
|
|
565
|
+
|
|
566
|
+
return True
|
|
567
|
+
except Exception as e:
|
|
568
|
+
logger.error(f"Failed to save agent config for {agent.name}: {e}")
|
|
569
|
+
return False
|
|
570
|
+
|
|
571
|
+
def _save_agent_config_to_path(
|
|
572
|
+
self, target_dir: Path, default_skills: List[str], agent: Agent
|
|
573
|
+
) -> bool:
|
|
574
|
+
"""
|
|
575
|
+
Save agent configuration to a specific directory.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
target_dir: Directory to save to
|
|
579
|
+
default_skills: List of default skill names
|
|
580
|
+
agent: Agent instance for reference data
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
True if saved, False otherwise
|
|
584
|
+
"""
|
|
585
|
+
try:
|
|
586
|
+
config_file = target_dir / "agent.json"
|
|
587
|
+
|
|
588
|
+
# Load existing config to preserve other fields
|
|
589
|
+
agent_json: Dict[str, Any] = {}
|
|
590
|
+
if config_file.exists():
|
|
591
|
+
try:
|
|
592
|
+
agent_json = json.loads(config_file.read_text(encoding="utf-8"))
|
|
593
|
+
except Exception:
|
|
594
|
+
pass
|
|
595
|
+
|
|
596
|
+
# Update default_skills
|
|
597
|
+
if default_skills:
|
|
598
|
+
agent_json["default_skills"] = default_skills
|
|
599
|
+
elif "default_skills" in agent_json:
|
|
600
|
+
del agent_json["default_skills"]
|
|
601
|
+
|
|
602
|
+
if agent_json:
|
|
603
|
+
config_file.write_text(
|
|
604
|
+
json.dumps(agent_json, indent=4, ensure_ascii=False),
|
|
605
|
+
encoding="utf-8"
|
|
606
|
+
)
|
|
607
|
+
elif config_file.exists():
|
|
608
|
+
config_file.unlink()
|
|
609
|
+
|
|
610
|
+
return True
|
|
611
|
+
except Exception as e:
|
|
612
|
+
logger.error(f"Failed to save agent config to {target_dir}: {e}")
|
|
613
|
+
return False
|
|
614
|
+
|
|
615
|
+
def _reload_agent(self, agent_name: str) -> None:
|
|
616
|
+
"""
|
|
617
|
+
Reload an agent from disk to pick up configuration changes.
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
agent_name: Name of agent to reload
|
|
621
|
+
"""
|
|
622
|
+
# Store active skills before reload
|
|
623
|
+
active_skills = []
|
|
624
|
+
if agent_name in self._agents:
|
|
625
|
+
active_skills = self._agents[agent_name].active_skills.copy()
|
|
626
|
+
|
|
627
|
+
# Reload from disk (local overrides global)
|
|
628
|
+
local_path = self.local_agents_dir / agent_name if self.local_agents_dir else None
|
|
629
|
+
global_path = self.global_agents_dir / agent_name if self.global_agents_dir else None
|
|
630
|
+
|
|
631
|
+
if local_path and local_path.exists():
|
|
632
|
+
# Check if this overrides a global agent
|
|
633
|
+
overrides = global_path and global_path.exists()
|
|
634
|
+
agent = Agent.from_directory(local_path, source="local", overrides_global=overrides)
|
|
635
|
+
if agent:
|
|
636
|
+
self._agents[agent_name] = agent
|
|
637
|
+
elif global_path and global_path.exists():
|
|
638
|
+
agent = Agent.from_directory(global_path, source="global", overrides_global=False)
|
|
639
|
+
if agent:
|
|
640
|
+
self._agents[agent_name] = agent
|
|
641
|
+
|
|
642
|
+
# Restore active skills
|
|
643
|
+
if agent_name in self._agents and active_skills:
|
|
644
|
+
for skill_name in active_skills:
|
|
645
|
+
if skill_name in self._agents[agent_name].skills:
|
|
646
|
+
self._agents[agent_name].load_skill(skill_name)
|
|
647
|
+
|
|
648
|
+
def get_system_prompt(self) -> Optional[str]:
|
|
649
|
+
"""
|
|
650
|
+
Get the full system prompt for the active agent.
|
|
651
|
+
|
|
652
|
+
Includes base system prompt and active skills.
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
System prompt string or None if no agent
|
|
656
|
+
"""
|
|
657
|
+
agent = self.get_active_agent()
|
|
658
|
+
if agent:
|
|
659
|
+
return agent.get_full_system_prompt()
|
|
660
|
+
return None
|
|
661
|
+
|
|
662
|
+
def get_preferred_profile(self) -> Optional[str]:
|
|
663
|
+
"""
|
|
664
|
+
Get the preferred LLM profile for the active agent.
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
Profile name or None
|
|
668
|
+
"""
|
|
669
|
+
agent = self.get_active_agent()
|
|
670
|
+
if agent:
|
|
671
|
+
return agent.profile
|
|
672
|
+
return None
|
|
673
|
+
|
|
674
|
+
@property
|
|
675
|
+
def active_agent_name(self) -> Optional[str]:
|
|
676
|
+
"""Get the name of the active agent."""
|
|
677
|
+
return self._active_agent_name
|
|
678
|
+
|
|
679
|
+
def is_active(self, name: str) -> bool:
|
|
680
|
+
"""Check if an agent is the active one."""
|
|
681
|
+
return name == self._active_agent_name
|
|
682
|
+
|
|
683
|
+
def get_agent_summary(self, name: Optional[str] = None) -> str:
|
|
684
|
+
"""
|
|
685
|
+
Get a human-readable summary of an agent.
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
name: Agent name (default: active agent)
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
Formatted summary string
|
|
692
|
+
"""
|
|
693
|
+
agent = self._agents.get(name) if name else self.get_active_agent()
|
|
694
|
+
if not agent:
|
|
695
|
+
return f"Agent '{name}' not found" if name else "No active agent"
|
|
696
|
+
|
|
697
|
+
lines = [
|
|
698
|
+
f"Agent: {agent.name}",
|
|
699
|
+
f" Directory: {agent.directory}",
|
|
700
|
+
]
|
|
701
|
+
if agent.description:
|
|
702
|
+
lines.append(f" Description: {agent.description}")
|
|
703
|
+
if agent.profile:
|
|
704
|
+
lines.append(f" Preferred Profile: {agent.profile}")
|
|
705
|
+
|
|
706
|
+
skills = agent.list_skills()
|
|
707
|
+
if skills:
|
|
708
|
+
lines.append(f" Skills ({len(skills)}):")
|
|
709
|
+
for skill in skills:
|
|
710
|
+
active = "*" if skill.name in agent.active_skills else " "
|
|
711
|
+
desc = f" - {skill.description[:40]}..." if skill.description else ""
|
|
712
|
+
lines.append(f" [{active}] {skill.name}{desc}")
|
|
713
|
+
else:
|
|
714
|
+
lines.append(" Skills: none")
|
|
715
|
+
|
|
716
|
+
return "\n".join(lines)
|
|
717
|
+
|
|
718
|
+
def refresh(self) -> None:
|
|
719
|
+
"""Re-discover agents from directories, preserving active skills."""
|
|
720
|
+
# Preserve active skills state before refresh
|
|
721
|
+
active_skills_backup: Dict[str, List[str]] = {}
|
|
722
|
+
for name, agent in self._agents.items():
|
|
723
|
+
if agent.active_skills:
|
|
724
|
+
active_skills_backup[name] = list(agent.active_skills)
|
|
725
|
+
|
|
726
|
+
self._agents.clear()
|
|
727
|
+
self._discover_agents()
|
|
728
|
+
|
|
729
|
+
# Restore active skills after refresh
|
|
730
|
+
for name, skills in active_skills_backup.items():
|
|
731
|
+
if name in self._agents:
|
|
732
|
+
self._agents[name].active_skills = skills
|
|
733
|
+
|
|
734
|
+
def create_agent(
|
|
735
|
+
self,
|
|
736
|
+
name: str,
|
|
737
|
+
description: str = "",
|
|
738
|
+
profile: Optional[str] = None,
|
|
739
|
+
system_prompt: str = "",
|
|
740
|
+
default_skills: Optional[List[str]] = None,
|
|
741
|
+
) -> Optional[Agent]:
|
|
742
|
+
"""
|
|
743
|
+
Create a new agent with directory structure.
|
|
744
|
+
|
|
745
|
+
Creates .kollabor-cli/agents/<name>/ directory with:
|
|
746
|
+
- system_prompt.md
|
|
747
|
+
- agent.json (if profile, description, or default_skills specified)
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
name: Agent name (becomes directory name)
|
|
751
|
+
description: Agent description
|
|
752
|
+
profile: Preferred LLM profile name
|
|
753
|
+
system_prompt: Base system prompt content
|
|
754
|
+
default_skills: List of skill names to auto-load when agent is activated
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
Created Agent or None on failure
|
|
758
|
+
"""
|
|
759
|
+
import json
|
|
760
|
+
|
|
761
|
+
# Check if agent already exists
|
|
762
|
+
if name in self._agents:
|
|
763
|
+
logger.warning(f"Agent already exists: {name}")
|
|
764
|
+
return None
|
|
765
|
+
|
|
766
|
+
# Create in .kollabor-cli/agents/ directory (creates local dir if needed)
|
|
767
|
+
local_path = get_local_agents_path()
|
|
768
|
+
agent_dir = local_path / name
|
|
769
|
+
|
|
770
|
+
if agent_dir.exists():
|
|
771
|
+
logger.warning(f"Agent directory already exists: {agent_dir}")
|
|
772
|
+
return None
|
|
773
|
+
|
|
774
|
+
try:
|
|
775
|
+
# Create directory structure
|
|
776
|
+
agent_dir.mkdir(parents=True, exist_ok=True)
|
|
777
|
+
|
|
778
|
+
# Create system_prompt.md
|
|
779
|
+
default_prompt = system_prompt or f"""# {name.replace('-', ' ').title()} Agent
|
|
780
|
+
|
|
781
|
+
You are a specialized assistant.
|
|
782
|
+
|
|
783
|
+
## Your Mission
|
|
784
|
+
|
|
785
|
+
{description or 'Help users with their tasks.'}
|
|
786
|
+
|
|
787
|
+
## Approach
|
|
788
|
+
|
|
789
|
+
1. Analyze the user's request
|
|
790
|
+
2. Provide clear, actionable guidance
|
|
791
|
+
3. Follow best practices
|
|
792
|
+
"""
|
|
793
|
+
prompt_file = agent_dir / "system_prompt.md"
|
|
794
|
+
prompt_file.write_text(default_prompt, encoding="utf-8")
|
|
795
|
+
|
|
796
|
+
# Create agent.json if profile, description, or default_skills specified
|
|
797
|
+
if profile or description or default_skills:
|
|
798
|
+
agent_json: Dict[str, Any] = {
|
|
799
|
+
"description": description or f"Agent: {name}",
|
|
800
|
+
}
|
|
801
|
+
if profile and profile != "(none)":
|
|
802
|
+
agent_json["profile"] = profile
|
|
803
|
+
if default_skills:
|
|
804
|
+
agent_json["default_skills"] = default_skills
|
|
805
|
+
|
|
806
|
+
json_file = agent_dir / "agent.json"
|
|
807
|
+
json_file.write_text(
|
|
808
|
+
json.dumps(agent_json, indent=4, ensure_ascii=False),
|
|
809
|
+
encoding="utf-8"
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
# Update local_agents_dir since we just created the local directory
|
|
813
|
+
self.local_agents_dir = get_local_agents_dir()
|
|
814
|
+
|
|
815
|
+
# Load the newly created agent
|
|
816
|
+
# Check if it overrides a global agent
|
|
817
|
+
overrides = (
|
|
818
|
+
self.global_agents_dir is not None
|
|
819
|
+
and (self.global_agents_dir / name).exists()
|
|
820
|
+
)
|
|
821
|
+
agent = Agent.from_directory(agent_dir, source="local", overrides_global=overrides)
|
|
822
|
+
if agent:
|
|
823
|
+
self._agents[name] = agent
|
|
824
|
+
logger.info(f"Created agent: {name} at {agent_dir}")
|
|
825
|
+
return agent
|
|
826
|
+
|
|
827
|
+
return None
|
|
828
|
+
|
|
829
|
+
except Exception as e:
|
|
830
|
+
logger.error(f"Failed to create agent {name}: {e}")
|
|
831
|
+
# Clean up on failure
|
|
832
|
+
if agent_dir.exists():
|
|
833
|
+
import shutil
|
|
834
|
+
shutil.rmtree(agent_dir, ignore_errors=True)
|
|
835
|
+
return None
|
|
836
|
+
|
|
837
|
+
def delete_agent(self, name: str) -> bool:
|
|
838
|
+
"""
|
|
839
|
+
Delete an agent by removing its directory.
|
|
840
|
+
|
|
841
|
+
Cannot delete the active agent or protected agents like "default".
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
name: Agent name to delete
|
|
845
|
+
|
|
846
|
+
Returns:
|
|
847
|
+
True if deleted, False if cannot delete
|
|
848
|
+
"""
|
|
849
|
+
import shutil
|
|
850
|
+
|
|
851
|
+
# Protected agents that cannot be deleted
|
|
852
|
+
protected_agents = {"default"}
|
|
853
|
+
|
|
854
|
+
# Check if agent exists
|
|
855
|
+
if name not in self._agents:
|
|
856
|
+
logger.warning(f"Agent not found: {name}")
|
|
857
|
+
return False
|
|
858
|
+
|
|
859
|
+
# Check if protected
|
|
860
|
+
if name in protected_agents:
|
|
861
|
+
logger.warning(f"Cannot delete protected agent: {name}")
|
|
862
|
+
return False
|
|
863
|
+
|
|
864
|
+
# Check if active
|
|
865
|
+
if self.is_active(name):
|
|
866
|
+
logger.warning(f"Cannot delete active agent: {name}")
|
|
867
|
+
return False
|
|
868
|
+
|
|
869
|
+
agent = self._agents[name]
|
|
870
|
+
agent_dir = agent.directory
|
|
871
|
+
|
|
872
|
+
# Only delete from local directory (never delete global agents)
|
|
873
|
+
if not agent_dir.is_relative_to(self.local_agents_dir):
|
|
874
|
+
logger.warning(f"Cannot delete agent from global directory: {name}")
|
|
875
|
+
return False
|
|
876
|
+
|
|
877
|
+
try:
|
|
878
|
+
# Remove the directory
|
|
879
|
+
shutil.rmtree(agent_dir)
|
|
880
|
+
# Remove from internal dict
|
|
881
|
+
del self._agents[name]
|
|
882
|
+
logger.info(f"Deleted agent: {name}")
|
|
883
|
+
return True
|
|
884
|
+
except Exception as e:
|
|
885
|
+
logger.error(f"Failed to delete agent {name}: {e}")
|
|
886
|
+
return False
|
|
887
|
+
|
|
888
|
+
def load_default_agent(self, cli_agent_name: Optional[str] = None) -> Optional[str]:
|
|
889
|
+
"""
|
|
890
|
+
Load the appropriate default agent based on priority.
|
|
891
|
+
|
|
892
|
+
Priority:
|
|
893
|
+
1. CLI agent name (highest, one-time override)
|
|
894
|
+
2. Project default (.kollabor-cli/config.json)
|
|
895
|
+
3. Global default (~/.kollabor-cli/config.json)
|
|
896
|
+
4. Fallback to "default" agent
|
|
897
|
+
|
|
898
|
+
Args:
|
|
899
|
+
cli_agent_name: Agent name from CLI --agent argument
|
|
900
|
+
|
|
901
|
+
Returns:
|
|
902
|
+
Name of agent that was activated, or None if failed
|
|
903
|
+
"""
|
|
904
|
+
from ..utils.config_utils import get_default_agent
|
|
905
|
+
|
|
906
|
+
# Priority 1: CLI argument (one-time override)
|
|
907
|
+
if cli_agent_name:
|
|
908
|
+
if self.set_active_agent(cli_agent_name):
|
|
909
|
+
logger.info(f"Loaded agent from CLI argument: {cli_agent_name}")
|
|
910
|
+
return cli_agent_name
|
|
911
|
+
else:
|
|
912
|
+
logger.warning(f"CLI agent '{cli_agent_name}' not found, trying defaults")
|
|
913
|
+
|
|
914
|
+
# Priority 2: Project default
|
|
915
|
+
project_agent, level = get_default_agent()
|
|
916
|
+
if level == "project" and project_agent:
|
|
917
|
+
if self.set_active_agent(project_agent):
|
|
918
|
+
logger.info(f"Loaded project default agent: {project_agent}")
|
|
919
|
+
return project_agent
|
|
920
|
+
else:
|
|
921
|
+
logger.warning(f"Project default agent '{project_agent}' not found, trying next level")
|
|
922
|
+
|
|
923
|
+
# Priority 3: Global default
|
|
924
|
+
global_agent, level = get_default_agent()
|
|
925
|
+
if level == "global" and global_agent:
|
|
926
|
+
if self.set_active_agent(global_agent):
|
|
927
|
+
logger.info(f"Loaded global default agent: {global_agent}")
|
|
928
|
+
return global_agent
|
|
929
|
+
else:
|
|
930
|
+
logger.warning(f"Global default agent '{global_agent}' not found, trying fallback")
|
|
931
|
+
|
|
932
|
+
# Priority 4: Fallback to "default" agent
|
|
933
|
+
if self.set_active_agent("default", load_defaults=True):
|
|
934
|
+
logger.info("Loaded fallback default agent")
|
|
935
|
+
return "default"
|
|
936
|
+
|
|
937
|
+
logger.error("Failed to load any agent")
|
|
938
|
+
return None
|
|
939
|
+
|
|
940
|
+
def update_agent(
|
|
941
|
+
self,
|
|
942
|
+
original_name: str,
|
|
943
|
+
new_name: str,
|
|
944
|
+
description: str = "",
|
|
945
|
+
profile: Optional[str] = None,
|
|
946
|
+
system_prompt: str = "",
|
|
947
|
+
default_skills: Optional[List[str]] = None,
|
|
948
|
+
) -> bool:
|
|
949
|
+
"""
|
|
950
|
+
Update an existing agent's configuration.
|
|
951
|
+
|
|
952
|
+
Can rename the agent (rename directory), update description,
|
|
953
|
+
profile, system prompt, and default skills. Only works for agents in the
|
|
954
|
+
local directory (.kollabor-cli/agents/).
|
|
955
|
+
|
|
956
|
+
Args:
|
|
957
|
+
original_name: Current name of the agent to update.
|
|
958
|
+
new_name: New name for the agent (can be same as original).
|
|
959
|
+
description: New description.
|
|
960
|
+
profile: New preferred LLM profile name.
|
|
961
|
+
system_prompt: New system prompt content.
|
|
962
|
+
default_skills: List of skill names to auto-load when agent is activated.
|
|
963
|
+
|
|
964
|
+
Returns:
|
|
965
|
+
True if updated successfully, False otherwise.
|
|
966
|
+
"""
|
|
967
|
+
import shutil
|
|
968
|
+
|
|
969
|
+
# Check if agent exists
|
|
970
|
+
if original_name not in self._agents:
|
|
971
|
+
logger.warning(f"Agent not found for update: {original_name}")
|
|
972
|
+
return False
|
|
973
|
+
|
|
974
|
+
agent = self._agents[original_name]
|
|
975
|
+
agent_dir = agent.directory
|
|
976
|
+
|
|
977
|
+
# Only update local agents (not global)
|
|
978
|
+
local_path = get_local_agents_path()
|
|
979
|
+
if not self.local_agents_dir or not agent_dir.is_relative_to(self.local_agents_dir):
|
|
980
|
+
logger.warning(f"Cannot edit agent from global directory: {original_name}")
|
|
981
|
+
return False
|
|
982
|
+
|
|
983
|
+
try:
|
|
984
|
+
# If renaming, we need to move the directory
|
|
985
|
+
if new_name != original_name:
|
|
986
|
+
# Check if new name already exists
|
|
987
|
+
if new_name in self._agents:
|
|
988
|
+
logger.warning(f"Agent already exists with new name: {new_name}")
|
|
989
|
+
return False
|
|
990
|
+
|
|
991
|
+
new_agent_dir = local_path / new_name
|
|
992
|
+
|
|
993
|
+
# Check if target directory already exists
|
|
994
|
+
if new_agent_dir.exists():
|
|
995
|
+
logger.warning(f"Target directory already exists: {new_agent_dir}")
|
|
996
|
+
return False
|
|
997
|
+
|
|
998
|
+
# Rename directory
|
|
999
|
+
shutil.move(str(agent_dir), str(new_agent_dir))
|
|
1000
|
+
agent_dir = new_agent_dir
|
|
1001
|
+
logger.info(f"Renamed agent directory: {original_name} -> {new_name}")
|
|
1002
|
+
|
|
1003
|
+
# Update system_prompt.md
|
|
1004
|
+
prompt_file = agent_dir / "system_prompt.md"
|
|
1005
|
+
if system_prompt:
|
|
1006
|
+
prompt_file.write_text(system_prompt, encoding="utf-8")
|
|
1007
|
+
logger.info(f"Updated system prompt for agent: {new_name}")
|
|
1008
|
+
|
|
1009
|
+
# Update or create agent.json for description, profile, and default_skills
|
|
1010
|
+
agent_json: Dict[str, Any] = {}
|
|
1011
|
+
if description or profile or default_skills:
|
|
1012
|
+
agent_json["description"] = description or f"Agent: {new_name}"
|
|
1013
|
+
if profile:
|
|
1014
|
+
agent_json["profile"] = profile
|
|
1015
|
+
if default_skills:
|
|
1016
|
+
agent_json["default_skills"] = default_skills
|
|
1017
|
+
|
|
1018
|
+
if agent_json:
|
|
1019
|
+
json_file = agent_dir / "agent.json"
|
|
1020
|
+
json_file.write_text(
|
|
1021
|
+
json.dumps(agent_json, indent=4, ensure_ascii=False),
|
|
1022
|
+
encoding="utf-8"
|
|
1023
|
+
)
|
|
1024
|
+
logger.info(f"Updated agent.json for agent: {new_name}")
|
|
1025
|
+
elif (agent_dir / "agent.json").exists():
|
|
1026
|
+
# Remove agent.json if no description or profile
|
|
1027
|
+
(agent_dir / "agent.json").unlink()
|
|
1028
|
+
|
|
1029
|
+
# If renamed, remove old entry from dict
|
|
1030
|
+
if new_name != original_name:
|
|
1031
|
+
del self._agents[original_name]
|
|
1032
|
+
|
|
1033
|
+
# Reload the agent from directory
|
|
1034
|
+
# Check if it overrides a global agent
|
|
1035
|
+
overrides = (
|
|
1036
|
+
self.global_agents_dir is not None
|
|
1037
|
+
and (self.global_agents_dir / new_name).exists()
|
|
1038
|
+
)
|
|
1039
|
+
updated_agent = Agent.from_directory(agent_dir, source="local", overrides_global=overrides)
|
|
1040
|
+
if updated_agent:
|
|
1041
|
+
self._agents[new_name] = updated_agent
|
|
1042
|
+
|
|
1043
|
+
# If this was the active agent, update the active name
|
|
1044
|
+
if self._active_agent_name == original_name:
|
|
1045
|
+
self._active_agent_name = new_name
|
|
1046
|
+
|
|
1047
|
+
logger.info(f"Updated agent: {new_name}")
|
|
1048
|
+
return True
|
|
1049
|
+
|
|
1050
|
+
return False
|
|
1051
|
+
|
|
1052
|
+
except Exception as e:
|
|
1053
|
+
logger.error(f"Failed to update agent {original_name}: {e}")
|
|
1054
|
+
# If rename failed, try to revert
|
|
1055
|
+
if new_name != original_name:
|
|
1056
|
+
original_dir = local_path / original_name
|
|
1057
|
+
new_dir = local_path / new_name
|
|
1058
|
+
if not original_dir.exists() and new_dir.exists():
|
|
1059
|
+
try:
|
|
1060
|
+
shutil.move(str(new_dir), str(original_dir))
|
|
1061
|
+
except:
|
|
1062
|
+
pass
|
|
1063
|
+
return False
|