llmail
v1.2.11
Published
Inbox and issue managementfor LLMs
Readme
llmail
An intent-based inbox and issue management system for LLM collaboration.
Core Concepts
- Intent-Based: Metadata is the source of truth. The system helps align file locations with metadata instead of enforcing rigid rules.
- Flexible Organization: Works with any combination of states and types through simple configuration.
- Simple Directory Structure:
- Active items live in state directories (
issues/_state/) - Inactive items live in type directories (
issues/type/) - New items start in the inbox (
inbox/)
- Active items live in state directories (
- Extensibility: llmail provides a simple base you can use to build much more powerful systems.
Directory Structure
/
├── inbox/ # New items needing triage
└── issues/ # Root for all organized items
├── _working/ # Active items in 'working' state
│ └── working-bug-a41d.md
├── _blocked/ # Active items in 'blocked' state
│ └── blocked-feat-789.md
├── bug/ # Inactive bugs
│ └── fixed-bug-456.md
└── feat/ # Inactive features
└── wontfix-feat-123.mdConfiguration (llmail.yaml)
# Define available types and states
types_list:
- bug
- feat
- spec
active_states:
- working # Creates _working folder
- blocked # Creates _blocked folder
inactive_reasons:
- fixed
- wontfix
- duplicate
# Plugin configuration
plugins:
# Format 1: Single plugin without config
plugins: llmail-testlink
# Format 2: Multiple plugins without config
plugins: [llmail-testlink, llmail-github]
# Format 3: List format with optional config
plugins:
- llmail-testlink # Without config
- name: llmail-github # With config
config:
owner: 'myorg'
repo: 'myrepo'
labels:
- bug
- featureQuick Start
npm install llmailimport { LLMail } from 'llmail';
const llmail = new LLMail();
// Initialize with default configuration
await llmail.init();
// Create a new issue with metadata
const id = await llmail.new('bug', {
content: 'This bug sucks.\n\nWe should fix it.'
});
// Create a new issue with metadata
const id = await llmail.new('bug', {
frontmatter: {
title: 'Critical Login Bug',
severity: 'high',
assignee: 'alice'
}
content: 'This bug sucks.\n\nWe should fix it.'
});
// Create a new inactive issue with metadata
const id = await llmail.new('bug', {
active: false,
content: 'We fixed this but it could come back if we do the dumb thing again, so recording this for future ref.'
});
// Mark it as done
await llmail.done(id);
// Mark it as done and add a state with more detail
await llmail.done(id, {
state: 'fixed'
});
// Mark it as done with additional custom metadata
await llmail.done(id, {
state: 'fixed',
frontmatter: {
resolution: 'Fixed in PR #123'
}
});
// Move it to a different type and state
await llmail.mv(id, { type: 'test', state: 'working' });
// Move it to being active and change the state
await llmail.mv(id, { active: true, state: 'failing' });
// Get frontmatter data as an object
await llmail.getFrontmatter(id);
// Get active status of a given ID
await llmail.getFrontmatter(id).active;
// Get the state of a given ID
await llmail.getFrontmatter(id).state;
// Get any frontmatter field of a given ID
await llmail.getFrontmatter(id).myfrontmatterfield;
// Get the (non-frontmatter) contents of a given file
await llmail.getContent(id);
// Set the contents of a file -- overrides current content
await llmail.setContent(id, content);
// Structured Updates
// Add timestamped pseudo-xml-wrapped content
// metadata will be added as xml attributes
await llmail.appendUpdate({
id,
content: "This content is wrapped in <Update> XML tags},
metadata: {
author: "Hank",
saying: "Say thanks"
}
})
// Allow skipping adding timestamp
await llmail.prependUpdate(id, content, metadata, time=false)CLI Usage
Initialize System
# Initialize with default configuration
llmail init
# Use specific config file
llmail init --config path/to/config.yamlCreate New Items
# Create in inbox (default for 'issue' type)
llmail new issue --title "New task"
# Create in specific state
llmail new bug --state active --title "Critical bug" --content "Some stuff about this bug"
# Create with frontmatter
llmail new feat --state active --frontmatter '{"priority": 1, "assignee": "alice"} --content "Some info about this feature\n\nIt will be a great thing."'
# Create with content
llmail new doc --content "Initial documentation draft"Move Items
# Change state
llmail mv abc1 --state blocked
# Change type
llmail mv abc1 --type feat
# Update metadata during move
llmail mv abc1 --state active --frontmatter '{"assignee": "bob"}'Add a Timestamped XML-wrapped Update to an Item
Prepended to top of content by default
llmail update abc1 --update "Everything broke"
# Change type
llmail update abc1 -u "Everything's fixed"
# Update metadata during update
llmail update abc1 -u "Everything's fixed" --state fixed --frontmatter '{"fixed_reason": "nancy fixed it with science"}'
# Update metadata during update
llmail update abc1 -u "Everything's fixed" --state fixed --frontmatter '{"fixed_reason": "nancy fixed it with science"}'
# Append it to the bottom of the doc instead of the top
llmail update abc1 -u "Everything's fixed" --state fixed --frontmatter '{"fixed_reason": "nancy fixed it with science"}' --append
# Prepend the update to the bottom of the doc
# This is the default from cli but this is a valid option
llmail update abc1 -u "Everything's fixed" --state fixed --frontmatter '{"fixed_reason": "nancy fixed it with science"}' --prependMark Done
# Mark as done with reason
llmail done abc1 --state fixed
# Include resolution details
llmail done abc1 --state fixed --frontmatter '{"resolution": "Fixed in PR #123"}'Sync Files
# Preview changes
llmail sync --dry-run
# Apply changes
llmail sync
# Force sync without confirmation
llmail sync --forceRead files
Output contents of individual files to stdout
# output the full contents of an individual item
llmail info abc1
# output just the frontmatter
llmail info abc1 --frontmatter
# output just updates that have been made
llmail info abc1 --updates
# output the last 3 updates that have been made
llmail info abc1 --updates 3
# output just the content
llmail info abc1 --content
# output the first 10 lines of content
llmail info abc1 --content 10
# output the updates then the frontmatter
llmail info abc1 --updates --frontmatter
# output a specific field
llmail info abc1.frontmatterfield
API Reference
Core Actions
Initialize System
await llmail.init(options?: {
config?: string, // Path to config file
}): Promise<void>Create New Item
await llmail.new(type: string, options?: {
state?: string, // Initial state (defaults to 'inbox' for issue type)
frontmatter?: { // Additional frontmatter fields
title?: string,
[key: string]: any
},
content?: string, // Initial content
}): Promise<string> // Returns file IDMove Item
await llmail.mv(id: string, options: {
state?: string,
type?: string,
frontmatter?: Record<string, any> // Additional frontmatter updates
}): Promise<void>Mark Done
await llmail.done(id: string, options?: {
state: string, // Optional inactive state (ie 'fixed' or 'wontfix')
frontmatter?: Record<string, any> // Additional frontmatter updates
}): Promise<void>Sync Files
await llmail.sync(options?: {
dryRun?: boolean, // Just show what would change
force?: boolean // Skip confirmations
}): Promise<SyncResult>Types
interface SyncResult {
changes: Array<{
id: string,
from: string,
to: string,
state: string
}>,
errors?: Array<{
id: string,
error: string
}>
}
interface FileInfo {
id: string,
path: string,
type?: string,
state?: string,
metadata: Record<string, any>
}Plugin System
LLMail provides a powerful plugin system that allows extending core functionality in several ways:
Custom States & Types
- Register new active states
- Add inactive reasons
- Define custom types
Metadata Validation
- Add custom validation rules
- Enforce metadata requirements
- Validate state transitions
Intent Processing
- Define custom file organization patterns
- Control file naming and locations
- Add metadata processing hooks
Command Line Extensions
- Add new CLI commands with lifecycle hooks
- Create command hierarchies with subcommands
- Access command context and plugin configuration
- Handle command errors gracefully
Plugin Configuration
- Define default plugin configuration
- Validate configuration changes
- Access configuration through plugin service
Command Lifecycle
- Pre-execution hooks for argument modification
- Post-execution hooks for result processing
- Error handling hooks for graceful failure
TypeScript Support
LLMail is written in TypeScript and provides comprehensive type definitions for plugin development. We strongly recommend using TypeScript and our provided types when developing plugins. The following types are available:
import {
Plugin, // Core plugin interface
PluginCommand, // Command registration
CommandContext, // Command execution context
ValidationResult, // Validation results
FrontmatterMetadata, // File metadata
IntentPattern, // File organization patterns
InterpretedIntent, // Processed intent
FileLocation // File location information
} from 'llmail';Using our types provides several benefits:
- Compile-time type checking prevents common errors
- Automatic error detection for interface changes
- Rich IDE support with autocompletion
- Self-documenting code through type information
- Easier upgrades when APIs change
- Ensures correct implementation of plugin interfaces
Important: While you can develop plugins in JavaScript, using TypeScript with our types will significantly improve your development experience and help prevent runtime errors. Our plugin system is designed with TypeScript in mind, and all official plugins use TypeScript.
Plugin Interface
Plugins implement the Plugin interface:
interface Plugin {
/** Unique identifier for the plugin */
name: string;
/** Plugin priority - higher numbers run first (default: 0) */
priority?: number;
/** Default configuration for this plugin */
defaultConfig?: Record<string, any>;
/** Optional function to validate plugin configuration */
validateConfig?: (config: Record<string, any>) => ValidationResult;
/** Register new or override existing intent patterns */
registerIntents?: (existingPatterns: IntentPattern[]) => IntentPattern[];
/** Register additional valid types */
registerTypes?: () => string[];
/** Register additional valid active states */
registerActiveStates?: () => string[];
/** Register additional valid inactive reasons */
registerInactiveReasons?: () => string[];
/** Custom metadata validation hook */
validateMetadata?: (metadata: FrontmatterMetadata) => ValidationResult;
/** Post-processing hook for interpreted intents */
postInterpretIntent?: (intent: InterpretedIntent) => InterpretedIntent;
/** Register additional CLI commands */
registerCommands?: () => PluginCommand[];
}Example Plugin: Test Triage
Here's a real-world example of a plugin that adds test state tracking:
export class TestTriagePlugin implements Plugin {
name = 'test-triage';
priority = 10; // High priority to ensure test patterns run first
// Default plugin configuration
defaultConfig = {
reportFormat: 'markdown',
autoSync: true,
maxRetries: 3
};
// Validate configuration changes
validateConfig(config: Record<string, any>) {
const errors: string[] = [];
if (!['markdown', 'html', 'json'].includes(config.reportFormat)) {
errors.push('reportFormat must be one of: markdown, html, json');
}
if (typeof config.autoSync !== 'boolean') {
errors.push('autoSync must be a boolean');
}
if (typeof config.maxRetries !== 'number' || config.maxRetries < 1) {
errors.push('maxRetries must be a positive number');
}
return { valid: errors.length === 0, errors };
}
// Add test-specific states
registerActiveStates() {
return ['pass'];
}
registerInactiveReasons() {
return ['fail'];
}
// Add test type
registerTypes() {
return ['test'];
}
// Validate test metadata
validateMetadata(metadata: FrontmatterMetadata) {
if (metadata.isTest) {
if (!metadata.testId) {
return {
valid: false,
errors: ['Test files must have a testId']
};
}
if (metadata.state === 'fail' && !metadata.failureReason) {
return {
valid: false,
errors: ['Failed tests must have a failure reason']
};
}
}
return { valid: true };
}
// Register test-specific commands with lifecycle hooks
registerCommands() {
return [{
name: 'test',
description: 'Test management commands',
subcommands: [
{
name: 'run',
description: 'Run tests',
options: [
{ name: 'suite', type: 'string', description: 'Test suite to run' }
],
beforeExecute: async (args) => {
// Validate test suite exists
console.log('Validating test suite...');
return args;
},
execute: async (args, context) => {
// Get plugin configuration
const config = context.pluginService.getPluginConfig('test-triage');
console.log(`Running tests with ${config.maxRetries} retries...`);
// Run test implementation
},
afterExecute: async (result) => {
console.log('Tests completed, generating report...');
},
onError: async (error, args) => {
console.error(`Test run failed: ${error.message}`);
return new Error(`Test suite "${args.suite}" failed: ${error.message}`);
}
},
{
name: 'report',
description: 'Generate test report',
execute: async (args, context) => {
const config = context.pluginService.getPluginConfig('test-triage');
console.log(`Generating ${config.reportFormat} report...`);
// Report generation implementation
}
}
]
}];
}
// Custom file organization for tests
registerIntents(existingPatterns: IntentPattern[]) {
const testPatterns = [
{
pattern: {
metadata: { specificState: 'pass' }
},
interpretation: (state) => ({
targetLocation: {
dirname: path.join('issues', '_pass'),
basename: `${state.metadata.testId}-passed.md`
},
targetMetadata: {
...state.metadata,
active: true,
state: 'pass'
}
})
},
// ... more patterns
];
return [...testPatterns, ...existingPatterns];
}
}Using Plugins
Install Plugin Package
npm install llmail-testlinkEnable Plugin in llmail.yaml
# Simple enable by name plugins: - llmail-testlink # Or with configuration plugins: - name: llmail-testlink config: reportFormat: 'html' autoSync: true maxRetries: 5Use Plugin Features
// Create a test with plugin-specific metadata const id = await llmail.new('test', { frontmatter: { isTest: true, testId: 'render-test', title: 'UI Rendering Test' } }); // Use plugin commands llmail test run --suite unit llmail test reportUpdate Plugin Configuration
// Get plugin configuration const config = llmail.getPluginService().getPluginConfig('llmail-testlink'); console.log(config); // { reportFormat: 'html', autoSync: true, maxRetries: 5 } // Update plugin configuration programmatically llmail.getPluginService().setPluginConfig('llmail-testlink', { reportFormat: 'json', autoSync: true, maxRetries: 5 });
Note: When a plugin is configured in
llmail.yaml, its configuration will be automatically loaded during initialization. You can still update the configuration programmatically using the plugin service.
Plugin Development Guidelines
Initialization
- Register early in the application lifecycle
- Use priority to control execution order
- Initialize any required resources
- Set up default configuration
State Management
- Keep plugin state isolated
- Use metadata for persistence
- Clean up resources when done
- Use plugin configuration for settings
Error Handling
- Validate inputs thoroughly
- Provide clear error messages
- Handle failures gracefully
- Use command lifecycle hooks for error handling
Integration
- Work with existing llmail patterns
- Follow file naming conventions
- Respect metadata constraints
- Access plugin context safely
Testing
- Test all plugin functionality
- Verify error conditions
- Check state transitions
- Test configuration validation
Plugin Best Practices
Metadata
- Use clear, namespaced keys
- Document all custom fields
- Validate required fields
- Consider configuration impact
File Organization
- Follow llmail directory patterns
- Use clear file naming
- Document structure changes
- Make paths configurable
Commands
- Use descriptive names
- Provide helpful descriptions
- Include usage examples
- Implement lifecycle hooks
Performance
- Minimize filesystem operations
- Cache when appropriate
- Handle large datasets
- Use configuration for tuning
Configuration
- Provide sensible defaults
- Validate configuration changes
- Document configuration options
- Use type-safe configuration
For more examples, see the examples/ directory in the repository.
File Organization Rules
Active Items
- Live in state-specific directories prefixed with underscore (e.g.,
issues/_working/) - Follow naming pattern:
{state}-{type}-{id}.md - Must have a state when outside inbox
Inactive Items
- Live in type directories (e.g.,
issues/bug/) - Follow naming pattern:
[{state}-]{type}-{id}.md - State is optional but preserved when present
Inbox Items
- Live in
inbox/directory - Follow naming pattern:
{type}-{id}.md - No state in filename or metadata
Intent System
LLMail uses an intent-based system where Location Follows Metadata: Running 'sync' will move files where their metadata says they should be.
Examples
// Sync aligns locations with metadata
await llmail.sync(); // Moves files to match their metadata state
// Everything else preserved
await llmail.done('abc1', {
state: 'fixed' // Only changes state-related fields
}); // All other metadata preservedContributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Publishing Plugins
When publishing a plugin to npm, follow these guidelines to ensure it can be loaded via llmail.yaml:
Package Naming
- Use the prefix
llmail-for your package name (e.g.,llmail-github) - This helps users discover your plugin and ensures proper loading
- Use the prefix
Package Structure
llmail-myplugin/ ├── package.json ├── README.md ├── dist/ │ └── index.js # Built plugin code └── src/ └── index.ts # Plugin source codePackage.json Configuration
{ "name": "llmail-myplugin", "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "keywords": ["llmail", "llmail-plugin"], "peerDependencies": { "llmail": "^1.1.0" } }Plugin Export
// src/index.ts import { Plugin } from 'llmail'; export class MyPlugin implements Plugin { name = 'llmail-myplugin'; // ... plugin implementation } // Important: Export plugin class as default export export default MyPlugin;Documentation
- Document all configuration options
- Provide example usage with
llmail.yaml - Include TypeScript types for configuration
Example plugin documentation:
# llmail-myplugin
A plugin for llmail that does amazing things.
## Installation
```bash
npm install llmail-mypluginConfiguration
Add to your llmail.yaml:
plugins:
- llmail-myplugin # Use default configuration
# Or with custom configuration:
- name: llmail-myplugin
config:
option1: value1
option2: value2Configuration Options
| Option | Type | Default | Description | |---------|---------|---------|----------------------| | option1 | string | 'def' | Controls feature X | | option2 | boolean | true | Enables feature Y |
