Skip to content
Draft
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
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ browser = [
]

[dependencies]
# Skills content from datadog-labs/agent-skills, consumed as a Cargo git dep.
# Pinned to a specific rev so updates are explicit and auditable, matching
# the same pattern used for datadog-api-client below.
# Requires agent-skills to carry a Cargo.toml + src/lib.rs (see companion PR).
agent-skills = { git = "https://github.com/datadog-labs/agent-skills", rev = "c447f4d42f05fa8497c6fa0d1ee3889b7020dce3" }

# CLI (optional — not needed for browser WASM library)
clap = { version = "4", features = ["derive"], optional = true }
clap_complete = { version = "4", optional = true }
Expand Down
67 changes: 61 additions & 6 deletions src/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ pub struct SkillEntry {
/// Platform slug for entry_type == "extension". One of: "pi".
/// Empty for skills and agents.
pub platform: &'static str,
/// Files to materialize for entry_type == "extension".
/// Each tuple is `(relative_path_within_extension_dir, file_contents)`.
/// Empty for skills and agents.
/// Extra files to materialize alongside the entry.
/// Each tuple is `(relative_path, file_contents)`, written verbatim.
/// - For `extension`: relative to the extension's install dir; this is
/// the only source of files (`content` is empty).
/// - For `skill`: relative to the parent skill's install dir, used to
/// ship nested sub-skill SKILL.md files (e.g. `dd-apm` ships
/// `service-remapping/SKILL.md` and the k8s-ssi/linux-ssi trees).
/// - For `agent`: unused; leave empty.
pub files: &'static [(&'static str, &'static str)],
}

Expand Down Expand Up @@ -64,9 +69,9 @@ pub static SKILLS: &[SkillEntry] = &[
name: "dd-apm",
description: "APM - traces, services, dependencies, performance analysis.",
entry_type: "skill",
content: include_str!("../skills/dd-apm/SKILL.md"),
content: agent_skills::DD_APM_SKILL,
platform: "",
files: &[],
files: agent_skills::DD_APM_SUB_SKILLS,
},
SkillEntry {
name: "dd-debugger",
Expand Down Expand Up @@ -905,7 +910,19 @@ pub fn install_paths(
else {
return Ok(vec![]);
};
Ok(vec![(path, format_content(entry, &fmt))])
let mut out = vec![(path.clone(), format_content(entry, &fmt))];
// Skills can ship nested sub-skill files alongside the root SKILL.md
// (e.g. dd-apm's k8s-ssi/, linux-ssi/, service-remapping/ trees).
// Only applies when the parent installs as a skill directory; subagent
// .md files have no surrounding directory to nest under.
if fmt == InstallFormat::SkillMd && !entry.files.is_empty() {
if let Some(parent_dir) = path.parent() {
for (rel, body) in entry.files {
out.push((parent_dir.join(rel), (*body).to_string()));
}
}
}
Ok(out)
}

#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -1466,6 +1483,44 @@ mod tests {
assert_eq!(extensions_dir("claude-code", &root, false), None);
}

#[test]
fn test_install_paths_skill_with_sub_skills() {
static SUB_FILES: &[(&str, &str)] = &[
("service-remapping/SKILL.md", "# Service Remapping"),
("k8s-ssi/agent-install/SKILL.md", "# K8s Agent Install"),
];
let root = PathBuf::from("/tmp/proj");
let e = SkillEntry {
files: SUB_FILES,
..entry("dd-apm", "skill", "body")
};
let paths = install_paths(&e, "claude-code", &root, None, false).unwrap();
assert_eq!(paths.len(), 3);
assert_eq!(paths[0].0, root.join(".claude/skills/dd-apm/SKILL.md"));
assert_eq!(
paths[1].0,
root.join(".claude/skills/dd-apm/service-remapping/SKILL.md")
);
assert_eq!(
paths[2].0,
root.join(".claude/skills/dd-apm/k8s-ssi/agent-install/SKILL.md")
);
}

#[test]
fn test_install_paths_sub_skills_skipped_for_agent_md() {
// AgentMd format (Claude Code agents dir) has no surrounding directory,
// so sub-skill files must not be written.
static SUB_FILES: &[(&str, &str)] = &[("sub/SKILL.md", "# Sub")];
let root = PathBuf::from("/tmp/proj");
let e = SkillEntry {
files: SUB_FILES,
..entry("dd-apm", "agent", "body")
};
let paths = install_paths(&e, "claude-code", &root, None, false).unwrap();
assert_eq!(paths.len(), 1, "sub-skills must not be written for agent-md format");
}

#[test]
fn test_install_paths_skill_single_file() {
let root = PathBuf::from("/tmp/proj");
Expand Down
Loading