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
22 changes: 22 additions & 0 deletions .agent/configs/skill-sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,28 @@
"last_checked": "2026-01-24T12:00:00Z",
"merge_strategy": "added",
"notes": "Includes 18 rule files in .agent/tools/video/heygen-skill/rules/"
},
{
"name": "caldav-calendar",
"upstream_url": "https://clawdhub.com/Asleep123/caldav-calendar",
"upstream_commit": "1.0.1",
"local_path": ".agent/tools/productivity/caldav-calendar-skill.md",
"format_detected": "clawdhub",
"imported_at": "2026-01-24T22:20:00Z",
"last_checked": "2026-01-24T22:20:00Z",
"merge_strategy": "added",
"notes": "ClawdHub v1.0.1 by @Asleep123 - CalDAV calendar sync via vdirsyncer + khal"
},
{
"name": "proxmox-full",
"upstream_url": "https://clawdhub.com/mSarheed/proxmox-full",
"upstream_commit": "1.0.0",
"local_path": ".agent/services/hosting/proxmox-full-skill.md",
"format_detected": "clawdhub",
"imported_at": "2026-01-24T22:20:00Z",
"last_checked": "2026-01-24T22:20:00Z",
"merge_strategy": "added",
"notes": "ClawdHub v1.0.0 by @mSarheed - Complete Proxmox VE management via REST API"
}
]
}
221 changes: 208 additions & 13 deletions .agent/scripts/add-skill-helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
# =============================================================================
# Add External Skill Helper
# =============================================================================
# Import external skills from GitHub repos, convert to aidevops format,
# handle conflicts, and track upstream sources for update detection.
# Import external skills from GitHub repos or ClawdHub, convert to aidevops
# format, handle conflicts, and track upstream sources for update detection.
#
# Usage:
# add-skill-helper.sh add <url|owner/repo> [--name <name>] [--force]
# add-skill-helper.sh add <url|owner/repo|clawdhub:slug> [--name <name>] [--force]
# add-skill-helper.sh list
# add-skill-helper.sh check-updates
# add-skill-helper.sh remove <name>
Expand All @@ -16,6 +16,8 @@
# add-skill-helper.sh add dmmulroy/cloudflare-skill
# add-skill-helper.sh add https://github.com/anthropics/skills/pdf
# add-skill-helper.sh add vercel-labs/agent-skills --name vercel
# add-skill-helper.sh add clawdhub:caldav-calendar
# add-skill-helper.sh add https://clawdhub.com/Asleep123/caldav-calendar
# add-skill-helper.sh check-updates
# =============================================================================

Expand Down Expand Up @@ -59,17 +61,17 @@ log_error() {

show_help() {
cat << 'EOF'
Add External Skill Helper - Import skills from GitHub to aidevops
Add External Skill Helper - Import skills from GitHub or ClawdHub to aidevops

USAGE:
add-skill-helper.sh <command> [options]

COMMANDS:
add <url|owner/repo> Import a skill from GitHub
list List all imported skills
check-updates Check for upstream updates
remove <name> Remove an imported skill
help Show this help message
add <url|owner/repo|clawdhub:slug> Import a skill
list List all imported skills
check-updates Check for upstream updates
remove <name> Remove an imported skill
help Show this help message

OPTIONS:
--name <name> Override the skill name
Expand All @@ -86,11 +88,21 @@ EXAMPLES:
# Import with custom name
add-skill-helper.sh add vercel-labs/agent-skills --name vercel-deploy

# Import from ClawdHub (shorthand)
add-skill-helper.sh add clawdhub:caldav-calendar

# Import from ClawdHub (full URL)
add-skill-helper.sh add https://clawdhub.com/Asleep123/caldav-calendar

# Check all imported skills for updates
add-skill-helper.sh check-updates

SUPPORTED SOURCES:
- GitHub repos (owner/repo or full URL)
- ClawdHub registry (clawdhub:slug or clawdhub.com URL)

SUPPORTED FORMATS:
- SKILL.md (OpenSkills/Claude Code format)
- SKILL.md (OpenSkills/Claude Code/ClawdHub format)
- AGENTS.md (aidevops/Windsurf format)
- .cursorrules (Cursor format)
- Raw markdown files
Expand Down Expand Up @@ -279,6 +291,10 @@ determine_target_path() {
category="tools/credentials"
elif echo "$content" | grep -qi "vercel\|coolify\|docker\|kubernetes"; then
category="tools/deployment"
elif echo "$content" | grep -qi "proxmox\|hypervisor\|virtualization\|vm.management"; then
category="services/hosting"
elif echo "$content" | grep -qi "calendar\|caldav\|ical\|scheduling"; then
category="tools/productivity"
elif echo "$content" | grep -qi "dns\|hosting\|domain"; then
category="services/hosting"
fi
Expand Down Expand Up @@ -483,16 +499,40 @@ cmd_add() {
esac
done

log_info "Parsing URL: $url"
log_info "Parsing source: $url"

# Detect ClawdHub source (clawdhub:slug or clawdhub.com URL)
local is_clawdhub=false
local clawdhub_slug=""

if [[ "$url" == clawdhub:* ]]; then
is_clawdhub=true
clawdhub_slug="${url#clawdhub:}"
elif [[ "$url" == *clawdhub.com* ]]; then
is_clawdhub=true
# Strip URL prefix and extract slug (last path segment)
clawdhub_slug="${url#*clawdhub.com/}"
clawdhub_slug="${clawdhub_slug#/}"
clawdhub_slug="${clawdhub_slug%/}"
# If format is owner/slug, take just the slug
if [[ "$clawdhub_slug" == */* ]]; then
clawdhub_slug="${clawdhub_slug##*/}"
fi
fi

if [[ "$is_clawdhub" == true ]]; then
cmd_add_clawdhub "$clawdhub_slug" "$custom_name" "$force" "$dry_run"
return $?
fi

# Parse GitHub URL
local parsed
parsed=$(parse_github_url "$url")
IFS='|' read -r owner repo subpath <<< "$parsed"

if [[ -z "$owner" || -z "$repo" ]]; then
log_error "Could not parse GitHub URL: $url"
log_info "Expected format: owner/repo or https://github.com/owner/repo"
log_error "Could not parse source URL: $url"
log_info "Expected: owner/repo, https://github.com/owner/repo, or clawdhub:slug"
return 1
fi

Expand Down Expand Up @@ -724,6 +764,161 @@ cmd_add() {
return 0
}

# Import a skill from ClawdHub registry
cmd_add_clawdhub() {
local slug="$1"
local custom_name="$2"
local force="$3"
local dry_run="$4"

if [[ -z "$slug" ]]; then
log_error "ClawdHub slug required"
return 1
fi

log_info "Importing from ClawdHub: $slug"

# Get skill metadata from API
local api_response
api_response=$(curl -s --connect-timeout 10 --max-time 30 "${CLAWDHUB_API:-https://clawdhub.com/api/v1}/skills/${slug}" 2>/dev/null)

Comment on lines +774 to +784
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate ClawdHub slugs before using them in filesystem paths.

slug is user-controlled and is embedded in fetch_dir that gets removed with rm -rf. A crafted value like ../ can escape the temp directory. Add strict validation (and strip query/fragment) before any path usage to keep the automation A‑grade secure.

🔒 Proposed fix
 cmd_add_clawdhub() {
     local slug="$1"
@@
     if [[ -z "$slug" ]]; then
         log_error "ClawdHub slug required"
         return 1
     fi
+    # Strip query/fragment and validate to prevent path traversal
+    slug="${slug%%\?*}"
+    slug="${slug%%#*}"
+    if [[ ! "$slug" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,63}$ ]]; then
+        log_error "Invalid ClawdHub slug: $slug"
+        return 1
+    fi

Also applies to: 851-856

🤖 Prompt for AI Agents
In @.agent/scripts/add-skill-helper.sh around lines 774 - 784, The ClawdHub
`slug` is used to build filesystem paths (e.g., `fetch_dir`) and must be
strictly validated and sanitized before any path operations or `rm -rf`; update
the code that reads `slug` to (1) strip any URL query/fragment and whitespace,
(2) reject or normalize values containing path separators or traversal tokens
(like `../`, `/`, `\`, or leading `-`), and (3) allow only a safe whitelist of
characters (e.g., lowercase letters, digits, hyphen, underscore, dot). Apply
this validation where `slug` is used (referencing the `slug` variable and the
`fetch_dir` usage and the removal block around lines 851-856) and fail early
with an error if validation fails to prevent directory escape.

if [[ -z "$api_response" ]] || ! echo "$api_response" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
log_error "Could not fetch skill info from ClawdHub API: $slug"
return 1
fi

# Extract metadata
local display_name summary owner_handle version
display_name=$(echo "$api_response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('skill',{}).get('displayName',''))" 2>/dev/null)
summary=$(echo "$api_response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('skill',{}).get('summary',''))" 2>/dev/null)
owner_handle=$(echo "$api_response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('owner',{}).get('handle',''))" 2>/dev/null)
version=$(echo "$api_response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('latestVersion',{}).get('version',''))" 2>/dev/null)
Comment on lines +792 to +795

Choose a reason for hiding this comment

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

medium

These repeated calls to python3 to parse the JSON response are inefficient as each call starts a new Python interpreter. Since jq is a dependency of this project, you can use it to extract all required values. This is more performant and consistent with other parts of the codebase.

Suggested change
display_name=$(echo "$api_response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('skill',{}).get('displayName',''))" 2>/dev/null)
summary=$(echo "$api_response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('skill',{}).get('summary',''))" 2>/dev/null)
owner_handle=$(echo "$api_response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('owner',{}).get('handle',''))" 2>/dev/null)
version=$(echo "$api_response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('latestVersion',{}).get('version',''))" 2>/dev/null)
display_name=$(echo "$api_response" | jq -r '.skill.displayName // ""')
summary=$(echo "$api_response" | jq -r '.skill.summary // ""')
owner_handle=$(echo "$api_response" | jq -r '.owner.handle // ""')
version=$(echo "$api_response" | jq -r '.latestVersion.version // ""')


log_info "Found: $display_name v${version} by @${owner_handle}"

# Determine skill name
local skill_name
if [[ -n "$custom_name" ]]; then
skill_name=$(to_kebab_case "$custom_name")
else
skill_name=$(to_kebab_case "$slug")
fi

# Determine target path
local target_path
target_path=$(determine_target_path "$skill_name" "$summary" ".")
log_info "Target path: .agent/$target_path"

# Check for conflicts
local conflicts
conflicts=$(check_conflicts "$target_path" ".agent") || true
if [[ -n "$conflicts" ]]; then
local blocking_conflicts
blocking_conflicts=$(echo "$conflicts" | grep -v "^INFO:" || true)

if [[ -n "$blocking_conflicts" && "$force" != true ]]; then
log_warning "Conflicts detected:"
echo "$blocking_conflicts" | while read -r conflict; do
echo " - ${conflict#*: }"
done
echo ""
echo "Options:"
echo " 1. Replace (overwrite existing)"
echo " 2. Separate (use different name)"
echo " 3. Skip (cancel import)"
echo ""
read -rp "Choose option [1-3]: " choice

case "$choice" in
1) log_info "Replacing existing..." ;;
2)
read -rp "Enter new name: " new_name
skill_name=$(to_kebab_case "$new_name")
target_path=$(determine_target_path "$skill_name" "$summary" ".")
;;
3|*) log_info "Import cancelled"; return 0 ;;
esac
fi
fi

if [[ "$dry_run" == true ]]; then
log_info "DRY RUN - Would create:"
echo " .agent/${target_path}.md"
return 0
fi

# Fetch SKILL.md content using clawdhub-helper.sh (Playwright-based)
local helper_script
helper_script="$(dirname "$0")/clawdhub-helper.sh"
local fetch_dir="${TMPDIR:-/tmp}/clawdhub-fetch/${slug}"

rm -rf "$fetch_dir"

if [[ -x "$helper_script" ]]; then
if ! "$helper_script" fetch "$slug" --output "$fetch_dir"; then
log_error "Failed to fetch SKILL.md from ClawdHub"
return 1
fi
else
log_error "clawdhub-helper.sh not found at: $helper_script"
return 1
fi

# Verify SKILL.md was fetched
if [[ ! -f "$fetch_dir/SKILL.md" || ! -s "$fetch_dir/SKILL.md" ]]; then
log_error "SKILL.md not found or empty after fetch"
return 1
fi

# Create target directory
local target_dir
target_dir=".agent/$(dirname "$target_path")"
mkdir -p "$target_dir"

# Convert to aidevops format
local target_file=".agent/${target_path}.md"

# Write aidevops-style header
local safe_summary
safe_summary=$(printf '%s' "${summary:-Imported from ClawdHub}" | sed 's/\\/\\\\/g; s/"/\\"/g')

cat > "$target_file" << EOF
---
description: "${safe_summary}"
mode: subagent
imported_from: clawdhub
clawdhub_slug: "${slug}"
clawdhub_version: "${version}"
---
# ${display_name:-$skill_name}

EOF

# Append the fetched SKILL.md content (skip any existing frontmatter)
awk '
BEGIN { in_frontmatter = 0; after_frontmatter = 0; has_frontmatter = 0 }
NR == 1 && /^---$/ { in_frontmatter = 1; has_frontmatter = 1; next }
in_frontmatter && /^---$/ { in_frontmatter = 0; after_frontmatter = 1; next }
in_frontmatter { next }
!has_frontmatter || after_frontmatter { print }
' "$fetch_dir/SKILL.md" >> "$target_file"

log_success "Created: $target_file"

# Register in skill-sources.json
local upstream_url="https://clawdhub.com/${owner_handle}/${slug}"
register_skill "$skill_name" "$upstream_url" ".agent/${target_path}.md" "clawdhub" "$version" "added" "ClawdHub v${version} by @${owner_handle}"

# Cleanup
rm -rf "$fetch_dir"

log_success "Skill '$skill_name' imported from ClawdHub successfully"
echo ""
log_info "Run './setup.sh' to create symlinks for other AI assistants"

return 0
}

cmd_list() {
ensure_skill_sources

Expand Down
Loading
Loading