Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,53 @@ The model respects your .gitignore file and will treat ignored files as if they

Working with git is strongly recommended. Models can occasionally damage code while attempting to implement features, and having version control makes recovery trivial. A productive workflow starts from a clean git state, lets the AI make progress on a feature, and commits only once the implementation is complete and working. If something goes wrong during development, you can simply revert the changes and try a different approach.

## Skills

Tycode supports Claude Code Agent Skills - modular capabilities that extend the agent with specialized workflows. Skills are automatically discovered and can be invoked when the AI detects a matching request.

### Skill Discovery

Skills are discovered from the following locations (in priority order):

1. `~/.claude/skills/` - User-level Claude Code compatibility
2. `~/.tycode/skills/` - User-level Tycode skills
3. `.claude/skills/` in workspace - Project-level Claude Code compatibility
4. `.tycode/skills/` in workspace - Project-level (highest priority)

### Creating a Skill

Each skill is a directory containing a `SKILL.md` file with YAML frontmatter:

```markdown
---
name: my-skill
description: When to use this skill
---

# My Skill Instructions

Step-by-step instructions for the AI to follow...
```

### Using Skills

List available skills:
```bash
/skills
```

Manually invoke a skill:
```bash
/skill <name>
```

View skill details:
```bash
/skills info <name>
```

Skills are also automatically invoked when the AI detects a user request matching a skill's description.

## MCP Server Configuration

Tycode supports locally running MCP servers over stdio transport. You can add or remove MCP servers using slash commands.
Expand Down
2 changes: 1 addition & 1 deletion tycode-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "tycode-cli"
version = "0.3.6"
version = "0.4.0"
edition = "2021"
authors = ["tigy"]
description = "CLI interface for TyCode"
Expand Down
2 changes: 1 addition & 1 deletion tycode-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "tycode-core"
version = "0.3.6"
version = "0.4.0"
edition = "2021"
authors = ["tigy"]
description = "Core chat actor and AI functionality for TyCode"
Expand Down
2 changes: 2 additions & 0 deletions tycode-core/src/agents/coder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::context::tracked_files::TrackedFilesManager;
use crate::memory::AppendMemoryTool;
use crate::modules::execution::RunBuildTestTool;
use crate::prompt::{autonomy, PromptComponentSelection};
use crate::skills::tool::InvokeSkillTool;
use crate::tools::analyzer::get_type_docs::GetTypeDocsTool;
use crate::tools::analyzer::search_types::SearchTypesTool;
use crate::tools::complete_task::CompleteTask;
Expand Down Expand Up @@ -62,6 +63,7 @@ impl Agent for CoderAgent {
SearchTypesTool::tool_name(),
GetTypeDocsTool::tool_name(),
AppendMemoryTool::tool_name(),
InvokeSkillTool::tool_name(),
]
}

Expand Down
2 changes: 2 additions & 0 deletions tycode-core/src/agents/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::context::tracked_files::TrackedFilesManager;
use crate::memory::AppendMemoryTool;
use crate::modules::execution::RunBuildTestTool;
use crate::modules::task_list::ManageTaskListTool;
use crate::skills::tool::InvokeSkillTool;
use crate::tools::analyzer::get_type_docs::GetTypeDocsTool;
use crate::tools::analyzer::search_types::SearchTypesTool;
use crate::tools::complete_task::CompleteTask;
Expand Down Expand Up @@ -76,6 +77,7 @@ impl Agent for CoordinatorAgent {
SearchTypesTool::tool_name(),
GetTypeDocsTool::tool_name(),
AppendMemoryTool::tool_name(),
InvokeSkillTool::tool_name(),
]
}
}
2 changes: 2 additions & 0 deletions tycode-core/src/agents/one_shot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::context::tracked_files::TrackedFilesManager;
use crate::memory::AppendMemoryTool;
use crate::modules::execution::RunBuildTestTool;
use crate::modules::task_list::ManageTaskListTool;
use crate::skills::tool::InvokeSkillTool;
use crate::tools::analyzer::get_type_docs::GetTypeDocsTool;
use crate::tools::analyzer::search_types::SearchTypesTool;
use crate::tools::ask_user_question::AskUserQuestion;
Expand Down Expand Up @@ -77,6 +78,7 @@ impl Agent for OneShotAgent {
SearchTypesTool::tool_name(),
GetTypeDocsTool::tool_name(),
AppendMemoryTool::tool_name(),
InvokeSkillTool::tool_name(),
]
}
}
10 changes: 10 additions & 0 deletions tycode-core/src/chat/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use crate::{
execution::{CommandResult, ExecutionModule},
task_list::TaskListModule,
},
skills::SkillsModule,
prompt::{
communication::CommunicationComponent, style::StyleMandatesComponent,
tools::ToolInstructionsComponent, PromptBuilder, PromptComponent,
Expand Down Expand Up @@ -198,6 +199,15 @@ impl ChatActorBuilder {
);
builder.install_module_components(&*execution_module);

// Install skills module
let home_dir = dirs::home_dir().expect("Failed to get home directory");
let skills_module = Arc::new(SkillsModule::new(
&builder.workspace_roots,
&home_dir,
&settings.skills,
));
builder.install_module_components(&*skills_module);

builder.context_builder.add(file_tree_manager);
builder.context_builder.add(tracked_files_manager.clone());
builder.context_builder.add(memories_manager.clone());
Expand Down
201 changes: 201 additions & 0 deletions tycode-core/src/chat/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::chat::{
use crate::context::ContextComponentSelection;
use crate::settings::config::FileModificationApi;
use crate::settings::config::{McpServerConfig, ProviderConfig, ReviewLevel};
use crate::skills::SkillsModule;
use chrono::Utc;
use dirs;
use serde_json::json;
Expand Down Expand Up @@ -213,6 +214,8 @@ pub async fn process_command(state: &mut ActorState, command: &str) -> Vec<ChatM
"sessions" => handle_sessions_command(state, &parts).await,
"debug_ui" => handle_debug_ui_command(state).await,
"memory" => handle_memory_command(state, &parts).await,
"skills" => handle_skills_command(state, &parts).await,
"skill" => handle_skill_invoke_command(state, &parts).await,
_ => vec![create_message(
format!("Unknown command: /{}", parts[0]),
MessageSender::Error,
Expand Down Expand Up @@ -345,6 +348,18 @@ pub fn get_available_commands() -> Vec<CommandInfo> {
usage: "/memory summarize".to_string(),
hidden: false,
},
CommandInfo {
name: "skills".to_string(),
description: "List and manage available skills".to_string(),
usage: "/skills [info <name>|reload]".to_string(),
hidden: false,
},
CommandInfo {
name: "skill".to_string(),
description: "Manually invoke a skill".to_string(),
usage: "/skill <name>".to_string(),
hidden: false,
},
]
}

Expand Down Expand Up @@ -2205,3 +2220,189 @@ async fn handle_sessions_gc_command(state: &ActorState, parts: &[&str]) -> Vec<C

vec![create_message(message, MessageSender::System)]
}

// =============================================================================
// Skills Commands
// =============================================================================

async fn handle_skills_command(state: &ActorState, parts: &[&str]) -> Vec<ChatMessage> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point I want to modularize slash commands as well. Besides just cleaning things up, I think it would make it possible to use CC plugins

let home_dir = match dirs::home_dir() {
Some(dir) => dir,
None => {
return vec![create_message(
"Failed to get home directory".to_string(),
MessageSender::Error,
)];
}
};

let settings = state.settings.settings();
let skills_module = SkillsModule::new(&state.workspace_roots, &home_dir, &settings.skills);

if parts.len() < 2 {
// List all skills
let skills = skills_module.get_all_skills();

if skills.is_empty() {
return vec![create_message(
"No skills found. Skills are discovered from (in priority order):\n\
- ~/.claude/skills/ (user-level Claude Code compatibility)\n\
- ~/.tycode/skills/ (user-level)\n\
- .claude/skills/ (project-level Claude Code compatibility)\n\
- .tycode/skills/ (project-level, highest priority)\n\n\
Each skill should be a directory containing a SKILL.md file."
.to_string(),
MessageSender::System,
)];
}

let mut message = format!("Available Skills ({} found):\n\n", skills.len());
for skill in &skills {
let status = if skill.enabled { "" } else { " [disabled]" };
message.push_str(&format!(
" {} ({}){}\n {}\n\n",
skill.name, skill.source, status, skill.description
));
}

message.push_str("Use `/skill <name>` to invoke a skill manually.\n");
message.push_str("Use `/skills info <name>` to see skill details.\n");
message.push_str("Use `/skills reload` to re-scan skill directories.");

return vec![create_message(message, MessageSender::System)];
}

match parts[1] {
"info" => handle_skills_info_command(&skills_module, parts).await,
"reload" => {
skills_module.reload();
let count = skills_module.get_all_skills().len();
vec![create_message(
format!("Skills reloaded. Found {} skill(s).", count),
MessageSender::System,
)]
}
_ => vec![create_message(
"Usage: /skills [info <name>|reload]\n\
Use `/skills` to list all available skills."
.to_string(),
MessageSender::Error,
)],
}
}

async fn handle_skills_info_command(
skills_module: &SkillsModule,
parts: &[&str],
) -> Vec<ChatMessage> {
if parts.len() < 3 {
return vec![create_message(
"Usage: /skills info <name>".to_string(),
MessageSender::Error,
)];
}

let name = parts[2];
match skills_module.get_skill(name) {
Some(skill) => {
let mut message = format!("# Skill: {}\n\n", skill.metadata.name);
message.push_str(&format!("**Source:** {}\n", skill.metadata.source));
message.push_str(&format!("**Path:** {}\n", skill.metadata.path.display()));
message.push_str(&format!(
"**Status:** {}\n\n",
if skill.metadata.enabled {
"Enabled"
} else {
"Disabled"
}
));
message.push_str(&format!("**Description:**\n{}\n\n", skill.metadata.description));
message.push_str("**Instructions:**\n\n");
message.push_str(&skill.instructions);

if !skill.reference_files.is_empty() {
message.push_str("\n\n**Reference Files:**\n");
for file in &skill.reference_files {
message.push_str(&format!("- {}\n", file.display()));
}
}

if !skill.scripts.is_empty() {
message.push_str("\n**Scripts:**\n");
for script in &skill.scripts {
message.push_str(&format!("- {}\n", script.display()));
}
}

vec![create_message(message, MessageSender::System)]
}
None => vec![create_message(
format!("Skill '{}' not found. Use `/skills` to list available skills.", name),
MessageSender::Error,
)],
}
}

async fn handle_skill_invoke_command(state: &ActorState, parts: &[&str]) -> Vec<ChatMessage> {
if parts.len() < 2 {
return vec![create_message(
"Usage: /skill <name>\n\
Use `/skills` to list available skills."
.to_string(),
MessageSender::Error,
)];
}

let name = parts[1];
let home_dir = match dirs::home_dir() {
Some(dir) => dir,
None => {
return vec![create_message(
"Failed to get home directory".to_string(),
MessageSender::Error,
)];
}
};

let settings = state.settings.settings();
let skills_module = SkillsModule::new(&state.workspace_roots, &home_dir, &settings.skills);

match skills_module.get_skill(name) {
Some(skill) => {
if !skill.metadata.enabled {
return vec![create_message(
format!("Skill '{}' is disabled.", name),
MessageSender::Error,
)];
}

let mut message = format!(
"## Skill Invoked: {}\n\n{}\n\n---\n\n**Instructions:**\n\n{}",
skill.metadata.name, skill.metadata.description, skill.instructions
);

if !skill.reference_files.is_empty() {
message.push_str("\n\n**Reference Files:**\n");
for file in &skill.reference_files {
message.push_str(&format!("- {}\n", file.display()));
}
}

if !skill.scripts.is_empty() {
message.push_str("\n**Scripts:**\n");
for script in &skill.scripts {
message.push_str(&format!("- {}\n", script.display()));
}
}

vec![create_message(message, MessageSender::System)]
}
None => vec![create_message(
format!(
"Skill '{}' not found. Use `/skills` to list available skills.",
name
),
MessageSender::Error,
)],
}
}
1 change: 1 addition & 0 deletions tycode-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod modules;
pub mod persistence;
pub mod prompt;
pub mod settings;
pub mod skills;
pub mod steering;
pub mod tools;
#[cfg(feature = "voice")]
Expand Down
Loading