Skip to content
Open
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
136 changes: 135 additions & 1 deletion docs/guides/building/building-plugins.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,144 @@ Create `.factory-plugin/marketplace.json`:
| `description` | No | Shown when browsing marketplaces |
| `owner` | No | Contact information |
| `plugins[].name` | Yes | Plugin identifier |
| `plugins[].source` | Yes | Relative path to plugin directory |
| `plugins[].source` | Yes | Where to fetch the plugin from. A relative path string or a source object. See [Plugin sources](#plugin-sources). |
| `plugins[].description` | No | Shown in plugin browser |
| `plugins[].category` | No | For organizing plugins |

### Plugin sources

Each plugin entry's `source` field tells Droid where to fetch the plugin from. The default form, a relative path string like `"./plugin-one"`, points at a directory inside the marketplace repository. For plugins that live elsewhere, use a source object.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Resolve pinning guidance ambiguity across source types

The new Plugin sources table lists per-plugin ref/sha for git-based sources, but the Version management note later says pinning a plugin requires pinning the marketplace, which is only true for relative-path plugins. This internal contradiction can mislead readers about how to pin plugins depending on the source type.

Suggested change
Each plugin entry's `source` field tells Droid where to fetch the plugin from. The default form, a relative path string like `"./plugin-one"`, points at a directory inside the marketplace repository. For plugins that live elsewhere, use a source object.
Each plugin entry's `source` field tells Droid where to fetch the plugin from. The default form, a relative path string like "./plugin-one", points at a directory inside the marketplace repository. For plugins that live elsewhere, use a source object. Pinning behavior depends on the source type: git-based sources can be pinned per-plugin (via `ref`/`sha` when supported), while relative-path plugins are pinned by pinning the marketplace source.


| Source type | Fields | Use when |
| :---------- | :----- | :------- |
| Relative path | `"./path/to/plugin"` | Plugin lives inside the marketplace repository. |
| `github` | `repo`, `ref?`, `sha?` | Plugin is hosted in its own GitHub repository. |
| `url` | `url`, `ref?`, `sha?` | Plugin is hosted on a non-GitHub git host (GitLab, Bitbucket, self-hosted). |
| `git-subdir` | `url`, `path`, `ref?`, `sha?` | Plugin lives inside a subdirectory of a larger git repository, such as a monorepo. |
| `npm` | `package`, `version?`, `registry?` | Plugin is published as an npm package. |

#### npm packages

Distribute plugins as npm packages when you already ship to a private registry (Artifactory, CodeArtifact, GitHub Packages, Verdaccio, and so on) and want to reuse that channel for your Droid plugins. Public packages on the npm registry work the same way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Clarify npm versioning vs git commit-hash note

This PR adds npm version (semver ranges and dist-tags) for plugin sources, but the Version management note later states Droid tracks plugin versions by Git commit hash, which can be read as contradicting npm-based installs. A short clarification here helps readers understand that the git commit-hash guidance applies to git-based sources, while npm sources follow npm version resolution.

Suggested change
Distribute plugins as npm packages when you already ship to a private registry (Artifactory, CodeArtifact, GitHub Packages, Verdaccio, and so on) and want to reuse that channel for your Droid plugins. Public packages on the npm registry work the same way.
Distribute plugins as npm packages when you already ship to a private registry (Artifactory, CodeArtifact, GitHub Packages, Verdaccio, and so on) and want to reuse that channel for your Droid plugins. Public packages on the npm registry work the same way. For npm sources, pinning and updates follow npm version resolution via the `version` field (the git commit-hash guidance in [Version management](#version-management) applies to git-based sources).


```json
{
"name": "pr-triage",
"source": {
"source": "npm",
"package": "@your-org/droid-pr-triage"
}
}
```

Pin to a specific version or range:

```json
{
"name": "pr-triage",
"source": {
"source": "npm",
"package": "@your-org/droid-pr-triage",
"version": "2.4.0"
}
}
```

Install from a private registry:

```json
{
"name": "pr-triage",
"source": {
"source": "npm",
"package": "@your-org/droid-pr-triage",
"version": "^2.0.0",
"registry": "https://npm.your-org.example"
}
}
```

| Field | Required | Description |
| :---- | :------- | :---------- |
| `package` | Yes | npm package name. Scoped packages (`@scope/name`) are supported. |
| `version` | No | npm version, range, or dist-tag (for example `2.4.0`, `^2.0.0`, `latest`). Defaults to `latest`. |
| `registry` | No | Custom registry URL. Defaults to the system npm registry. The setting is scoped to the install run and never written to your global `~/.npmrc`. |

**Layout requirements.** The published package root must contain a plugin manifest. Both the native Droid layout (`.factory-plugin/plugin.json`, `droids/`, `mcp.json`) and the Claude Code layout (`.claude-plugin/plugin.json`, `agents/`, `.mcp.json`) are accepted. Claude Code layouts are translated into Droid form when the package is copied into the plugin cache.

A typical published package looks like this:

```
@your-org/droid-pr-triage/
├── .factory-plugin/
│ └── plugin.json
├── package.json
├── skills/
│ └── classify/
│ └── SKILL.md
├── droids/
│ └── triage-reviewer.md
└── README.md
```

Set the `files` field in `package.json` so only the plugin payload ships:

```json
{
"name": "@your-org/droid-pr-triage",
"version": "2.4.0",
"files": [
".factory-plugin",
"skills",
"droids",
"mcp.json",
"hooks",
"README.md"
]
}
```

**Hardening.** Droid runs `npm install` in a per-plugin scratch directory with `--ignore-scripts`, `--no-save`, `--no-audit`, and `--no-fund`. Lifecycle scripts (`postinstall`, `preinstall`, and so on) are not executed, and your global npm configuration is not consulted or mutated.

<Note>
`npm:` is not a valid marketplace source. Droid only accepts `npm` as a per-plugin source inside a marketplace's `marketplace.json`. To ship a single npm-published plugin to your team without standing up a full marketplace repo, publish a thin wrapper marketplace (see below).
</Note>

##### Wrapper marketplace for a single npm plugin

To distribute one npm-published plugin without a dedicated marketplace repo, commit a small `marketplace.json` to any folder. The folder name becomes the marketplace name.

```
your-org-plugins/
└── .factory-plugin/
└── marketplace.json
```

```json
{
"name": "your-org-plugins",
"owner": { "name": "Your Org Platform Team" },
"plugins": [
{
"name": "pr-triage",
"description": "Triage and label new pull requests for the on-call rotation",
"source": {
"source": "npm",
"package": "@your-org/droid-pr-triage",
"version": "^2.0.0"
}
}
]
}
```

Teammates add it like any other local marketplace:

```bash
droid plugin marketplace add ./your-org-plugins
droid plugin install pr-triage@your-org-plugins
```

### Version management

Use semantic versioning in your plugin manifest for documentation purposes:
Expand Down
Loading