Coding Pet is built around a plugin system. The desktop app ships with reference plugins (see official plugins below for the current list) — but everything they do is available through the same interfaces exposed to third-party plugins.
A plugin is a PetPlugin object with an id, a displayName, and a setup(host) method. Inside setup you call registration methods on PetHostApi:
| Method | What it registers |
|---|---|
registerPackageAdapter(adapter) |
Support for a new pet package format |
registerDiscoverySource(source) |
A source that lists installable pets |
registerAnimationResolver(resolver) |
Maps animation state names to sprite sequences |
registerActionCatalog(catalog) |
The set of actions/states the runtime exposes |
registerPetProvider(provider) |
Built-in pets that need no package directory |
registerSnapshotSourceFactory(factory) |
A new source of RuntimeSnapshot events |
registerNotificationResolver(resolver) |
Maps session projections to notification levels |
registerLayoutResolver(resolver) |
Controls overlay/tray layout geometry |
You can register any combination. A plugin that only adds a new pet package format registers just a PackageAdapter. One that only adds a new tool integration registers just a RuntimeSnapshotSourceFactory.
import type { PetPlugin } from '@coding-pet/runtime-core';
export function createMyPlugin(): PetPlugin {
return {
id: 'my-plugin',
displayName: 'My Plugin',
setup(host) {
host.registerDiscoverySource({
id: 'my-discovery',
displayName: 'My Pets',
listPets() {
return [
{ id: 'my-pet', displayName: 'My Pet', petDir: '/path/to/my-pet' }
];
},
});
},
};
}Implement CompatiblePetPackageAdapter:
import type { CompatiblePetPackageAdapter } from '@coding-pet/runtime-core';
const myAdapter: CompatiblePetPackageAdapter = {
id: 'my-format-v1',
supports(formatId) { return formatId === 'my-format-v1'; },
load({ petDir, manifest, manifestPath, resolveFileUrl }) {
const spritesheetPath = path.join(petDir, manifest.spritesheet ?? 'spritesheet.png');
return {
id: String(manifest.id),
displayName: String(manifest.displayName),
description: String(manifest.description ?? ''),
spritesheetPath,
spritesheetUrl: resolveFileUrl?.(spritesheetPath) ?? spritesheetPath,
manifestPath,
formatId: 'my-format-v1',
};
},
};A Host Bridge listens to events from an external tool and writes a RuntimeSnapshot to a file, then registers that path in stateFiles. The simplest approach is to write a standalone file (no npm package needed) that the tool can load directly. See the bundled bridges under packages/*-host-bridge/ for reference implementations.
The snapshot schema:
interface RuntimeSnapshot {
state: string; // animation state name, e.g. "running", "idle", "failed"
title: string; // shown in the speech bubble title
body: string; // shown in the speech bubble body
}Write it as JSON to ~/.coding-pet/<tool>-state.json, then add the path to stateFiles in the desktop app config:
{
"adapter": "file",
"stateFiles": [
"~/.coding-pet/<tool>-state.json"
]
}The desktop app (apps/desktop) is plain JavaScript and does not load TypeScript plugins at runtime. It implements its own equivalent directly in preload.js and renderer.js.
This means:
- The
PetPlugin/PetHostApisystem is for programmatic embedding — building your own runtime in TypeScript that usesruntime-core. - To connect your work to the desktop app, use the config-level equivalents:
| What you built | How to connect it to the desktop |
|---|---|
| A new pet package (any format) | Add its directory to discoverySources in config.json, or point petDir at it directly |
| A host bridge for a new tool | Write a RuntimeSnapshot JSON to a file; set adapter: "file" and add the path to stateFiles |
There is no plugin loading step for the desktop — adding a path to discoverySources is the entire integration.
Test the TypeScript plugin API in isolation — write a script and run it with tsx:
// my-plugin-test.ts
import { PetRuntimeRegistry } from '@coding-pet/runtime-core';
import { createMyPlugin } from './my-plugin.ts';
const registry = new PetRuntimeRegistry();
createMyPlugin().setup(registry);
const pets = registry.discoverySources.flatMap(s => s.listPets());
console.log('Discovered pets:', pets);npx tsx my-plugin-test.tsTest a new pet package in the desktop — place the package directory anywhere, then add it to apps/desktop/config.json:
{
"discoverySources": ["./sample-data/codex-pet-adapter", "/path/to/my-new-pet-dir"]
}Restart the desktop and open the ⚙ selector — your pet should appear.
Test a host bridge in the desktop — write a snapshot file to disk and add it to stateFiles in the config:
{
"adapter": "file",
"stateFiles": [
"~/.coding-pet/my-tool-state.json"
]
}Then write the file from your tool or manually:
{ "state": "running", "title": "Working", "body": "test" }The pet updates within one second.
import { createCodexPetAdapterPlugin } from '@coding-pet/plugin-codex-pet-adapter';
import { PetRuntimeRegistry } from '@coding-pet/runtime-core';
const registry = new PetRuntimeRegistry();
createCodexPetAdapterPlugin().setup(registry);
// List all discovered pets
const pets = registry.discoverySources.flatMap(s => s.listPets());
// Load a specific pet package
const adapter = registry.packageAdapters.find(a => a.supports('codex-pet-v1'));
const pkg = adapter?.load({ petDir: '/path/to/pet', manifest, manifestPath });| Package | What it does |
|---|---|
@coding-pet/plugin-codex-pet-adapter |
codex-pet-v1 format support + ~/.codex/pets/ discovery |
@coding-pet/plugin-opencode-host-bridge |
Bridges OpenCode session events to RuntimeSnapshot |
@coding-pet/plugin-claude-code-host-bridge |
Claude Code hooks bridge |
@coding-pet/plugin-gemini-cli-host-bridge |
Gemini CLI hooks bridge |
@coding-pet/plugin-qwen-code-host-bridge |
Qwen Code hooks bridge |
See each package's own README for detailed API documentation.