Custom Plugin
Build, publish, and configure an installable Downcity Plugin
Custom Plugin
A third-party directory defines exactly one Plugin. The Plugin's globally unique ID is also its final directory name, Agent reference key, and runtime Registry key.
Development directory
github-plugin/
├── plugin.json
├── package.json
├── README.md
├── icon.svg
├── src/
│ ├── plugin.ts
│ └── setup.ts
├── tsconfig.json
└── dist/setup.jssrc/ and tsconfig.json are development-only files. Downcity does not prescribe a build tool and does not require tsup.config.ts. package.json belongs to the Plugin package and establishes an explicit ESM package boundary, so it must contain "type": "module" and is installed with the Plugin. The build script only needs to bundle the entry and its runtime dependencies into one self-contained ESM file. For example, package.json can invoke esbuild directly:
{
"type": "module",
"scripts": {
"build": "esbuild src/setup.ts --bundle --platform=node --format=esm --outfile=dist/setup.js"
}
}The SDK Plugin class is independent from the CLI configuration protocol. Its constructor parameters are entirely owned by the Plugin:
import type { Plugin, PluginActions } from "@downcity/agent";
export interface GithubPluginConfig {
api_url: string;
token: string;
}
export class GithubPlugin implements Plugin {
readonly name = "github";
readonly title = "GitHub";
readonly description = "GitHub integration";
readonly actions: PluginActions;
constructor(config: GithubPluginConfig) {
this.actions = {};
}
}SDK users assemble the instance directly:
const plugin = new GithubPlugin({
api_url: "https://api.github.com",
token: process.env.GITHUB_TOKEN ?? "",
});
const agent = new Agent({
id: "coding-pro",
model,
plugins: [plugin],
});The instance name, plugin.json id, directory name, and Agent reference must match. The SDK does not require a fixed constructor export or require the constructor to accept a profile.
Plugin definition
The source directory contains one plugin.json:
{
"schema_version": 1,
"id": "github",
"version": "1.0.0",
"title": "GitHub",
"description": "GitHub integration",
"icon": "./icon.svg",
"setup": "dist/setup.js"
}setup points to a self-contained ESM module that exports the configuration Schema and host assembly function:
import type { PluginHostContext } from "@downcity/agent";
export const schema = {
type: "object",
properties: {
api_url: { type: "string", format: "uri" },
token: { type: "string", minLength: 1, writeOnly: true },
},
required: ["api_url", "token"],
additionalProperties: false,
} as const;
export function setup(context: PluginHostContext): GithubPlugin {
return new GithubPlugin(context.profile as GithubPluginConfig);
}setup is City's assembly boundary, not an install script or Plugin lifecycle hook. Each call creates a new Plugin instance. context.data_path is private runtime storage and never stores profile configuration. The global profile remains under ~/.downcity/plugins/<plugin_id>/config.toml and can be selected by multiple Agents.
Plugin authors declare the complete JSON Schema from the setup module. Field-level default values initialize new profile forms only; they are not merged into runtime configuration when an Agent does not select a profile.
The setup path must point to one self-contained ESM file. The installer does not install dependencies, run build scripts, import the setup module, or copy source and development files outside the declared artifact set. If the Plugin uses runtime dependencies such as Zod, the build tool must bundle them into setup.
The installed layout is deterministic:
~/.downcity/plugins/github/
├── plugin.json
├── config.toml
├── package.json
├── README.md
├── icon.svg
└── dist/setup.jsREADME.md is required. config.toml contains only named plaintext profile values and uses file mode 0600. CLI and Desktop load the setup Schema to build forms and redact fields marked writeOnly.
schema_version = 1
[profiles.production]
api_url = "https://api.github.com"
token = "plain-local-token"An Agent reference without an explicit profile always passes {} and never implicitly reads a persisted default profile. An explicitly selected profile is loaded from the global Plugin configuration and is an error when missing. Both paths are validated against the setup Schema. A Plugin whose Schema accepts {} can be enabled directly; a Plugin with required fields needs a named profile.
Install and configure
city plugin install ./github-plugin
city plugin config github production --interactive
city plugin enable github my-agent --profile production
city plugin update github
city plugin uninstall githubUpdates atomically replace the complete Plugin directory while preserving config.toml. The CLI rejects an update when saved profiles fail the new Schema, and rejects profile or Plugin removal while an Agent still references it.
Installation reads and copies only plugin.json, package.json, README.md, the declared setup file, and an optional local icon. It does not run npm install, build scripts, Plugin lifecycle hooks, or setup(). The setup exports and instance ID are validated when needed, and the installed artifacts receive a SHA-256 digest. Install only trusted sources.
Continue with: