Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
/dbxcli
/dist/
/tmp/
92 changes: 92 additions & 0 deletions plugin/claude/skills/dbxcli/SKILL.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions plugin/claude/skills/dbxcli/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions plugin/claude/skills/dbxcli/references/automation.md
Original file line number Diff line number Diff line change
@@ -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.
41 changes: 41 additions & 0 deletions plugin/claude/skills/dbxcli/references/safety.md
Original file line number Diff line number Diff line change
@@ -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 <remote> -` and `dbxcli share-link download <url> -` 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.
55 changes: 55 additions & 0 deletions plugin/claude/skills/dbxcli/references/tool-integration.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 27 additions & 0 deletions scripts/sync-claude-skill.sh
Original file line number Diff line number Diff line change
@@ -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"
57 changes: 48 additions & 9 deletions scripts/test-plugin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 ==="
Expand Down