Photon
Official skills repository for Portel Photon
npx -y skills add portel-dev/skills --skill photonAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Build, test, validate, and improve Photon MCPs β single-file TypeScript MCP servers. Use for creating photons with @format annotations (table, chart:bar, slides), stateful photons using this.memory for persistent storage, photons with @readOnly/@destructive annotations, custom UI using @ui tags/HTML templates, photons wrapping APIs (Stripe, payments), task scheduler photons with cron, user-configurable settings (protected settings), this.render() for live output, photon build for standalone binaries, mermaid diagrams for photon architecture, editing .photon.ts files, @auth for MCP OAuth identity (this.caller), identity-aware locks for multiplayer/turn-based photons, @format slides for Marp-style presentations. Also use for validating photon UIs (promise checking, visual QA, functional testing), improving photon quality via autoloop, and auditing whether a UI consumes all backend methods. DO NOT trigger for general TypeScript or non-photon MCP.
SKILL.md
18.2 KB, ~4.3k tokens by cl100k_base, as published. Nobody here has run it
Photon Lifecycle Guide
Photons are single-file TypeScript MCP servers. No compilation β runs directly with tsx.
Quick Start
npm install -g @portel/photon # Install runtime
photon maker new my-weather # Create photon
photon beam # Launch Beam UI
photon mcp my-weather # Run as MCP server
photon cli my-weather current --city London # CLI
Files go in ~/.photon/. See Directory Structure for the full layout. Connect to Claude Desktop:
{ "mcpServers": { "my-weather": { "command": "photon", "args": ["mcp", "my-weather"] } } }
Minimal Photon
import { Photon } from '@portel/photon-core';
/**
* Weather API
* @version 1.0.0
* @dependencies axios@^1.0.0
* @icon π€οΈ
*/
export default class Weather extends Photon {
constructor(private apiKey: string) { super(); }
/**
* Get current weather for a city
* @param city City name {@example London}
* @readOnly
* @title Current Weather
* @format markdown
*/
async current({ city }: { city: string }): Promise<string> {
const res = await fetch(`https://api.weather.com/v1/current?q=${city}&key=${this.apiKey}`);
if (!res.ok) throw new Error(`API error: ${res.statusText}`);
const data = await res.json();
return `**${data.name}** β ${data.temp}Β°C, ${data.description}`;
}
}
Core Principles
- Return values directly β no
{ success: true, data }wrappers. If it returns, it succeeded. - Throw on errors β let the runtime handle them. Don't catch and wrap.
- Single-word method names β they read as CLI commands:
weather current, notweather getCurrentWeather. - Constructor = config β constructor params auto-map to env vars:
PHOTON_WEATHER_APIKEY.
Class Structure
/**
* Brief description (becomes photon description)
*
* @version 1.0.0
* @runtime ^1.5.0
* @dependencies package1, package2@^2.0.0
* @icon π§
* @tags api, utility
*/
export default class MyTool extends Photon {
constructor(private apiKey: string) { super(); }
/**
* Method description (becomes tool description)
* @param query Search query {@example "typescript"} {@min 1}
* @readOnly
* @title Search Items
* @format table
*/
async search({ query }: { query: string }) { ... }
}
MCP Annotations
JSDoc tags map to MCP protocol annotations (spec 2025-11-25):
/**
* @readOnly β no side effects, safe to auto-approve
* @destructive β requires confirmation
* @idempotent β safe to retry
* @openWorld β calls external systems
* @closedWorld β local data only
* @title My Tool β human-readable display name
* @audience user β who sees results: user, assistant, or both
* @priority 0.9 β content importance (0.0β1.0)
*/
UI-only methods: Combine @internal + @audience user for methods the dashboard can call but the LLM never sees:
/**
* Dashboard-only admin panel data.
* @internal
* @audience user
* @readOnly
*/
async metrics() { return { cpu: 42 }; }
@internal hides from tools/list. @audience user marks results as human-only. The UI still calls via window.photon.callTool('metrics', {}).
Structured Output
Auto-generated from TypeScript return types β no tags needed:
async create(params: { title: string }): Promise<{ id: string; done: boolean }> { ... }
For field descriptions, use an interface with JSDoc:
interface Task {
/** Unique identifier */
id: string;
/** Whether complete */
done: boolean;
}
async create(params: { title: string }): Promise<Task> { ... }
Output Formats
Use @format to control rendering. Common values:
| Format | Use For |
|---|---|
table | Array of objects |
list | Styled list with {@title name, @subtitle email} |
checklist | Interactive todo list β {text, done}[] with checkboxes, drag reorder |
article | Magazine-style text flow around images β {text, images?[]} |
markdown | Rich text, diagrams |
slides | Marp-style presentation deck |
chart:bar / chart:line / chart:pie | Data visualization |
json | Raw JSON |
dashboard | Composite panels (auto-detected) |
For complete format reference with layout hints, containers, and auto-detection rules, see references/output-formats.md.
MCP OAuth & Caller Identity
Use @auth for authenticated photons. Enables this.caller in every method.
/**
* Multiplayer chess
* @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') {
await this.acquireLock('turn', this.players.white);
}
return { color: slot, name: this.caller.name };
}
/** @locked turn */
async move({ from, to }: { 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;
}
}
this.caller: { id, name, anonymous, scope, claims } β populated from MCP OAuth JWT.
Identity-aware locks:
this.acquireLock(name, callerId)β assign lock to a callerthis.transferLock(name, toCallerId)β move lock to another callerthis.releaseLock(name)β release, open to anyonethis.getLock(name)β query who holds the lock@lockedmethods auto-checkthis.caller.idagainst lock holder
Lifecycle & Hot-Reload
// Lifecycle hooks β receive optional context for hot-reload support
async onInitialize(ctx?: { reason?: string; oldInstance?: any }) {
if (ctx?.reason === 'hot-reload' && ctx.oldInstance) {
// Transfer non-serializable resources (sockets, timers, connections)
this.socket = ctx.oldInstance.socket;
ctx.oldInstance.socket = null; // prevent old instance from using it
return;
}
// Normal first-time initialization
this.socket = await createConnection();
}
async onShutdown(ctx?: { reason?: string }) {
if (ctx?.reason === 'hot-reload') {
return; // DON'T close resources β new instance will take them
}
// Real shutdown: clean up everything
this.socket?.close();
}
Hot-reload rules:
onShutdown({ reason: 'hot-reload' })β skip resource cleanuponInitialize({ reason: 'hot-reload', oldInstance })β transfer resources from old instance- Normal shutdown/init (no context) β full cleanup/setup
- Backward compatible β photons without context param still work
Events & Channels
// Simple emit (local only β goes to current caller's UI)
this.emit({ status: 'processing', progress: 50 });
// Channel emit (cross-photon pub/sub via daemon broker)
// Framework auto-prefixes with photon name: 'messages' β 'whatsapp:messages'
this.emit({ channel: 'messages', type: 'message', data: msg });
// Subscribe to another photon's events (use the full prefixed name)
const broker = getBroker();
const sub = await broker.subscribe('whatsapp:messages', (msg) => {
// handle message
});
sub.unsubscribe(); // when done
Channel naming: Emit with simple names (channel: 'messages'). The framework auto-prefixes with the photon name. Channels with a colon are left as-is.
Generator Workflows
// Multi-step with user interaction
async *deploy({ env }: { env: string }) {
yield { emit: 'status', message: 'Deploying...' };
const ok = yield { ask: 'confirm', message: `Deploy to ${env}?` };
if (!ok) return 'Cancelled';
return 'Done';
}
// Waiting for external async events (WebSocket, library callbacks, etc.)
async *connect() {
yield { emit: 'status', message: 'Connecting...' };
let resolve: (v: any) => void;
const promise = new Promise(r => { resolve = r; });
this.socket.on('ready', (data) => resolve(data));
await this.initSocket();
const event = await promise; // blocks until external event fires
yield { emit: 'toast', message: 'Connected!', type: 'success' };
return event;
}
Scoped Memory
Zero-config persistent storage via this.memory:
await this.memory.set('key', value); // photon scope (default)
const val = await this.memory.get<T>('key');
await this.memory.set('shared', data, 'global'); // cross-photon
Three scopes: photon (private), session (per-user), global (shared). Full API: get, set, delete, has, keys, clear, getAll, update.
Runtime Scheduling
Dynamic task scheduling via this.schedule β complements static @scheduled/@cron tags:
// Create a scheduled task at runtime
await this.schedule.create({
name: 'nightly-cleanup',
schedule: '0 0 * * *', // 5-field cron or @daily, @hourly, @weekly, @monthly
method: 'purge',
params: { olderThan: 30 },
});
// Manage tasks
await this.schedule.pause(id);
await this.schedule.resume(id);
await this.schedule.cancel(id);
const tasks = await this.schedule.list('active');
Full API: create, get, getByName, list, update, pause, resume, cancel, cancelByName, cancelAll, has. Tasks persist to disk; the daemon executes them.
Use @scheduled/@cron for fixed schedules known at build time. Use this.schedule for dynamic schedules created at runtime (user-configured intervals, conditional jobs, etc.).
User Settings
Expose configurable options via protected settings. Runtime auto-generates a settings tool and persists to disk:
export default class MyAgent extends Photon {
protected settings = {
/** Polling interval in ms */
pollIntervalMs: 5000,
/** Max concurrent operations */
maxConcurrent: 3,
/** Auto-resume after restart */
autoResume: true,
};
async doWork() {
const interval = this.settings.pollIntervalMs; // read-only Proxy
}
}
Users change settings via CLI (photon cli my-agent settings). Values persist to ~/.photon/state/<name>/<instance>-settings.json.
Live Rendering (v1.14+)
this.render(format, value) pushes formatted output that replaces the previous render zone (instead of appending):
export default class Monitor {
async status() {
while (true) {
const metrics = await this.collectMetrics();
this.render('table', metrics); // Replaces previous output
await new Promise(r => setTimeout(r, 5000));
}
}
}
Accepts the same format values as @format tags. Call this.render() with no arguments to clear the render zone.
Compile to Binary (v1.13+)
Build standalone executables from any photon β no Node.js required on the target machine:
photon build my-tool # Binary for current platform
photon build my-tool -t bun-linux-x64 # Cross-compile for Linux
photon build my-tool --with-app # Embed Beam UI as a desktop app
Uses Bun's compiler. The binary bundles the photon, its @dependencies, and transitive @photon deps.
Installing Photons
photon add β Install a photon
photon add claw # Search all marketplaces by name
photon add my-marketplace/claw # From a specific marketplace
photon add Arul-/photons/claw # From a GitHub repo (owner/repo/name)
If a photon has @photon dependencies, they are auto-installed from the same source recursively.
photon beam / photon cli β Run directly from GitHub
photon beam Arul-/photons/claw # Install + open in Beam
photon cli Arul-/photons/todo add # Install + run method
Manage marketplaces
photon marketplace add Arul-/photons # Add a GitHub repo as marketplace source
photon marketplace update # Refresh all marketplace caches
photon marketplace list # Show configured marketplaces
Custom UIs
Link HTML files as interactive result renderers:
/** @ui dashboard */
export default class MyApp extends Photon {
/** @ui dashboard */
async getData({ range }: { range: string }) { return { metrics: 42 }; }
}
Convention: @ui <id> resolves to <photon>/ui/<id>.html. The id MUST match the HTML filename.
@ui dashboard β my-app/ui/dashboard.html β
@ui slides β slides/ui/slides.html β
Text after the id is treated as a description, NOT a file path.
The UI gets a photon-named global proxy: window.myApp.getData(...), window.myApp.onResult(...).
For full MCP Apps guide, see references/mcp-apps.md.
Directory Structure
Photon separates source/assets from runtime data under ~/.photon/:
~/.photon/
βββ <name>.photon.ts # Source (or symlink)
βββ <name>/ # Assets (@ui templates, images)
βββ state/<name>/ # Runtime: this.memory, settings (automatic)
βββ data/<name>/ # Runtime: photon-written files (manual)
βββ cache/ # Runtime: compiled .mjs cache
βββ logs/<name>/ # Runtime: execution logs
Key rule: Photons that write runtime files (auth tokens, downloaded media, databases) MUST use ~/.photon/data/<name>/, never ~/.photon/<name>/. The asset folder is watched for hot-reload β writing there causes reload loops.
// Correct: runtime data goes in data/<name>/
const dataDir = path.join(os.homedir(), '.photon', 'data', 'my-app');
// Wrong: asset folder triggers hot-reload!
const dataDir = path.join(os.homedir(), '.photon', 'my-app', 'downloads');
For the full convention, see Directory Structure.
Validating Photon UIs β Promise Checking
Every @ui photon makes implicit promises through its backend methods. The UI must deliver on them.
The Principle
A photon's backend methods are its capabilities. The UI is the interface to those capabilities. If a method exists but the UI has no way to trigger it, that's a broken promise.
Three signal levels:
| Signal | Meaning |
|---|---|
@ui <id> on a method | This method's result is rendered by the UI β the UI MUST handle it |
@audience user | Result is for the human, not the LLM β the UI SHOULD surface it |
Public method (no @internal) | Capability exists β the UI or CLI should make it accessible |
If a method is public but the UI doesn't use it, either:
- Add a UI control for it, or
- Mark it
@internalto be honest about what's exposed
Promise Documentation
Every @ui photon should have a ## UI Promises section in its class docblock:
/**
* Slides β AI-Native Presentation Tool
*
* ## UI Promises
*
* - Filmstrip with thumbnails for slide navigation
* - Drag-and-drop to reorder slides
* - Theme selector (default, gaia, uncover)
* - Fullscreen presentation with scaled slides
* - Markdown editor with live preview
* - Speaker notes editor
* - Deck picker to switch or create presentations
*/
These are the acceptance criteria. Each line is a testable claim. Validation means checking every claim is true.
Gap Detection
To validate, compare two lists:
- Backend capabilities β all public methods in the
.photon.ts, especially those tagged@uior@audience user - UI consumption β all method calls in the HTML template (e.g.,
window["slides"].move(...))
Any method in list 1 but not in list 2 is a gap. Any promise in the docblock that has no corresponding UI control is a broken promise.
Two Layers of Quality
| Layer | What it catches | What it misses |
|---|---|---|
| Cosmetic (screenshot review) | Broken layout, contrast, overflow, clipping | Missing features, interaction bugs |
| Functional (promise check) | Missing features, unconsumed methods, state gaps | Subtle visual bugs |
Both layers are needed. A UI can look perfect while missing half its features.
References
| Topic | When to Read |
|---|---|
| Directory Structure | Need the ~/.photon/ layout rules for assets vs runtime data |
| Docblock Tags | Need the complete tag reference (class, method, inline, daemon, MCP) |
| Output Formats | Need layout hints, chart mapping, containers, or auto-detection rules |
| Dependency Injection | Using @mcp, @photon, or this.call() for cross-photon communication |
| Daemon Features | Setting up webhooks, cron jobs, or distributed locks |
| User Settings | protected settings, persistence, auto-resume patterns |
| MCP Apps | Building custom HTML UIs with the photon bridge |
| Visualization | Generating Mermaid diagrams from photons |
| Mermaid Syntax | Flowchart shapes, arrows, subgraphs |
| Photon Patterns | Common emit/ask/yield patterns, path resolution (this.callerCwd), Mermaid equivalents |
| Examples | Complete Photon-to-Mermaid conversion examples |
| Validation | Promise checking, gap detection, UI testing checklist |
What ships with it: 13 files
123.5 KB alongside SKILL.md
references/
- daemon-features.md4.2 KB
- dependency-injection.md4.5 KB
- directory-structure.md4.7 KB
- docblock-tags.md13.2 KB
- examples.md17.5 KB
- lifecycle-and-ingress.md5.7 KB
- mcp-apps.md24.2 KB
- mermaid-syntax.md6.0 KB
- output-formats.md10.1 KB
- photon-patterns.md11.5 KB
- user-settings.md2.1 KB
- validation.md2.3 KB
- visualization.md17.6 KB