From 51a1a48633a7b859cc34058a3227d7709132f82b Mon Sep 17 00:00:00 2001 From: Andrey Markelov Date: Thu, 3 Sep 2026 14:44:01 -0700 Subject: [PATCH] Add Claude Code marketplace manifest with proper architecture Implements proper marketplace distribution architecture: - Canonical skill source: skills/dbxcli/ (platform-neutral) - Self-contained plugin: plugin/claude/ with synced skill copy - Marketplace manifest: .claude-plugin/marketplace.json - Uses "source" field (not "path") - Includes required "owner" object - Points to ./plugin/claude (checked-in, not dist/) - Omits version fields (auto-updates via commit SHA) New tooling: - scripts/sync-claude-skill.sh: Syncs canonical skill to plugin - CI validation: Fails if plugin/claude/skills/dbxcli != skills/dbxcli - Updated test-plugin.sh: Validates marketplace structure and sync Enables zero-clone installation: /plugin marketplace add Dropbox/dbxcli /plugin install dbxcli@dbxcli Benefits: - Marketplace gets self-contained plugin (no path escaping after install) - Canonical skill remains platform-neutral (ChatGPT/OpenClaw can use it) - Claude packaging is not source of truth - No need to commit dist/ - CI enforces synchronization - Auto-updates via commit SHA (no manual version bumps during development) Co-Authored-By: Claude Sonnet 4.5 --- .claude-plugin/marketplace.json | 15 +++ .github/workflows/ci.yml | 19 ++++ .gitignore | 1 + plugin/claude/skills/dbxcli/SKILL.md | 92 +++++++++++++++++++ .../claude/skills/dbxcli/agents/openai.yaml | 6 ++ .../skills/dbxcli/references/automation.md | 48 ++++++++++ .../claude/skills/dbxcli/references/safety.md | 41 +++++++++ .../dbxcli/references/tool-integration.md | 55 +++++++++++ scripts/sync-claude-skill.sh | 27 ++++++ scripts/test-plugin.sh | 57 ++++++++++-- 10 files changed, 352 insertions(+), 9 deletions(-) create mode 100644 .claude-plugin/marketplace.json create mode 100644 plugin/claude/skills/dbxcli/SKILL.md create mode 100644 plugin/claude/skills/dbxcli/agents/openai.yaml create mode 100644 plugin/claude/skills/dbxcli/references/automation.md create mode 100644 plugin/claude/skills/dbxcli/references/safety.md create mode 100644 plugin/claude/skills/dbxcli/references/tool-integration.md create mode 100755 scripts/sync-claude-skill.sh diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 00000000..b73d6b00 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,15 @@ +{ + "name": "dbxcli", + "owner": { + "name": "Dropbox" + }, + "description": "Official Dropbox CLI plugin for Claude Code", + "plugins": [ + { + "name": "dbxcli", + "version": "1.0.0", + "source": "./plugin/claude", + "description": "Safely operate Dropbox through locally installed dbxcli CLI" + } + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29c6e8b4..97fe26d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,6 +97,25 @@ jobs: - run: test -z "$(git status --porcelain -- docs/commands)" - run: test -z "$(git status --porcelain -- docs/json-schema/v1)" + claude-plugin: + name: Claude Code plugin sync + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Validate marketplace.json + run: python3 -c 'import json; json.load(open(".claude-plugin/marketplace.json"))' + - name: Check plugin skill is synced with canonical source + run: | + if ! diff -qr skills/dbxcli plugin/claude/skills/dbxcli; then + echo "error: plugin/claude/skills/dbxcli is out of sync with skills/dbxcli" + echo "Run: ./scripts/sync-claude-skill.sh" + exit 1 + fi + - name: Package plugin artifacts + run: ./scripts/package-skills.sh + - name: Run plugin validation tests + run: ./scripts/test-plugin.sh + chocolatey: name: Chocolatey package runs-on: windows-latest diff --git a/.gitignore b/.gitignore index 15dd780e..4683fc00 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea /dbxcli /dist/ +/tmp/ diff --git a/plugin/claude/skills/dbxcli/SKILL.md b/plugin/claude/skills/dbxcli/SKILL.md new file mode 100644 index 00000000..76da5bb4 --- /dev/null +++ b/plugin/claude/skills/dbxcli/SKILL.md @@ -0,0 +1,92 @@ +--- +name: dbxcli +description: Safely operate Dropbox through a locally installed dbxcli command, using its JSON manifest and schema-backed machine contract. Use for Dropbox file, shared-link, team, or account work; do not call the Dropbox API directly. +--- + +# dbxcli + +Use the local `dbxcli` executable as the only Dropbox integration. Do not +reimplement Dropbox API calls, scrape text help, or maintain a command catalog +in this skill. The CLI's JSON help manifest is authoritative for the installed +version. + +## Start safely + +1. Check availability with `command -v dbxcli`, then run + `dbxcli version --output=json`. If it is unavailable, say so and give + installation guidance for the user's operating system, using the + [dbxcli releases](https://github.com/dropbox/dbxcli/releases) page. Do not + download or install it unless the user authorizes that action. +2. Before a command you have not already discovered in the current task, run + `dbxcli [command path] --help --output=json`. Begin with + `dbxcli --help --output=json` when the command path is unknown. +3. Read the manifest's `supports_structured_output`, `input_schema`, + `stdin_stdout`, `destructive_level`, `flags`, and `args`. Do not infer a + command or flag from memory. +4. Represent the intended arguments and flags as JSON-shaped input and validate + it against that command's `input_schema` before building the shell command. + Map fields to command-line names using each field's `x-cli-name`. + +Read [tool-integration.md](references/tool-integration.md) for the discovery, +validation, result, and error protocol. Read [automation.md](references/automation.md) +for writes and confirmation behavior. Read [safety.md](references/safety.md) +before handling credentials, transfers, deletion, replacement, or sharing. + +## Execution contract + +For normal command execution, always pass `--output=json` and parse stdout as a +single JSON envelope. Treat stderr as diagnostics only. Check both the process +exit code and `.ok`: + +- If `.ok` is `true`, use documented `results[].status`, `results[].kind`, and + `warnings`; do not rely on prose or undocumented fields. +- If `.ok` is `false`, branch on stable `.error.code`, not `.error.message`. + Surface a concise, redacted explanation and use structured `.error.details` + only when relevant. Do not blindly retry writes or auth errors. +- If the manifest says `supports_structured_output: false`, do not run that + command as a machine-action. Explain the limitation or use a safe supported + alternative. JSON help itself remains available. + +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. 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 + +The CLI follows Dropbox pagination internally; agents must not invent or pass +cursors. For a broad `ls` or `search`, discover the command and use its +`--limit` flag to bound the result delivered to the tool. Start with the +narrowest sensible folder or search path; do not recursively enumerate a whole +Dropbox when a scoped query will answer the request. A limited result is a +selection, not proof that no additional matches exist. + +For search requests about text *inside* files, inspect the `search` manifest +and pass `--content` only when it is available and the user requested a +content search. Otherwise search is filename-oriented. Scope the search path +and limit whenever practical. + +For a search → get → process task: discover and validate `search`, select the +exact result path from its JSON metadata, then discover and validate `get`. +Download to a named local file (never stdout), process that local file, and +report the output path or a concise result. A later upload, share-link, or +replacement is a separate externally visible action and needs its own +discovery, safety policy, and authorization. + +## Boundaries + +- Never put tokens, auth codes, app secrets, environment dumps, or auth-file + contents in prompts, commands, logs, JSON fixtures, tool results, commits, + or artifacts. Refer to secret names and paths only when needed. +- Do not use `DBXCLI_ACCESS_TOKEN=value` inline. Pass an already-provisioned + secret through the execution environment. Keep `DBXCLI_AUTH_FILE` outside + the repository and do not read, upload, or commit it. +- Never send binary file data through a tool result. For `get` or + `share-link download`, download to a local file and report its path and + metadata. `local operand -` is a byte stream and cannot be combined with + JSON output. +- Do not use this skill to expose shared links, alter permissions, overwrite, + move, restore, or delete without user-authorized scope. diff --git a/plugin/claude/skills/dbxcli/agents/openai.yaml b/plugin/claude/skills/dbxcli/agents/openai.yaml new file mode 100644 index 00000000..58486130 --- /dev/null +++ b/plugin/claude/skills/dbxcli/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "dbxcli" + short_description: "Safely operate Dropbox with dbxcli" + default_prompt: "Use $dbxcli to inspect Dropbox safely through the local CLI." +policy: + allow_implicit_invocation: true diff --git a/plugin/claude/skills/dbxcli/references/automation.md b/plugin/claude/skills/dbxcli/references/automation.md new file mode 100644 index 00000000..79169cd0 --- /dev/null +++ b/plugin/claude/skills/dbxcli/references/automation.md @@ -0,0 +1,48 @@ +# Write operations and interaction + +Discover the specific command first. The manifest tells you whether it supports +`--dry-run`, `--if-exists`, `--yes`, structured output, and prompts. These flags +are command-specific; never attach one speculatively. + +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 +silently choose `overwrite`. + +Use `--yes` only after confirmation has been established and only if the +discovered manifest exposes it. It acknowledges an operation; it does not +replace authorization. + +Before an automated job that needs Dropbox access, use +`dbxcli account --output=json` as an auth and identity check. Prefer a +short-lived, pre-provisioned `DBXCLI_ACCESS_TOKEN` in the execution environment +for CI. When saved credentials are required, set `DBXCLI_AUTH_FILE` to a +private secret-backed or temporary location outside the repository. Do not +commit, cache, upload, or print that file. + +The full public source is +[Automation and JSON output](https://github.com/dropbox/dbxcli/blob/master/docs/automation.md); +this reference intentionally does not duplicate its command catalog or schema. diff --git a/plugin/claude/skills/dbxcli/references/safety.md b/plugin/claude/skills/dbxcli/references/safety.md new file mode 100644 index 00000000..8c2b02a0 --- /dev/null +++ b/plugin/claude/skills/dbxcli/references/safety.md @@ -0,0 +1,41 @@ +# Safety and data handling + +Treat tokens, refresh tokens, authorization codes, app secrets, and auth files +as secrets. Never ask users to paste them into a chat or command line; do not +read or display an auth file. Avoid environment dumps and shell tracing. Redact +any secret accidentally present in command output before reporting it. + +`dbxcli get -` and `dbxcli share-link download -` write raw +bytes to stdout. They cannot use `--output=json`. Do not use these forms when a +tool captures stdout, because binary data can corrupt a tool result or consume +context. Use a named local destination instead, then report only safe metadata +such as the destination path, size, and checksum if necessary. + +Likewise, do not upload binary data into a chat transcript. For a local source, +pass its path to the CLI. For a generated stream, use a pipe only when the +execution environment will not return those bytes as a tool result. + +Treat delete, overwrite, move, restore, permission changes, team member +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. diff --git a/plugin/claude/skills/dbxcli/references/tool-integration.md b/plugin/claude/skills/dbxcli/references/tool-integration.md new file mode 100644 index 00000000..92ab78cc --- /dev/null +++ b/plugin/claude/skills/dbxcli/references/tool-integration.md @@ -0,0 +1,55 @@ +# Machine-contract integration + +The installed CLI, not this reference, owns command discovery. JSON help works +without Dropbox authentication: + +```sh +dbxcli --help --output=json +dbxcli put --help --output=json +dbxcli share-link create --help --output=json +``` + +Each help result describes one command. Inspect +`results[].result.supports_structured_output` before normal execution. Its +`input_schema` is JSON Schema for positional arguments and flags. It uses +JSON-friendly names (for example `if_exists`) and retains the CLI spelling in +`x-cli-name`; validate an intended JSON input against it before constructing an +invocation. + +Run supported operations with `--output=json`. Stdout is exactly one JSON +success or error envelope; stderr can contain progress, warnings, and +diagnostics. Never parse text output as a fallback. + +```sh +dbxcli ls --output=json / +dbxcli put --if-exists fail --output=json report.md /Reports/report.md +``` + +Success has `ok: true`, `schema_version`, `command`, `input`, `results`, and +`warnings`. Use `results[].status` and `results[].kind` as the stable outcome. +An error has `ok: false` and an `error` object. Check the shell exit status as +well as `error.code`: the latter is the stable remediation key; message text is +human-facing and may change. Known error details are structured context, not a +license to expose sensitive values. + +Common exit-code classes: auth (2), permission (3), not found (4), conflict +(5), rate limit (6), validation/unsupported structured output (7), and partial +stdout transfer (8). A rate-limit response may contain +`error.details.retry_after_seconds`; wait only when the user task remains safe +to retry. Never automatically retry a non-idempotent or destructive operation. + +`ls` and `search` can span multiple Dropbox result pages, but dbxcli retrieves +those pages internally. Do not create a cursor loop in the agent. Use the +command's discovered `--limit` to bound tool output; a limit caps returned +items and does not establish that the full Dropbox result set has been seen. + +For deep search, the discovered `search` manifest exposes `--content` when the +installed CLI supports it. It searches file contents in addition to filenames. +Use it only when the request is specifically about content, and use the +optional Dropbox path scope to avoid a broad account-wide search. + +For schema-level validation of help and results, use the public +[JSON schema v1 documentation](https://github.com/dropbox/dbxcli/blob/master/docs/json-schema/v1/README.md): +`manifest.schema.json`, `commands.schema.json`, and `error.schema.json`. Pin a +release tag rather than `master` when a wrapper needs reproducible remote +schema URLs. diff --git a/scripts/sync-claude-skill.sh b/scripts/sync-claude-skill.sh new file mode 100755 index 00000000..5d34337e --- /dev/null +++ b/scripts/sync-claude-skill.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Sync the canonical skill source to the Claude Code plugin directory. +# The canonical source is skills/dbxcli/. +# The Claude plugin must be self-contained, so it includes a copy. +set -euo pipefail + +root_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +source_dir="$root_dir/skills/dbxcli" +target_dir="$root_dir/plugin/claude/skills/dbxcli" + +if [[ ! -d "$source_dir" ]]; then + echo "error: canonical skill source not found: $source_dir" >&2 + exit 1 +fi + +echo "Syncing canonical skill to Claude plugin..." +echo " source: $source_dir" +echo " target: $target_dir" + +rm -rf "$target_dir" +mkdir -p "$(dirname "$target_dir")" +cp -R "$source_dir" "$target_dir" + +echo "✓ Sync complete" +echo "" +echo "The plugin is now self-contained. Verify with:" +echo " ./scripts/test-plugin.sh" diff --git a/scripts/test-plugin.sh b/scripts/test-plugin.sh index 6dbba0b6..683e84d8 100755 --- a/scripts/test-plugin.sh +++ b/scripts/test-plugin.sh @@ -21,7 +21,33 @@ check() { fi } -echo "=== Plugin structure ===" +echo "=== Marketplace structure ===" + +marketplace_file="$root_dir/.claude-plugin/marketplace.json" +check "marketplace.json exists" test -f "$marketplace_file" +check "marketplace.json is valid JSON" python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$marketplace_file" +check "marketplace.json has owner field" python3 -c " +import json,sys +d = json.load(open(sys.argv[1])) +assert 'owner' in d, 'owner missing' +assert 'name' in d.get('owner', {}), 'owner.name missing' +" "$marketplace_file" +check "marketplace.json has plugins array" python3 -c " +import json,sys +d = json.load(open(sys.argv[1])) +assert 'plugins' in d, 'plugins missing' +assert isinstance(d['plugins'], list), 'plugins must be array' +" "$marketplace_file" + +echo "" +echo "=== Plugin structure (plugin/claude) ===" + +source_plugin_dir="$root_dir/plugin/claude" +check "plugin source directory exists" test -d "$source_plugin_dir" +check "plugin source plugin.json exists" test -f "$source_plugin_dir/.claude-plugin/plugin.json" + +echo "" +echo "=== Plugin structure (dist) ===" check "plugin dist directory exists" test -d "$plugin_dir" check "plugin.json exists" test -f "$plugin_dir/.claude-plugin/plugin.json" @@ -45,16 +71,29 @@ check "detect-dbxcli.sh is executable" test -x "$plugin_dir/scripts/detect-dbxcl check "README.md exists" test -f "$plugin_dir/README.md" echo "" -echo "=== Skill content ===" +echo "=== Skill content (plugin/claude) ===" + +source_plugin_dir="$root_dir/plugin/claude" + +check "plugin/claude/skills/dbxcli/ exists" test -d "$source_plugin_dir/skills/dbxcli" +check "plugin/claude skill SKILL.md exists" test -f "$source_plugin_dir/skills/dbxcli/SKILL.md" +check "plugin/claude skill references/ exists" test -d "$source_plugin_dir/skills/dbxcli/references" + +echo "" +echo "=== Skill content (dist) ===" -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 "dist skills/dbxcli/ directory exists" test -d "$plugin_dir/skills/dbxcli" +check "dist SKILL.md exists" test -f "$plugin_dir/skills/dbxcli/SKILL.md" +check "dist references/safety.md exists" test -f "$plugin_dir/skills/dbxcli/references/safety.md" +check "dist references/automation.md exists" test -f "$plugin_dir/skills/dbxcli/references/automation.md" +check "dist references/tool-integration.md exists" test -f "$plugin_dir/skills/dbxcli/references/tool-integration.md" + +echo "" +echo "=== Skill sync validation ===" -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" +check "plugin/claude/skills/dbxcli matches canonical source" diff -qr "$source_dir" "$source_plugin_dir/skills/dbxcli" +check "dist SKILL.md matches canonical source" diff -q "$source_dir/SKILL.md" "$plugin_dir/skills/dbxcli/SKILL.md" +check "dist safety.md matches canonical source" diff -q "$source_dir/references/safety.md" "$plugin_dir/skills/dbxcli/references/safety.md" echo "" echo "=== Host validation ==="