# @majkapp/plugin-kit

Build MAJK plugins with functions, UI screens, and services. Test with `@majkapp/plugin-test`.

## Quick Start

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

const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
  .pluginRoot(__dirname)  // REQUIRED FIRST - enables resource loading

  .function('health', {
    description: 'Check plugin health status',
    input: {
      type: 'object',
      properties: {},
      additionalProperties: false
    },
    output: {
      type: 'object',
      properties: {
        status: { type: 'string', enum: ['ok', 'error'] },
        timestamp: { type: 'string', format: 'date-time' }
      },
      required: ['status', 'timestamp']
    },
    handler: async (input, ctx) => {
      // ctx.logger - scoped logging (debug, info, warn, error)
      ctx.logger.info('Health check requested');

      return {
        status: 'ok',
        timestamp: new Date().toISOString()
      };
    },
    tags: ['monitoring']
  })

  .screenReact({
    id: 'my-plugin-main',
    name: 'My Plugin',
    description: 'Main plugin screen',
    route: '/plugin-screens/my-plugin/main',
    pluginPath: '/index.html',      // Path to built React app
    pluginPathHash: '#/'             // Route within React app (HashRouter)
  })

  .build();

export = plugin;
```

## Testing Your Function

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

test('health check returns ok status', async () => {
  // Create mock context - no real MAJK instance needed
  const context = mock().build();

  // Invoke function with mock context
  const result = await invoke('health', {}, { context });

  // Verify output matches schema
  assert.strictEqual(result.status, 'ok');
  assert.ok(result.timestamp);
  assert.ok(new Date(result.timestamp).getTime() > 0);
});
```

Run test:
```bash
cd my-plugin
npx majk-test tests/plugin/functions/unit/health.test.js
```

Output:
```
✓ health check returns ok status (2ms)
```

## Project Structure

```
my-plugin/
├── src/
│   ├── index.ts              # Plugin definition
│   └── core/                 # Business logic (no plugin deps)
│       └── analytics.ts      # Pure functions, easy to test
├── ui/
│   ├── src/
│   │   ├── App.tsx           # React app
│   │   ├── components/       # UI components
│   │   └── generated/        # Auto-generated hooks
│   │       ├── hooks.ts      # useHealth(), etc.
│   │       └── client.ts     # RPC client
│   └── index.html
├── tests/
│   └── plugin/
│       ├── functions/unit/   # Function tests
│       └── ui/unit/          # UI tests with bot
├── vite.config.js            # EXACT format required (see below)
├── package.json              # REQUIRED: majk metadata section
└── tsconfig.json
```

## package.json (REQUIRED)

**CRITICAL:** Your package.json MUST include the `majk` metadata section and use an npm organization scope:

```json
{
  "name": "@yourorg/my-plugin",
  "version": "1.0.0",
  "description": "My awesome MAJK plugin",
  "main": "index.js",
  "keywords": ["majk", "plugin"],
  "majk": {
    "type": "in-process",
    "entry": "index.js"
  },
  "scripts": {
    "generate": "npx plugin-kit generate -e ./index.js -o ./ui/src/generated",
    "dev": "vite",
    "build": "tsc && npm run generate && vite build",
    "preview": "vite preview",
    "test": "npm run test:plugin",
    "test:plugin": "npm run test:plugin:functions && npm run test:plugin:ui",
    "test:plugin:functions": "npx majk-test --type functions",
    "test:plugin:ui": "node tests/plugin/ui/unit/basic-navigation.test.js",
    "clean": "rm -rf dist index.js index.d.ts ui/src/generated"
  },
  "dependencies": {
    "@majkapp/plugin-kit": "^3.3.0",
    "@majkapp/plugin-theme": "^1.0.0",
    "@majkapp/plugin-ui": "^1.4.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@majkapp/plugin-test": "^1.0.2",
    "@types/react": "^18.3.1",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.4",
    "typescript": "^5.3.3",
    "vite": "^6.0.3"
  }
}
```

### Package Naming (REQUIRED)

**Your plugin name MUST use an npm organization scope** (format: `@org/plugin-name`).

Replace `@yourorg` with your organization:
- If you have an npm organization: `@mycompany/my-plugin`
- If you don't have one: Use `@majkical/my-plugin`

Example: `@majkical/weather-widget`

### majk Metadata Section

The `majk` object tells MAJK how to load your plugin:

**`type`**: `"in-process"` or `"remote"`
- `"in-process"` - Plugin runs in MAJK's Node.js process (most common)
- `"remote"` - Plugin runs in separate process (for isolation)

**`entry`**: Path to compiled plugin file (relative to package root)
- Usually `"index.js"` (compiled from `src/index.ts`)
- MAJK loads this file to get your plugin definition

### Important Scripts

**`generate`** - Generate TypeScript client from plugin
```bash
npm run generate  # Regenerate after adding/changing functions
```

**`build`** - Complete build pipeline
```bash
npm run build     # 1. Compile TS → index.js
                  # 2. Generate client
                  # 3. Build React UI → dist/
```

**`test:plugin:functions`** - Test plugin functions
```bash
npm run test:plugin:functions  # Run all function tests
```

**`test:plugin:ui`** - Test plugin UI with bot
```bash
npm run test:plugin:ui        # Run UI tests with browser automation
```

## vite.config.js (REQUIRED FORMAT)

**CRITICAL:** Use this EXACT format for your vite.config.js:

```javascript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  root: 'ui',                    // React source is in ui/ directory
  base: '',                      // IMPORTANT: Empty string, NOT './'
  server: {
    port: 3000,
    strictPort: false,
  },
  build: {
    outDir: '../dist',           // Build output to dist/ (parent of ui/)
    assetsDir: 'assets',
    emptyOutDir: true,
  },
});
```

**Why `base: ''`?**
- MAJK loads plugins in iframes with dynamic paths
- Empty base ensures assets load correctly
- `base: './'` will cause 404 errors for assets

### Hash Routing for Multiple Screens

**Use `pluginPathHash` to route to different screens in your React app:**

```typescript
// Multiple screens using hash routes
.screenReact({
  id: 'my-plugin-dashboard',
  name: 'Dashboard',
  route: '/plugin-screens/my-plugin/dashboard',
  pluginPath: '/index.html',
  pluginPathHash: '#/'           // Main dashboard
})

.screenReact({
  id: 'my-plugin-settings',
  name: 'Settings',
  route: '/plugin-screens/my-plugin/settings',
  pluginPath: '/index.html',
  pluginPathHash: '#/settings'   // Settings page
})
```

Your React app (ui/src/App.tsx) uses HashRouter:

```typescript
import { HashRouter, Routes, Route } from 'react-router-dom';

export function App() {
  return (
    <HashRouter>
      <Routes>
        <Route path="/" element={<DashboardPage />} />
        <Route path="/settings" element={<SettingsPage />} />
      </Routes>
    </HashRouter>
  );
}
```

## Best Practice: Decoupled Business Logic

**ALWAYS separate business logic from plugin code.** This makes testing easier and allows logic reuse.

```typescript
// src/core/analytics.ts - Pure business logic, NO plugin dependencies
export interface AnalyticsData {
  period: '24h' | '7d' | '30d';
  metrics: {
    activeUsers: number;
    totalSessions: number;
    avgDuration: number;
  };
}

export function calculateAnalytics(
  sessions: Array<{ userId: string; duration: number; timestamp: Date }>,
  period: '24h' | '7d' | '30d'
): AnalyticsData {
  const now = Date.now();
  const periodMs = period === '24h' ? 86400000 : period === '7d' ? 604800000 : 2592000000;

  const recentSessions = sessions.filter(s =>
    now - s.timestamp.getTime() < 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
    }
  };
}

// Test this with pure Jest/Vitest - NO plugin-test needed
```

```typescript
// src/index.ts - Plugin function wraps business logic
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: {
      type: 'object',
      properties: {
        period: { type: 'string', enum: ['24h', '7d', '30d'] }
      },
      required: ['period'],
      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']
        }
      },
      required: ['period', 'metrics']
    },
    handler: async (input, ctx) => {
      // Get data from storage (plugin layer)
      const sessions = await ctx.storage.get('sessions') || [];

      // Pure business logic (easy to test)
      const analytics = calculateAnalytics(sessions, input.period);

      ctx.logger.info(`Analytics calculated for ${input.period}`, analytics);

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

## Best Practice: Service Interfaces for External Dependencies

When using 3rd party services (AWS, databases, APIs), create interfaces for easy mocking.

```typescript
// src/core/storage-service.ts - Interface for abstraction
export interface StorageService {
  save(key: string, data: any): Promise<void>;
  load(key: string): Promise<any>;
  delete(key: string): Promise<void>;
}

// src/core/aws-storage.ts - Real implementation
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';

export class AWSStorageService implements StorageService {
  constructor(private s3: S3Client, private bucket: string) {}

  async save(key: string, data: any): Promise<void> {
    await this.s3.send(new PutObjectCommand({
      Bucket: this.bucket,
      Key: key,
      Body: JSON.stringify(data)
    }));
  }

  async load(key: string): Promise<any> {
    const result = await this.s3.send(new GetObjectCommand({
      Bucket: this.bucket,
      Key: key
    }));
    const body = await result.Body?.transformToString();
    return body ? JSON.parse(body) : null;
  }

  async delete(key: string): Promise<void> {
    // Implementation...
  }
}

// src/core/mock-storage.ts - Mock for testing
export class MockStorageService implements StorageService {
  private data = new Map<string, any>();

  async save(key: string, data: any): Promise<void> {
    this.data.set(key, data);
  }

  async load(key: string): Promise<any> {
    return this.data.get(key);
  }

  async delete(key: string): Promise<void> {
    this.data.delete(key);
  }
}
```

## React UI with Generated Hooks

The plugin-kit auto-generates **performance-optimized** React hooks from your functions. Dependencies are extracted from your input schemas for O(1) comparison instead of serialization. Review `ui/src/generated/generation.log` for optimization analysis and schema warnings.

```typescript
// ui/src/DashboardPage.tsx
import { useHealth, useGetAnalytics, useUiLog } from './generated/hooks';
import { PluginStatGrid, PluginActionPanel } from '@majkapp/plugin-ui';
import { useEffect } from 'react';

export function DashboardPage() {
  // Auto-generated hooks - query hooks auto-fetch on mount
  const { data: health, loading: healthLoading, refetch: refetchHealth } = useHealth();
  const { data: analytics, loading: analyticsLoading } = useGetAnalytics(
    { period: '7d' },
    { refetchInterval: 30000 }  // Auto-refresh every 30s
  );

  // Mutation hook for logging - call .mutate() manually
  const { mutate: uiLog } = useUiLog();

  // Log UI lifecycle events for debugging
  useEffect(() => {
    uiLog({
      level: 'info',
      component: 'DashboardPage',
      message: 'Dashboard mounted',
      data: { timestamp: new Date().toISOString() }
    });
  }, [uiLog]);

  // data-majk-id: Enables bot testing - identifies clickable elements
  // data-majk-state: Tracks UI state for testing - empty/loading/populated
  return (
    <div data-majk-id="dashboardContainer">
      <PluginActionPanel
        actions={[
          {
            label: '🔄 Refresh Health',
            onClick: () => {
              uiLog({
                level: 'info',
                component: 'DashboardPage',
                message: 'Health refresh triggered by user'
              });
              refetchHealth();
            },
            'data-majk-id': 'refreshHealthButton',
            'data-majk-state': healthLoading ? 'loading' : 'idle'
          }
        ]}
      />

      <PluginStatGrid
        stats={[
          {
            label: 'Health Status',
            value: health?.status || 'unknown',
            'data-majk-id': 'healthStatus',
            'data-majk-state': healthLoading ? 'loading' : health ? 'populated' : 'empty'
          },
          {
            label: 'Active Users (7d)',
            value: analytics?.metrics.activeUsers || 0,
            'data-majk-id': 'activeUsersCount',
            'data-majk-state': analyticsLoading ? 'loading' : analytics ? 'populated' : 'empty'
          },
          {
            label: 'Total Sessions',
            value: analytics?.metrics.totalSessions || 0,
            'data-majk-id': 'totalSessionsCount'
          }
        ]}
      />
    </div>
  );
}
```

## Testing Your UI

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

test('dashboard displays health status', async () => {
  // Create mock context with handlers for functions
  const context = mock()
    .withMockHandler('health', async () => ({
      status: 'ok',
      timestamp: new Date().toISOString()
    }))
    .withMockHandler('getAnalytics', async () => ({
      period: '7d',
      metrics: {
        activeUsers: 42,
        totalSessions: 150,
        avgDuration: 320
      }
    }))
    .build();

  // Start browser bot
  const b = await bot(context);

  // Navigate to screen
  await b.goto('/plugin-screens/my-plugin/main');

  // Wait for data to load - check data-majk-state
  await b.waitForSelector('[data-majk-id="healthStatus"][data-majk-state="populated"]');

  // Verify displayed values
  const healthText = await b.getText('[data-majk-id="healthStatus"]');
  assert.ok(healthText.includes('ok'));

  const usersText = await b.getText('[data-majk-id="activeUsersCount"]');
  assert.ok(usersText.includes('42'));

  // Test interaction - click refresh button
  await b.click('[data-majk-id="refreshHealthButton"]');

  // Verify loading state
  const buttonState = await b.getAttribute('[data-majk-id="refreshHealthButton"]', 'data-majk-state');
  assert.strictEqual(buttonState, 'loading');

  await b.close();
});
```

## Key Concepts

### Query vs Mutation Hooks

Generated hooks follow React Query patterns:

**Query Hooks** - Auto-fetch data on mount:
- `useHealth()` - fetches immediately
- `useGetAnalytics({ period: '7d' })` - fetches immediately with params
- Returns: `{ data, error, loading, refetch }`

**Mutation Hooks** - Call manually:
- `useUiLog()` - call `.mutate(input)` when needed
- `useClearEvents()` - call `.mutate({})` on button click
- Returns: `{ mutate, data, error, loading }`

### data-majk-* Attributes

**Critical for testing.** The bot uses these to find and verify elements.

- `data-majk-id="uniqueId"` - Identifies clickable elements, key stats
- `data-majk-state="loading|empty|populated|error"` - Tracks UI state for assertions

### uiLog Function

**Best practice:** Create a `uiLog` function in your plugin for frontend debugging.

```typescript
.function('uiLog', {
  description: 'Log messages from UI for debugging',
  input: {
    type: 'object',
    properties: {
      level: { type: 'string', enum: ['debug', 'info', 'warn', 'error'] },
      component: { type: 'string' },
      message: { type: 'string' },
      data: { type: 'object' }
    },
    required: ['level', 'component', 'message'],
    additionalProperties: false
  },
  output: {
    type: 'object',
    properties: {
      success: { type: 'boolean' }
    }
  },
  handler: async (input, ctx) => {
    const prefix = `[UI:${input.component}]`;
    ctx.logger[input.level](`${prefix} ${input.message}`, input.data);
    return { success: true };
  },
  tags: ['debugging']
})
```

Then use in React:

```typescript
// Log errors
try {
  await someAsyncOperation();
} catch (error) {
  uiLog({
    level: 'error',
    component: 'DashboardPage',
    message: 'Failed to load data',
    data: { error: error.message }
  });
}

// Log user actions
onClick={() => {
  uiLog({
    level: 'info',
    component: 'SettingsPage',
    message: 'User clicked save button',
    data: { settingsChanged: ['theme', 'notifications'] }
  });
  handleSave();
}}
```

## Additional MAJK APIs

The `ctx.majk` object provides access to many powerful APIs beyond the basics covered above:

| API | Command | Description |
|-----|---------|-------------|
| **Delegation** | `--llm:delegation` | Multi-agent task delegation with witness-based verification. Delegate tasks to teammates, fan-out/fan-in patterns, and automated completion checking. |
| **Batch** | `--llm:batch` | Batch processing for large datasets. Process files, lists, or glob patterns with parallelism, retry logic, and progress tracking. |
| **Reports** | `--llm:reports` | Knowledge management system with hierarchical reports, witness validation, views, and import/export capabilities. |
| **Scripting** | `--llm:scripting` | Execute JavaScript scripts that orchestrate tool calls. Includes dry-run mode, tracing, and script re-execution. |
| **Tool Discovery** | `--llm:tool-discovery` | Discover available service functions dynamically and invoke them. Build adaptive workflows based on available tools. |
| **Skills** | `--llm:skills` | Register and manage skills - instructions on how to accomplish tasks using tools. Supports runtime skills with setup/teardown. |
| **Auth** | `--llm:auth` | Core Cognito/MAJK authentication. Retrieve access tokens for calling MAJK backend services and AWS resources. |
| **Secrets** | `--llm:secrets` | Secure secret storage with scoping (global, project, integration). Store and retrieve API keys, tokens, and credentials. |
| **Config** | `--llm:config-api` | Access MAJK configuration using dot-notation paths. Read AWS, auth, Bedrock, and plugin settings. |
| **Plugins** | `--llm:plugins` | Plugin lifecycle management. Install, start, stop, reload plugins programmatically. Health checks and logging. |
| **Tasks** | `--llm:tasks` | Start and monitor asynchronous tasks. Send messages, track progress, and handle completion events. |
| **Knowledge** | `--llm:knowledge` | Knowledge Fabric for semantic search and contribution. Search all system knowledge, contribute plugin-specific nodes, browse hierarchy. |
| **Agents** | `--llm:agents` | Create, configure, test, and clone AI agents. Manage MCP servers and agent settings. |
| **Conversations** | `--llm:conversations` | Access and manage conversations. List, filter, search, get messages, and subscribe to changes. |
| **Plugin Self** | `--llm:me` | Identity-scoped API pre-bound to the plugin's own ID. Manage the plugin's own conversations and teammates, spawn background chats, and stream results. |

## Next Steps

**Core Plugin Development:**
Run `npx @majkapp/plugin-kit --llm:functions` - Deep dive into function patterns
Run `npx @majkapp/plugin-kit --llm:screens` - Screen configuration details
Run `npx @majkapp/plugin-kit --llm:hooks` - Generated hooks reference
Run `npx @majkapp/plugin-kit --llm:context` - ctx.majk and ctx.logger APIs
Run `npx @majkapp/plugin-kit --llm:ai` - AI providers and LLMs
Run `npx @majkapp/plugin-kit --llm:services` - Service grouping patterns
Run `npx @majkapp/plugin-kit --llm:teammates` - AI teammates and agents
Run `npx @majkapp/plugin-kit --llm:lifecycle` - onReady and cleanup
Run `npx @majkapp/plugin-kit --llm:testing` - Complete testing guide
Run `npx @majkapp/plugin-kit --llm:config` - vite.config.js and project setup
Run `npx @majkapp/plugin-kit --llm:full` - Complete reference

**Additional APIs:**
Run `npx @majkapp/plugin-kit --llm:delegation` - Task delegation with witnesses
Run `npx @majkapp/plugin-kit --llm:batch` - Batch processing
Run `npx @majkapp/plugin-kit --llm:reports` - Knowledge management
Run `npx @majkapp/plugin-kit --llm:scripting` - Script execution
Run `npx @majkapp/plugin-kit --llm:tool-discovery` - Dynamic tool invocation
Run `npx @majkapp/plugin-kit --llm:skills` - Skill management
Run `npx @majkapp/plugin-kit --llm:auth` - Cognito authentication
Run `npx @majkapp/plugin-kit --llm:secrets` - Secret storage
Run `npx @majkapp/plugin-kit --llm:config-api` - Configuration access
Run `npx @majkapp/plugin-kit --llm:plugins` - Plugin management
Run `npx @majkapp/plugin-kit --llm:tasks` - Task monitoring
Run `npx @majkapp/plugin-kit --llm:knowledge` - Knowledge Fabric
Run `npx @majkapp/plugin-kit --llm:agents` - Agent configuration
Run `npx @majkapp/plugin-kit --llm:conversations` - Conversation access
Run `npx @majkapp/plugin-kit --llm:me` - Plugin Self API: own conversations and background chat
