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:
- Registry (
lib/tools/registry.ts) — Define the tool metadata
- Engine (
lib/tools/engine.ts) — Implement the tool logic
- 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.
Open the registry file
# Location
lib/tools/registry.ts
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']
},
];
Choose the appropriate category for your tool:
| Category | Label | Use For |
|---|
encoding | Encoding & Encryption | Base64, JWT, timestamps, hashing |
escaping | Escaping & Entities | HTML entities, backslash escaping, IDs |
preview | Preview & Comparison | HTML preview, text diffing |
format | Format Converters | YAML/JSON, number bases, color formats |
beautify | Code Beautifiers | HTML/CSS/JS formatting |
generator | Generators | Lorem ipsum, QR codes, random strings |
data | Data Transformers | CSV/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.
Open the engine file
# Location
lib/tools/engine.ts
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('') };
}
}
If your tool supports multiple actions (like Beautify/Minify), add action configuration to the workbench.
Open the workbench file
# Location
components/tools/ToolWorkbench.tsx
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.
After implementing your tool:
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');
});
Test in browser
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
Test static 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 };
}
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:
Next Steps