feat(tools): implement Slash Commands with frontmatter and file refs (M5 complete)

This commit implements the complete M5 milestone (Slash Commands) including:

Slash Command Parser (tools-slash):
- YAML frontmatter parsing with serde_yaml
- Metadata extraction (description, author, tags, version)
- Arbitrary frontmatter fields via flattened HashMap
- Graceful fallback for commands without frontmatter

Argument Substitution:
- $ARGUMENTS - all arguments joined by space
- $1, $2, $3, etc. - positional arguments
- Unmatched placeholders remain unchanged
- Empty arguments result in empty string for $ARGUMENTS

File Reference Resolution:
- @path syntax to include file contents inline
- Regex-based matching for file references
- Multiple file references supported
- Clear error messages for missing files

CLI Integration:
- Added `slash` subcommand: `owlen slash <command> <args...>`
- Loads commands from `.claude/commands/<name>.md`
- Permission checks for SlashCommand tool
- Automatic file reference resolution before output

Command Structure:
---
description: "Command description"
author: "Author name"
tags:
  - tag1
  - tag2
---
Command body with $ARGUMENTS and @file.txt references

Permission Enforcement:
- Plan mode: SlashCommand allowed (utility tool)
- All modes: SlashCommand respects permissions
- File references respect filesystem permissions

Testing:
- 10 tests in tools-slash for parser functionality
  - Frontmatter parsing with complex YAML
  - Argument substitution (all variants)
  - File reference resolution (single and multiple)
  - Edge cases (no frontmatter, empty args, etc.)
- 3 new tests in CLI for integration
  - slash_command_works (with args and frontmatter)
  - slash_command_file_refs (file inclusion)
  - slash_command_not_found (error handling)
- All 56 workspace tests passing 

Dependencies Added:
- serde_yaml 0.9 for YAML frontmatter parsing
- regex 1.12 for file reference pattern matching

M5 milestone complete! 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-01 19:41:42 +01:00
parent d7ddc365ec
commit 5134462deb
7 changed files with 413 additions and 0 deletions

View File

@@ -177,3 +177,79 @@ fn bash_command_timeout_works() {
.arg("1000");
cmd.assert().failure();
}
#[test]
fn slash_command_works() {
// Create .claude/commands directory in temp dir
let dir = tempdir().unwrap();
let commands_dir = dir.path().join(".claude/commands");
fs::create_dir_all(&commands_dir).unwrap();
// Create a test slash command
let command_content = r#"---
description: "Test command"
---
Hello from slash command!
Args: $ARGUMENTS
First: $1
"#;
let command_file = commands_dir.join("test.md");
fs::write(&command_file, command_content).unwrap();
// Execute slash command with args from the temp directory
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
cmd.current_dir(dir.path())
.arg("--mode")
.arg("code")
.arg("slash")
.arg("test")
.arg("arg1");
cmd.assert()
.success()
.stdout(predicates::str::contains("Hello from slash command!"))
.stdout(predicates::str::contains("Args: arg1"))
.stdout(predicates::str::contains("First: arg1"));
}
#[test]
fn slash_command_file_refs() {
let dir = tempdir().unwrap();
let commands_dir = dir.path().join(".claude/commands");
fs::create_dir_all(&commands_dir).unwrap();
// Create a file to reference
let data_file = dir.path().join("data.txt");
fs::write(&data_file, "Referenced content").unwrap();
// Create slash command with file reference
let command_content = format!("File content: @{}", data_file.display());
fs::write(commands_dir.join("reftest.md"), command_content).unwrap();
// Execute slash command
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
cmd.current_dir(dir.path())
.arg("--mode")
.arg("code")
.arg("slash")
.arg("reftest");
cmd.assert()
.success()
.stdout(predicates::str::contains("Referenced content"));
}
#[test]
fn slash_command_not_found() {
let dir = tempdir().unwrap();
// Try to execute non-existent slash command
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
cmd.current_dir(dir.path())
.arg("--mode")
.arg("code")
.arg("slash")
.arg("nonexistent");
cmd.assert().failure();
}