Version: 1.0.0
A Yandu plugin is an npm package that exports a Plugin object. The plugin registers capabilities into Yandu's runtime through a single register(system) call. Two package-name namespaces are supported:
@yandu/plugin-*— official / built-in plugins shipped under the@yanduscope.yandu-plugin-*— third-party plugins published unscoped on npm.
Yandu treats both namespaces identically at load time. The distinction is naming convention only.
Key design principles:
- Uniform interface: All plugins use the same
Plugininterface regardless of capability type. - Capability registry: Plugins register capabilities via
system.capabilities.register(descriptor, implementation). Thetypefield distinguishes translation, feed, converter, etc. - No core distinction: Yandu's core does not distinguish between "built-in" and "external" plugins. All plugins are discovered and loaded the same way.
- Sandboxed: Plugins run in a sandbox that limits tool registration and protects core tools.
export interface Plugin {
name: string;
version: string;
register(system: KernelSystem): void;
}When Yandu scans node_modules for plugin packages (yandu-plugin-* and @yandu/plugin-*), it resolves the entry point in this order:
package.jsonfieldyandu.mainpackage.jsonfieldmodulepackage.jsonfieldmain
The resolved module is dynamically imported. The plugin object is extracted from mod.default, mod.plugin, or mod itself if it has a register method. If the module exports a function, it is called and the return value is used.
{
"name": "yandu-plugin-hello",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.mjs",
"yandu": {
"main": "dist/index.mjs"
},
"peerDependencies": {
"@yandu/types": "^1.0.0"
}
}KernelSystem is the dependency injection surface passed to plugin.register().
export interface KernelSystem {
registry: ToolRegistry;
acpRegistry: AcpRegistry;
capabilities: CapabilityRegistry;
loop: KernelLoop;
createContext(workingDirectory?: string): KernelContext;
}The CapabilityRegistry is the unified registration surface for all plugin-provided capabilities.
export interface CapabilityRegistry {
register<T>(descriptor: CapabilityDescriptor, implementation: T): void;
unregister(capabilityId: string): boolean;
get<T>(capabilityId: string): T | undefined;
getByType<T>(type: CapabilityType): Map<string, T>;
listDescriptors(type?: CapabilityType): CapabilityDescriptor[];
}
export interface CapabilityDescriptor {
type: CapabilityType;
id: string;
name: string;
description?: string;
version?: string;
}
export type CapabilityType =
| 'translation'
| 'feed'
| 'converter'
| 'search'
| 'im'
| 'embedding'
| 'import'
| 'tool'
| 'config';Capability IDs should be namespaced: {vendor}.{name} or reverse-domain style.
export interface TranslationAdapter {
id: string;
name: string;
supportsStreaming: boolean;
maxTextLength: number;
translate(text: string, from: string, to: string): Promise<string>;
translateStream?(text: string, from: string, to: string): AsyncIterable<string>;
}export interface FeedAdapter {
id: string;
name: string;
availableFormats: string[];
configSchema?: ConfigSchema;
fetch(query: FeedQuery): Promise<FeedResult>;
fetchFormat(query: FeedQuery, format: string): Promise<FeedResult>;
resolveDownload?(paper: Paper): Promise<string | null>;
}export interface ContentConverter {
id: string;
name: string;
inputFormats: string[];
settingsSchema?: ConfigSchema;
convert(input: ConvertInput, options?: ConvertOptions): Promise<ConvertResult>;
}export interface SearchAdapter {
id: string;
name: string;
search(query: SearchQuery): Promise<SearchResult[]>;
cancel?(searchId: string): void;
}export interface IMAdapter {
id: string;
name: string;
initialize(config: IMConfig): Promise<void>;
send(chatId: string, text: string): Promise<void>;
onMessage(handler: (msg: IMMessage) => void): void;
}export interface EmbeddingAdapter {
id: string;
name: string;
model: string;
dimensions: number;
embed(texts: string[]): Promise<number[][]>;
}export interface ImportAdapter {
id: string;
name: string;
import(options: ImportOptions): Promise<ImportResult>;
validate?(options: ImportOptions): Promise<boolean>;
}Tools are registered directly on system.registry:
system.registry.register({
name: 'my_tool',
description: '...',
inputSchema: { type: 'object', properties: { ... } },
execute: async (args, ctx) => { ... },
});Config-only plugins register a ConfigSchema:
system.capabilities.register(
{ type: 'config', id: 'my-plugin', name: 'My Plugin Settings' },
myConfigSchema
);Scan node_modules for yandu-plugin-* and @yandu/plugin-*
|
v
For each: resolve entry, dynamic import
|
v
Validate Plugin interface
|
v
Create sandboxed KernelSystem
|
v
Call plugin.register(sandboxedSystem)
|
v
Capabilities/tools/configs registered
No hot-reload. Restart Yandu to unload a plugin.
| Limit | Default | Config Key |
|---|---|---|
| Max tools | 20 | kernel.sandbox.maxTools |
| Max slash commands | 10 | kernel.sandbox.maxSlashCommands |
| Protected tools | read, write, bash, enter_plan_mode, exit_plan_mode |
N/A |
Protected tools cannot be overridden. The CapabilityRegistry is sandboxed: plugins can register but cannot unregister core capabilities.
Package name follows one of two conventions:
- Official / built-in:
@yandu/plugin-{type}-{name}— published under the@yanduscope. - Third-party:
yandu-plugin-{type}-{name}— unscoped on npm.
Both are auto-discovered by Yandu at startup. Use the scoped form only if the package is part of the official @yandu ecosystem; third-party authors should use the unscoped form.
Examples (third-party):
yandu-plugin-translate-google-freeyandu-plugin-feed-arxivyandu-plugin-converter-pdfyandu-plugin-im-telegram
Examples (built-in):
@yandu/plugin-translate-baidu@yandu/plugin-feed-rss@yandu/plugin-converter-paddleocr
Capability IDs: {vendor}.{name}
Examples:
google-freeorg.arxivconverter.pdf
npm install --save-dev @yandu/typesExports: Plugin, KernelSystem, CapabilityRegistry, CapabilityDescriptor, CapabilityType, all adapter interfaces, Tool, ConfigSchema.
Plugins should never throw during register(). Defer errors to runtime. If register() throws, Yandu logs the error and skips the plugin. Other plugins continue loading.
Semantic versioning. Major = breaking interface changes, Minor = new features, Patch = bug fixes. Yandu does not enforce compatibility. Document minimum Yandu version in README.
# third-party plugin
npm install yandu-plugin-translate-google-free
# official / built-in plugin (scoped)
npm install @yandu/plugin-translate-baiduFor development:
cd yandu
npm link ../my-plugin