Plugin Setup & Agent-Kit
Plugin Setup & Agent-Kit
Plugin Developer Agent Kit — documentation written for AI agents, readable by humans.
Plugins can provide two hooks for project configuration and AI agent discovery:
- Setup handler (
setupinplugin.yaml) — runs duringjay-stack setup <plugin>. Creates config files, validates credentials. - Agent-kit handler (
agentkitinplugin.yaml) — runs duringjay-stack agent-kit. Generates discovery data (add-menu catalogs, reference files, skills, thumbnails) using live services when needed.
When Each Runs
jay-stack setup <plugin> → setup handler (config + credentials)
jay-stack agent-kit → agentkit handler (after contract materialization)
Setup runs when a project configures the plugin. Agent-kit runs whenever the developer regenerates the agent kit — it can use live services to produce fresh data.
Declaring in plugin.yaml
name: my-plugin
setup: setupMyPlugin # export name (NPM) or ./path (local) — optional
agentkit: generateMyAgentKit # export name (NPM) or ./path (local) — optional
description: Validate credentials and install config # optional, top-level
NPM plugins: setup and agentkit are export names from the package entry point (lib/index.ts).
Local plugins: relative paths to handler modules (e.g. agentkit: ./agentkit — export agentkit or default from that module).
jay-stack validate-plugin checks that declared handlers exist and are correctly exported.
Writing a Setup Handler
The setup handler creates config files, validates services, and can prompt the user for credentials. It receives a PluginSetupContext and returns a PluginSetupResult.
Do not write add-menu catalogs in setup — use the agent-kit handler.
Basic setup (non-interactive)
import type { PluginSetupContext, PluginSetupResult } from '@jay-framework/stack-server-runtime';
import fs from 'node:fs';
import path from 'node:path';
export async function setupMyPlugin(ctx: PluginSetupContext): Promise<PluginSetupResult> {
if (ctx.initError) {
return { status: 'error', message: `Init failed: ${ctx.initError.message}` };
}
const configCreated: string[] = [];
const configPath = path.join(ctx.configDir, '.my-plugin.yaml');
if (!fs.existsSync(configPath) || ctx.force) {
fs.mkdirSync(ctx.configDir, { recursive: true });
fs.writeFileSync(configPath, '# My Plugin config\napiKey: "<your-api-key>"\n', 'utf-8');
configCreated.push('config/.my-plugin.yaml');
}
return {
status: 'configured',
configCreated,
message:
configCreated.length > 0
? 'My Plugin config installed.'
: 'My Plugin config already present (use --force to rewrite).',
};
}
Interactive setup (with prompts)
When the setup handler needs user input (API keys, credentials, configuration choices), use ctx.prompt:
export async function setupMyPlugin(ctx: PluginSetupContext): Promise<PluginSetupResult> {
const configPath = path.join(ctx.configDir, '.my-plugin.yaml');
// Already configured — skip unless --force
if (fs.existsSync(configPath) && !ctx.force) {
return { status: 'configured', message: 'Already configured' };
}
// In non-interactive mode, create a template and ask the user to fill it in later
if (!ctx.interactive) {
fs.mkdirSync(ctx.configDir, { recursive: true });
fs.writeFileSync(configPath, 'apiKey: "<your-api-key>"\n', 'utf-8');
return {
status: 'needs-config',
configCreated: ['config/.my-plugin.yaml'],
message: 'Run `jay-stack-cli setup` interactively to enter your API key',
};
}
// Interactive mode — prompt the user
const apiKey = await ctx.prompt.input({
message: 'Enter your API key (create one at https://example.com/api-keys):',
validate: (v) => (v.trim() ? true : 'API key is required'),
});
const region = await ctx.prompt.select({
message: 'Select your region:',
choices: [
{ name: 'US East', value: 'us-east' },
{ name: 'EU West', value: 'eu-west' },
],
});
fs.mkdirSync(ctx.configDir, { recursive: true });
fs.writeFileSync(configPath, `apiKey: "${apiKey.trim()}"\nregion: ${region}\n`, 'utf-8');
return {
status: 'configured',
configCreated: ['config/.my-plugin.yaml'],
message: 'Credentials configured successfully',
};
}
Setup modes
Setup runs in three modes:
| Mode | Command | ctx.interactive |
ctx.prompt behavior |
|---|---|---|---|
| Default (agents, CI) | jay-stack-cli setup |
false |
Throws SetupNeedsAnswerError with structured output |
| Interactive (humans) | jay-stack-cli setup --interactive |
true |
Prompts via terminal |
| Answers file (automation) | jay-stack-cli setup --answers file.yaml |
false |
Reads from file, throws if missing |
In default mode, when a prompt has no answer, the CLI exits with structured YAML telling the caller what's needed. Agents can then provide the answer via --answers and re-run.
Idempotency requirement
Setup handlers must be idempotent — re-running with the same answers must produce the same result without side effects. This is critical because:
- Agents re-run setup iteratively as they provide answers one at a time
- Users re-run setup after fixing credentials
- CI pipelines may run setup on every deploy
Rules:
- Check if config already exists before creating it — skip if present (unless
ctx.force) - Check if credentials are already valid before prompting — skip if configured
- Never append to files — write the complete content each time
- Use
ctx.forceto allow explicit re-creation when the user asks for it
export async function setupMyPlugin(ctx: PluginSetupContext): Promise<PluginSetupResult> {
const configPath = path.join(ctx.configDir, '.my-plugin.yaml');
// Idempotent: skip if already configured (unless --force)
if (fs.existsSync(configPath) && !ctx.force) {
// Optionally validate the existing config
return { status: 'configured', message: 'Already configured' };
}
// Prompt only when needed
const apiKey = await ctx.prompt.input({
key: 'api-key',
message: 'Enter your API key:',
});
// Write complete config (not append)
fs.writeFileSync(configPath, `apiKey: "${apiKey}"\n`);
return { status: 'configured', configCreated: ['config/.my-plugin.yaml'] };
}
PluginSetupContext
| Field | Type | Description |
|---|---|---|
pluginName |
string |
Plugin name from plugin.yaml |
projectRoot |
string |
Absolute project root path |
configDir |
string |
Config directory (from .jay configBase, defaults to ./config) |
services |
Map |
Registered services (may be empty if init failed) |
initError |
Error? |
Present if plugin init failed — check this before using services |
force |
boolean |
Whether --force flag was passed |
interactive |
boolean |
Whether running in interactive mode (can prompt user) |
prompt |
PluginSetupPrompt |
Prompt functions for user input (see below) |
PluginSetupPrompt
| Method | Signature | Description |
|---|---|---|
input |
(opts: { message, validate? }) → Promise<string> |
Text input. Non-interactive: returns "" |
confirm |
(opts: { message, default? }) → Promise<boolean> |
Yes/no. Non-interactive: returns default or false |
select |
(opts: { message, choices }) → Promise<string> |
Single choice. Non-interactive: returns first choice value |
PluginSetupResult
| Field | Type | Description |
|---|---|---|
status |
'configured' | 'needs-config' | 'error' |
Overall result |
configCreated |
string[]? |
Config files created (relative to project root) |
message |
string? |
Human-readable status message |
Writing an Agent-Kit Handler
The agent-kit handler generates discovery data at agent-kit time: add-menu catalogs, agent-kit/references/<plugin>/ files, skills, thumbnails. It can use live services (database queries, API calls) to produce dynamic content.
import type {
PluginAgentKitContext,
PluginAgentKitResult,
} from '@jay-framework/stack-server-runtime';
import fs from 'node:fs';
import path from 'node:path';
import yaml from 'yaml';
export async function generateMyAgentKit(
ctx: PluginAgentKitContext,
): Promise<PluginAgentKitResult> {
if (ctx.initError) {
return { agentKitCreated: [], message: `Skipped: ${ctx.initError.message}` };
}
const outputPath = path.join(ctx.projectRoot, 'agent-kit/aiditor/add-menu/my-plugin.yaml');
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
const items = [
{ id: 'my-plugin:feature-1', title: 'Feature 1', category: 'My Plugin', prompt: '...' },
];
fs.writeFileSync(outputPath, yaml.stringify({ items }), 'utf-8');
return {
agentKitCreated: ['agent-kit/aiditor/add-menu/my-plugin.yaml'],
message: `Generated ${items.length} add-menu items`,
};
}
PluginAgentKitContext
| Field | Type | Description |
|---|---|---|
pluginName |
string |
Plugin name from plugin.yaml |
projectRoot |
string |
Absolute project root path |
referencesDir |
string |
Directory for reference data (agent-kit/references/<plugin>/) |
services |
Map |
Registered services |
initError |
Error? |
Present if plugin init failed |
force |
boolean |
Whether --force flag was passed |
PluginAgentKitResult
| Field | Type | Description |
|---|---|---|
agentKitCreated |
string[] |
Files created (relative to project root) |
message |
string? |
Human-readable status message |
Setup vs Agent-Kit — When to Use Which
| Use case | Hook | Why |
|---|---|---|
| Copy static add-menu template, skills, thumbnails | agentkit |
Discovery data — regenerated on jay-stack agent-kit |
| Generate data from live services (product catalogs, CMS schemas) | agentkit |
Needs services initialized; refreshed on each agent-kit run |
| Validate credentials / API keys | setup |
Part of initial project configuration |
| Write AIditor add-menu from project-specific data (DESIGN.md tokens) | agentkit |
Data comes from project files at agent-kit time |
AIditor Add-Menu Items
See add-menu-guide.md for the complete add-menu item schema, interaction modes, browse sizes, presentation formats, and TypeScript types.
Exporting Handlers
For NPM plugins, export handlers from the package entry point:
// lib/index.ts
export { setupMyPlugin } from './setup.js';
export { generateMyAgentKit } from './agentkit.js';
// ... other exports (components, actions, services)
For local plugins, use relative paths in plugin.yaml and export agentkit or default from the handler module.
About this document
This page is part of the Jay Stack Agent Kit — documentation generated from the framework source and written primarily for AI agents. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation. Learn more about the Agent Kit →