Plugin DeveloperPlugin Setup & Agent-Kit
← Documentation

Plugin Setup & Agent-Kit

Plugin Setup & Agent-Kit

Plugin DeveloperPlugin 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 (setup in plugin.yaml) — runs during jay-stack setup <plugin>. Creates config files, validates credentials.
  • Agent-kit handler (agentkit in plugin.yaml) — runs during jay-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:

  1. Check if config already exists before creating it — skip if present (unless ctx.force)
  2. Check if credentials are already valid before prompting — skip if configured
  3. Never append to files — write the complete content each time
  4. Use ctx.force to 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 →