# Plugin Self API (`ctx.majk.me`)

The `ctx.majk.me` API provides each plugin with an identity-scoped interface for managing its own conversations and teammates. It is pre-bound to the plugin's own ID — you never need to pass a plugin ID manually.

## Interface

```typescript
export interface PluginSelfAPI {
  readonly pluginId: string;
  teammates(): Promise<PluginTeammate[]>;
  getDefaultTeammate(): Promise<PluginTeammate | null>;
  conversations(filter?: PluginConversationFilter): Promise<PluginConversationSummary[]>;
  startChat(options: PluginStartChatOptions): Promise<PluginChatHandle>;
  attachChat(conversationId: string): Promise<PluginChatHandle | null>;
}

export interface PluginTeammate {
  id: string;
  name: string;
}

export interface PluginStartChatOptions {
  message: string;
  title?: string;
  /** Teammate to use. Defaults to the plugin's registered default teammate. */
  teammateId?: string;
  projectId?: string;
  workingDirectory?: string;
  tags?: string[];
  metadata?: Record<string, unknown>;
}

export interface PluginChatHandle {
  readonly conversationId: string;
  readonly teammateId: string | undefined;
  dispose(): void;
  getMessages(options?: { afterSeq?: number; limit?: number }): Promise<PluginMessage[]>;
  send(message: string): Promise<void>;
  waitForNextResponse(options?: { timeoutMs?: number }): Promise<PluginMessage>;
  waitForCompletion(options?: { timeoutMs?: number }): Promise<{
    status: 'completed' | 'failed' | 'cancelled';
    lastMessage: PluginMessage | null;
  }>;
  onMessage(
    callback: (event: PluginMessageEvent) => void | Promise<void>,
    options?: { fromSeq?: number }
  ): PluginSubscription;
  promote(): Promise<void>;
  hide(): Promise<void>;
}

export interface PluginSubscription {
  unsubscribe(): void;
  readonly disposed: boolean;
}

export interface PluginMessage {
  id: string;
  seq: number;
  role: 'user' | 'assistant' | 'tool_use' | 'tool_result';
  content: string;
  createdAt: Date;
}

export interface PluginConversationSummary {
  id: string;
  title: string;
  status: 'active' | 'completed' | 'failed' | 'cancelled';
  teammateId: string | undefined;
  createdAt: Date;
  lastMessageAt: Date | null;
  lastMessagePreview: string | null;
  metadata: Record<string, unknown>;
}

export interface PluginConversationFilter {
  status?: 'active' | 'completed' | 'failed' | 'cancelled';
  teammateId?: string;
  tags?: string[];
  since?: Date;
  limit?: number;
}

export type PluginMessageEvent =
  | { type: 'message_created'; messageId: string; seq: number; role: string; content: string }
  | { type: 'message_updated'; messageId: string; seq: number; role: string; content: string }
  | { type: 'conversation_complete'; status: 'completed' | 'failed' | 'cancelled' };
```

## Quick Start

```typescript
// Fire-and-forget background analysis
const chat = await ctx.majk.me.startChat({
  message: 'Analyze the uploaded dataset and summarize key findings.',
  title: 'Dataset Analysis'
});

const { status, lastMessage } = await chat.waitForCompletion({ timeoutMs: 60_000 });
console.log('Status:', status);
console.log('Result:', lastMessage?.content);
chat.dispose();
```

## Methods

### `teammates(): Promise<PluginTeammate[]>`

List all teammates registered to this plugin.

```typescript
const teammates = await ctx.majk.me.teammates();
teammates.forEach(t => console.log(`${t.name} (${t.id})`));
```

### `getDefaultTeammate(): Promise<PluginTeammate | null>`

Get the plugin's default teammate — the earliest-registered teammate. Returns `null` if the plugin has no registered teammates.

```typescript
const defaultTeammate = await ctx.majk.me.getDefaultTeammate();
if (defaultTeammate) {
  console.log('Default:', defaultTeammate.name);
}
```

### `conversations(filter?): Promise<PluginConversationSummary[]>`

List all conversations owned by this plugin, optionally filtered.

```typescript
// All conversations
const all = await ctx.majk.me.conversations();

// Only active conversations with a specific teammate
const active = await ctx.majk.me.conversations({
  status: 'active',
  teammateId: 'tm_abc123'
});

// Recently completed, limit 5
const recent = await ctx.majk.me.conversations({
  status: 'completed',
  since: new Date(Date.now() - 24 * 60 * 60 * 1000),
  limit: 5
});
```

### `startChat(options): Promise<PluginChatHandle>`

Spawn a new background conversation. Uses the plugin's default teammate unless `teammateId` is specified. The conversation is created as hidden (not shown in the nav pane by default) and is tagged with the plugin's ownership tag.

```typescript
// Use the default teammate
const chat = await ctx.majk.me.startChat({
  message: 'Summarize this content',
  title: 'Content Summary'
});

// Use a specific teammate
const chat = await ctx.majk.me.startChat({
  message: 'Review the code in /src',
  teammateId: 'tm_code_reviewer',
  workingDirectory: '/path/to/project',
  tags: ['code-review'],
  metadata: { triggeredBy: 'webhook' }
});
```

**Options**:
| Field | Type | Description |
|---|---|---|
| `message` | `string` | **Required.** Initial user message. |
| `title` | `string` | Conversation title. Defaults to the first 60 chars of the message. |
| `teammateId` | `string` | Teammate to use. Defaults to the plugin's registered default teammate. |
| `projectId` | `string` | Associate with a project. |
| `workingDirectory` | `string` | Working directory for the agent. |
| `tags` | `string[]` | Additional tags to attach to the conversation. |
| `metadata` | `Record<string, unknown>` | Arbitrary metadata stored on the conversation. |

### `attachChat(conversationId): Promise<PluginChatHandle | null>`

Reattach to an existing conversation owned by this plugin. Returns `null` if the conversation does not exist or is not owned by this plugin.

Ownership is verified via plugin tag, `configuration.pluginId`, or `plugin_entities` record.

```typescript
const chat = await ctx.majk.me.attachChat('conv_abc123');
if (!chat) {
  console.log('Conversation not found or not owned by this plugin');
  return;
}

// Send a follow-up message
await chat.send('Please also check the test coverage.');
const response = await chat.waitForNextResponse({ timeoutMs: 30_000 });
console.log('Response:', response.content);
chat.dispose();
```

## Working with `PluginChatHandle`

### `send(message): Promise<void>`

Send a follow-up message to the conversation.

```typescript
await chat.send('Now focus on the authentication module.');
```

### `getMessages(options?): Promise<PluginMessage[]>`

Retrieve messages from the conversation, optionally paginated.

```typescript
// All messages
const all = await chat.getMessages();

// Messages after seq 10, max 20
const recent = await chat.getMessages({ afterSeq: 10, limit: 20 });
```

### `waitForNextResponse(options?): Promise<PluginMessage>`

Wait for the next assistant message. Rejects with a timeout error if no response arrives within `timeoutMs` (default: 30 seconds).

```typescript
await chat.send('What is 2 + 2?');
const response = await chat.waitForNextResponse({ timeoutMs: 10_000 });
console.log(response.content); // "4"
```

### `waitForCompletion(options?): Promise<{ status, lastMessage }>`

Wait for the conversation to reach a terminal state (`completed`, `failed`, or `cancelled`). Returns the final status and the last message received.

```typescript
const { status, lastMessage } = await chat.waitForCompletion({ timeoutMs: 120_000 });

if (status === 'completed') {
  console.log('Done:', lastMessage?.content);
} else {
  console.warn('Ended with status:', status);
}
```

### `onMessage(callback, options?): PluginSubscription`

Subscribe to live message events. The subscription is race-free: live events are subscribed first, then any stored messages since `fromSeq` are replayed to close the gap. Returns synchronously.

```typescript
const sub = chat.onMessage((event) => {
  if (event.type === 'message_created') {
    console.log(`[${event.role}] ${event.content}`);
  } else if (event.type === 'conversation_complete') {
    console.log('Conversation ended with status:', event.status);
    sub.unsubscribe();
  }
}, { fromSeq: 0 }); // replay all messages from the start

// Later:
sub.unsubscribe();
```

**Event types**:
| Event | Fields | Description |
|---|---|---|
| `message_created` | `messageId`, `seq`, `role`, `content` | New message arrived. |
| `message_updated` | `messageId`, `seq`, `role`, `content` | Existing message was updated (e.g., streaming complete). |
| `conversation_complete` | `status` | Conversation reached a terminal state. |

### `promote(): Promise<void>`

Make the conversation visible in the navigation pane.

```typescript
// Reveal the background chat to the user
await chat.promote();
```

### `hide(): Promise<void>`

Hide the conversation from the navigation pane (background mode).

```typescript
await chat.hide();
```

### `dispose(): void`

Release all subscriptions and resources held by this handle. Always call `dispose()` when you are done with a handle.

```typescript
chat.dispose();
```

## Common Patterns

### Fire-and-Forget Analysis

```typescript
async function analyzeInBackground(data: string) {
  const chat = await ctx.majk.me.startChat({
    message: `Analyze this data and produce a summary:\n\n${data}`,
    title: 'Background Analysis',
    metadata: { source: 'webhook', timestamp: Date.now() }
  });

  const { status, lastMessage } = await chat.waitForCompletion({ timeoutMs: 120_000 });
  chat.dispose();

  if (status !== 'completed') {
    throw new Error(`Analysis failed with status: ${status}`);
  }

  return lastMessage?.content ?? '';
}
```

### Multi-Turn Conversation

```typescript
async function reviewCode(filePath: string) {
  const chat = await ctx.majk.me.startChat({
    message: `Review the code at ${filePath} for correctness and style issues.`,
    workingDirectory: '/project'
  });

  // Wait for initial review
  const review = await chat.waitForNextResponse({ timeoutMs: 60_000 });
  console.log('Initial review:', review.content);

  // Ask for a fix
  await chat.send('Please fix the most critical issue you found.');
  const fix = await chat.waitForNextResponse({ timeoutMs: 60_000 });
  console.log('Fix:', fix.content);

  chat.dispose();
}
```

### Streaming Progress Updates

```typescript
async function withProgress(message: string, onUpdate: (text: string) => void) {
  const chat = await ctx.majk.me.startChat({ message });

  return new Promise<string>((resolve, reject) => {
    let lastContent = '';

    const sub = chat.onMessage((event) => {
      if (event.type === 'message_created' || event.type === 'message_updated') {
        if (event.role === 'assistant') {
          lastContent = event.content;
          onUpdate(event.content);
        }
      } else if (event.type === 'conversation_complete') {
        sub.unsubscribe();
        chat.dispose();
        if (event.status === 'completed') {
          resolve(lastContent);
        } else {
          reject(new Error(`Chat ended with status: ${event.status}`));
        }
      }
    });
  });
}

// Usage
const result = await withProgress(
  'Generate a report on Q4 metrics',
  (partial) => console.log('Progress:', partial.slice(0, 80))
);
```

### Reusing Existing Conversations

```typescript
async function getOrCreateChat(conversationId: string | null) {
  if (conversationId) {
    const existing = await ctx.majk.me.attachChat(conversationId);
    if (existing) return existing;
    // Fall through if not found / not owned
  }

  return ctx.majk.me.startChat({
    message: 'Hello, let us continue our work.',
    title: 'Ongoing Task'
  });
}
```

### Listing and Resuming Active Chats

```typescript
async function resumeActiveChats() {
  const active = await ctx.majk.me.conversations({ status: 'active' });

  for (const summary of active) {
    console.log(`[${summary.id}] ${summary.title} — last: ${summary.lastMessagePreview}`);

    const chat = await ctx.majk.me.attachChat(summary.id);
    if (chat) {
      await chat.send('Please continue and provide a status update.');
      const update = await chat.waitForNextResponse({ timeoutMs: 30_000 });
      console.log('Update:', update.content);
      chat.dispose();
    }
  }
}
```

### Using a Specific Teammate

```typescript
async function runWithSpecialist(specialistId: string, task: string) {
  // Verify teammate belongs to this plugin before using
  const teammates = await ctx.majk.me.teammates();
  const isOwned = teammates.some(t => t.id === specialistId);

  if (!isOwned) {
    throw new Error(`Teammate ${specialistId} does not belong to this plugin`);
  }

  const chat = await ctx.majk.me.startChat({
    message: task,
    teammateId: specialistId
  });

  const { status, lastMessage } = await chat.waitForCompletion({ timeoutMs: 60_000 });
  chat.dispose();
  return { status, result: lastMessage?.content };
}
```

## Important Notes

### Teammate Configuration Drives Available Tools

The tools and MCP servers available inside a spawned conversation are determined by the teammate's configuration, not by `startChat()` options. If your plugin needs specific tools available, configure a dedicated teammate with those MCP servers.

### Ownership

All conversations created by `startChat()` are tagged with `plugin:{pluginId}` and tracked in the `plugin_entities` table. `attachChat()` enforces this ownership — a plugin cannot attach to another plugin's conversations.

### Hidden by Default

Conversations started with `startChat()` are hidden from the navigation pane (`hidden: true`). Call `chat.promote()` to make them visible to the user.

### Always Dispose

Always call `chat.dispose()` when done to release event subscriptions and avoid memory leaks.

```typescript
// Pattern: try/finally guarantees disposal
const chat = await ctx.majk.me.startChat({ message: 'Do the work' });
try {
  const { status, lastMessage } = await chat.waitForCompletion();
  return lastMessage?.content;
} finally {
  chat.dispose();
}
```

### Timeouts

Both `waitForNextResponse()` and `waitForCompletion()` default to 30 seconds. For long-running tasks, always pass an explicit `timeoutMs`.

```typescript
// Long-running task — give it 5 minutes
const result = await chat.waitForCompletion({ timeoutMs: 5 * 60 * 1000 });
```
