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 ./tools entry (lib/tools.ts). They are tools-time handlers (may use the compiler) and load only from ./tools — do not re-export them from lib/index.ts, or the compiler can leak into the serve bundle.
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 |
| Materialize AIditor Project settings tab discovery | agentkit |
Copy/generate agent-kit/aiditor/settings/<plugin>.yaml |
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.
AIditor Project Settings
See aiditor-settings-guide.md for the full checklist: settings.template.yaml in the package, walk-up path resolution from bundled dist/, devOnly route in plugin.yaml, and settings page responsibilities (actions, postMessage, no secrets in forms).
Exporting Handlers
For NPM plugins, export setup/agent-kit handlers from the ./tools entry (they may use the
compiler and must stay out of the serve bundle):
// lib/tools.ts (./tools) — compiler allowed, toolchain-only
export { setupMyPlugin } from './setup.js';
export { generateMyAgentKit } from './agentkit.js';
// lib/index.ts (.) — serve entry, compiler-free
// ... components, actions, services, init (NOT setup/agentkit)
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 →