Repository navigation
fix: unify skill sync across all AI agents #757
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| #!/usr/bin/env bash | ||
| # ============================================================================ | ||
| # Claude Code Skills Installer | ||
| # skills.txt からスキルをインストールするスクリプト | ||
| # Agent Skills Installer | ||
| # skills.txt からスキルを全エージェント向けにインストールするスクリプト | ||
| # 対象: Claude Code, Codex, Cursor, Gemini CLI | ||
| # ============================================================================ | ||
|
|
||
| set -euo pipefail | ||
|
|
@@ -18,8 +19,9 @@ else | |
| fi | ||
|
|
||
| AGENTS_DIR="${HOME}/.agents/skills" | ||
| PROJECT_AGENTS_DIR="${PWD}/.agents/skills" | ||
|
|
||
| echo "[INFO] Claude スキルのインストールを開始します..." | ||
| echo "[INFO] エージェントスキルのインストールを開始します..." | ||
|
|
||
| # スキルリストが存在するか確認 | ||
| if [[ -z "$SKILLS_FILE" ]] || [[ ! -f "$SKILLS_FILE" ]]; then | ||
|
|
@@ -38,6 +40,12 @@ fi | |
| # スキルディレクトリを作成 | ||
| mkdir -p "$AGENTS_DIR" | ||
|
|
||
| # スキルの存在チェック(グローバルとプロジェクトの両方を確認) | ||
| skill_exists() { | ||
| local name="$1" | ||
| [[ -d "${AGENTS_DIR}/${name}" ]] || [[ -d "${PROJECT_AGENTS_DIR}/${name}" ]] | ||
| } | ||
|
|
||
| installed=0 | ||
| failed=0 | ||
| skipped=0 | ||
|
|
@@ -52,7 +60,7 @@ while IFS= read -r line || [[ -n "$line" ]]; do | |
|
|
||
| echo "[INFO] チェック中: ${skill}" | ||
|
|
||
| # スキルがすでにインストールされているか npx skills list -g で確認 | ||
| # スキルがすでにインストールされているか確認 | ||
| # 既知のリポジトリとスキル名のマッピング | ||
| case "$skill" in | ||
| "vercel-labs/agent-skills") | ||
|
|
@@ -65,21 +73,26 @@ while IFS= read -r line || [[ -n "$line" ]]; do | |
| check_skills=("find-skills") | ||
| ;; | ||
| "vercel-labs/agent-browser") | ||
| check_skills=("agent-browser" "skill-creator") | ||
| check_skills=("agent-browser") | ||
| ;; | ||
| "intellectronica/agent-skills") | ||
| check_skills=("context7") | ||
| ;; | ||
| *) | ||
| # 不明なリポジトリは basename を使用 | ||
| check_skills=("$(basename "$skill")") | ||
| # @tag 付きエントリ: owner/repo@skill-name → skill-name | ||
| # @tag なしエントリ: owner/repo → basename (repo) | ||
| if [[ "$skill" == */*@* ]]; then | ||
| check_skills=("${skill##*@}") | ||
| else | ||
| check_skills=("$(basename "$skill")") | ||
| fi | ||
| ;; | ||
| esac | ||
|
|
||
| # いずれかのスキルがインストールされているかチェック | ||
| all_installed=true | ||
| for check_skill in "${check_skills[@]}"; do | ||
| if [[ ! -d "${AGENTS_DIR}/${check_skill}" ]]; then | ||
| if ! skill_exists "$check_skill"; then | ||
| all_installed=false | ||
| break | ||
| fi | ||
|
|
@@ -93,8 +106,8 @@ while IFS= read -r line || [[ -n "$line" ]]; do | |
|
|
||
| echo "[INFO] インストール中: ${skill}" | ||
|
|
||
| # npx skills add でインストール(-y で自動確認、-g でグローバル) | ||
| if output=$(npx skills add "$skill" -y -g 2>&1); then | ||
| # npx skills add でインストール(-y で自動確認、-g でグローバル、-a '*' で全エージェント対応) | ||
| if output=$(npx skills add "$skill" -y -g -a '*' 2>&1); then | ||
| echo "[SUCCESS] 完了: ${skill}" | ||
| installed=$((installed + 1)) | ||
| else | ||
|
|
@@ -111,4 +124,159 @@ while IFS= read -r line || [[ -n "$line" ]]; do | |
| done < "$SKILLS_FILE" | ||
|
|
||
| echo "[INFO] スキル: ${installed} インストール完了、${skipped} スキップ、${failed} 失敗" | ||
| echo "[SUCCESS] Claude スキルのインストールが完了しました" | ||
|
|
||
| # Claude Code のローカルファイルを SKILL.md 形式に変換して .agents/skills/ に同期する関数 | ||
| # 引数: $1=ソースディレクトリ, $2=ラベル(ログ表示用) | ||
| sync_claude_to_agents() { | ||
| local src_dir="$1" | ||
| local label="$2" | ||
| local synced=0 | ||
|
|
||
| [[ -d "$src_dir" ]] || return 0 | ||
|
|
||
| for src_file in "$src_dir"/*.md; do | ||
| [[ -f "$src_file" ]] || continue | ||
| local name | ||
| name="$(basename "$src_file" .md)" | ||
| # README.md はスキップ | ||
| [[ "$name" == "README" ]] && continue | ||
|
|
||
| local target_dir="${PROJECT_AGENTS_DIR}/${name}" | ||
| local target_file="${target_dir}/SKILL.md" | ||
|
|
||
| # frontmatter の有無を判定 | ||
| local has_frontmatter | ||
| has_frontmatter=$(head -1 "$src_file" | grep -c '^---$' || true) | ||
|
|
||
| local description body | ||
| if [[ "$has_frontmatter" -gt 0 ]]; then | ||
| # frontmatter から description を抽出 | ||
| description=$(awk '/^---$/{n++; next} n==1 && /^description:/{sub(/^description: */, ""); print; exit}' "$src_file") | ||
| # frontmatter の後の本文を取得(2つ目の --- 以降) | ||
| body=$(awk 'BEGIN{n=0} /^---$/{n++; next} n>=2{print}' "$src_file") | ||
| # description が空なら本文の最初の見出しをフォールバック | ||
| if [[ -z "$description" ]]; then | ||
| description=$(echo "$body" | awk '/^#+ /{sub(/^#+ */, ""); print; exit}') | ||
| fi | ||
| else | ||
| # frontmatter なし: 最初の # 見出しを description として使用 | ||
| description=$(awk '/^#+ /{sub(/^#+ */, ""); print; exit}' "$src_file") | ||
| body=$(cat "$src_file") | ||
| fi | ||
|
|
||
| # description が空の場合はスキル名をフォールバック | ||
| [[ -z "$description" ]] && description="$name" | ||
|
|
||
| mkdir -p "$target_dir" | ||
| printf '%s\n' "---" \ | ||
| "name: ${name}" \ | ||
| "description: ${description}" \ | ||
| "metadata:" \ | ||
| " author: keito4" \ | ||
| " version: \"1.0.0\"" \ | ||
| "---" > "$target_file" | ||
| printf '%s\n' "$body" >> "$target_file" | ||
|
Comment on lines
+171
to
+178
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. YAML description field should be quoted to handle special characters. If the extracted description contains YAML special characters ( Proposed fix mkdir -p "$target_dir"
+ # Escape double quotes and wrap in quotes for YAML safety
+ local safe_description="${description//\"/\\\"}"
printf '%s\n' "---" \
"name: ${name}" \
- "description: ${description}" \
+ "description: \"${safe_description}\"" \
"metadata:" \🤖 Prompt for AI Agents |
||
|
|
||
| echo "[INFO] ${label}同期: ${name} → ${target_dir}" | ||
| synced=$((synced + 1)) | ||
| done | ||
| echo "[INFO] ${label}: ${synced} 件同期完了" | ||
| } | ||
|
|
||
| # カスタムスキル(.claude/skills/*.md)を同期 | ||
| CLAUDE_SKILLS_DIR="" | ||
| if [[ -n "$SKILLS_FILE" ]]; then | ||
| CLAUDE_SKILLS_DIR="$(dirname "$SKILLS_FILE")" | ||
| elif [[ -d "${PWD}/.claude/skills" ]]; then | ||
| CLAUDE_SKILLS_DIR="${PWD}/.claude/skills" | ||
| fi | ||
| sync_claude_to_agents "${CLAUDE_SKILLS_DIR:-}" "カスタムスキル" | ||
|
|
||
| # コマンド(.claude/commands/*.md)をスキルとして同期 | ||
| CLAUDE_COMMANDS_DIR="" | ||
| if [[ -n "$CLAUDE_SKILLS_DIR" ]]; then | ||
| CLAUDE_COMMANDS_DIR="$(dirname "$CLAUDE_SKILLS_DIR")/commands" | ||
| elif [[ -d "${PWD}/.claude/commands" ]]; then | ||
| CLAUDE_COMMANDS_DIR="${PWD}/.claude/commands" | ||
| fi | ||
| sync_claude_to_agents "${CLAUDE_COMMANDS_DIR:-}" "コマンド" | ||
|
|
||
| # ルール(.claude/rules/*.md)をスキルとして同期 | ||
| CLAUDE_RULES_DIR="" | ||
| if [[ -n "$CLAUDE_SKILLS_DIR" ]]; then | ||
| CLAUDE_RULES_DIR="$(dirname "$CLAUDE_SKILLS_DIR")/rules" | ||
| elif [[ -d "${PWD}/.claude/rules" ]]; then | ||
| CLAUDE_RULES_DIR="${PWD}/.claude/rules" | ||
| fi | ||
| sync_claude_to_agents "${CLAUDE_RULES_DIR:-}" "ルール" | ||
|
|
||
| # コマンドを Codex prompts(/command で呼び出し可能)にシンボリックリンク | ||
| # .codex/prompts/<name>.md → .claude/commands/<name>.md | ||
| CODEX_PROMPTS_DIR="${PWD}/.codex/prompts" | ||
| if [[ -n "${CLAUDE_COMMANDS_DIR:-}" && -d "$CLAUDE_COMMANDS_DIR" ]]; then | ||
| mkdir -p "$CODEX_PROMPTS_DIR" | ||
| prompts_synced=0 | ||
| for cmd_file in "$CLAUDE_COMMANDS_DIR"/*.md; do | ||
| [[ -f "$cmd_file" ]] || continue | ||
| cmd_name="$(basename "$cmd_file")" | ||
| [[ "$cmd_name" == "README.md" ]] && continue | ||
| link_path="${CODEX_PROMPTS_DIR}/${cmd_name}" | ||
| [[ -e "$link_path" || -L "$link_path" ]] && continue | ||
| ln -s "../../.claude/commands/${cmd_name}" "$link_path" | ||
| prompts_synced=$((prompts_synced + 1)) | ||
| done | ||
| echo "[INFO] Codex prompts: ${prompts_synced} 件リンク作成" | ||
| fi | ||
|
|
||
| # .agents/skills/ のスキルを各エージェントのスキルディレクトリにシンボリックリンク | ||
| # Codex (.codex/skills/), Cursor (.cursor/skills/), Gemini (.gemini/skills/) 等 | ||
| # 対象: プロジェクトレベル (.agents/skills/) の全スキル + カスタムスキル | ||
| # グローバル (~/.agents/skills/) は skills.txt 外のスキルを含むため全量リンクしない | ||
| AGENT_SKILL_DIRS=(".codex/skills" ".cursor/skills" ".gemini/skills") | ||
| linked=0 | ||
|
|
||
| # リンク対象スキル名を収集(プロジェクトレベル + カスタムスキル、重複排除) | ||
| declare -A all_skills | ||
| # プロジェクトレベルのスキル(npx skills add で project に入ったもの) | ||
| if [[ -d "$PROJECT_AGENTS_DIR" ]]; then | ||
| for skill_dir in "${PROJECT_AGENTS_DIR}"/*/; do | ||
| [[ -d "$skill_dir" ]] || continue | ||
| all_skills["$(basename "$skill_dir")"]=1 | ||
| done | ||
| fi | ||
| # カスタムスキル + コマンド + ルール(プロジェクトに同期済み) | ||
| for src_dir in "${CLAUDE_SKILLS_DIR:-}" "${CLAUDE_COMMANDS_DIR:-}" "${CLAUDE_RULES_DIR:-}"; do | ||
| [[ -n "$src_dir" && -d "$src_dir" ]] || continue | ||
| for skill_file in "$src_dir"/*.md; do | ||
| [[ -f "$skill_file" ]] || continue | ||
| skill_name="$(basename "$skill_file" .md)" | ||
| [[ "$skill_name" == "README" ]] && continue | ||
| all_skills["$skill_name"]=1 | ||
| done | ||
| done | ||
|
|
||
| for agent_dir in "${AGENT_SKILL_DIRS[@]}"; do | ||
| target_base="${PWD}/${agent_dir}" | ||
| mkdir -p "$target_base" | ||
| for skill_name in "${!all_skills[@]}"; do | ||
| link_path="${target_base}/${skill_name}" | ||
| # 既にリンクまたはディレクトリが存在する場合はスキップ(-L で壊れたリンクも検出) | ||
| if [[ -e "$link_path" || -L "$link_path" ]]; then | ||
| continue | ||
| fi | ||
| # プロジェクトレベルを優先、なければグローバルへリンク | ||
| # 相対パスはエージェントディレクトリが2階層深い前提(.codex/skills/, .cursor/skills/ 等) | ||
| if [[ -d "${PROJECT_AGENTS_DIR}/${skill_name}" ]]; then | ||
| ln -s "../../.agents/skills/${skill_name}" "$link_path" | ||
| elif [[ -d "${AGENTS_DIR}/${skill_name}" ]]; then | ||
| ln -s "${AGENTS_DIR}/${skill_name}" "$link_path" | ||
| else | ||
| continue | ||
| fi | ||
| echo "[INFO] リンク作成: ${agent_dir}/${skill_name}" | ||
| linked=$((linked + 1)) | ||
| done | ||
| done | ||
| echo "[INFO] シンボリックリンク: ${linked} 件作成" | ||
|
|
||
| echo "[SUCCESS] エージェントスキルのインストールが完了しました" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
macOS-specific path will fail in DevContainer/Linux environments.
/private/tmp/is a macOS-specific path (where/tmpis a symlink to/private/tmp). On Linux systems, including the DevContainer which runs Ubuntu, this path doesn't exist and will cause the doppler MCP server to fail or behave unexpectedly.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents