# Functions

Functions are the core of your plugin. They define callable operations with strongly-typed inputs and outputs.

## Basic Function

```typescript
// src/index.ts
import { definePlugin } from '@majkapp/plugin-kit';

const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
  .pluginRoot(__dirname)

  .function('health', {
    // REQUIRED: 2-3 sentence description for LLM context
    description: 'Check plugin health status. Returns current status and timestamp.',

    // REQUIRED: JSON Schema for input validation
    input: {
      type: 'object',
      properties: {},
      additionalProperties: false  // Reject unknown properties
    },

    // REQUIRED: JSON Schema for output validation
    output: {
      type: 'object',
      properties: {
        status: { type: 'string', enum: ['ok', 'error'] },
        timestamp: { type: 'string', format: 'date-time' }
      },
      required: ['status', 'timestamp']
    },

    // REQUIRED: Handler function
    handler: async (input, ctx) => {
      // ctx.logger - scoped logging (automatically includes plugin name)
      ctx.logger.info('Health check requested');

      // Return MUST match output schema or validation fails
      return {
        status: 'ok',
        timestamp: new Date().toISOString()
      };
    },

    // OPTIONAL: Tags for organization and discovery
    tags: ['monitoring', 'health']
  });
```

Test it:

```typescript
// tests/plugin/functions/unit/health.test.js
import { test, invoke, mock } from '@majkapp/plugin-test';
import assert from 'assert';

test('health check returns ok status', async () => {
  const context = mock().build();
  const result = await invoke('health', {}, { context });

  assert.strictEqual(result.status, 'ok');
  assert.ok(result.timestamp);
});
```

## Function with Input Parameters

```typescript
// src/index.ts
.function('getAnalytics', {
  description: 'Get analytics for specified time period. Returns user activity metrics.',

  input: {
    type: 'object',
    properties: {
      period: {
        type: 'string',
        enum: ['24h', '7d', '30d'],
        description: 'Time period for analytics'
      },
      includeDetails: {
        type: 'boolean',
        default: false,
        description: 'Include detailed breakdown'
      }
    },
    required: ['period'],  // period is required, includeDetails is optional
    additionalProperties: false
  },

  output: {
    type: 'object',
    properties: {
      period: { type: 'string' },
      metrics: {
        type: 'object',
        properties: {
          activeUsers: { type: 'number' },
          totalSessions: { type: 'number' },
          avgDuration: { type: 'number' }
        },
        required: ['activeUsers', 'totalSessions', 'avgDuration']
      },
      details: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            date: { type: 'string' },
            count: { type: 'number' }
          }
        }
      }
    },
    required: ['period', 'metrics']
  },

  handler: async (input, ctx) => {
    ctx.logger.info(`Analytics requested for ${input.period}`);

    // Access plugin storage (key-value store scoped to your plugin)
    const sessions = await ctx.storage.get('sessions') || [];

    // Calculate metrics (best practice: move to separate file)
    const now = Date.now();
    const periodMs = input.period === '24h' ? 86400000 :
                     input.period === '7d' ? 604800000 : 2592000000;

    const recentSessions = sessions.filter(s =>
      now - s.timestamp < periodMs
    );

    const uniqueUsers = new Set(recentSessions.map(s => s.userId)).size;

    const result = {
      period: input.period,
      metrics: {
        activeUsers: uniqueUsers,
        totalSessions: recentSessions.length,
        avgDuration: recentSessions.length > 0
          ? Math.round(recentSessions.reduce((sum, s) => sum + s.duration, 0) / recentSessions.length)
          : 0
      }
    };

    // Conditionally include details based on input
    if (input.includeDetails) {
      result.details = calculateDailyBreakdown(recentSessions);
    }

    return result;
  },

  tags: ['analytics', 'reporting']
});
```

Test with different inputs:

```typescript
// tests/plugin/functions/unit/analytics.test.js
import { test, invoke, mock } from '@majkapp/plugin-test';
import assert from 'assert';

test('getAnalytics returns metrics for 7d period', async () => {
  const context = mock()
    .storage({
      sessions: [
        { userId: 'user1', duration: 300, timestamp: Date.now() - 86400000 },  // 1 day ago
        { userId: 'user2', duration: 450, timestamp: Date.now() - 172800000 }, // 2 days ago
        { userId: 'user1', duration: 600, timestamp: Date.now() - 259200000 }  // 3 days ago
      ]
    })
    .build();

  const result = await invoke('getAnalytics', { period: '7d' }, { context });

  assert.strictEqual(result.period, '7d');
  assert.strictEqual(result.metrics.activeUsers, 2);  // user1 and user2
  assert.strictEqual(result.metrics.totalSessions, 3);
  assert.strictEqual(result.metrics.avgDuration, 450); // (300+450+600)/3
});

test('getAnalytics includes details when requested', async () => {
  const context = mock()
    .storage({ sessions: [] })
    .build();

  const result = await invoke('getAnalytics', {
    period: '24h',
    includeDetails: true
  }, { context });

  assert.ok(result.details);
  assert.ok(Array.isArray(result.details));
});
```

## Best Practice: Decouple Business Logic

**ALWAYS separate core logic from plugin code.** Makes testing easier and logic reusable.

```typescript
// src/core/analytics.ts - Pure business logic, NO plugin dependencies
export interface Session {
  userId: string;
  duration: number;
  timestamp: number;
}

export interface AnalyticsResult {
  period: string;
  metrics: {
    activeUsers: number;
    totalSessions: number;
    avgDuration: number;
  };
}

// Pure function - testable with plain Jest/Vitest
export function calculateAnalytics(
  sessions: Session[],
  period: '24h' | '7d' | '30d'
): AnalyticsResult {
  const now = Date.now();
  const periodMs = {
    '24h': 86400000,
    '7d': 604800000,
    '30d': 2592000000
  }[period];

  const recentSessions = sessions.filter(s =>
    now - s.timestamp < periodMs
  );

  const uniqueUsers = new Set(recentSessions.map(s => s.userId)).size;
  const totalDuration = recentSessions.reduce((sum, s) => sum + s.duration, 0);

  return {
    period,
    metrics: {
      activeUsers: uniqueUsers,
      totalSessions: recentSessions.length,
      avgDuration: recentSessions.length > 0
        ? Math.round(totalDuration / recentSessions.length)
        : 0
    }
  };
}
```

```typescript
// tests/core/analytics.test.js - Test with plain Jest (faster, no plugin overhead)
import { calculateAnalytics } from '../src/core/analytics';
import { describe, it, expect } from 'vitest';

describe('calculateAnalytics', () => {
  it('calculates metrics for 7d period', () => {
    const sessions = [
      { userId: 'user1', duration: 300, timestamp: Date.now() - 86400000 },
      { userId: 'user2', duration: 450, timestamp: Date.now() - 172800000 },
      { userId: 'user1', duration: 600, timestamp: Date.now() - 259200000 }
    ];

    const result = calculateAnalytics(sessions, '7d');

    expect(result.metrics.activeUsers).toBe(2);
    expect(result.metrics.totalSessions).toBe(3);
    expect(result.metrics.avgDuration).toBe(450);
  });

  it('filters out old sessions', () => {
    const sessions = [
      { userId: 'user1', duration: 300, timestamp: Date.now() - 86400000 },   // 1 day ago - included
      { userId: 'user2', duration: 450, timestamp: Date.now() - 172800000 },  // 2 days ago - excluded
    ];

    const result = calculateAnalytics(sessions, '24h');

    expect(result.metrics.activeUsers).toBe(1);
    expect(result.metrics.totalSessions).toBe(1);
  });
});
```

```typescript
// src/index.ts - Plugin function is thin wrapper
import { calculateAnalytics } from './core/analytics';

const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
  .pluginRoot(__dirname)

  .function('getAnalytics', {
    description: 'Get analytics for specified time period',
    input: { /* schema */ },
    output: { /* schema */ },
    handler: async (input, ctx) => {
      // Plugin layer: Get data from storage
      const sessions = await ctx.storage.get('sessions') || [];

      // Business logic: Pure computation (easy to test)
      const result = calculateAnalytics(sessions, input.period);

      // Plugin layer: Log and return
      ctx.logger.info(`Analytics calculated for ${input.period}`, result);
      return result;
    }
  });
```

## Accessing MAJK APIs

Use `ctx.majk` to access MAJK's conversation, todo, project, and teammate APIs.

```typescript
.function('getConversationSummary', {
  description: 'Get summary of a specific conversation with recent messages',

  input: {
    type: 'object',
    properties: {
      conversationId: { type: 'string' }
    },
    required: ['conversationId'],
    additionalProperties: false
  },

  output: {
    type: 'object',
    properties: {
      id: { type: 'string' },
      title: { type: 'string' },
      messageCount: { type: 'number' },
      lastActivity: { type: 'string' }
    },
    required: ['id', 'title', 'messageCount']
  },

  handler: async (input, ctx) => {
    // Access MAJK conversation API
    const conversation = await ctx.majk.conversations.get(input.conversationId);

    if (!conversation) {
      ctx.logger.warn(`Conversation not found: ${input.conversationId}`);
      throw new Error(`Conversation ${input.conversationId} not found`);
    }

    ctx.logger.info(`Retrieved conversation: ${conversation.title}`);

    return {
      id: conversation.id,
      title: conversation.title || 'Untitled',
      messageCount: conversation.messages?.length || 0,
      lastActivity: conversation.updatedAt || conversation.createdAt
    };
  },

  tags: ['conversations']
});
```

Available `ctx.majk` APIs:

```typescript
// Conversations
await ctx.majk.conversations.list();
await ctx.majk.conversations.get(id);

// Todos
await ctx.majk.todos.list();
await ctx.majk.todos.get(id);

// Projects
await ctx.majk.projects.list();
await ctx.majk.projects.get(id);

// Teammates
await ctx.majk.teammates.list();
await ctx.majk.teammates.get(id);

// MCP Servers
await ctx.majk.mcpServers.list();
await ctx.majk.mcpServers.get(id);

// Event Bus
const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
  console.log('Event:', event);
});
// Later: subscription.unsubscribe();
```

## Mutation Functions

Functions that modify state (create, update, delete) are **mutations**.

```typescript
.function('clearEvents', {
  description: 'Clear all logged events from storage',

  input: {
    type: 'object',
    properties: {},
    additionalProperties: false
  },

  output: {
    type: 'object',
    properties: {
      success: { type: 'boolean' },
      clearedCount: { type: 'number' }
    },
    required: ['success', 'clearedCount']
  },

  handler: async (input, ctx) => {
    const events = await ctx.storage.get('events') || [];
    const count = events.length;

    // Clear storage
    await ctx.storage.set('events', []);

    ctx.logger.info(`Cleared ${count} events`);

    return {
      success: true,
      clearedCount: count
    };
  },

  tags: ['mutations', 'events']
})
```

Generated React hook will be a **mutation hook** (call `.mutate()` manually):

```typescript
// ui/src/EventsPage.tsx
import { useClearEvents } from './generated/hooks';

export function EventsPage() {
  const { mutate: clearEvents, loading } = useClearEvents();

  return (
    <button
      onClick={() => clearEvents({})}
      disabled={loading}
      data-majk-id="clearEventsButton"
      data-majk-state={loading ? 'loading' : 'idle'}
    >
      {loading ? 'Clearing...' : 'Clear Events'}
    </button>
  );
}
```

## UI Logging Function (Best Practice)

**Always include a `uiLog` function** for debugging frontend issues.

```typescript
.function('uiLog', {
  description: 'Log messages from UI for debugging. Allows frontend to send logs to backend.',

  input: {
    type: 'object',
    properties: {
      level: {
        type: 'string',
        enum: ['debug', 'info', 'warn', 'error'],
        description: 'Log level'
      },
      component: {
        type: 'string',
        description: 'React component name'
      },
      message: {
        type: 'string',
        description: 'Log message'
      },
      data: {
        type: 'object',
        description: 'Additional context data'
      }
    },
    required: ['level', 'component', 'message'],
    additionalProperties: false
  },

  output: {
    type: 'object',
    properties: {
      success: { type: 'boolean' }
    },
    required: ['success']
  },

  handler: async (input, ctx) => {
    const prefix = `[UI:${input.component}]`;
    const message = `${prefix} ${input.message}`;

    // Route to appropriate log level
    switch (input.level) {
      case 'debug':
        ctx.logger.debug(message, input.data);
        break;
      case 'info':
        ctx.logger.info(message, input.data);
        break;
      case 'warn':
        ctx.logger.warn(message, input.data);
        break;
      case 'error':
        ctx.logger.error(message, input.data);
        break;
    }

    return { success: true };
  },

  tags: ['debugging', 'ui']
})
```

Use in React:

```typescript
import { useUiLog } from './generated/hooks';
import { useEffect } from 'react';

export function DashboardPage() {
  const { mutate: uiLog } = useUiLog();

  // Log component lifecycle
  useEffect(() => {
    uiLog({
      level: 'info',
      component: 'DashboardPage',
      message: 'Component mounted'
    });

    return () => {
      uiLog({
        level: 'info',
        component: 'DashboardPage',
        message: 'Component unmounted'
      });
    };
  }, [uiLog]);

  // Log errors
  const handleClick = async () => {
    try {
      await someOperation();
    } catch (error) {
      uiLog({
        level: 'error',
        component: 'DashboardPage',
        message: 'Operation failed',
        data: { error: error.message, stack: error.stack }
      });
    }
  };

  return <div>...</div>;
}
```

## Error Handling

```typescript
.function('riskyOperation', {
  description: 'Performs an operation that might fail',

  input: { /* schema */ },
  output: { /* schema */ },

  handler: async (input, ctx) => {
    try {
      // Attempt operation
      const result = await performOperation(input);

      ctx.logger.info('Operation succeeded', { result });
      return result;

    } catch (error) {
      // Log error with context
      ctx.logger.error('Operation failed', {
        error: error.message,
        stack: error.stack,
        input
      });

      // Re-throw or return error response
      throw new Error(`Operation failed: ${error.message}`);
    }
  }
})
```

## Function Tags

Tags help organize and discover functions.

```typescript
.function('health', { /* ... */, tags: ['monitoring', 'health'] })
.function('getAnalytics', { /* ... */, tags: ['analytics', 'reporting'] })
.function('clearEvents', { /* ... */, tags: ['mutations', 'events'] })
.function('uiLog', { /* ... */, tags: ['debugging', 'ui'] })
```

Tags are used by:
- Service grouping (see SERVICES.md)
- Function discovery
- Documentation organization

## Next Steps

Run `npx @majkapp/plugin-kit --hooks` - Generated React hooks reference
Run `npx @majkapp/plugin-kit --context` - Complete ctx API reference
Run `npx @majkapp/plugin-kit --services` - Group functions into services
Run `npx @majkapp/plugin-kit --testing` - Test your functions
