An intent-based inbox and issue management system for LLM collaboration.
- 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-core provides a simple base you can use to build much more powerful systems.
/
├── 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.md
# 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
npm install llmail-core
import { LLMail } from 'llmail-core';
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)
# Initialize with default configuration
llmail init
# Use specific config file
llmail init --config path/to/config.yaml
# 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"
# 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"}'
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"}' --prepend
# Mark as done with reason
llmail done abc1 --state fixed
# Include resolution details
llmail done abc1 --state fixed --frontmatter '{"resolution": "Fixed in PR #123"}'
# Preview changes
llmail sync --dry-run
# Apply changes
llmail sync
# Force sync without confirmation
llmail sync --force
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
await llmail.init(options?: {
config?: string, // Path to config file
}): Promise<void>
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 ID
await llmail.mv(id: string, options: {
state?: string,
type?: string,
frontmatter?: Record<string, any> // Additional frontmatter updates
}): Promise<void>
await llmail.done(id: string, options?: {
state: string, // Optional inactive state (ie 'fixed' or 'wontfix')
frontmatter?: Record<string, any> // Additional frontmatter updates
}): Promise<void>
await llmail.sync(options?: {
dryRun?: boolean, // Just show what would change
force?: boolean // Skip confirmations
}): Promise<SyncResult>
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>
}
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
- Create command hierarchies
- Integrate with external tools
Plugins implement the Plugin
interface:
interface Plugin {
/** Unique identifier for the plugin */
name: string;
/** Plugin priority - higher numbers run first (default: 0) */
priority?: number;
/** 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[];
}
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
// 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
registerCommands() {
return [{
name: 'test',
description: 'Test management commands',
subcommands: [
{
name: 'run',
description: 'Run tests',
options: [
{ name: 'suite', type: 'string', description: 'Test suite to run' }
],
execute: async (args) => {
// Run test implementation
}
},
{
name: 'report',
description: 'Generate test report',
execute: async () => {
// 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];
}
}
-
Register the Plugin
const llmail = new LLMail({ plugins: [new TestTriagePlugin()] }); await llmail.init();
-
Use 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 report
-
Initialization
- Register early in the application lifecycle
- Use priority to control execution order
- Initialize any required resources
-
State Management
- Keep plugin state isolated
- Use metadata for persistence
- Clean up resources when done
-
Error Handling
- Validate inputs thoroughly
- Provide clear error messages
- Handle failures gracefully
-
Integration
- Work with existing llmail patterns
- Follow file naming conventions
- Respect metadata constraints
-
Testing
- Test all plugin functionality
- Verify error conditions
- Check state transitions
-
Metadata
- Use clear, namespaced keys
- Document all custom fields
- Validate required fields
-
File Organization
- Follow llmail directory patterns
- Use clear file naming
- Document structure changes
-
Commands
- Use descriptive names
- Provide helpful descriptions
- Include usage examples
-
Performance
- Minimize filesystem operations
- Cache when appropriate
- Handle large datasets
For more examples, see the examples/
directory in the repository.
- 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
- Live in type directories (e.g.,
issues/bug/
) - Follow naming pattern:
[{state}-]{type}-{id}.md
- State is optional but preserved when present
- Live in
inbox/
directory - Follow naming pattern:
{type}-{id}.md
- No state in filename or metadata
LLMail uses an intent-based system where Location Follows Metadata: Running 'sync' will move files where their metadata says they should be.
// 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 preserved
- 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
This project is licensed under the MIT License - see the LICENSE file for details.