diff --git a/plugin/claude/.claude-plugin/plugin.json b/plugin/claude/.claude-plugin/plugin.json new file mode 100644 index 0000000..4d2c17e --- /dev/null +++ b/plugin/claude/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "dbxcli", + "displayName": "dbxcli", + "description": "Safely operate Dropbox through a locally installed dbxcli CLI, using its JSON manifest and schema-backed machine contract.", + "version": "0.1.0", + "author": { + "name": "Dropbox" + }, + "repository": "https://github.com/dropbox/dbxcli", + "license": "Apache-2.0", + "keywords": ["dropbox", "cli", "files", "cloud-storage"], + "defaultEnabled": true +} diff --git a/plugin/claude/README.md b/plugin/claude/README.md new file mode 100644 index 0000000..35b0372 --- /dev/null +++ b/plugin/claude/README.md @@ -0,0 +1,77 @@ +# dbxcli plugin for Claude Code + +A thin packaging layer that exposes the canonical `dbxcli` skill to Claude Code +as an installable plugin. + +## What this plugin does + +- Packages the portable `dbxcli` skill so Claude Code discovers it as + `/dbxcli:dbxcli`. +- Provides a Setup hook that detects whether `dbxcli` is installed and prints + platform-specific installation guidance if it is missing. + +The plugin does not implement Dropbox API calls, maintain a command catalog, or +store credentials. The local `dbxcli` executable is the runtime, and its JSON +help manifest is the source of truth for available commands, flags, and schemas. + +## Prerequisites + +- **Claude Code** — the plugin host. +- **dbxcli** — installed and on `PATH`. The plugin detects its absence but does + not install it automatically. + +Install dbxcli: + +| Platform | Command | +|----------|---------| +| macOS | `brew install dbxcli` | +| Windows | `winget install --exact --id Dropbox.dbxcli` | +| Linux | Download from [releases](https://github.com/dropbox/dbxcli/releases) or `go install github.com/dropbox/dbxcli/v3@latest` | + +For interactive use, authenticate with `dbxcli login`; automation may use +supported non-interactive authentication. + +## How the canonical skill is included + +The skill source of truth is `skills/dbxcli/` in the dbxcli repository. The +packaging script (`scripts/package-skills.sh`) copies that portable skill into +each platform artifact. Claude Code packaging adds only its manifest, hook, +script, and README; it does not own the skill's behavior. + +## Structure + +``` +dist/claude/dbxcli-plugin/ +├── .claude-plugin/plugin.json # Plugin manifest +├── skills/dbxcli/ # Copied from skills/dbxcli/ +│ ├── SKILL.md +│ ├── agents/ +│ └── references/ +├── hooks/hooks.json # Setup hook +├── scripts/detect-dbxcli.sh # Detection script +└── README.md # This file +``` + +## Build + +```bash +./scripts/package-skills.sh +``` + +## Test locally + +Load the plugin without installing: + +```bash +claude --plugin-dir dist/claude/dbxcli-plugin +``` + +Run the structural validation (no Dropbox auth required): + +```bash +./scripts/test-plugin.sh +``` + +If dbxcli is installed, the test also verifies that the Setup hook detection +script runs correctly and that `dbxcli version --output=json` returns a valid +response. diff --git a/plugin/claude/hooks/hooks.json b/plugin/claude/hooks/hooks.json new file mode 100644 index 0000000..049f29b --- /dev/null +++ b/plugin/claude/hooks/hooks.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "Setup": [ + { + "hooks": [ + { + "type": "command", + "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/detect-dbxcli.sh\"" + } + ] + } + ] + } +} diff --git a/plugin/claude/scripts/detect-dbxcli.sh b/plugin/claude/scripts/detect-dbxcli.sh new file mode 100755 index 0000000..a1fd2b8 --- /dev/null +++ b/plugin/claude/scripts/detect-dbxcli.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Detect dbxcli and print installation guidance if missing. +# This script is informational only — it never installs software. +set -euo pipefail + +if command -v dbxcli >/dev/null 2>&1; then + version_json="$(dbxcli version --output=json 2>/dev/null)" || true + + if printf '%s' "$version_json" | grep -q '"ok":true'; then + version="$(printf '%s' "$version_json" | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4)" + echo "dbxcli ${version} is installed and responding." + exit 0 + fi + + echo "dbxcli is on PATH but did not return valid JSON." + echo "Try running: dbxcli version --output=json" + exit 0 +fi + +echo "dbxcli is not installed." +echo "" + +case "$(uname -s)" in + Darwin) + echo "Install on macOS:" + echo " brew install dbxcli" + ;; + Linux) + echo "Install on Linux (pick one):" + echo " • Download from https://github.com/dropbox/dbxcli/releases" + echo " • go install github.com/dropbox/dbxcli/v3@latest" + ;; + MINGW*|MSYS*|CYGWIN*) + echo "Install on Windows:" + echo " winget install --exact --id Dropbox.dbxcli" + ;; + *) + echo "Install from source or download a release:" + echo " https://github.com/dropbox/dbxcli/releases" + ;; +esac + +echo "" +echo "After installing, run: dbxcli version --output=json" diff --git a/scripts/package-skills.sh b/scripts/package-skills.sh index 2ca7cf0..f34e133 100755 --- a/scripts/package-skills.sh +++ b/scripts/package-skills.sh @@ -3,6 +3,7 @@ set -euo pipefail root_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" source_dir="$root_dir/skills/dbxcli" +plugin_dir="$root_dir/plugin/claude" dist_dir="$root_dir/dist" chatgpt_dir="$dist_dir/chatgpt" claude_dir="$dist_dir/claude/dbxcli-plugin" @@ -12,9 +13,17 @@ for required in "$source_dir/SKILL.md" "$source_dir/agents/openai.yaml"; do [[ -f "$required" ]] || { echo "missing source file: $required" >&2; exit 1; } done +for required in "$plugin_dir/.claude-plugin/plugin.json" \ + "$plugin_dir/hooks/hooks.json" \ + "$plugin_dir/scripts/detect-dbxcli.sh" \ + "$plugin_dir/README.md"; do + [[ -f "$required" ]] || { echo "missing plugin file: $required" >&2; exit 1; } +done + rm -rf "$chatgpt_dir" "$claude_dir" "$openclaw_dir" -mkdir -p "$chatgpt_dir" "$claude_dir/.claude-plugin" "$claude_dir/skills" "$openclaw_dir/skills" +mkdir -p "$chatgpt_dir" "$claude_dir/skills" "$openclaw_dir/skills" +# ChatGPT: skill zip cp -R "$source_dir" "$chatgpt_dir/dbxcli" ( cd "$chatgpt_dir" @@ -22,15 +31,15 @@ cp -R "$source_dir" "$chatgpt_dir/dbxcli" zip -qr skill.zip dbxcli ) -cp -R "$source_dir" "$claude_dir/skills/dbxcli" -cat > "$claude_dir/.claude-plugin/plugin.json" <<'EOF' -{ - "name": "dbxcli", - "version": "0.1.0", - "description": "Safely operate Dropbox through a locally installed dbxcli CLI" -} -EOF +# Claude Code: plugin with skill, hooks, and scripts +cp -R "$plugin_dir/.claude-plugin" "$claude_dir/.claude-plugin" +cp -R "$plugin_dir/hooks" "$claude_dir/hooks" +cp -R "$plugin_dir/scripts" "$claude_dir/scripts" +cp "$plugin_dir/README.md" "$claude_dir/README.md" +cp -R "$source_dir" "$claude_dir/skills/dbxcli" +chmod +x "$claude_dir/scripts/detect-dbxcli.sh" +# OpenClaw: skill directory cp -R "$source_dir" "$openclaw_dir/skills/dbxcli" cat > "$openclaw_dir/openclaw.plugin.json" <<'EOF' { diff --git a/scripts/test-plugin.sh b/scripts/test-plugin.sh new file mode 100755 index 0000000..6dbba0b --- /dev/null +++ b/scripts/test-plugin.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Validate the Claude Code dbxcli plugin structure and detection script. +# Does not require Dropbox authentication. +set -euo pipefail + +root_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +plugin_dir="$root_dir/dist/claude/dbxcli-plugin" +source_dir="$root_dir/skills/dbxcli" +pass=0 +fail=0 + +check() { + local desc="$1" + shift + if "$@" >/dev/null 2>&1; then + echo " ok $desc" + pass=$((pass + 1)) + else + echo " FAIL $desc" + fail=$((fail + 1)) + fi +} + +echo "=== Plugin structure ===" + +check "plugin dist directory exists" test -d "$plugin_dir" +check "plugin.json exists" test -f "$plugin_dir/.claude-plugin/plugin.json" +check "plugin.json is valid JSON" python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$plugin_dir/.claude-plugin/plugin.json" +check "plugin.json has name field" python3 -c " +import json,sys +d = json.load(open(sys.argv[1])) +assert d.get('name') == 'dbxcli', 'name mismatch' +" "$plugin_dir/.claude-plugin/plugin.json" + +check "hooks.json exists" test -f "$plugin_dir/hooks/hooks.json" +check "hooks.json is valid JSON" python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$plugin_dir/hooks/hooks.json" +check "hooks.json has Setup hook" python3 -c " +import json,sys +d = json.load(open(sys.argv[1])) +assert 'Setup' in d.get('hooks', {}), 'no Setup hook' +" "$plugin_dir/hooks/hooks.json" + +check "detect-dbxcli.sh exists" test -f "$plugin_dir/scripts/detect-dbxcli.sh" +check "detect-dbxcli.sh is executable" test -x "$plugin_dir/scripts/detect-dbxcli.sh" +check "README.md exists" test -f "$plugin_dir/README.md" + +echo "" +echo "=== Skill content ===" + +check "skills/dbxcli/ directory exists" test -d "$plugin_dir/skills/dbxcli" +check "SKILL.md exists in packaged skill" test -f "$plugin_dir/skills/dbxcli/SKILL.md" +check "references/safety.md exists" test -f "$plugin_dir/skills/dbxcli/references/safety.md" +check "references/automation.md exists" test -f "$plugin_dir/skills/dbxcli/references/automation.md" +check "references/tool-integration.md exists" test -f "$plugin_dir/skills/dbxcli/references/tool-integration.md" + +check "SKILL.md matches canonical source" diff -q "$source_dir/SKILL.md" "$plugin_dir/skills/dbxcli/SKILL.md" +check "safety.md matches canonical source" diff -q "$source_dir/references/safety.md" "$plugin_dir/skills/dbxcli/references/safety.md" + +echo "" +echo "=== Host validation ===" + +if command -v claude >/dev/null 2>&1; then + check "claude plugin validate passes" claude plugin validate "$plugin_dir" +else + echo " skip claude CLI not found — host validation skipped" +fi + +echo "" +echo "=== Detection script ===" + +output="$("$plugin_dir/scripts/detect-dbxcli.sh" 2>&1)" || true +if command -v dbxcli >/dev/null 2>&1; then + check "detect script finds installed dbxcli" grep -q "installed" <<< "$output" + check "dbxcli version --output=json returns ok" bash -c 'dbxcli version --output=json | python3 -c "import json,sys; assert json.load(sys.stdin)[\"ok\"]"' +else + check "detect script reports dbxcli missing" grep -q "not installed" <<< "$output" +fi + +echo "" +echo "=== Results: $pass passed, $fail failed ===" +[[ $fail -eq 0 ]] diff --git a/skills/dbxcli/SKILL.md b/skills/dbxcli/SKILL.md index 3354abb..76da5bb 100644 --- a/skills/dbxcli/SKILL.md +++ b/skills/dbxcli/SKILL.md @@ -50,11 +50,10 @@ exit code and `.ok`: For destructive or externally visible actions, first discover the command and validate inputs, then prefer `--dry-run` if the manifest exposes it. Use an explicit `--if-exists` policy whenever it is available; never assume that a -default overwrite or conflict policy matches the user's intent. Require clear -user confirmation before the real destructive action unless the user has -already explicitly requested the exact action. When a command exposes `--yes`, -use it only after that confirmation to prevent an interactive prompt from -blocking automation. +default overwrite or conflict policy matches the user's intent. A successful +dry-run does not authorize execution; obtain explicit user confirmation before +the real destructive action. When a command exposes `--yes`, use it only after +that confirmation to prevent an interactive prompt from blocking automation. ## Large listings, search, and multi-step work diff --git a/skills/dbxcli/references/automation.md b/skills/dbxcli/references/automation.md index b3d6ab5..79169cd 100644 --- a/skills/dbxcli/references/automation.md +++ b/skills/dbxcli/references/automation.md @@ -8,6 +8,25 @@ Use `--dry-run` to preview a user-authorized mutation when it is offered. A successful preview is not permission to perform the real action: obtain or use the user's explicit confirmation for the real scope. +When a dry-run succeeds, it returns structured JSON with `ok: true` and +`results[].status` set to `"planned"`. Inspect all planned results before +proceeding: + +- Check that every planned target matches the user's requested scope. +- Verify file vs folder types, target paths, and operation count. +- Do not proceed if planned scope is broader than requested. + +Present a concise summary of the planned action to the user: + + Planned: delete /Reports/old-file.txt (file, 2.1MB) + Method: move to Dropbox trash (recoverable) + No changes have been made yet. + + Proceed with this deletion? + +Obtain explicit confirmation. A `"planned"` status proves feasibility only; it +does not authorize execution. + When `--if-exists` is available, pass an explicit value. Typical policies are `fail`, `skip`, and `autorename`; some commands also offer `overwrite`. Select only a policy compatible with the user's stated intent. In particular, do not diff --git a/skills/dbxcli/references/safety.md b/skills/dbxcli/references/safety.md index cc4cd3c..8c2b02a 100644 --- a/skills/dbxcli/references/safety.md +++ b/skills/dbxcli/references/safety.md @@ -20,6 +20,22 @@ changes, and creation or sharing of public links as meaningful external effects. Scope them to the user's request, preview when available, and confirm before executing the real mutation. +## Scope verification + +Before executing a mutation, verify that the operation matches the exact scope +the user authorized. Check: + +- Exact Dropbox paths or shared-link targets +- File vs folder type (if a path resolves to a folder when a file was expected, confirm) +- Single vs multiple targets (if dry-run shows more than expected, stop) +- Recursive behavior (do not add `--recursive` unless requested) +- Permanent vs recoverable deletion (do not add `--permanent` unless explicitly requested) +- Overwrite/conflict policy (use explicit `--if-exists`, do not silently choose `overwrite`) + +Do not silently expand scope beyond the user's request. If the requested target, +operation mode, or affected path count differs from what the user described, +stop and confirm the actual scope before proceeding. + The public [security policy](https://github.com/dropbox/dbxcli/blob/master/SECURITY.md) and [automation contract](https://github.com/dropbox/dbxcli/blob/master/docs/automation.md) contain the authoritative protocol and credential details.