npm package: @portel/photon-core
Description: Core library for parsing, loading, and managing .photon.ts files - runtime-agnostic foundation for building custom Photon runtimes
Author: Portel
Latest version: 2.18.1

# @portel/photon-core

**Core library for parsing, loading, and managing `.photon.ts` files**

[![npm version](https://badge.fury.io/js/@portel%2Fphoton-core.svg)](https://www.npmjs.com/package/@portel/photon-core)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## 🎯 What is Photon Core?

Photon Core is the **runtime-agnostic foundation** for working with `.photon.ts` files. It provides:

- ✅ **Base class** with lifecycle hooks (`onInitialize`, `onShutdown`)
- ✅ **Dependency management** - Extract and install `@dependencies` from JSDoc
- ✅ **Schema extraction** - Parse TypeScript types and JSDoc into JSON schemas
- ❌ **NO runtime layers** - No MCP server, no CLI, no transport

This allows you to build **custom runtimes** on top of the Photon format:
- **Multi-protocol API servers** (REST, GraphQL, RPC, MCP) → [Lumina](https://github.com/portel-dev/lumina)
- **Orchestrators** → [NCP](https://github.com/portel-dev/ncp)
- **MCP/CLI runners** → [@portel/photon](https://github.com/portel-dev/photon)

---

## 📦 Installation

```bash
npm install @portel/photon-core
```

---

## 🚀 Quick Start

### 1. Create a Photon class

```typescript
// calculator.photon.ts

/**
 * Simple calculator with basic math operations
 * @dependencies lodash@^4.17.21
 */
export default class Calculator {
  /**
   * Add two numbers together
   * @param a First number
   * @param b Second number
   */
  async add(params: { a: number; b: number }) {
    return params.a + params.b;
  }

  /**
   * Multiply two numbers
   * @param a First number
   * @param b Second number
   */
  async multiply(params: { a: number; b: number }) {
    return params.a * params.b;
  }

  /**
   * Lifecycle hook called when photon is initialized
   */
  async onInitialize() {
    console.log('Calculator initialized!');
  }

  /**
   * Lifecycle hook called when photon is shut down
   */
  async onShutdown() {
    console.log('Calculator shutting down!');
  }
}
```

### 2. Use Photon Core to load and manage it

```typescript
import { PhotonMCP, DependencyManager, SchemaExtractor } from '@portel/photon-core';

// 1. Extract and install dependencies
const depManager = new DependencyManager();
const deps = await depManager.extractDependencies('./calculator.photon.ts');
// Found: [{ name: 'lodash', version: '^4.17.21' }]

await depManager.ensureDependencies('calculator', deps);
// ✅ Dependencies installed

// 2. Load the class
const CalculatorClass = (await import('./calculator.photon.ts')).default;
const instance = new CalculatorClass();

// 3. Call lifecycle hook
if (instance.onInitialize) {
  await instance.onInitialize();
}

// 4. Extract schemas (optional)
const extractor = new SchemaExtractor();
const schemas = await extractor.extractFromFile('./calculator.photon.ts');
console.log(schemas);
// [
//   {
//     name: 'add',
//     description: 'Add two numbers together',
//     inputSchema: {
//       type: 'object',
//       properties: {
//         a: { type: 'number', description: 'First number' },
//         b: { type: 'number', description: 'Second number' }
//       },
//       required: ['a', 'b']
//     }
//   },
//   ...
// ]

// 5. Execute methods
const result = await instance.add({ a: 5, b: 3 });
console.log(result); // 8

// 6. Cleanup
if (instance.onShutdown) {
  await instance.onShutdown();
}
```

---

## 📚 API Reference

### `Photon` (alias: `PhotonMCP`)

Base class for creating Photon classes (optional - you can use plain classes too).

```typescript
import { Photon } from '@portel/photon-core';

export default class MyPhoton extends Photon {
  async myMethod(params: { input: string }) {
    return `Hello ${params.input}`;
  }

  async onInitialize() {
    // Called when photon is loaded
  }

  async onShutdown() {
    // Called when photon is unloaded
  }
}
```

**Static methods:**
- `getMCPName()` - Convert class name to kebab-case (e.g., `MyAwesomeMCP` → `my-awesome-mcp`)
- `getToolMethods()` - Get all public async methods (excludes lifecycle hooks and private methods)

**Instance methods:**
- `executeTool(name, params)` - Execute a method by name
- `emit(data)` - Emit events to local handler or pub/sub channel
- `render(format, value)` - Send formatted intermediate results to the client
- `render()` - Clear the render zone
- `storage(subpath)` - Get path to persistent data directory
- `assets(subpath)` - Get path to bundled assets directory

**Caller identity** (requires `@auth` tag):
- `this.caller` - Authenticated caller info (`{ id, name, anonymous, scope, claims }`)

**Identity-aware locks** (for multiplayer/turn-based):
- `acquireLock(name, callerId, timeout?)` - Assign a lock to a specific caller
- `transferLock(name, toCallerId, fromCallerId?)` - Move lock to another caller
- `releaseLock(name, callerId?)` - Release a lock
- `getLock(name)` - Query who holds a lock
- `withLock(name, fn, timeout?)` - Execute function with a binary mutex lock

**Lifecycle hooks:**
- `onInitialize()` - Called when photon is initialized
- `onShutdown()` - Called when photon is shut down

---

### `DependencyManager`

Extracts and installs npm dependencies declared in JSDoc `@dependencies` tags.

```typescript
import { DependencyManager } from '@portel/photon-core';

const depManager = new DependencyManager();

// Extract dependencies from source file
const deps = await depManager.extractDependencies('./my-tool.photon.ts');
// [{ name: 'axios', version: '^1.0.0' }, { name: 'date-fns', version: '^2.0.0' }]

// Install dependencies to isolated cache
await depManager.ensureDependencies('my-tool', deps);
// Creates ~/.cache/photon-mcp/dependencies/my-tool/node_modules/

// Clear cache for a specific photon
await depManager.clearCache('my-tool');

// Clear all caches
await depManager.clearAllCache();
```

**Supported JSDoc format:**
```typescript
/**
 * @dependencies axios@^1.0.0, date-fns@^2.0.0
 * @dependencies @octokit/rest@^19.0.0
 */
```

---

### MCP SDK Transport

Connect to external MCPs using the official `@modelcontextprotocol/sdk`. Supports multiple transports:

```typescript
import {
  SDKMCPClientFactory,
  SDKMCPTransport,
  loadMCPConfig,
  createSDKMCPClientFactory,
  resolveMCPSource
} from '@portel/photon-core';

// Create from config
const config = {
  mcpServers: {
    // stdio transport (local process)
    github: {
      command: 'npx',
      args: ['-y', '@modelcontextprotocol/server-github'],
      env: { GITHUB_TOKEN: 'your-token' }
    },
    // SSE transport (HTTP)
    remote: {
      url: 'http://localhost:3000/mcp',
      transport: 'sse'
    },
    // WebSocket transport
    realtime: {
      url: 'ws://localhost:8080/mcp',
      transport: 'websocket'
    },
    // Streamable HTTP transport
    streaming: {
      url: 'http://localhost:3000/mcp',
      transport: 'streamable-http'
    }
  }
};

const factory = new SDKMCPClientFactory(config);
const github = factory.create('github');

// List tools
const tools = await github.list();

// Call a tool
const issues = await github.call('list_issues', { repo: 'owner/repo' });

// Or use the proxy for fluent API
import { createMCPProxy } from '@portel/photon-core';
const githubProxy = createMCPProxy(github);
const issues = await githubProxy.list_issues({ repo: 'owner/repo' });
```

**Transport Types:**

| Transport | Config | Use Case |
|-----------|--------|----------|
| `stdio` | `command`, `args` | Local CLI-based MCPs |
| `sse` | `url`, `transport: 'sse'` | HTTP Server-Sent Events |
| `streamable-http` | `url`, `transport: 'streamable-http'` | HTTP streaming |
| `websocket` | `url`, `transport: 'websocket'` | WebSocket connections |

**Helper Functions:**

```typescript
// Load config from standard locations
// Checks: PHOTON_MCP_CONFIG env, ./photon.mcp.json, ~/.config/photon/mcp.json
const config = await loadMCPConfig();

// Create factory from default config
const factory = await createSDKMCPClientFactory();

// Resolve marketplace sources to config
const config = resolveMCPSource('github', 'anthropics/mcp-server-github', 'github');
// → { command: 'npx', args: ['-y', '@anthropics/mcp-server-github'], transport: 'stdio' }
```

---

### `SchemaExtractor`

Extracts JSON schemas from TypeScript method signatures and JSDoc comments.

```typescript
import { SchemaExtractor } from '@portel/photon-core';

const extractor = new SchemaExtractor();

// Extract from file
const schemas = await extractor.extractFromFile('./my-tool.photon.ts');

// Extract from source string
const source = await fs.readFile('./my-tool.photon.ts', 'utf-8');
const schemas = extractor.extractFromSource(source);

// Extract constructor parameters
const constructorParams = extractor.extractConstructorParams(source);
```

**Schema format:**
```typescript
interface ExtractedSchema {
  name: string;
  description: string;
  inputSchema: {
    type: 'object';
    properties: Record<string, any>;
    required?: string[];
  };
}
```

**Supported JSDoc constraints:**
```typescript
/**
 * @param age User age {@min 0} {@max 120}
 * @param email Email address {@format email}
 * @param username Username {@pattern ^[a-z0-9_]+$}
 * @param count Count {@default 10}
 * @param env Target environment {@choice-from environments}
 * @param service Service to deploy {@choice-from services.name}
 */
```

---

### Generator Support (Ask/Emit Pattern)

Photon methods can be async generators that yield interactive prompts and progress updates:

```typescript
import { PhotonMCP } from '@portel/photon-core';

export default class DeployTool extends PhotonMCP {
  /**
   * Deploy with confirmation and progress
   */
  async *deploy(params: { env: string }) {
    // Ask for confirmation
    const confirmed = yield { ask: 'confirm', message: `Deploy to ${params.env}?` };
    if (!confirmed) return { status: 'cancelled' };

    // Show progress
    yield { emit: 'progress', value: 0.3, message: 'Building...' };
    await this.build();

    yield { emit: 'progress', value: 0.7, message: 'Deploying...' };
    const result = await this.push(params.env);

    yield { emit: 'progress', value: 1.0, message: 'Done!' };
    return result;
  }
}
```

**Ask yields** (input from user):
- `{ ask: 'text', message: string }` - Text input
- `{ ask: 'confirm', message: string }` - Yes/no confirmation
- `{ ask: 'select', message: string, options: string[] }` - Selection
- `{ ask: 'number', message: string }` - Number input
- `{ ask: 'password', message: string }` - Hidden input

**Emit yields** (output to user):
- `{ emit: 'progress', value: number, message?: string }` - Progress bar (0-1)
- `{ emit: 'status', message: string }` - Status update
- `{ emit: 'log', level: 'info'|'warn'|'error', message: string }` - Log message
- `{ emit: 'stream', data: string }` - Streaming output

---

### MCP OAuth & Caller Identity

Photons can require authenticated callers using the `@auth` class-level tag. When enabled, `this.caller` provides the authenticated identity in every method.

```typescript
/**
 * Chess game with authenticated players
 * @stateful
 * @auth required
 */
export default class Chess extends Photon {
  players: Record<string, string> = {};

  async join() {
    const slot = !this.players.white ? 'white' : 'black';
    this.players[slot] = this.caller.id;

    if (slot === 'black') {
      // Both joined — white goes first
      await this.acquireLock('turn', this.players.white);
    }
    return { color: slot, playerId: this.caller.id, name: this.caller.name };
  }

  /** @locked turn */
  async move(params: { from: string; to: string }) {
    // Only reaches here if this.caller.id holds the 'turn' lock
    const next = this.turn === 'white' ? this.players.black : this.players.white;
    await this.transferLock('turn', next);
    return this.board;
  }
}
```

**`@auth` tag values:**

| Value | Behavior |
|-------|----------|
| `@auth required` | All methods require a valid JWT. Anonymous callers get 401. |
| `@auth optional` | Caller populated if token present, anonymous allowed. |
| `@auth https://accounts.google.com` | Specifies the OIDC provider URL (implies required). |

**`this.caller` properties:**

| Property | Type | Description |
|----------|------|-------------|
| `id` | `string` | Stable user ID (JWT `sub` claim) |
| `name` | `string?` | Display name from OIDC profile |
| `anonymous` | `boolean` | `true` if no valid JWT was provided |
| `scope` | `string?` | OAuth scopes |
| `claims` | `Record<string, unknown>?` | Raw JWT claims |

The MCP transport handles the OAuth 2.1 flow per the [MCP authorization spec](https://modelcontextprotocol.io/specification/latest/basic/authorization). The photon author never deals with tokens or OAuth directly.

---

### Stateful Workflows (Checkpoint Pattern)

For long-running workflows that need to survive interruptions, use checkpoint yields:

```typescript
export default class ReportGenerator extends PhotonMCP {
  /**
   * Generate weekly report with checkpoints for resume
   */
  async *generate(params: { week: number }) {
    // Step 1: Collect data
    const commits = await this.github.list_commits({ since: params.week });
    yield { checkpoint: true, state: { step: 1, commits } };

    // Step 2: Analyze
    const analysis = await this.analyze(commits);
    yield { checkpoint: true, state: { step: 2, commits, analysis } };

    // Step 3: Generate report
    const report = await this.format(analysis);
    yield { checkpoint: true, state: { step: 3, report } };

    return report;
  }
}
```

**Key concepts:**
- **Checkpoint** - marks a safe resume point with accumulated state
- **State** - preserved data to restore on resume
- **Idempotency** - place checkpoint AFTER side effects to avoid repeating them

**Using the stateful executor:**

```typescript
import { maybeStatefulExecute } from '@portel/photon-core';

// Execute with implicit checkpoint detection
const result = await maybeStatefulExecute(
  () => instance.generate({ week: 52 }),
  {
    photon: 'report-generator',
    tool: 'generate',
    params: { week: 52 },
    inputProvider: async (ask) => { /* handle asks */ },
    outputHandler: (emit) => { /* handle emits */ },
  }
);

// If workflow used checkpoints, result includes runId
if (result.isStateful) {
  console.log(`Run ID: ${result.runId}`);
}

// Resume an interrupted workflow
const resumed = await maybeStatefulExecute(
  () => instance.generate({ week: 52 }),
  {
    photon: 'report-generator',
    tool: 'generate',
    params: { week: 52 },
    resumeRunId: 'run_abc123_xyz',  // Resume from this run
    inputProvider,
    outputHandler,
  }
);
```

**JSONL persistence:** Workflows are persisted to `~/.photon/runs/{runId}.jsonl`:

```jsonl
{"t":"start","tool":"generate","params":{"week":52},"ts":1704067200}
{"t":"checkpoint","id":"cp_0","state":{"step":1,"commits":[...]},"ts":1704067205}
{"t":"checkpoint","id":"cp_1","state":{"step":2,"analysis":{...}},"ts":1704067210}
{"t":"return","value":{"report":"..."},"ts":1704067215}
```

---

### Channel-Based Pub/Sub

Photon Core provides a pluggable channel broker architecture for cross-process messaging. This enables real-time updates between MCP processes, Beam UI, and other services.

```typescript
import { PhotonMCP } from '@portel/photon-core';

export default class KanbanBoard extends PhotonMCP {
  async moveTask(params: { taskId: string; column: string }) {
    const task = await this.updateTask(params);

    // Emit to local output handler (current caller)
    this.emit({ emit: 'board-update', board: 'default' });

    // Also publish to channel for cross-process subscribers
    this.emit({
      channel: 'kanban:default',  // Format: photonName:subChannel
      event: 'task-moved',
      data: { task }
    });

    return task;
  }
}
```

**Broker Types:**

| Broker | Use Case | Configuration |
|--------|----------|---------------|
| `daemon` | Local dev, single-server | Default (uses Unix sockets) |
| `redis` | Multi-server, production | `PHOTON_REDIS_URL` |
| `http` | Webhook integrations | `PHOTON_CHANNEL_HTTP_URL` |
| `noop` | Testing, disabled | `PHOTON_CHANNEL_BROKER=noop` |

**Subscribing to channels:**

```typescript
import { getBroker } from '@portel/photon-core';

const broker = getBroker();

// Subscribe
const sub = await broker.subscribe('kanban:default', (message) => {
  console.log('Received:', message.event, message.data);
});

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

**Environment Variables:**
- `PHOTON_CHANNEL_BROKER` - Explicit broker type (`daemon`, `redis`, `http`, `noop`)
- `PHOTON_NAME` - Photon name for daemon socket path
- `PHOTON_REDIS_URL` - Redis connection URL (auto-enables redis broker)
- `PHOTON_CHANNEL_HTTP_URL` - HTTP webhook URL (auto-enables http broker)

See [CHANNELS.md](./CHANNELS.md) for full architecture documentation.

**Channel auto-prefixing:** When emitting with a `channel` property, the channel name is automatically prefixed with the photon name unless it already contains a colon:

```typescript
// In a photon named "whatsapp":
this.emit({ channel: 'messages', event: 'new', data: msg });
// → publishes to 'whatsapp:messages'

// Explicit namespace (colon present = no auto-prefix)
this.emit({ channel: 'board:updates', event: 'moved', data });
// → publishes to 'board:updates' as-is
```

---

### Intermediate Rendering (`this.render()`)

Send formatted intermediate results to the client during method execution. Each call replaces the previous render in the result panel. Uses the same rendering pipeline as `@format` return values.

```typescript
export default class Scanner extends Photon {
  async scan(params: { target: string }) {
    // Show a progress table mid-execution
    this.render('table', [{ host: params.target, status: 'scanning...' }]);

    const results = await this.runScan(params.target);

    // Update with final results
    this.render('table', results);

    // Clear the render zone
    this.render();

    return results;
  }
}
```

**Signatures:**
- `this.render(format: string, value: any)` — render a formatted value
- `this.render()` — clear the render zone (emits `render:clear`)

---

### Dynamic Enums (`{@choice-from}`)

Populate parameter enum values dynamically by calling another tool at schema resolution time:

```typescript
export default class Deploy extends Photon {
  /** @readOnly */
  async environments() {
    return ['staging', 'production', 'dev'];
  }

  /**
   * Deploy to an environment
   * @param env Target environment {@choice-from environments}
   * @param service Service name {@choice-from services.name}
   */
  async deploy(params: { env: string; service: string }) { ... }
}
```

The `{@choice-from toolName}` tag calls the specified tool and uses the result array as enum values. Use `{@choice-from toolName.field}` to extract a specific field from each result object.

---

### Namespace System & Storage/Assets

Every photon gets scoped filesystem APIs for persistent data and bundled assets:

```typescript
export default class WhatsApp extends Photon {
  async onInitialize() {
    // Persistent data directory (follows symlink path)
    const authDir = this.storage('auth');
    // → ~/.photon/portel-dev/whatsapp/auth/

    // Bundled assets directory (follows realpath to source)
    const templates = this.assets('templates');
    // → /real/path/to/whatsapp/assets/templates/
  }
}
```

Cross-namespace photon resolution uses `namespace:name` syntax:
```typescript
const wa = await this.photon.use('portel-dev:whatsapp', 'work');
```

---

### Shared UI Assets (`linkedTools`)

Multiple methods can reference the same `@ui` template. The first method becomes the primary link (used for app detection via `main()`), while all methods are tracked in `linkedTools[]`:

```typescript
export default class Dashboard extends Photon {
  /** @ui dashboard ./ui/dashboard.html */
  async main() { return this.status(); }

  /** @ui dashboard */
  async status() { return { uptime: process.uptime() }; }

  /** @ui dashboard */
  async metrics() { return { requests: 1234 }; }
}
```

The `UIAsset` type tracks both:
- `linkedTool` — first method (used for app detection)
- `linkedTools[]` — all methods sharing this template

---

### Managed Collections

Reactive collections that auto-emit events on mutations, enabling seamless real-time sync between server and client.

#### Level 1: Zero Effort (just import)

```typescript
import { PhotonMCP } from '@portel/photon-core';

export default class TodoList extends PhotonMCP {
  items: Task[] = [];  // Compiler transforms to ReactiveArray

  async add(text: string) {
    this.items.push({ id: crypto.randomUUID(), text });
    // Auto-emits 'items:added' — no manual wiring
  }
}
```

The compiler detects `extends PhotonMCP` with `= []` array initializers and transforms them to `ReactiveArray` instances. The runtime auto-wires `_propertyName` and `_emitter` after instantiation.

**Available reactive types:** `Array` (alias for `ReactiveArray`), `Map` (`ReactiveMap`), `Set` (`ReactiveSet`)

**Event naming (Firebase-style):**

| Operation | Event | Data |
|-----------|-------|------|
| `push(item)` | `{prop}:added` | The item |
| `pop()` / `splice(i, 1)` | `{prop}:removed` | The removed item |
| `set(i, val)` | `{prop}:updated` | `{ index, value, previous }` |
| `replaceAll(items)` | `{prop}:changed` | All items |

#### Level 2: Rich Collections

`Collection<T>` extends `ReactiveArray<T>` with Laravel-style query chaining and auto-UI rendering hints.

```typescript
import { PhotonMCP, Collection } from '@portel/photon-core';

type Product = { name: string; price: number; stock: number; category: string };

export default class ProductCatalog extends PhotonMCP {
  products = new Collection<Product>();

  async catalog() {
    return this.products.where('stock', '>', 0).sortBy('price');
  }

  async dashboard() {
    return this.products.where('category', 'Electronics').as('cards');
  }

  async cheapest() {
    return this.products.sortBy('price').first();
  }
}
```

**Query methods** (immutable — return new `Collection<T>`):

| Method | Example |
|--------|---------|
| `where(key, op?, val)` | `.where('price', '>', 100)` or `.where('status', 'active')` |
| `query(fn)` | `.query(p => p.stock > 0)` |
| `collect(fn)` | `.collect(p => ({ ...p, label: p.name }))` |
| `pluck(key)` | `.pluck('name')` → `Collection<string>` |
| `sortBy(key \| fn, dir?)` | `.sortBy('price', 'desc')` |
| `groupBy(key \| fn)` | `.groupBy('category')` → `Record<string, Collection<T>>` |
| `unique(key?)` | `.unique('category')` |
| `take(n)` / `skip(n)` | `.take(10)` |

**Terminal methods:**

| Method | Return |
|--------|--------|
| `first(fn?)` / `last(fn?)` | `T \| undefined` |
| `count()` / `isEmpty()` | `number` / `boolean` |
| `sum(key?)` / `avg(key?)` | `number` |
| `min(key?)` / `max(key?)` | `T \| undefined` |
| `aggregate(fn, init)` | `U` |

**Rendering hints** for auto-UI:

```typescript
// .as() attaches a format hint — auto-UI renders accordingly
this.products.where('stock', '>', 0).as('table');
this.products.where('category', 'Electronics').as('cards');
```

Formats: `'table'` | `'cards'` | `'list'` | `'chart'` | `'grid'` | `'chips'`

Without `.as()`, `toJSON()` returns a plain array (backward compatible). With `.as()`, it returns:
```json
{ "_photonType": "collection:cards", "items": [...], "count": 3 }
```

---

## 🏗️ Building Custom Runtimes

Photon Core is designed to be the foundation for custom runtimes. Here are examples:

### Example 1: REST API Server

```typescript
import express from 'express';
import { PhotonMCP, DependencyManager, SchemaExtractor } from '@portel/photon-core';

class PhotonRESTServer {
  async serve(photonPath: string) {
    const app = express();

    // Load photon
    const PhotonClass = (await import(photonPath)).default;
    const instance = new PhotonClass();
    await instance.onInitialize?.();

    // Extract schemas
    const extractor = new SchemaExtractor();
    const schemas = await extractor.extractFromFile(photonPath);

    // Create REST endpoints
    schemas.forEach(schema => {
      app.post(`/api/${schema.name}`, async (req, res) => {
        try {
          const result = await instance[schema.name](req.body);
          res.json({ result });
        } catch (error) {
          res.status(500).json({ error: error.message });
        }
      });
    });

    app.listen(3000);
  }
}
```

### Example 2: GraphQL Server

```typescript
import { buildSchema } from 'graphql';
import { PhotonMCP, SchemaExtractor } from '@portel/photon-core';

class PhotonGraphQLServer {
  async generateSchema(photonPath: string) {
    const extractor = new SchemaExtractor();
    const schemas = await extractor.extractFromFile(photonPath);

    const mutations = schemas.map(s =>
      `${s.name}(${this.paramsToGraphQL(s.inputSchema)}): JSON`
    ).join('\n');

    return buildSchema(`
      type Mutation {
        ${mutations}
      }
    `);
  }
}
```

### Example 3: MCP Orchestrator

```typescript
import { PhotonMCP, DependencyManager } from '@portel/photon-core';

class PhotonOrchestrator {
  private photons = new Map();

  async loadPhoton(path: string) {
    // Install dependencies
    const depManager = new DependencyManager();
    const deps = await depManager.extractDependencies(path);
    const name = basename(path, '.photon.ts');
    await depManager.ensureDependencies(name, deps);

    // Load class
    const PhotonClass = (await import(path)).default;
    const instance = new PhotonClass();
    await instance.onInitialize?.();

    this.photons.set(name, instance);
  }

  async execute(photonName: string, method: string, params: any) {
    const photon = this.photons.get(photonName);
    return await photon[method](params);
  }
}
```

---

## 🎯 Use Cases

| Project | Description | Runtime Layer |
|---------|-------------|---------------|
| **[@portel/photon](https://github.com/portel-dev/photon)** | CLI tool for running Photons as MCP servers or CLI tools | MCP server (stdio), CLI runner |
| **[NCP](https://github.com/portel-dev/ncp)** | Multi-MCP orchestrator with discovery and semantic search | Orchestrator, unified MCP interface |
| **[Lumina](https://github.com/portel-dev/lumina)** | Multi-protocol API server | REST, GraphQL, RPC, HTTP streaming MCP |

All three projects use `@portel/photon-core` for the shared format, but each adds its own runtime layer.

---

## 🔧 Development

```bash
# Clone the repository
git clone https://github.com/portel-dev/photon-core.git
cd photon-core

# Install dependencies
npm install

# Build
npm run build

# Test
npm test

# Lint
npm run lint
```

---

## 📄 License

MIT © [Portel](https://github.com/portel-dev)

---

## 🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

---

## 🔗 Related Projects

- **[@portel/photon](https://github.com/portel-dev/photon)** - CLI tool for running Photons
- **[NCP](https://github.com/portel-dev/ncp)** - Multi-MCP orchestrator
- **[Lumina](https://github.com/portel-dev/lumina)** - Multi-protocol API server

---

## 📖 Documentation

For more information about the Photon format and ecosystem:
- [Photon Documentation](https://github.com/portel-dev/photon)
- [NCP Documentation](https://github.com/portel-dev/ncp)
- [Lumina Documentation](https://github.com/portel-dev/lumina)