# Context API

Every function handler receives a `ctx` parameter with access to MAJK APIs, storage, and logging.

```typescript
.function('myFunction', {
  description: 'Example function',
  input: { /* ... */ },
  output: { /* ... */ },
  handler: async (input, ctx) => {
    // ctx.majk - Access MAJK data
    // ctx.storage - Plugin-scoped key-value store
    // ctx.logger - Scoped logging
    return { /* ... */ };
  }
})
```

## ctx.logger

Scoped logging automatically includes your plugin name.

```typescript
handler: async (input, ctx) => {
  // Debug - verbose information for development
  ctx.logger.debug('Processing request', { input });

  // Info - normal operational messages
  ctx.logger.info('Operation started', { userId: input.userId });

  // Warn - warning conditions, non-critical issues
  ctx.logger.warn('Rate limit approaching', { count: 95, limit: 100 });

  // Error - error conditions, failures
  ctx.logger.error('Operation failed', {
    error: error.message,
    stack: error.stack,
    input
  });

  return { /* ... */ };
}
```

**Best practice:** Always log errors with context:

```typescript
handler: async (input, ctx) => {
  try {
    const result = await performOperation(input);
    ctx.logger.info('Operation succeeded', { result });
    return result;
  } catch (error) {
    ctx.logger.error('Operation failed', {
      error: error.message,
      stack: error.stack,
      input,  // Include input for debugging
      timestamp: new Date().toISOString()
    });
    throw error;  // Re-throw or return error response
  }
}
```

## ctx.storage

Plugin-scoped key-value storage. Data persists between function calls.

```typescript
// Set a value
await ctx.storage.set('counter', 42);
await ctx.storage.set('user-settings', { theme: 'dark', notifications: true });

// Get a value (returns undefined if not found)
const counter = await ctx.storage.get('counter');  // 42
const settings = await ctx.storage.get('user-settings');  // { theme: 'dark', ... }

// Check if key exists
const exists = await ctx.storage.has('counter');  // true

// Delete a key
await ctx.storage.delete('counter');

// Clear all storage (use with caution!)
await ctx.storage.clear();
```

### Example: Counter

```typescript
.function('incrementCounter', {
  description: 'Increment counter and return new value',
  input: {
    type: 'object',
    properties: {},
    additionalProperties: false
  },
  output: {
    type: 'object',
    properties: {
      counter: { type: 'number' }
    }
  },
  handler: async (input, ctx) => {
    // Get current counter (default to 0)
    const current = await ctx.storage.get('counter') || 0;

    // Increment
    const newValue = current + 1;

    // Save
    await ctx.storage.set('counter', newValue);

    ctx.logger.info(`Counter incremented to ${newValue}`);

    return { counter: newValue };
  }
})
```

### Example: Session Tracking

```typescript
.function('trackSession', {
  description: 'Track user session',
  input: {
    type: 'object',
    properties: {
      userId: { type: 'string' },
      duration: { type: 'number' }
    },
    required: ['userId', 'duration']
  },
  output: {
    type: 'object',
    properties: {
      totalSessions: { type: 'number' }
    }
  },
  handler: async (input, ctx) => {
    // Get existing sessions
    const sessions = await ctx.storage.get('sessions') || [];

    // Add new session
    sessions.push({
      userId: input.userId,
      duration: input.duration,
      timestamp: Date.now()
    });

    // Save updated sessions
    await ctx.storage.set('sessions', sessions);

    ctx.logger.info(`Session tracked for user ${input.userId}`, {
      duration: input.duration,
      totalSessions: sessions.length
    });

    return { totalSessions: sessions.length };
  }
})
```

## ctx.majk.conversations

Access MAJK conversations.

```typescript
handler: async (input, ctx) => {
  // List all conversations
  const conversations = await ctx.majk.conversations.list();
  // Returns: Array<{ id, title, createdAt, updatedAt, ... }>

  // Get specific conversation
  const conversation = await ctx.majk.conversations.get('conv-id');
  // Returns: { id, title, messages, ... } or null if not found

  ctx.logger.info(`Found ${conversations.length} conversations`);

  return { count: conversations.length };
}
```

### Example: Conversation Summary

```typescript
.function('getConversationStats', {
  description: 'Get statistics across all conversations',
  input: {
    type: 'object',
    properties: {},
    additionalProperties: false
  },
  output: {
    type: 'object',
    properties: {
      totalConversations: { type: 'number' },
      totalMessages: { type: 'number' },
      mostActive: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          title: { type: 'string' },
          messageCount: { type: 'number' }
        }
      }
    }
  },
  handler: async (input, ctx) => {
    const conversations = await ctx.majk.conversations.list();

    let totalMessages = 0;
    let mostActive = null;
    let maxMessages = 0;

    for (const conv of conversations) {
      const messageCount = conv.messages?.length || 0;
      totalMessages += messageCount;

      if (messageCount > maxMessages) {
        maxMessages = messageCount;
        mostActive = {
          id: conv.id,
          title: conv.title || 'Untitled',
          messageCount
        };
      }
    }

    ctx.logger.info('Conversation stats calculated', {
      totalConversations: conversations.length,
      totalMessages
    });

    return {
      totalConversations: conversations.length,
      totalMessages,
      mostActive: mostActive || { id: '', title: '', messageCount: 0 }
    };
  }
})
```

## ctx.majk.todos

Access MAJK todos.

```typescript
handler: async (input, ctx) => {
  // List all todos
  const todos = await ctx.majk.todos.list();
  // Returns: Array<{ id, title, status, ... }>

  // Get specific todo
  const todo = await ctx.majk.todos.get('todo-id');
  // Returns: { id, title, status, ... } or null

  // Filter by status
  const pending = todos.filter(t => t.status === 'pending');
  const completed = todos.filter(t => t.status === 'completed');

  return { pending: pending.length, completed: completed.length };
}
```

## ctx.majk.projects

Access MAJK projects.

```typescript
handler: async (input, ctx) => {
  // List all projects
  const projects = await ctx.majk.projects.list();
  // Returns: Array<{ id, name, ... }>

  // Get specific project
  const project = await ctx.majk.projects.get('project-id');
  // Returns: { id, name, ... } or null

  return { projectCount: projects.length };
}
```

## ctx.majk.teammates

Access MAJK teammates (AI assistants).

```typescript
handler: async (input, ctx) => {
  // List all teammates
  const teammates = await ctx.majk.teammates.list();
  // Returns: Array<{ id, name, expertise, ... }>

  // Get specific teammate
  const teammate = await ctx.majk.teammates.get('teammate-id');
  // Returns: { id, name, expertise, skills, ... } or null

  ctx.logger.info(`Found ${teammates.length} teammates`);

  return { teammates: teammates.map(t => ({ id: t.id, name: t.name })) };
}
```

## ctx.majk.mcpServers

Access MCP (Model Context Protocol) servers.

```typescript
handler: async (input, ctx) => {
  // List all MCP servers
  const servers = await ctx.majk.mcpServers.list();
  // Returns: Array<{ id, name, config, ... }>

  // Get specific server
  const server = await ctx.majk.mcpServers.get('server-id');
  // Returns: { id, name, config, ... } or null

  return { serverCount: servers.length };
}
```

## ctx.majk.eventBus

Subscribe to MAJK events (conversations, todos, projects, etc.).

**CRITICAL:** Use in `.onReady()` to register cleanup.

```typescript
.onReady(async (ctx, cleanup) => {
  ctx.logger.info('Subscribing to event bus');

  // Subscribe to ALL events
  const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
    ctx.logger.info('Event received', {
      entityType: event.entityType,   // 'conversation', 'todo', 'project', etc.
      eventType: event.type,          // 'created', 'updated', 'deleted'
      entityId: event.entity?.id
    });

    // Store event for later display
    // ... (see LIFECYCLE.md for full example)
  });

  // ALWAYS register cleanup to unsubscribe
  cleanup(() => {
    if (subscription && typeof subscription.unsubscribe === 'function') {
      subscription.unsubscribe();
      ctx.logger.info('Unsubscribed from event bus');
    }
  });
})
```

## ctx.majk.me

Identity-scoped Plugin Self API, pre-bound to this plugin's own ID. Manage the plugin's own conversations and teammates without needing to pass a plugin ID.

```typescript
handler: async (input, ctx) => {
  // List teammates registered to this plugin
  const teammates = await ctx.majk.me.teammates();

  // Get the default teammate (earliest-registered)
  const defaultTeammate = await ctx.majk.me.getDefaultTeammate();

  // List conversations owned by this plugin
  const active = await ctx.majk.me.conversations({ status: 'active' });

  // Spawn a background conversation with the default teammate
  const chat = await ctx.majk.me.startChat({
    message: 'Analyze the data and produce a summary',
    title: 'Background Analysis'
  });

  // Wait for the agent to finish
  const { status, lastMessage } = await chat.waitForCompletion({ timeoutMs: 60_000 });
  chat.dispose();

  return { status, result: lastMessage?.content };
}
```

### Reattach to an Existing Conversation

```typescript
handler: async (input, ctx) => {
  const chat = await ctx.majk.me.attachChat(input.conversationId);
  if (!chat) {
    return { error: 'Conversation not found or not owned by this plugin' };
  }

  await chat.send('Please also check the error handling.');

  const sub = chat.onMessage((event) => {
    if (event.type === 'conversation_complete') {
      sub.unsubscribe();
      chat.dispose();
    }
  });

  return { conversationId: chat.conversationId };
}
```

**For comprehensive patterns and method reference:** Run `npx @majkapp/plugin-kit --llm:me`

## ctx.rpc

Enable inter-plugin communication with RPC services and callbacks.

### Register Service

Make your plugin's functions available to other plugins:

```typescript
.onLoad(async (ctx) => {
  // Register a service that other plugins can call
  await ctx.rpc.registerService('fileProcessor', {
    async processFile(path: string, onProgress: (percent: number) => void): Promise<string> {
      await onProgress(0);
      // ... process file ...
      await onProgress(50);
      // ... more processing ...
      await onProgress(100);
      return `Processed: ${path}`;
    }
  });
})
```

### Call Service with Inline Callbacks

Consume services from other plugins:

```typescript
handler: async (input, ctx) => {
  // Create proxy to another plugin's service
  const processor = await ctx.rpc.createProxy<{
    processFile(path: string, onProgress: (p: number) => void): Promise<string>
  }>('fileProcessor');

  // Pass callback - it just works!
  const result = await processor.processFile('/file.txt', (progress) => {
    ctx.logger.info(`Progress: ${progress}%`);
  });

  return { result };
}
```

### Explicit Callbacks with Cleanup

For long-lived callbacks (subscriptions, event listeners), use `createCallback()`:

```typescript
handler: async (input, ctx) => {
  // Auto-cleanup after 10 calls
  const callback = await ctx.rpc.createCallback!(
    (event) => ctx.logger.info('Event:', event),
    { maxCalls: 10 }
  );
  await eventService.subscribe(callback);

  // Auto-cleanup after 5 seconds
  const callback2 = await ctx.rpc.createCallback!(
    (data) => ctx.logger.info('Data:', data),
    { timeout: 5000 }
  );
  await dataStream.subscribe(callback2);

  // Manual cleanup
  const callback3 = await ctx.rpc.createCallback!((msg) => ctx.logger.info(msg));
  // ... later
  ctx.rpc.cleanupCallback!(callback3);
}
```

**For comprehensive RPC patterns and best practices:** Run `npx @majkapp/plugin-kit --rpc`

## Complete Example: Dashboard Data

```typescript
.function('getDashboardData', {
  description: 'Get comprehensive dashboard data from MAJK APIs',

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

  output: {
    type: 'object',
    properties: {
      conversations: {
        type: 'object',
        properties: {
          total: { type: 'number' },
          recent: { type: 'array' }
        }
      },
      todos: {
        type: 'object',
        properties: {
          total: { type: 'number' },
          pending: { type: 'number' },
          completed: { type: 'number' }
        }
      },
      projects: {
        type: 'object',
        properties: {
          total: { type: 'number' }
        }
      },
      timestamp: { type: 'string' }
    }
  },

  handler: async (input, ctx) => {
    ctx.logger.info('Fetching dashboard data');

    try {
      // Fetch all data in parallel for better performance
      const [conversations, todos, projects] = await Promise.all([
        ctx.majk.conversations.list(),
        ctx.majk.todos.list(),
        ctx.majk.projects.list()
      ]);

      // Process todos
      const pendingTodos = todos.filter(t => t.status === 'pending');
      const completedTodos = todos.filter(t => t.status === 'completed');

      // Get recent conversations (last 5)
      const recentConversations = conversations
        .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
        .slice(0, 5)
        .map(c => ({
          id: c.id,
          title: c.title || 'Untitled',
          updatedAt: c.updatedAt
        }));

      const result = {
        conversations: {
          total: conversations.length,
          recent: recentConversations
        },
        todos: {
          total: todos.length,
          pending: pendingTodos.length,
          completed: completedTodos.length
        },
        projects: {
          total: projects.length
        },
        timestamp: new Date().toISOString()
      };

      ctx.logger.info('Dashboard data fetched successfully', {
        conversationCount: conversations.length,
        todoCount: todos.length,
        projectCount: projects.length
      });

      return result;

    } catch (error) {
      ctx.logger.error('Failed to fetch dashboard data', {
        error: error.message,
        stack: error.stack
      });
      throw new Error(`Dashboard data fetch failed: ${error.message}`);
    }
  },

  tags: ['dashboard', 'aggregation']
})
```

## Context in Tests

Mock context for testing:

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

test('getDashboardData aggregates MAJK data', async () => {
  // Create mock context with storage and MAJK data
  const context = mock()
    .storage({
      'user-settings': { theme: 'dark' }
    })
    .withMajkData({
      conversations: [
        { id: 'conv1', title: 'Test 1', updatedAt: '2024-01-01T00:00:00Z', messages: [] },
        { id: 'conv2', title: 'Test 2', updatedAt: '2024-01-02T00:00:00Z', messages: [] }
      ],
      todos: [
        { id: 'todo1', status: 'pending' },
        { id: 'todo2', status: 'completed' }
      ],
      projects: [
        { id: 'proj1', name: 'Project 1' }
      ]
    })
    .build();

  const result = await invoke('getDashboardData', {}, { context });

  assert.strictEqual(result.conversations.total, 2);
  assert.strictEqual(result.todos.total, 2);
  assert.strictEqual(result.todos.pending, 1);
  assert.strictEqual(result.todos.completed, 1);
  assert.strictEqual(result.projects.total, 1);
});
```

## Next Steps

Run `npx @majkapp/plugin-kit --rpc` - Inter-plugin communication with callbacks
Run `npx @majkapp/plugin-kit --lifecycle` - onReady and event subscriptions
Run `npx @majkapp/plugin-kit --services` - Group functions into services
Run `npx @majkapp/plugin-kit --testing` - Test functions with mock context
