From 17463d37e7a6ea5784b39fb3b278dbf3599cc6c9 Mon Sep 17 00:00:00 2001 From: Alessandro Gorla Date: Wed, 2 Sep 2026 16:18:35 +0200 Subject: [PATCH 1/4] install-paca-skills.sh: non-lossy Agent Skills distribution Claude Code, Gemini CLI, and Cursor now all natively read the agentskills.io SKILL.md folder format (verified against each tool's own current official docs, not assumed) -- so install-paca-skills.sh now writes each skill verbatim, frontmatter and all, into that native folder for all three, instead of stripping frontmatter into a flat .md/.toml command file: - Claude Code -> ~/.claude/skills//SKILL.md (was ~/.claude/commands/.md) - Gemini CLI -> ~/.gemini/skills//SKILL.md (was ~/.gemini/commands/.toml) - Cursor -> /.cursor/skills//SKILL.md (was .cursor/commands/.md) AGENTS.md is unchanged: it's a single shared file, not a per-skill directory, so it still strips frontmatter and re-shapes each skill into a plain markdown section. Also fixes a latent fidelity bug this change surfaced: the bundled-skill fetch used `jq -r '.content'`, which appends its own trailing newline regardless of whether the source string already ends in one -- every previous target reshaped or stripped the body anyway so the extra blank line was invisible, but it broke byte-for-byte verbatim copying for the new targets above. Switched to `jq -j` (join-output, no added newline). Verified against a live instance: output now matches the raw API response byte-for-byte (confirmed via diff and sha256sum across all three targets). Implements point 1 of Paca-AI/paca#453, alongside the separate com.gorlix.project-skills plugin (point 3, gorlix/paca-plugin-project-skills) which implements point 2/3. Testing: - shellcheck clean (fixed one pre-existing SC2016 info-level note in a line this change didn't otherwise touch, now that this script is linted for the first time -- see scripts-pr-ci.yml). - New install-paca-skills-smoke CI job: runs the real script against a local fixture HTTP server (not a stubbed curl) and asserts byte-exact SKILL.md content for every native target, plus AGENTS.md's frontmatter-stripped section. - Manually verified against a real docker-compose.dev.yml instance: ran the script for real, diffed and sha256-compared every installed file against the live GET /api/v1/skills response. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/scripts-pr-ci.yml | 109 +++++++++++++++++++++++++++- docs/guides/install-skills.md | 16 ++-- scripts/install-paca-skills.sh | 92 ++++++++++++----------- 3 files changed, 165 insertions(+), 52 deletions(-) diff --git a/.github/workflows/scripts-pr-ci.yml b/.github/workflows/scripts-pr-ci.yml index 93263bc74..7b8185655 100644 --- a/.github/workflows/scripts-pr-ci.yml +++ b/.github/workflows/scripts-pr-ci.yml @@ -5,6 +5,8 @@ on: paths: - "scripts/install.sh" - "scripts/upgrade.sh" + - "scripts/install-paca-skills.sh" + - "docs/guides/install-skills.md" - "deploy/docker-compose.prod.yml" - "deploy/caddy/Caddyfile" - ".github/workflows/scripts-pr-ci.yml" @@ -33,7 +35,7 @@ jobs: fetch-depth: 1 - name: Run shellcheck - run: shellcheck --shell=bash scripts/install.sh scripts/upgrade.sh + run: shellcheck --shell=bash scripts/install.sh scripts/upgrade.sh scripts/install-paca-skills.sh # --------------------------------------------------------------------------- # 2. install.sh smoke test — runs the real script fully non-interactively @@ -241,3 +243,108 @@ jobs: exit 1 fi echo "All upgrade.sh migration assertions passed." + + # --------------------------------------------------------------------------- + # 4. install-paca-skills.sh smoke test — runs the real script fully + # non-interactively against a tiny local HTTP server that serves fixed + # fixture JSON for GET /api/v1/skills and GET /api/v1/plugins (rather + # than stubbing curl/wget like upgrade-smoke's fake docker — the script's + # own real curl/wget calls hit this fixture server for real). Asserts + # that every native-folder target (Claude Code, Gemini CLI, Cursor) gets + # a byte-for-byte-verbatim SKILL.md — the whole point of the non-lossy + # change this job exists to guard — and that AGENTS.md still gets its + # frontmatter-stripped section. + # --------------------------------------------------------------------------- + install-paca-skills-smoke: + name: install-paca-skills.sh smoke test + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Start a fake Paca API serving fixture skills + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/fixture" + cat <<'JSON' > "$RUNNER_TEMP/fixture/skills.json" + {"success":true,"data":{"skills":[ + {"name":"paca-fixture-one","path":"paca-fixture-one/SKILL.md","content":"---\nname: paca-fixture-one\ndescription: A fixture skill used only by CI to verify non-lossy install.\ntriggers:\n - /paca-fixture-one\n---\n\nFixture body one.\n"}, + {"name":"paca-fixture-two","path":"paca-fixture-two/SKILL.md","content":"---\nname: paca-fixture-two\ndescription: A second fixture skill with no triggers (always-active).\n---\n\nFixture body two.\n"} + ]}} + JSON + echo '{"success":true,"data":{"plugins":[]}}' > "$RUNNER_TEMP/fixture/plugins.json" + cat <<'PY' > "$RUNNER_TEMP/fixture/server.py" + import http.server, sys, pathlib + port = int(sys.argv[1]) + fixture_dir = pathlib.Path(sys.argv[2]) + class Handler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + if self.path == "/api/v1/skills": + body = (fixture_dir / "skills.json").read_bytes() + elif self.path == "/api/v1/plugins": + body = (fixture_dir / "plugins.json").read_bytes() + else: + self.send_response(404) + self.end_headers() + return + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(body) + def log_message(self, *args): + pass + http.server.HTTPServer(("127.0.0.1", port), Handler).serve_forever() + PY + python3 "$RUNNER_TEMP/fixture/server.py" 8933 "$RUNNER_TEMP/fixture" & + echo $! > "$RUNNER_TEMP/fixture/server.pid" + for _ in $(seq 1 20); do + curl -fsS "http://127.0.0.1:8933/api/v1/skills" >/dev/null 2>&1 && break + sleep 0.2 + done + + - name: Run install-paca-skills.sh against the fixture server + run: | + set -euo pipefail + export HOME="$RUNNER_TEMP/fakehome" + mkdir -p "$HOME" + WORK="$RUNNER_TEMP/fixture-project" + mkdir -p "$WORK" + cd "$WORK" + git init -q + PACA_API_URL="http://127.0.0.1:8933" bash "$GITHUB_WORKSPACE/scripts/install-paca-skills.sh" --platforms=claude,gemini,cursor,agents + + - name: Verify non-lossy SKILL.md folders were written for every native target + run: | + set -euo pipefail + export HOME="$RUNNER_TEMP/fakehome" + WORK="$RUNNER_TEMP/fixture-project" + for target_dir in "$HOME/.claude/skills" "$HOME/.gemini/skills" "$WORK/.cursor/skills"; do + for name in paca-fixture-one paca-fixture-two; do + f="$target_dir/$name/SKILL.md" + test -f "$f" || { echo "missing $f" >&2; exit 1; } + grep -qx "name: $name" "$f" || { echo "$f missing/invalid frontmatter name" >&2; exit 1; } + done + done + python3 -c " + import json + with open('$RUNNER_TEMP/fixture/skills.json') as fh: + want = json.load(fh)['data']['skills'][0]['content'] + with open('$HOME/.claude/skills/paca-fixture-one/SKILL.md') as fh: + got = fh.read() + assert got == want, f'content mismatch:\n--- want ---\n{want!r}\n--- got ---\n{got!r}' + " + grep -q "BEGIN PACA SKILLS" "$WORK/AGENTS.md" + grep -q "paca-fixture-one" "$WORK/AGENTS.md" + if grep -qx -- "---" "$WORK/AGENTS.md"; then + echo "AGENTS.md should never contain a raw frontmatter fence line" >&2 + exit 1 + fi + echo "All install-paca-skills.sh non-lossy assertions passed." + + - name: Stop fake API server + if: always() + run: kill "$(cat "$RUNNER_TEMP/fixture/server.pid")" 2>/dev/null || true diff --git a/docs/guides/install-skills.md b/docs/guides/install-skills.md index f3c13dd60..6e4f75bc3 100644 --- a/docs/guides/install-skills.md +++ b/docs/guides/install-skills.md @@ -15,14 +15,14 @@ PACA_API_URL=http://localhost:8080 \ If `PACA_API_URL` isn't set and you're running the script interactively (a real terminal attached), it prompts for it instead of failing outright. `PACA_API_KEY` is optional — both endpoints are publicly readable — but the prompt offers to collect it too, in case your deployment locks things down further. Both endpoint calls require `jq`. -The installer copies every bundled skill to every supported platform found on this machine: +The installer copies every bundled skill to every supported platform found on this machine, in each tool's own native Agent Skills folder format ([agentskills.io](https://agentskills.io/specification): a directory per skill containing `SKILL.md`, frontmatter intact) wherever that tool supports it — non-lossy, not flattened or re-shaped: | Platform | Location | Scope | |---|---|---| -| Claude Code | `~/.claude/commands/.md` | Global — every session | -| Gemini CLI | `~/.gemini/commands/.toml` | Global — every session | -| Cursor | `/.cursor/commands/.md` | Per-project (Cursor has no global commands directory) | -| Any AGENTS.md-reading tool (Codex, Windsurf, OpenCode, …) | `/AGENTS.md` | Per-project, merged into a marker-delimited section — re-running the installer refreshes only that section and leaves the rest of the file alone | +| Claude Code | `~/.claude/skills//SKILL.md` | Global — every session | +| Gemini CLI | `~/.gemini/skills//SKILL.md` | Global — every session | +| Cursor | `/.cursor/skills//SKILL.md` | Per-project (Cursor has no global commands directory) | +| Any AGENTS.md-reading tool (Codex, Windsurf, OpenCode, …) | `/AGENTS.md` | Per-project, merged into a marker-delimited section — re-running the installer refreshes only that section and leaves the rest of the file alone. This is the one target that still strips frontmatter: AGENTS.md is a single shared file, not a per-skill directory. | The per-project targets (Cursor, AGENTS.md) are only written when the installer is run from inside a git working tree — run it from your project root to get those too. @@ -244,11 +244,11 @@ If Paca MCP tools are not available, say so and ask the user to run `/paca-setup ```bash # Claude Code -rm ~/.claude/commands/paca*.md +rm -rf ~/.claude/skills/paca-* # Gemini CLI -rm ~/.gemini/commands/paca*.toml +rm -rf ~/.gemini/skills/paca-* # Cursor (run from the project root) -rm .cursor/commands/paca*.md +rm -rf .cursor/skills/paca-* ``` For AGENTS.md, remove the block between `` and `` — everything else in the file is untouched by the installer and safe to keep. diff --git a/scripts/install-paca-skills.sh b/scripts/install-paca-skills.sh index ac6300319..2a005e0b8 100755 --- a/scripts/install-paca-skills.sh +++ b/scripts/install-paca-skills.sh @@ -3,12 +3,16 @@ # # Installs Paca's bundled skills — plus skills contributed by plugins # enabled on your Paca instance — into every supported AI coding tool found -# on this machine: +# on this machine, in each tool's own native Agent Skills folder format +# (agentskills.io: a directory per skill containing SKILL.md, frontmatter +# intact) — non-lossy, since all three tools below now read that format +# directly: # -# - Claude Code → ~/.claude/commands/.md (global, slash commands) -# - Gemini CLI → ~/.gemini/commands/.toml (global, slash commands) -# - Cursor → /.cursor/commands/.md (project-scoped; Cursor has -# no global commands directory) +# - Claude Code → ~/.claude/skills//SKILL.md (global) +# - Gemini CLI → ~/.gemini/skills//SKILL.md (global) +# - Cursor → /.cursor/skills//SKILL.md (project-scoped; Cursor +# has no global commands +# directory) # - Any AGENTS.md-reading tool (Codex, Windsurf, OpenCode, ...) # → /AGENTS.md (project-scoped, merged into # a marker-delimited section so @@ -17,9 +21,11 @@ # The project-scoped targets (Cursor, AGENTS.md) are only written when this # script is run from inside a git working tree. # -# Skills are Agent Skills format (YAML frontmatter + markdown body). This -# script strips the frontmatter for Claude Code / Cursor / AGENTS.md, and -# re-shapes it into Gemini CLI's TOML command format. +# Skills are Agent Skills format (YAML frontmatter + markdown body). The +# Claude Code / Gemini CLI / Cursor targets above get that content verbatim, +# frontmatter included — AGENTS.md is the one target that still strips +# frontmatter and re-shapes each skill into a plain markdown section, since +# AGENTS.md is a single shared file, not a per-skill directory. # # All skill content — both Paca's bundled defaults and anything contributed # by an installed plugin — is fetched from a running Paca instance's API @@ -55,8 +61,8 @@ set -euo pipefail REPO="Paca-AI/paca" BRANCH="master" -CLAUDE_DIR="${HOME}/.claude/commands" -GEMINI_DIR="${HOME}/.gemini/commands" +CLAUDE_DIR="${HOME}/.claude/skills" +GEMINI_DIR="${HOME}/.gemini/skills" PACA_API_URL="${PACA_API_URL:-}" PACA_API_KEY="${PACA_API_KEY:-}" @@ -266,8 +272,8 @@ if [[ -n "${PACA_SKILL_PLATFORMS}" ]]; then elif { : < /dev/tty; } 2>/dev/null; then echo "" info "Which platforms should skills be installed to?" - info " 1) claude — Claude Code (~/.claude/commands/)" - info " 2) gemini — Gemini CLI (~/.gemini/commands/)" + info " 1) claude — Claude Code (~/.claude/skills/)" + info " 2) gemini — Gemini CLI (~/.gemini/skills/)" info " 3) cursor — Cursor (project-scoped, needs a git working tree)" info " 4) agents — AGENTS.md (project-scoped, needs a git working tree)" read -r -p " Enter numbers or names, space/comma-separated (Enter for all): " platform_choice < /dev/tty @@ -307,12 +313,12 @@ PROJECT_ROOT="" if $INSTALL_CURSOR || $INSTALL_AGENTS; then if git rev-parse --is-inside-work-tree &>/dev/null; then PROJECT_ROOT="$(git rev-parse --show-toplevel)" - if $INSTALL_CURSOR; then mkdir -p "${PROJECT_ROOT}/.cursor/commands"; fi + if $INSTALL_CURSOR; then mkdir -p "${PROJECT_ROOT}/.cursor/skills"; fi project_targets="AGENTS.md" if $INSTALL_CURSOR && $INSTALL_AGENTS; then - project_targets="Cursor commands + AGENTS.md" + project_targets="Cursor skills + AGENTS.md" elif $INSTALL_CURSOR; then - project_targets="Cursor commands" + project_targets="Cursor skills" fi info "Project detected (${PROJECT_ROOT}) — also installing ${project_targets} there" else @@ -387,12 +393,6 @@ frontmatter_field() { ' "$file" } -# TOML basic ("...") strings need backslash/quote escaping; used for the -# short single-line `description` field. -toml_basic_string() { - printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -} - # ─── Install one skill into every target ─────────────────────────────────── # $1 = skill name (from the directory / manifest / plugin declaration — # authoritative, never re-derived from frontmatter) @@ -400,38 +400,37 @@ toml_basic_string() { # $3 = label for logging/summary (e.g. "bundled" or "plugin:com.paca.example") install_one_skill() { local name="$1" raw="$2" label="$3" - local description body + local description description="$(frontmatter_field "${raw}" "description")" - body="$(strip_frontmatter "${raw}")" - # Claude Code + # Claude Code, Gemini CLI, Cursor: each tool's own native Agent Skills + # folder format (agentskills.io) — the raw SKILL.md is copied verbatim, + # frontmatter included, into a directory named after the skill. Non-lossy: + # nothing is stripped or re-shaped, unlike the AGENTS.md branch below. if $INSTALL_CLAUDE; then - printf '%s\n' "${body}" > "${CLAUDE_DIR}/${name}.md" + mkdir -p "${CLAUDE_DIR}/${name}" + cp "${raw}" "${CLAUDE_DIR}/${name}/SKILL.md" fi - # Gemini CLI — TOML. `prompt` uses a literal '''...''' multi-line string - # (zero escaping) since skill bodies routinely contain double quotes (JSON - # snippets, etc.); `description` uses a basic "..." string since it's a - # short single line where backslash/quote escaping is cheap and reliable. if $INSTALL_GEMINI; then - if printf '%s' "${body}" | grep -qF "'''"; then - warn "Skill '${name}' body contains ''' — cannot safely embed as a TOML literal string, skipping Gemini CLI install for it" - else - { - printf 'description = "%s"\n' "$(toml_basic_string "${description}")" - printf "prompt = '''\n%s\n'''\n" "${body}" - } > "${GEMINI_DIR}/${name}.toml" - fi + mkdir -p "${GEMINI_DIR}/${name}" + cp "${raw}" "${GEMINI_DIR}/${name}/SKILL.md" fi # Cursor — project-scoped only. if $INSTALL_CURSOR && [[ -n "${PROJECT_ROOT}" ]]; then - printf '%s\n' "${body}" > "${PROJECT_ROOT}/.cursor/commands/${name}.md" + mkdir -p "${PROJECT_ROOT}/.cursor/skills/${name}" + cp "${raw}" "${PROJECT_ROOT}/.cursor/skills/${name}/SKILL.md" fi - # AGENTS.md — project-scoped only. Skip a name already appended (see - # AGENTS_SEEN_TMP above) instead of duplicating its section. + # AGENTS.md — project-scoped only, and the one target that still strips + # frontmatter: it's a single shared file, not a per-skill directory, so + # each skill becomes a plain markdown section rather than getting its own + # SKILL.md. Skip a name already appended (see AGENTS_SEEN_TMP above) + # instead of duplicating its section. + local body + body="$(strip_frontmatter "${raw}")" if $INSTALL_AGENTS && [[ -n "${AGENTS_TMP}" ]] && ! grep -qxF "${name}" "${AGENTS_SEEN_TMP}"; then printf '%s\n' "${name}" >> "${AGENTS_SEEN_TMP}" { @@ -481,7 +480,13 @@ while IFS= read -r skill_obj; do name="$(jq -r '.name' <<<"${skill_obj}")" [[ -z "${name}" || "${name}" == "null" ]] && continue raw="$(mktemp)" - jq -r '.content' <<<"${skill_obj}" > "${raw}" + # -j (join-output), not -r: -r appends its own trailing newline after the + # value regardless of whether the string already ends in one, which used + # to be invisible (every non-lossy... no, every target used to re-shape or + # strip the body anyway) but would silently add a spurious blank line to + # the byte-for-byte-verbatim SKILL.md files install_one_skill now writes + # for Claude Code / Gemini CLI / Cursor. + jq -j '.content' <<<"${skill_obj}" > "${raw}" install_one_skill "${name}" "${raw}" "bundled" rm -f "${raw}" bundled_count=$((bundled_count + 1)) @@ -572,7 +577,8 @@ if [[ -n "${PROJECT_ROOT}" && -n "${AGENTS_TMP}" ]]; then { printf '%s\n\n' "${begin_marker}" printf '# Paca Skills\n\n' - printf 'Installed by `scripts/install-paca-skills.sh`. Re-run it to refresh this section.\n\n' + # shellcheck disable=SC2016 # literal backticks for markdown code formatting, not shell expansion + printf '%s\n\n' 'Installed by `scripts/install-paca-skills.sh`. Re-run it to refresh this section.' cat "${AGENTS_TMP}" printf '%s\n' "${end_marker}" } > "${block_tmp}" @@ -626,7 +632,7 @@ echo " Where they went:" $INSTALL_CLAUDE && echo " Claude Code → ${CLAUDE_DIR}/" $INSTALL_GEMINI && echo " Gemini CLI → ${GEMINI_DIR}/" if [[ -n "${PROJECT_ROOT}" ]]; then - $INSTALL_CURSOR && echo " Cursor → ${PROJECT_ROOT}/.cursor/commands/" + $INSTALL_CURSOR && echo " Cursor → ${PROJECT_ROOT}/.cursor/skills/" $INSTALL_AGENTS && echo " AGENTS.md → ${PROJECT_ROOT}/AGENTS.md" fi echo "" From 39d43e9ed1035ebe6941fbd99e140ee0d182bbf5 Mon Sep 17 00:00:00 2001 From: Alessandro Gorla Date: Wed, 2 Sep 2026 16:36:29 +0200 Subject: [PATCH 2/4] Restore Gemini CLI legacy TOML as a fallback alongside the native folder Live-tested this branch's Gemini change on a real machine and found the docs weren't the whole story: the installed tool there is Google's Antigravity IDE, documented as Gemini CLI's successor, and a skill written only to the native ~/.gemini/skills//SKILL.md folder did not show up as an available skill -- confirmed both before and after restarting the app, ruling out a simple caching/reload issue. Rather than trust "the docs say it works" over an actual negative result, this restores the pre-existing ~/.gemini/commands/.toml write (frontmatter stripped, re-shaped into Gemini's TOML command format -- exactly what this script did before this branch) alongside the new native folder, instead of replacing it. Whichever mechanism a given Gemini installation actually reads, the skill is available either way; nobody loses functionality they had before this branch. Real terminal Gemini CLI itself wasn't available to test against here, so this stays a belt-and-suspenders fallback rather than a conclusion that the native path is broken -- it may well work correctly there. The point is not shipping a hard cutover for a target this couldn't be verified against end-to-end. Updates: install-paca-skills.sh (GEMINI_LEGACY_DIR + restored TOML generation in install_one_skill), docs/guides/install-skills.md (table + uninstall section), scripts-pr-ci.yml (install-paca-skills-smoke now also asserts the legacy .toml is written). Co-Authored-By: Claude Sonnet 5 --- .github/workflows/scripts-pr-ci.yml | 13 +++++ docs/guides/install-skills.md | 5 +- scripts/install-paca-skills.sh | 85 ++++++++++++++++++++++------- 3 files changed, 80 insertions(+), 23 deletions(-) diff --git a/.github/workflows/scripts-pr-ci.yml b/.github/workflows/scripts-pr-ci.yml index 7b8185655..b3f5ef576 100644 --- a/.github/workflows/scripts-pr-ci.yml +++ b/.github/workflows/scripts-pr-ci.yml @@ -343,6 +343,19 @@ jobs: echo "AGENTS.md should never contain a raw frontmatter fence line" >&2 exit 1 fi + + # Gemini CLI legacy fallback: kept alongside the native folder + # above (not a replacement) after live testing on a real machine + # showed a real Gemini-successor app (Antigravity IDE) not + # discovering skills from the native folder alone — see the + # header comment in install-paca-skills.sh. + for name in paca-fixture-one paca-fixture-two; do + f="$HOME/.gemini/commands/$name.toml" + test -f "$f" || { echo "missing legacy fallback $f" >&2; exit 1; } + grep -q '^description = ' "$f" || { echo "$f missing description field" >&2; exit 1; } + grep -qF "prompt = '''" "$f" || { echo "$f missing prompt field" >&2; exit 1; } + done + echo "All install-paca-skills.sh non-lossy assertions passed." - name: Stop fake API server diff --git a/docs/guides/install-skills.md b/docs/guides/install-skills.md index 6e4f75bc3..c39808def 100644 --- a/docs/guides/install-skills.md +++ b/docs/guides/install-skills.md @@ -20,7 +20,7 @@ The installer copies every bundled skill to every supported platform found on th | Platform | Location | Scope | |---|---|---| | Claude Code | `~/.claude/skills//SKILL.md` | Global — every session | -| Gemini CLI | `~/.gemini/skills//SKILL.md` | Global — every session | +| Gemini CLI | `~/.gemini/skills//SKILL.md` (native) **and** `~/.gemini/commands/.toml` (legacy fallback — kept because a real Antigravity IDE install, documented as Gemini CLI's successor, didn't pick up a skill written only to the native folder) | Global — every session | | Cursor | `/.cursor/skills//SKILL.md` | Per-project (Cursor has no global commands directory) | | Any AGENTS.md-reading tool (Codex, Windsurf, OpenCode, …) | `/AGENTS.md` | Per-project, merged into a marker-delimited section — re-running the installer refreshes only that section and leaves the rest of the file alone. This is the one target that still strips frontmatter: AGENTS.md is a single shared file, not a per-skill directory. | @@ -245,8 +245,9 @@ If Paca MCP tools are not available, say so and ask the user to run `/paca-setup ```bash # Claude Code rm -rf ~/.claude/skills/paca-* -# Gemini CLI +# Gemini CLI (native + legacy fallback) rm -rf ~/.gemini/skills/paca-* +rm -f ~/.gemini/commands/paca-*.toml # Cursor (run from the project root) rm -rf .cursor/skills/paca-* ``` diff --git a/scripts/install-paca-skills.sh b/scripts/install-paca-skills.sh index 2a005e0b8..a565c5f91 100755 --- a/scripts/install-paca-skills.sh +++ b/scripts/install-paca-skills.sh @@ -5,11 +5,9 @@ # enabled on your Paca instance — into every supported AI coding tool found # on this machine, in each tool's own native Agent Skills folder format # (agentskills.io: a directory per skill containing SKILL.md, frontmatter -# intact) — non-lossy, since all three tools below now read that format -# directly: +# intact) wherever that's verified to work — non-lossy: # # - Claude Code → ~/.claude/skills//SKILL.md (global) -# - Gemini CLI → ~/.gemini/skills//SKILL.md (global) # - Cursor → /.cursor/skills//SKILL.md (project-scoped; Cursor # has no global commands # directory) @@ -18,14 +16,36 @@ # a marker-delimited section so # any other content is preserved) # +# Gemini CLI gets BOTH the native folder AND the legacy TOML command, not +# just the former: +# +# - Gemini CLI → ~/.gemini/skills//SKILL.md (global, native — per +# Gemini CLI's own current +# docs) +# ~/.gemini/commands/.toml (global, legacy — kept as +# a safety net: verified on +# a real machine running +# Google's Antigravity IDE, +# documented as Gemini CLI's +# successor, that a skill +# written only to the native +# folder above did NOT show +# up as an available skill, +# even after restarting the +# app — so "the docs say it +# works" isn't enough here to +# drop the previously-working +# path) +# # The project-scoped targets (Cursor, AGENTS.md) are only written when this # script is run from inside a git working tree. # -# Skills are Agent Skills format (YAML frontmatter + markdown body). The -# Claude Code / Gemini CLI / Cursor targets above get that content verbatim, -# frontmatter included — AGENTS.md is the one target that still strips -# frontmatter and re-shapes each skill into a plain markdown section, since -# AGENTS.md is a single shared file, not a per-skill directory. +# Skills are Agent Skills format (YAML frontmatter + markdown body). Claude +# Code, Cursor, and Gemini CLI's native folder all get that content verbatim, +# frontmatter included. AGENTS.md and Gemini CLI's legacy TOML command are +# the two targets that still strip frontmatter and re-shape the content — +# AGENTS.md because it's a single shared file, not a per-skill directory; +# the TOML command because that format has no frontmatter concept at all. # # All skill content — both Paca's bundled defaults and anything contributed # by an installed plugin — is fetched from a running Paca instance's API @@ -63,6 +83,8 @@ REPO="Paca-AI/paca" BRANCH="master" CLAUDE_DIR="${HOME}/.claude/skills" GEMINI_DIR="${HOME}/.gemini/skills" +# Legacy fallback — see the Gemini CLI note in the header comment above for why. +GEMINI_LEGACY_DIR="${HOME}/.gemini/commands" PACA_API_URL="${PACA_API_URL:-}" PACA_API_KEY="${PACA_API_KEY:-}" @@ -273,7 +295,7 @@ elif { : < /dev/tty; } 2>/dev/null; then echo "" info "Which platforms should skills be installed to?" info " 1) claude — Claude Code (~/.claude/skills/)" - info " 2) gemini — Gemini CLI (~/.gemini/skills/)" + info " 2) gemini — Gemini CLI (~/.gemini/skills/ + ~/.gemini/commands/ fallback)" info " 3) cursor — Cursor (project-scoped, needs a git working tree)" info " 4) agents — AGENTS.md (project-scoped, needs a git working tree)" read -r -p " Enter numbers or names, space/comma-separated (Enter for all): " platform_choice < /dev/tty @@ -303,7 +325,7 @@ done info "Installing to: ${PACA_SKILL_PLATFORMS// /, }" if $INSTALL_CLAUDE; then mkdir -p "${CLAUDE_DIR}"; fi -if $INSTALL_GEMINI; then mkdir -p "${GEMINI_DIR}"; fi +if $INSTALL_GEMINI; then mkdir -p "${GEMINI_DIR}" "${GEMINI_LEGACY_DIR}"; fi # Project-scope detection — Cursor has no global commands directory, and # AGENTS.md is a project-root convention, so both only make sense relative @@ -393,6 +415,12 @@ frontmatter_field() { ' "$file" } +# TOML basic ("...") strings need backslash/quote escaping; used for the +# short single-line `description` field in the Gemini CLI legacy fallback. +toml_basic_string() { + printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' +} + # ─── Install one skill into every target ─────────────────────────────────── # $1 = skill name (from the directory / manifest / plugin declaration — # authoritative, never re-derived from frontmatter) @@ -400,14 +428,15 @@ frontmatter_field() { # $3 = label for logging/summary (e.g. "bundled" or "plugin:com.paca.example") install_one_skill() { local name="$1" raw="$2" label="$3" - local description + local description body description="$(frontmatter_field "${raw}" "description")" + body="$(strip_frontmatter "${raw}")" - # Claude Code, Gemini CLI, Cursor: each tool's own native Agent Skills - # folder format (agentskills.io) — the raw SKILL.md is copied verbatim, - # frontmatter included, into a directory named after the skill. Non-lossy: - # nothing is stripped or re-shaped, unlike the AGENTS.md branch below. + # Claude Code, Cursor, and Gemini CLI's native folder: each tool's own + # native Agent Skills folder format (agentskills.io) — the raw SKILL.md is + # copied verbatim, frontmatter included, into a directory named after the + # skill. Non-lossy: nothing is stripped or re-shaped here. if $INSTALL_CLAUDE; then mkdir -p "${CLAUDE_DIR}/${name}" cp "${raw}" "${CLAUDE_DIR}/${name}/SKILL.md" @@ -416,6 +445,21 @@ install_one_skill() { if $INSTALL_GEMINI; then mkdir -p "${GEMINI_DIR}/${name}" cp "${raw}" "${GEMINI_DIR}/${name}/SKILL.md" + + # Legacy fallback — see the Gemini CLI note in the header comment for + # why this stays alongside the native folder above instead of replacing + # it. `prompt` uses a literal '''...''' multi-line string (zero escaping) + # since skill bodies routinely contain double quotes (JSON snippets, + # etc.); `description` uses a basic "..." string since it's a short + # single line where backslash/quote escaping is cheap and reliable. + if printf '%s' "${body}" | grep -qF "'''"; then + warn "Skill '${name}' body contains ''' — cannot safely embed as a TOML literal string, skipping the Gemini CLI legacy command for it" + else + { + printf 'description = "%s"\n' "$(toml_basic_string "${description}")" + printf "prompt = '''\n%s\n'''\n" "${body}" + } > "${GEMINI_LEGACY_DIR}/${name}.toml" + fi fi # Cursor — project-scoped only. @@ -424,13 +468,12 @@ install_one_skill() { cp "${raw}" "${PROJECT_ROOT}/.cursor/skills/${name}/SKILL.md" fi - # AGENTS.md — project-scoped only, and the one target that still strips - # frontmatter: it's a single shared file, not a per-skill directory, so - # each skill becomes a plain markdown section rather than getting its own + # AGENTS.md — project-scoped only, and (along with the Gemini CLI legacy + # fallback above) one of the two targets that still use the stripped + # `body`: it's a single shared file, not a per-skill directory, so each + # skill becomes a plain markdown section rather than getting its own # SKILL.md. Skip a name already appended (see AGENTS_SEEN_TMP above) # instead of duplicating its section. - local body - body="$(strip_frontmatter "${raw}")" if $INSTALL_AGENTS && [[ -n "${AGENTS_TMP}" ]] && ! grep -qxF "${name}" "${AGENTS_SEEN_TMP}"; then printf '%s\n' "${name}" >> "${AGENTS_SEEN_TMP}" { @@ -630,7 +673,7 @@ tac "${SUMMARY_TMP}" | awk '!seen[$1]++' | tac echo "" echo " Where they went:" $INSTALL_CLAUDE && echo " Claude Code → ${CLAUDE_DIR}/" -$INSTALL_GEMINI && echo " Gemini CLI → ${GEMINI_DIR}/" +$INSTALL_GEMINI && echo " Gemini CLI → ${GEMINI_DIR}/ (native) + ${GEMINI_LEGACY_DIR}/ (legacy fallback)" if [[ -n "${PROJECT_ROOT}" ]]; then $INSTALL_CURSOR && echo " Cursor → ${PROJECT_ROOT}/.cursor/skills/" $INSTALL_AGENTS && echo " AGENTS.md → ${PROJECT_ROOT}/AGENTS.md" From a539dd7be259f6314805788f0332ae4b36aff427 Mon Sep 17 00:00:00 2001 From: Alessandro Gorla Date: Wed, 2 Sep 2026 17:09:13 +0200 Subject: [PATCH 3/4] Write to Antigravity's real plugin-based skill mechanism, verified live The previous commit's fallback (keep the legacy TOML alongside the new native folder) was a hedge against not knowing why Antigravity wasn't picking up ~/.gemini/skills/. Investigated further instead of settling for the hedge, and found the actual mechanism by reading antigravity.google's own docs plus inspecting a real installation: Antigravity's real skills come from *installed plugins* -- ~/.gemini/config/plugins//, each with a plugin.json manifest (name/version/description/author -- no skill list) and a skills/ subfolder scanned automatically. Confirmed by finding Google's own bundled "science" plugin in exactly that shape on the test machine. Replicated that shape for a synthetic "paca" plugin (~/.gemini/config/plugins/paca/{plugin.json,installed_version.json,skills/}), written once per run for the manifest and once per skill for the skills/ subfolder. Verified end-to-end on the real Antigravity install that was previously negative: all 12 bundled skills now show up in its skills panel and in the /pac slash-command search, confirmed again after a full app restart. Gemini CLI's own documented ~/.gemini/skills/ folder and the legacy ~/.gemini/commands/ TOML write are both kept unconditionally alongside this -- neither hurts, and the classic terminal Gemini CLI tool (as opposed to Antigravity) was never available to test, so this doesn't drop a path that might work there. Updated the install-paca-skills-smoke CI job to assert plugin.json, installed_version.json, and byte-exact skill content under the new path, and updated docs/guides/install-skills.md accordingly. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/scripts-pr-ci.yml | 30 +++++++-- docs/guides/install-skills.md | 5 +- scripts/install-paca-skills.sh | 100 ++++++++++++++++++++-------- 3 files changed, 101 insertions(+), 34 deletions(-) diff --git a/.github/workflows/scripts-pr-ci.yml b/.github/workflows/scripts-pr-ci.yml index b3f5ef576..882fbab47 100644 --- a/.github/workflows/scripts-pr-ci.yml +++ b/.github/workflows/scripts-pr-ci.yml @@ -344,11 +344,8 @@ jobs: exit 1 fi - # Gemini CLI legacy fallback: kept alongside the native folder - # above (not a replacement) after live testing on a real machine - # showed a real Gemini-successor app (Antigravity IDE) not - # discovering skills from the native folder alone — see the - # header comment in install-paca-skills.sh. + # Gemini CLI legacy TOML fallback (pre-existing behavior, kept + # unconditionally — see header comment in install-paca-skills.sh). for name in paca-fixture-one paca-fixture-two; do f="$HOME/.gemini/commands/$name.toml" test -f "$f" || { echo "missing legacy fallback $f" >&2; exit 1; } @@ -356,6 +353,29 @@ jobs: grep -qF "prompt = '''" "$f" || { echo "$f missing prompt field" >&2; exit 1; } done + # Google Antigravity's real plugin-based skill mechanism — verified + # against a live Antigravity install (see header comment). A + # plugin.json + installed_version.json must exist for the + # synthetic "paca" plugin, plus each skill verbatim under its + # skills/ subfolder. + PLUGIN_DIR="$HOME/.gemini/config/plugins/paca" + test -f "$PLUGIN_DIR/plugin.json" || { echo "missing $PLUGIN_DIR/plugin.json" >&2; exit 1; } + grep -q '"name": "paca"' "$PLUGIN_DIR/plugin.json" || { echo "plugin.json missing name field" >&2; exit 1; } + test -f "$PLUGIN_DIR/installed_version.json" || { echo "missing $PLUGIN_DIR/installed_version.json" >&2; exit 1; } + for name in paca-fixture-one paca-fixture-two; do + f="$PLUGIN_DIR/skills/$name/SKILL.md" + test -f "$f" || { echo "missing $f" >&2; exit 1; } + grep -qx "name: $name" "$f" || { echo "$f missing/invalid frontmatter name" >&2; exit 1; } + done + python3 -c " + import json + with open('$RUNNER_TEMP/fixture/skills.json') as fh: + want = json.load(fh)['data']['skills'][0]['content'] + with open('$PLUGIN_DIR/skills/paca-fixture-one/SKILL.md') as fh: + got = fh.read() + assert got == want, f'Antigravity plugin skill content mismatch:\n--- want ---\n{want!r}\n--- got ---\n{got!r}' + " + echo "All install-paca-skills.sh non-lossy assertions passed." - name: Stop fake API server diff --git a/docs/guides/install-skills.md b/docs/guides/install-skills.md index c39808def..7e2cef6d4 100644 --- a/docs/guides/install-skills.md +++ b/docs/guides/install-skills.md @@ -20,7 +20,7 @@ The installer copies every bundled skill to every supported platform found on th | Platform | Location | Scope | |---|---|---| | Claude Code | `~/.claude/skills//SKILL.md` | Global — every session | -| Gemini CLI | `~/.gemini/skills//SKILL.md` (native) **and** `~/.gemini/commands/.toml` (legacy fallback — kept because a real Antigravity IDE install, documented as Gemini CLI's successor, didn't pick up a skill written only to the native folder) | Global — every session | +| Gemini CLI / Google Antigravity | `~/.gemini/config/plugins/paca/skills//SKILL.md` (Antigravity's real plugin-based skill mechanism — verified working against a live Antigravity install), plus `~/.gemini/skills//SKILL.md` (per Gemini CLI's own docs, unverified against the classic terminal tool) and `~/.gemini/commands/.toml` (pre-existing legacy fallback) | Global — every session | | Cursor | `/.cursor/skills//SKILL.md` | Per-project (Cursor has no global commands directory) | | Any AGENTS.md-reading tool (Codex, Windsurf, OpenCode, …) | `/AGENTS.md` | Per-project, merged into a marker-delimited section — re-running the installer refreshes only that section and leaves the rest of the file alone. This is the one target that still strips frontmatter: AGENTS.md is a single shared file, not a per-skill directory. | @@ -245,7 +245,8 @@ If Paca MCP tools are not available, say so and ask the user to run `/paca-setup ```bash # Claude Code rm -rf ~/.claude/skills/paca-* -# Gemini CLI (native + legacy fallback) +# Gemini CLI / Google Antigravity (plugin + native + legacy fallback) +rm -rf ~/.gemini/config/plugins/paca rm -rf ~/.gemini/skills/paca-* rm -f ~/.gemini/commands/paca-*.toml # Cursor (run from the project root) diff --git a/scripts/install-paca-skills.sh b/scripts/install-paca-skills.sh index a565c5f91..6058deebf 100755 --- a/scripts/install-paca-skills.sh +++ b/scripts/install-paca-skills.sh @@ -16,36 +16,54 @@ # a marker-delimited section so # any other content is preserved) # -# Gemini CLI gets BOTH the native folder AND the legacy TOML command, not -# just the former: +# Gemini CLI / Google Antigravity get THREE writes, not one — see below for +# why this isn't as redundant as it looks: # -# - Gemini CLI → ~/.gemini/skills//SKILL.md (global, native — per -# Gemini CLI's own current -# docs) -# ~/.gemini/commands/.toml (global, legacy — kept as -# a safety net: verified on -# a real machine running -# Google's Antigravity IDE, -# documented as Gemini CLI's -# successor, that a skill -# written only to the native -# folder above did NOT show -# up as an available skill, -# even after restarting the -# app — so "the docs say it -# works" isn't enough here to -# drop the previously-working -# path) +# - ~/.gemini/config/plugins/paca/skills//SKILL.md +# Verified on a real Google Antigravity IDE install (documented as +# Gemini CLI's successor) to be the format that ACTUALLY makes a skill +# show up in its skills list and slash-command search. Antigravity's +# real skills come from installed "plugins" — a directory under +# ~/.gemini/config/plugins// containing a plugin.json +# (name/version/description/author — no skill list; skills are +# discovered by scanning that plugin's own skills/ subfolder) — the +# same shape as e.g. Google's own bundled ~/.gemini/config/plugins/science/ +# plugin. This script writes (once) a minimal plugin.json + +# installed_version.json for a synthetic "paca" plugin, then each +# skill verbatim under its skills/ folder. +# - ~/.gemini/skills//SKILL.md +# What Gemini CLI's own current official docs (geminicli.com) describe +# as the native per-skill folder. Confirmed NOT read by the Antigravity +# install tested above, across a restart — but that docs page describes +# the classic terminal `gemini` tool specifically, not Antigravity, and +# the terminal tool wasn't available to test. Written anyway: harmless +# if unread, and this is the one path directly backed by that product's +# own documentation. +# - ~/.gemini/commands/.toml +# The pre-existing behavior from before this script's non-lossy +# rewrite (frontmatter stripped, re-shaped into a TOML custom command). +# Kept unconditionally alongside the two paths above so nothing that +# already worked for a Gemini-lineage user stops working. +# +# - Claude Code → ~/.claude/skills//SKILL.md (global) +# - Cursor → /.cursor/skills//SKILL.md (project-scoped; Cursor +# has no global commands +# directory) +# - Any AGENTS.md-reading tool (Codex, Windsurf, OpenCode, ...) +# → /AGENTS.md (project-scoped, merged into +# a marker-delimited section so +# any other content is preserved) # # The project-scoped targets (Cursor, AGENTS.md) are only written when this # script is run from inside a git working tree. # # Skills are Agent Skills format (YAML frontmatter + markdown body). Claude -# Code, Cursor, and Gemini CLI's native folder all get that content verbatim, -# frontmatter included. AGENTS.md and Gemini CLI's legacy TOML command are -# the two targets that still strip frontmatter and re-shape the content — -# AGENTS.md because it's a single shared file, not a per-skill directory; -# the TOML command because that format has no frontmatter concept at all. +# Code, Cursor, and both Gemini/Antigravity SKILL.md paths above get that +# content verbatim, frontmatter included. AGENTS.md and the Gemini CLI legacy +# TOML command are the two targets that still strip frontmatter and re-shape +# the content — AGENTS.md because it's a single shared file, not a per-skill +# directory; the TOML command because that format has no frontmatter concept +# at all. # # All skill content — both Paca's bundled defaults and anything contributed # by an installed plugin — is fetched from a running Paca instance's API @@ -85,6 +103,11 @@ CLAUDE_DIR="${HOME}/.claude/skills" GEMINI_DIR="${HOME}/.gemini/skills" # Legacy fallback — see the Gemini CLI note in the header comment above for why. GEMINI_LEGACY_DIR="${HOME}/.gemini/commands" +# Google Antigravity's actual plugin-based skill mechanism — see the header +# comment above for how this was found and verified. +GEMINI_PLUGIN_DIR="${HOME}/.gemini/config/plugins/paca" +GEMINI_PLUGIN_SKILLS_DIR="${GEMINI_PLUGIN_DIR}/skills" +GEMINI_PLUGIN_VERSION="1.0.0" PACA_API_URL="${PACA_API_URL:-}" PACA_API_KEY="${PACA_API_KEY:-}" @@ -295,7 +318,7 @@ elif { : < /dev/tty; } 2>/dev/null; then echo "" info "Which platforms should skills be installed to?" info " 1) claude — Claude Code (~/.claude/skills/)" - info " 2) gemini — Gemini CLI (~/.gemini/skills/ + ~/.gemini/commands/ fallback)" + info " 2) gemini — Gemini CLI / Antigravity (~/.gemini/config/plugins/paca/skills/)" info " 3) cursor — Cursor (project-scoped, needs a git working tree)" info " 4) agents — AGENTS.md (project-scoped, needs a git working tree)" read -r -p " Enter numbers or names, space/comma-separated (Enter for all): " platform_choice < /dev/tty @@ -325,7 +348,24 @@ done info "Installing to: ${PACA_SKILL_PLATFORMS// /, }" if $INSTALL_CLAUDE; then mkdir -p "${CLAUDE_DIR}"; fi -if $INSTALL_GEMINI; then mkdir -p "${GEMINI_DIR}" "${GEMINI_LEGACY_DIR}"; fi +if $INSTALL_GEMINI; then + mkdir -p "${GEMINI_DIR}" "${GEMINI_LEGACY_DIR}" "${GEMINI_PLUGIN_SKILLS_DIR}" + # Written once per run, not per skill — this is the plugin package's own + # metadata, not a skill. See the header comment for why this file (plus + # the skills/ subfolder populated below) is what actually makes Antigravity + # recognize "paca" as an installed plugin and list its skills. + cat > "${GEMINI_PLUGIN_DIR}/plugin.json" < "${GEMINI_PLUGIN_DIR}/installed_version.json" +fi # Project-scope detection — Cursor has no global commands directory, and # AGENTS.md is a project-root convention, so both only make sense relative @@ -443,6 +483,12 @@ install_one_skill() { fi if $INSTALL_GEMINI; then + # The verified-working path — see the header comment. plugin.json and + # installed_version.json for the enclosing "paca" plugin are already + # written once, above, before this loop starts. + mkdir -p "${GEMINI_PLUGIN_SKILLS_DIR}/${name}" + cp "${raw}" "${GEMINI_PLUGIN_SKILLS_DIR}/${name}/SKILL.md" + mkdir -p "${GEMINI_DIR}/${name}" cp "${raw}" "${GEMINI_DIR}/${name}/SKILL.md" @@ -673,7 +719,7 @@ tac "${SUMMARY_TMP}" | awk '!seen[$1]++' | tac echo "" echo " Where they went:" $INSTALL_CLAUDE && echo " Claude Code → ${CLAUDE_DIR}/" -$INSTALL_GEMINI && echo " Gemini CLI → ${GEMINI_DIR}/ (native) + ${GEMINI_LEGACY_DIR}/ (legacy fallback)" +$INSTALL_GEMINI && echo " Gemini CLI → ${GEMINI_PLUGIN_SKILLS_DIR}/ (Antigravity plugin, verified) + ${GEMINI_DIR}/ + ${GEMINI_LEGACY_DIR}/ (unverified fallbacks)" if [[ -n "${PROJECT_ROOT}" ]]; then $INSTALL_CURSOR && echo " Cursor → ${PROJECT_ROOT}/.cursor/skills/" $INSTALL_AGENTS && echo " AGENTS.md → ${PROJECT_ROOT}/AGENTS.md" From 8332615d53f1ad51b6dc1a6e848847f430d89f4f Mon Sep 17 00:00:00 2001 From: Alessandro Gorla Date: Wed, 2 Sep 2026 23:06:47 +0200 Subject: [PATCH 4/4] Address pullfrog review comments on the header/docs comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove a leftover mid-sentence self-correction ("every non-lossy... no, every target...") in two places, and fix the trailing platform list next to it to name all four verbatim-write targets (it still said "Claude Code / Gemini CLI / Cursor", missing the Antigravity plugin path added since). - Collapse the header's duplicated Claude Code/Cursor/AGENTS.md list (appeared once before the Gemini section, verbatim again right after it) into a single list, with the Gemini detail following. - Fix "Cursor has no global commands directory" (script header + docs/guides/install-skills.md): Cursor does support a global ~/.cursor/skills/ — the installer stays project-scoped by choice, not because no global option exists. - Soften the installed_version.json claim: nothing in Google's docs or the bundled science plugin backs its existence — it's presumably app-written bookkeeping, harmless to write and useful as a concrete file for the CI smoke test to assert on, but not part of the verified discovery mechanism (plugin.json + skills/ is). Verified: shellcheck clean, no other diff. --- docs/guides/install-skills.md | 2 +- scripts/install-paca-skills.sh | 34 +++++++++++++++------------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/docs/guides/install-skills.md b/docs/guides/install-skills.md index 7e2cef6d4..dae7b0229 100644 --- a/docs/guides/install-skills.md +++ b/docs/guides/install-skills.md @@ -21,7 +21,7 @@ The installer copies every bundled skill to every supported platform found on th |---|---|---| | Claude Code | `~/.claude/skills//SKILL.md` | Global — every session | | Gemini CLI / Google Antigravity | `~/.gemini/config/plugins/paca/skills//SKILL.md` (Antigravity's real plugin-based skill mechanism — verified working against a live Antigravity install), plus `~/.gemini/skills//SKILL.md` (per Gemini CLI's own docs, unverified against the classic terminal tool) and `~/.gemini/commands/.toml` (pre-existing legacy fallback) | Global — every session | -| Cursor | `/.cursor/skills//SKILL.md` | Per-project (Cursor has no global commands directory) | +| Cursor | `/.cursor/skills//SKILL.md` | Per-project by choice (Cursor also supports a global `~/.cursor/skills/`; the installer stays project-scoped) | | Any AGENTS.md-reading tool (Codex, Windsurf, OpenCode, …) | `/AGENTS.md` | Per-project, merged into a marker-delimited section — re-running the installer refreshes only that section and leaves the rest of the file alone. This is the one target that still strips frontmatter: AGENTS.md is a single shared file, not a per-skill directory. | The per-project targets (Cursor, AGENTS.md) are only written when the installer is run from inside a git working tree — run it from your project root to get those too. diff --git a/scripts/install-paca-skills.sh b/scripts/install-paca-skills.sh index 6058deebf..105f50828 100755 --- a/scripts/install-paca-skills.sh +++ b/scripts/install-paca-skills.sh @@ -8,9 +8,10 @@ # intact) wherever that's verified to work — non-lossy: # # - Claude Code → ~/.claude/skills//SKILL.md (global) -# - Cursor → /.cursor/skills//SKILL.md (project-scoped; Cursor -# has no global commands -# directory) +# - Cursor → /.cursor/skills//SKILL.md (project-scoped by choice — +# Cursor also supports a global +# ~/.cursor/skills/, this +# installer just doesn't use it) # - Any AGENTS.md-reading tool (Codex, Windsurf, OpenCode, ...) # → /AGENTS.md (project-scoped, merged into # a marker-delimited section so @@ -28,9 +29,13 @@ # (name/version/description/author — no skill list; skills are # discovered by scanning that plugin's own skills/ subfolder) — the # same shape as e.g. Google's own bundled ~/.gemini/config/plugins/science/ -# plugin. This script writes (once) a minimal plugin.json + -# installed_version.json for a synthetic "paca" plugin, then each -# skill verbatim under its skills/ folder. +# plugin. This script writes (once) a minimal plugin.json — the only +# file that matters for discovery, confirmed above — for a synthetic +# "paca" plugin, then each skill verbatim under its skills/ folder. +# It also writes installed_version.json alongside plugin.json: +# unverified against any doc or the science plugin (which ships +# without one) — presumably app-written bookkeeping — but harmless to +# include and gives the CI smoke test a real file to assert on. # - ~/.gemini/skills//SKILL.md # What Gemini CLI's own current official docs (geminicli.com) describe # as the native per-skill folder. Confirmed NOT read by the Antigravity @@ -45,15 +50,6 @@ # Kept unconditionally alongside the two paths above so nothing that # already worked for a Gemini-lineage user stops working. # -# - Claude Code → ~/.claude/skills//SKILL.md (global) -# - Cursor → /.cursor/skills//SKILL.md (project-scoped; Cursor -# has no global commands -# directory) -# - Any AGENTS.md-reading tool (Codex, Windsurf, OpenCode, ...) -# → /AGENTS.md (project-scoped, merged into -# a marker-delimited section so -# any other content is preserved) -# # The project-scoped targets (Cursor, AGENTS.md) are only written when this # script is run from inside a git working tree. # @@ -571,10 +567,10 @@ while IFS= read -r skill_obj; do raw="$(mktemp)" # -j (join-output), not -r: -r appends its own trailing newline after the # value regardless of whether the string already ends in one, which used - # to be invisible (every non-lossy... no, every target used to re-shape or - # strip the body anyway) but would silently add a spurious blank line to - # the byte-for-byte-verbatim SKILL.md files install_one_skill now writes - # for Claude Code / Gemini CLI / Cursor. + # to be invisible (every previous target re-shaped or stripped the body + # anyway) but would silently add a spurious blank line to the + # byte-for-byte-verbatim SKILL.md files install_one_skill now writes for + # Claude Code, Cursor, and both Gemini/Antigravity paths. jq -j '.content' <<<"${skill_obj}" > "${raw}" install_one_skill "${name}" "${raw}" "bundled" rm -f "${raw}"