Skip to main content
Adding a new tool to Kayston’s Forge follows a consistent 3-step pattern. All tools share a unified UI and processing architecture.

The 3-Step Process

To add a new tool, you must modify three key files:
  1. Registry (lib/tools/registry.ts) — Define the tool metadata
  2. Engine (lib/tools/engine.ts) — Implement the tool logic
  3. Workbench Config (components/tools/ToolWorkbench.tsx) — Configure actions (optional)

Step 1: Add to Registry

The registry defines all tools with their metadata, icons, and keywords for search.
1

Open the registry file

# Location
lib/tools/registry.ts
2

Add your tool definition

Add a new entry to the tools array:
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';

export const tools: ToolDefinition[] = [
  // ... existing tools
  {
    id: 'my-new-tool',
    name: 'My New Tool',
    description: 'Brief description of what it does',
    category: 'data', // encoding, escaping, preview, format, beautify, generator, data
    icon: MagnifyingGlassIcon,
    keywords: ['search', 'terms', 'for', 'command', 'palette']
  },
];

Tool Categories

Choose the appropriate category for your tool:
CategoryLabelUse For
encodingEncoding & EncryptionBase64, JWT, timestamps, hashing
escapingEscaping & EntitiesHTML entities, backslash escaping, IDs
previewPreview & ComparisonHTML preview, text diffing
formatFormat ConvertersYAML/JSON, number bases, color formats
beautifyCode BeautifiersHTML/CSS/JS formatting
generatorGeneratorsLorem ipsum, QR codes, random strings
dataData TransformersCSV/JSON conversion, SQL formatting

Available Icons

Icons come from @heroicons/react/24/outline. Common choices:
import {
  CodeBracketIcon,      // Code/JSON
  DocumentTextIcon,     // Documents/text
  HashtagIcon,          // Numbers/encoding
  KeyIcon,              // Security/keys
  LinkIcon,             // URLs/links
  MagnifyingGlassIcon,  // Search/inspect
  SparklesIcon,         // Generate/transform
  TableCellsIcon,       // Tables/CSV
} from '@heroicons/react/24/outline';

Step 2: Implement in Engine

The engine contains the processing logic for all tools in a large switch statement.
1

Open the engine file

# Location
lib/tools/engine.ts
2

Add a case for your tool

Add a new case to the switch (toolId) statement:
export async function processTool(
  toolId: string,
  input: string,
  options: ProcessOptions = {}
): Promise<ProcessResult> {
  const action = options.action ?? 'default';

  try {
    switch (toolId) {
      // ... existing cases

      case 'my-new-tool': {
        // Your processing logic here
        const result = processInput(input);
        
        return {
          output: result,
          meta: 'Optional metadata',
          // previewHtml: 'For HTML preview',
          // previewDataUrl: 'For image preview',
          // table: [{key: 'val'}], // For tabular data
        };
      }

      default:
        return { output: 'Tool not implemented.' };
    }
  } catch (error) {
    return { output: error instanceof Error ? error.message : 'Processing failed.' };
  }
}

Processing Options

Tools receive three parameters:
processTool(
  toolId: string,          // From registry
  input: string,           // Primary input from textarea
  options?: {
    action?: string,       // Current action (e.g., 'beautify', 'minify')
    secondInput?: string,  // Secondary input (for diff, compare, etc.)
    dedupe?: boolean,      // Deduplication flag
    pattern?: string,      // Regex pattern
    flags?: string,        // Regex flags
    [key: string]: unknown // Additional options
  }
)

Return Value

Return a ProcessResult object:
interface ProcessResult {
  output: string;           // Required: main output text
  previewHtml?: string;     // Optional: HTML to render in preview pane
  previewDataUrl?: string;  // Optional: image data URL for preview
  table?: Array<Record<string, string>>; // Optional: tabular data
  meta?: string;            // Optional: metadata shown below output
}

Example Implementations

case 'string-case': {
  const words = input
    .replace(/([a-z])([A-Z])/g, '$1 $2')
    .replace(/[_\-]+/g, ' ')
    .trim()
    .split(/\s+/)
    .filter(Boolean)
    .map((w) => w.toLowerCase());
  
  if (!words.length) return { output: '' };
  
  switch (action) {
    case 'pascal': 
      return { output: words.map((w) => w[0].toUpperCase() + w.slice(1)).join('') };
    case 'snake': 
      return { output: words.join('_') };
    case 'kebab': 
      return { output: words.join('-') };
    default: 
      return { output: words[0] + words.slice(1).map((w) => w[0].toUpperCase() + w.slice(1)).join('') };
  }
}

Step 3: Configure Actions (Optional)

If your tool supports multiple actions (like Beautify/Minify), add action configuration to the workbench.
1

Open the workbench file

# Location
components/tools/ToolWorkbench.tsx
2

Add to actionConfig

Find the actionConfig object and add your tool:
const actionConfig: Record<string, { label: string; actions: Action[] }> = {
  // ... existing configs
  
  'my-new-tool': {
    label: 'Transform',
    actions: [
      { id: 'default', label: 'Default' },
      { id: 'option-a', label: 'Option A' },
      { id: 'option-b', label: 'Option B' },
    ],
  },
};

Action Configuration Examples

'html-beautify': {
  label: 'Action',
  actions: [
    { id: 'default', label: 'Beautify' },
    { id: 'minify', label: 'Minify' },
  ],
}
If your tool doesn’t need multiple actions, skip Step 3. The tool will work with the default action.

Testing Your Tool

After implementing your tool:
1

Write unit tests

Add tests in tests/unit/engine.test.ts:
it('processes my new tool', async () => {
  const out = await processTool('my-new-tool', 'test input', {
    action: 'default',
  });
  expect(out.output).toBe('expected output');
});
2

Run tests

npm run test
3

Test in browser

npm run dev
Navigate to /tools/my-new-tool and verify:
  • Tool appears in sidebar
  • Actions work correctly
  • Input/output behave as expected
  • Command palette search finds the tool
4

Test static build

npm run build
Ensure the static export includes your new tool.

Advanced Patterns

Using Auxiliary Modules

For complex logic, create a separate module:
// lib/tools/my-tool-logic.ts
export function processMyTool(input: string): string {
  // Complex processing logic
  return result;
}

// lib/tools/engine.ts
import { processMyTool } from './my-tool-logic';

case 'my-new-tool': {
  const result = processMyTool(input);
  return { output: result };
}

Dynamic Imports

For large dependencies, use dynamic imports:
case 'my-new-tool': {
  const { heavyLibrary } = await import('heavy-library');
  const result = heavyLibrary.process(input);
  return { output: result };
}

Input Validation

Always validate input:
case 'my-new-tool': {
  if (!input.trim()) {
    return { output: 'Input is required' };
  }
  
  try {
    const parsed = JSON.parse(input);
    // Process parsed data
  } catch (error) {
    return { output: `Parse error: ${error.message}` };
  }
}

Static Export Compatibility

Ensure your tool works with Next.js static export:
  • No server-side APIs: All processing must run client-side
  • No Node.js APIs: Use browser-compatible alternatives
  • Synchronous preferred: Avoid unnecessary async operations
  • Bundle size: Be mindful of dependency size
Avoid using Node.js-specific APIs like fs, path, or child_process. These won’t work in the browser.

Checklist

Before submitting your new tool:
  • Added to lib/tools/registry.ts with proper category and keywords
  • Implemented in lib/tools/engine.ts with error handling
  • Added action config to components/tools/ToolWorkbench.tsx (if needed)
  • Written unit tests in tests/unit/engine.test.ts
  • Tested in browser with npm run dev
  • Verified static build works with npm run build
  • All existing tests still pass
  • Tool appears in command palette search
  • Tool handles empty input gracefully
  • Tool handles invalid input with clear error messages

Next Steps

Build docs developers (and LLMs) love