# 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/`)
- **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.md
```

## Configuration (llmail.yaml)

```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
          - feature
```

## Quick Start

```bash
npm install llmail
```

```typescript
import { 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

```bash
# Initialize with default configuration
llmail init

# Use specific config file
llmail init --config path/to/config.yaml
```

### Create New Items

```bash
# 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

```bash
# 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

```bash
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 Done

```bash
# 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

```bash
# Preview changes
llmail sync --dry-run

# Apply changes
llmail sync

# Force sync without confirmation
llmail sync --force
```

### Read files
Output contents of individual files to stdout

```bash
# 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

```typescript
await llmail.init(options?: {
  config?: string,  // Path to config file
}): Promise<void>
```

#### Create New Item

```typescript
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
```

#### Move Item

```typescript
await llmail.mv(id: string, options: {
  state?: string,
  type?: string,
  frontmatter?: Record<string, any>  // Additional frontmatter updates
}): Promise<void>
```

#### Mark Done

```typescript
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

```typescript
await llmail.sync(options?: {
  dryRun?: boolean,    // Just show what would change
  force?: boolean      // Skip confirmations
}): Promise<SyncResult>
```

### Types

```typescript
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:

1. **Custom States & Types**
   - Register new active states
   - Add inactive reasons
   - Define custom types

2. **Metadata Validation**
   - Add custom validation rules
   - Enforce metadata requirements
   - Validate state transitions

3. **Intent Processing**
   - Define custom file organization patterns
   - Control file naming and locations
   - Add metadata processing hooks

4. **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

5. **Plugin Configuration**
   - Define default plugin configuration
   - Validate configuration changes
   - Access configuration through plugin service

6. **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:

```typescript
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:

```typescript
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:

```typescript
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

1. **Install Plugin Package**
   ```bash
   npm install llmail-testlink
   ```

2. **Enable Plugin in llmail.yaml**
   ```yaml
   # Simple enable by name
   plugins:
     - llmail-testlink

   # Or with configuration
   plugins:
     - name: llmail-testlink
       config:
         reportFormat: 'html'
         autoSync: true
         maxRetries: 5
   ```

3. **Use Plugin Features**
   ```typescript
   // 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
   ```

4. **Update Plugin Configuration**
   ```typescript
   // 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

1. **Initialization**
   - Register early in the application lifecycle
   - Use priority to control execution order
   - Initialize any required resources
   - Set up default configuration

2. **State Management**
   - Keep plugin state isolated
   - Use metadata for persistence
   - Clean up resources when done
   - Use plugin configuration for settings

3. **Error Handling**
   - Validate inputs thoroughly
   - Provide clear error messages
   - Handle failures gracefully
   - Use command lifecycle hooks for error handling

4. **Integration**
   - Work with existing llmail patterns
   - Follow file naming conventions
   - Respect metadata constraints
   - Access plugin context safely

5. **Testing**
   - Test all plugin functionality
   - Verify error conditions
   - Check state transitions
   - Test configuration validation

### Plugin Best Practices

1. **Metadata**
   - Use clear, namespaced keys
   - Document all custom fields
   - Validate required fields
   - Consider configuration impact

2. **File Organization**
   - Follow llmail directory patterns
   - Use clear file naming
   - Document structure changes
   - Make paths configurable

3. **Commands**
   - Use descriptive names
   - Provide helpful descriptions
   - Include usage examples
   - Implement lifecycle hooks

4. **Performance**
   - Minimize filesystem operations
   - Cache when appropriate
   - Handle large datasets
   - Use configuration for tuning

5. **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 preserved
```

## Contributing

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. 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`:

1. **Package Naming**
   - Use the prefix `llmail-` for your package name (e.g., `llmail-github`)
   - This helps users discover your plugin and ensures proper loading

2. **Package Structure**
   ```
   llmail-myplugin/
   ├── package.json
   ├── README.md
   ├── dist/
   │   └── index.js      # Built plugin code
   └── src/
       └── index.ts      # Plugin source code
   ```

3. **Package.json Configuration**
   ```json
   {
     "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"
     }
   }
   ```

4. **Plugin Export**
   ```typescript
   // 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;
   ```

5. **Documentation**
   - Document all configuration options
   - Provide example usage with `llmail.yaml`
   - Include TypeScript types for configuration

Example plugin documentation:
```markdown
# llmail-myplugin

A plugin for llmail that does amazing things.

## Installation

```bash
npm install llmail-myplugin
```

## Configuration

Add to your `llmail.yaml`:

```yaml
plugins:
  - llmail-myplugin  # Use default configuration

  # Or with custom configuration:
  - name: llmail-myplugin
    config:
      option1: value1
      option2: value2
```

## Configuration Options

| Option  | Type    | Default | Description           |
|---------|---------|---------|----------------------|
| option1 | string  | 'def'   | Controls feature X   |
| option2 | boolean | true    | Enables feature Y    |
```