@portel/photon 1.7.0 ā 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -24
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +117 -42
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/design-system/tokens.d.ts +1 -1
- package/dist/auto-ui/design-system/tokens.d.ts.map +1 -1
- package/dist/auto-ui/design-system/tokens.js +1 -1
- package/dist/auto-ui/design-system/tokens.js.map +1 -1
- package/dist/auto-ui/frontend/index.html +1 -1
- package/dist/auto-ui/rendering/components.d.ts.map +1 -1
- package/dist/auto-ui/rendering/components.js +568 -0
- package/dist/auto-ui/rendering/components.js.map +1 -1
- package/dist/auto-ui/rendering/field-analyzer.d.ts +56 -0
- package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -1
- package/dist/auto-ui/rendering/field-analyzer.js +177 -0
- package/dist/auto-ui/rendering/field-analyzer.js.map +1 -1
- package/dist/auto-ui/rendering/layout-selector.d.ts +14 -2
- package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -1
- package/dist/auto-ui/rendering/layout-selector.js +125 -1
- package/dist/auto-ui/rendering/layout-selector.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +353 -19
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +7 -1
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +22441 -4216
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +37 -0
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/package.d.ts.map +1 -1
- package/dist/cli/commands/package.js +16 -0
- package/dist/cli/commands/package.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +628 -14
- package/dist/cli.js.map +1 -1
- package/dist/context-store.d.ts +79 -0
- package/dist/context-store.d.ts.map +1 -0
- package/dist/context-store.js +210 -0
- package/dist/context-store.js.map +1 -0
- package/dist/daemon/client.d.ts +13 -4
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +138 -77
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +0 -25
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +10 -38
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/protocol.d.ts +7 -2
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/server.js +257 -35
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +24 -4
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +62 -12
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +3 -20
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +53 -75
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +258 -218
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +2 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +42 -6
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/maker.photon.d.ts.map +1 -1
- package/dist/photons/maker.photon.js +3 -1
- package/dist/photons/maker.photon.js.map +1 -1
- package/dist/photons/maker.photon.ts +3 -1
- package/dist/serv/index.d.ts.map +1 -1
- package/dist/serv/index.js.map +1 -1
- package/dist/server.d.ts +32 -15
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +468 -469
- package/dist/server.js.map +1 -1
- package/dist/shared/security.d.ts.map +1 -1
- package/dist/shared/security.js +4 -8
- package/dist/shared/security.js.map +1 -1
- package/dist/shell-completions.d.ts +21 -0
- package/dist/shell-completions.d.ts.map +1 -0
- package/dist/shell-completions.js +102 -0
- package/dist/shell-completions.js.map +1 -0
- package/dist/template-manager.d.ts.map +1 -1
- package/dist/template-manager.js.map +1 -1
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -278,7 +278,7 @@ VideoProcessor requires the following CLI tools to be installed:
|
|
|
278
278
|
- ffmpeg: Install from https://ffmpeg.org/download.html
|
|
279
279
|
```
|
|
280
280
|
|
|
281
|
-
> See the full [Tag Reference](./DOCBLOCK-TAGS.md) for all available tags. There are 30+ covering validation, UI hints, scheduling, webhooks, and more.
|
|
281
|
+
> See the full [Tag Reference](./docs/reference/DOCBLOCK-TAGS.md) for all available tags. There are 30+ covering validation, UI hints, scheduling, webhooks, and more.
|
|
282
282
|
|
|
283
283
|
<div align="center">
|
|
284
284
|
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/readme-step-4.png" alt="Step 4 ā Validation and formatting in Beam" width="100%">
|
|
@@ -322,7 +322,7 @@ export default class Weather {
|
|
|
322
322
|
|
|
323
323
|
**What changes in Beam:** Instead of the auto-generated table, results render inside your custom HTML (a weather dashboard with icons, charts, or any visualization you build). The `window.photon` API bridges your UI to the tool system.
|
|
324
324
|
|
|
325
|
-
> Custom UIs follow the [MCP Apps Extension (SEP-1865)](https://github.com/nicolo-ribaudo/modelcontextprotocol/blob/nicolo/sep-1865/docs/specification/draft/extensions/apps.mdx) standard and work across compatible hosts. See the [Custom UI Guide](./CUSTOM-UI.md).
|
|
325
|
+
> Custom UIs follow the [MCP Apps Extension (SEP-1865)](https://github.com/nicolo-ribaudo/modelcontextprotocol/blob/nicolo/sep-1865/docs/specification/draft/extensions/apps.mdx) standard and work across compatible hosts. See the [Custom UI Guide](./docs/guides/CUSTOM-UI.md).
|
|
326
326
|
|
|
327
327
|
<div align="center">
|
|
328
328
|
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/readme-step-5.png" alt="Step 5 ā Custom UI result in Beam" width="100%">
|
|
@@ -351,12 +351,12 @@ If you are just skimming, here is what you need to know:
|
|
|
351
351
|
| Concept | What it is | Learn more |
|
|
352
352
|
|---------|-----------|------------|
|
|
353
353
|
| **MCP** | A way for AI to use your tools. It's a standard. | [modelcontextprotocol.io](https://modelcontextprotocol.io/introduction) |
|
|
354
|
-
| **Photon file** | A `.photon.ts` file. You define tools as methods in a class. | [Guide](./GUIDE.md) |
|
|
354
|
+
| **Photon file** | A `.photon.ts` file. You define tools as methods in a class. | [Guide](./docs/GUIDE.md) |
|
|
355
355
|
| **Beam** | A web dashboard. It shows your tools as forms. | [Beam UI](#beam) |
|
|
356
356
|
| **Marketplace** | A way to get other people's photons. | [Marketplace](#marketplace) |
|
|
357
|
-
| **Daemon** | A background thing that handles messages and jobs. | [Daemon Pub/Sub](./DAEMON-PUBSUB.md) |
|
|
358
|
-
| **Tags** | JSDoc comments that tell Photon what to do. | [Tag Reference](./DOCBLOCK-TAGS.md) |
|
|
359
|
-
| **Custom UI** | When the auto-generated forms aren't enough. | [Custom UI Guide](./CUSTOM-UI.md) |
|
|
357
|
+
| **Daemon** | A background thing that handles messages and jobs. | [Daemon Pub/Sub](./docs/core/DAEMON-PUBSUB.md) |
|
|
358
|
+
| **Tags** | JSDoc comments that tell Photon what to do. | [Tag Reference](./docs/reference/DOCBLOCK-TAGS.md) |
|
|
359
|
+
| **Custom UI** | When the auto-generated forms aren't enough. | [Custom UI Guide](./docs/guides/CUSTOM-UI.md) |
|
|
360
360
|
|
|
361
361
|
---
|
|
362
362
|
|
|
@@ -377,7 +377,7 @@ photon maker new <name> # Scaffold a new photon
|
|
|
377
377
|
# Manage
|
|
378
378
|
photon info # List all photons
|
|
379
379
|
photon info <name> --mcp # Get MCP client config (paste into Claude/Cursor)
|
|
380
|
-
photon validate <name>
|
|
380
|
+
photon maker validate <name> # Check for errors
|
|
381
381
|
|
|
382
382
|
# Marketplace
|
|
383
383
|
photon add <name> # Install photon
|
|
@@ -386,7 +386,6 @@ photon upgrade # Upgrade all
|
|
|
386
386
|
|
|
387
387
|
# Ops
|
|
388
388
|
photon doctor # Diagnose environment
|
|
389
|
-
photon audit # Security audit
|
|
390
389
|
photon test # Run tests
|
|
391
390
|
```
|
|
392
391
|
|
|
@@ -412,7 +411,7 @@ Tags are JSDoc annotations that control how Photon processes your code. Here are
|
|
|
412
411
|
| `@mcp` | Class | Inject another MCP server as a dependency |
|
|
413
412
|
| `@icon` | Class/Method | Set emoji icon |
|
|
414
413
|
|
|
415
|
-
> This is a subset. See the full [Tag Reference](./DOCBLOCK-TAGS.md) for all 30+ tags with examples.
|
|
414
|
+
> This is a subset. See the full [Tag Reference](./docs/reference/DOCBLOCK-TAGS.md) for all 30+ tags with examples.
|
|
416
415
|
|
|
417
416
|
---
|
|
418
417
|
|
|
@@ -422,33 +421,33 @@ Tags are JSDoc annotations that control how Photon processes your code. Here are
|
|
|
422
421
|
|
|
423
422
|
| Guide | |
|
|
424
423
|
|-------|-|
|
|
425
|
-
| [Getting Started](./GUIDE.md) | Create your first photon, step by step |
|
|
426
|
-
| [Tag Reference](./DOCBLOCK-TAGS.md) | Complete JSDoc tag reference with examples |
|
|
427
|
-
| [Naming Conventions](./NAMING-CONVENTIONS.md) | How to name methods so they read naturally as CLI commands |
|
|
428
|
-
| [Troubleshooting](./TROUBLESHOOTING.md) | Common issues and solutions |
|
|
424
|
+
| [Getting Started](./docs/GUIDE.md) | Create your first photon, step by step |
|
|
425
|
+
| [Tag Reference](./docs/reference/DOCBLOCK-TAGS.md) | Complete JSDoc tag reference with examples |
|
|
426
|
+
| [Naming Conventions](./docs/guides/NAMING-CONVENTIONS.md) | How to name methods so they read naturally as CLI commands |
|
|
427
|
+
| [Troubleshooting](./docs/TROUBLESHOOTING.md) | Common issues and solutions |
|
|
429
428
|
|
|
430
429
|
**Build more:**
|
|
431
430
|
|
|
432
431
|
| Topic | |
|
|
433
432
|
|-------|-|
|
|
434
|
-
| [Custom UI](./CUSTOM-UI.md) | Build rich interactive interfaces with `window.photon` |
|
|
435
|
-
| [OAuth](./AUTH.md) | Built-in OAuth 2.1 with Google, GitHub, Microsoft |
|
|
436
|
-
| [Daemon Pub/Sub](./DAEMON-PUBSUB.md) | Real-time cross-process messaging |
|
|
437
|
-
| [Webhooks](./WEBHOOKS.md) | HTTP endpoints for external services |
|
|
438
|
-
| [Locks](./LOCKS.md) | Distributed locks for exclusive access |
|
|
439
|
-
| [Advanced Patterns](./ADVANCED.md) | Lifecycle hooks, dependency injection, interactive workflows |
|
|
440
|
-
| [Deployment](./DEPLOYMENT.md) | Docker, Cloudflare Workers, AWS Lambda, Systemd |
|
|
433
|
+
| [Custom UI](./docs/guides/CUSTOM-UI.md) | Build rich interactive interfaces with `window.photon` |
|
|
434
|
+
| [OAuth](./docs/guides/AUTH.md) | Built-in OAuth 2.1 with Google, GitHub, Microsoft |
|
|
435
|
+
| [Daemon Pub/Sub](./docs/core/DAEMON-PUBSUB.md) | Real-time cross-process messaging |
|
|
436
|
+
| [Webhooks](./docs/reference/WEBHOOKS.md) | HTTP endpoints for external services |
|
|
437
|
+
| [Locks](./docs/reference/LOCKS.md) | Distributed locks for exclusive access |
|
|
438
|
+
| [Advanced Patterns](./docs/guides/ADVANCED.md) | Lifecycle hooks, dependency injection, interactive workflows |
|
|
439
|
+
| [Deployment](./docs/guides/DEPLOYMENT.md) | Docker, Cloudflare Workers, AWS Lambda, Systemd |
|
|
441
440
|
|
|
442
441
|
**Operate:**
|
|
443
442
|
|
|
444
443
|
| Topic | |
|
|
445
444
|
|-------|-|
|
|
446
445
|
| [Security](./SECURITY.md) | Best practices and audit checklist |
|
|
447
|
-
| [Marketplace Publishing](./MARKETPLACE-PUBLISHING.md) | Create and share team marketplaces |
|
|
448
|
-
| [Best Practices](./
|
|
449
|
-
| [Comparison](./COMPARISON.md) | Benchmarks vs official MCP implementations |
|
|
446
|
+
| [Marketplace Publishing](./docs/guides/MARKETPLACE-PUBLISHING.md) | Create and share team marketplaces |
|
|
447
|
+
| [Best Practices](./docs/guides/BEST-PRACTICES.md) | Patterns for production photons |
|
|
448
|
+
| [Comparison](./docs/COMPARISON.md) | Benchmarks vs official MCP implementations |
|
|
450
449
|
|
|
451
|
-
**Reference:** [Architecture](./ARCHITECTURE.md) Ā· [Changelog](./CHANGELOG.md) Ā· [Contributing](./CONTRIBUTING.md)
|
|
450
|
+
**Reference:** [Architecture](./docs/core/ARCHITECTURE.md) Ā· [Changelog](./CHANGELOG.md) Ā· [Contributing](./CONTRIBUTING.md)
|
|
452
451
|
|
|
453
452
|
---
|
|
454
453
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"beam.d.ts","sourceRoot":"","sources":["../../src/auto-ui/beam.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"beam.d.ts","sourceRoot":"","sources":["../../src/auto-ui/beam.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA2xBH,wBAAsB,SAAS,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwkFlF;AAiYD;;;GAGG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB9C"}
|
package/dist/auto-ui/beam.js
CHANGED
|
@@ -14,7 +14,7 @@ import * as os from 'os';
|
|
|
14
14
|
import { spawn } from 'child_process';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { createHash } from 'crypto';
|
|
17
|
-
import { isPathWithin, isLocalRequest, setSecurityHeaders, readBody, SimpleRateLimiter } from '../shared/security.js';
|
|
17
|
+
import { isPathWithin, isLocalRequest, setSecurityHeaders, readBody, SimpleRateLimiter, } from '../shared/security.js';
|
|
18
18
|
/**
|
|
19
19
|
* Generate a unique ID for a photon based on its path.
|
|
20
20
|
* This ensures photons with the same name from different paths are distinguishable.
|
|
@@ -29,15 +29,17 @@ const __dirname = path.dirname(__filename);
|
|
|
29
29
|
import { listPhotonMCPs, resolvePhotonPath } from '../path-resolver.js';
|
|
30
30
|
import { PhotonLoader } from '../loader.js';
|
|
31
31
|
import { logger, createLogger } from '../shared/logger.js';
|
|
32
|
+
import { getErrorMessage } from '../shared/error-handler.js';
|
|
32
33
|
import { toEnvVarName } from '../shared/config-docs.js';
|
|
33
34
|
import { MarketplaceManager } from '../marketplace-manager.js';
|
|
34
35
|
import { PhotonDocExtractor } from '../photon-doc-extractor.js';
|
|
35
36
|
import { TemplateManager } from '../template-manager.js';
|
|
36
37
|
import { subscribeChannel, pingDaemon } from '../daemon/client.js';
|
|
38
|
+
import { ensureDaemon } from '../daemon/manager.js';
|
|
37
39
|
import { SchemaExtractor, } from '@portel/photon-core';
|
|
38
40
|
import { generateOpenAPISpec } from './openapi-generator.js';
|
|
39
41
|
import { handleStreamableHTTP, broadcastNotification, broadcastToBeam, sendToSession, requestExternalElicitation, } from './streamable-http-transport.js';
|
|
40
|
-
import { SDKMCPClientFactory } from '
|
|
42
|
+
import { SDKMCPClientFactory } from '@portel/photon-core';
|
|
41
43
|
import { getBundledPhotonPath, BEAM_BUNDLED_PHOTONS } from '../shared-utils.js';
|
|
42
44
|
// SDK imports for direct resource access (transport wrapper doesn't expose these yet)
|
|
43
45
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
@@ -46,7 +48,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|
|
46
48
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
47
49
|
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
48
50
|
// Config file path
|
|
49
|
-
const CONFIG_FILE = path.join(os.homedir(), '.photon', 'config.json');
|
|
51
|
+
const CONFIG_FILE = process.env.PHOTON_CONFIG_FILE || path.join(os.homedir(), '.photon', 'config.json');
|
|
50
52
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
51
53
|
// EXTERNAL MCP STATE (module-level for MCP transport access)
|
|
52
54
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
@@ -277,19 +279,10 @@ async function loadExternalMCPs(config) {
|
|
|
277
279
|
mcpInfo.methods = methods;
|
|
278
280
|
}
|
|
279
281
|
catch (sdkError) {
|
|
280
|
-
// SDK client failed
|
|
281
|
-
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
methods = (tools || []).map((tool) => ({
|
|
285
|
-
name: tool.name,
|
|
286
|
-
description: tool.description || '',
|
|
287
|
-
params: tool.inputSchema || { type: 'object', properties: {} },
|
|
288
|
-
returns: { type: 'object' },
|
|
289
|
-
icon: tool['x-icon'],
|
|
290
|
-
}));
|
|
291
|
-
mcpInfo.connected = true;
|
|
292
|
-
mcpInfo.methods = methods;
|
|
282
|
+
// SDK client failed ā don't fall back to wrapper for stdio MCPs
|
|
283
|
+
// (same command would fail identically, and wrapper spawns a process
|
|
284
|
+
// without stderr suppression, leaking raw Node.js stack traces)
|
|
285
|
+
throw sdkError;
|
|
293
286
|
}
|
|
294
287
|
}
|
|
295
288
|
else {
|
|
@@ -318,7 +311,17 @@ async function loadExternalMCPs(config) {
|
|
|
318
311
|
catch (error) {
|
|
319
312
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
320
313
|
mcpInfo.errorMessage = errorMsg.slice(0, 200);
|
|
321
|
-
|
|
314
|
+
// User-friendly error messages for common failures
|
|
315
|
+
const shortMsg = errorMsg.includes('Cannot find module')
|
|
316
|
+
? `Module not found (run npm build in the MCP directory)`
|
|
317
|
+
: errorMsg.includes('ENOENT')
|
|
318
|
+
? `Command not found: ${serverConfig.command}`
|
|
319
|
+
: errorMsg.includes('Connection timeout')
|
|
320
|
+
? `Connection timed out (server may not be running)`
|
|
321
|
+
: errorMsg.includes('Connection closed')
|
|
322
|
+
? `Server exited immediately (check configuration)`
|
|
323
|
+
: errorMsg.slice(0, 120);
|
|
324
|
+
logger.warn(`ā ļø External MCP "${name}" ā ${shortMsg}`);
|
|
322
325
|
}
|
|
323
326
|
results.push(mcpInfo);
|
|
324
327
|
}
|
|
@@ -799,6 +802,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
799
802
|
catch {
|
|
800
803
|
// No install metadata - that's fine
|
|
801
804
|
}
|
|
805
|
+
const isStateful = schemaSource ? /@stateful\b/.test(schemaSource) : false;
|
|
802
806
|
return {
|
|
803
807
|
id: generatePhotonId(photonPath),
|
|
804
808
|
name,
|
|
@@ -818,6 +822,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
818
822
|
resourceCount,
|
|
819
823
|
promptCount,
|
|
820
824
|
installSource,
|
|
825
|
+
...(isStateful && { stateful: true }),
|
|
821
826
|
...(constructorParams.length > 0 && { requiredParams: constructorParams }),
|
|
822
827
|
...(mcp.injectedPhotons &&
|
|
823
828
|
mcp.injectedPhotons.length > 0 && { injectedPhotons: mcp.injectedPhotons }),
|
|
@@ -840,57 +845,58 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
840
845
|
}
|
|
841
846
|
}
|
|
842
847
|
const channelSubscriptions = new Map();
|
|
843
|
-
|
|
848
|
+
/** Buffer retention window ā events older than this are purged */
|
|
849
|
+
const EVENT_BUFFER_DURATION_MS = 5 * 60 * 1000; // 5 minutes
|
|
844
850
|
const channelEventBuffers = new Map();
|
|
845
851
|
// Store an event in the channel buffer
|
|
846
852
|
function bufferEvent(channel, method, params) {
|
|
847
853
|
let buffer = channelEventBuffers.get(channel);
|
|
848
854
|
if (!buffer) {
|
|
849
|
-
buffer = { events: []
|
|
855
|
+
buffer = { events: [] };
|
|
850
856
|
channelEventBuffers.set(channel, buffer);
|
|
851
857
|
}
|
|
852
|
-
const
|
|
858
|
+
const now = Date.now();
|
|
853
859
|
const event = {
|
|
854
|
-
id:
|
|
860
|
+
id: now,
|
|
855
861
|
method,
|
|
856
862
|
params,
|
|
857
|
-
timestamp:
|
|
863
|
+
timestamp: now,
|
|
858
864
|
};
|
|
859
865
|
buffer.events.push(event);
|
|
860
|
-
//
|
|
861
|
-
|
|
866
|
+
// Purge events older than retention window
|
|
867
|
+
const cutoff = now - EVENT_BUFFER_DURATION_MS;
|
|
868
|
+
while (buffer.events.length > 0 && buffer.events[0].timestamp < cutoff) {
|
|
862
869
|
buffer.events.shift();
|
|
863
870
|
}
|
|
864
|
-
return
|
|
871
|
+
return now;
|
|
865
872
|
}
|
|
866
|
-
// Replay missed events to a specific session, or signal
|
|
867
|
-
function replayEventsToSession(sessionId, channel,
|
|
873
|
+
// Replay missed events to a specific session, or signal full sync needed
|
|
874
|
+
function replayEventsToSession(sessionId, channel, lastTimestamp) {
|
|
868
875
|
const buffer = channelEventBuffers.get(channel);
|
|
869
876
|
// No buffer = no events ever sent on this channel
|
|
870
877
|
if (!buffer || buffer.events.length === 0) {
|
|
871
878
|
return { replayed: 0, refreshNeeded: false };
|
|
872
879
|
}
|
|
873
|
-
// No
|
|
874
|
-
if (
|
|
880
|
+
// No lastTimestamp = client is fresh, no replay needed
|
|
881
|
+
if (lastTimestamp === undefined) {
|
|
875
882
|
return { replayed: 0, refreshNeeded: false };
|
|
876
883
|
}
|
|
877
884
|
const oldestEvent = buffer.events[0];
|
|
878
|
-
//
|
|
879
|
-
if (
|
|
885
|
+
// Stale: client's timestamp is older than buffer window ā full sync needed
|
|
886
|
+
if (lastTimestamp < oldestEvent.timestamp) {
|
|
880
887
|
sendToSession(sessionId, 'photon/refresh-needed', { channel });
|
|
881
|
-
logger.info(`š”
|
|
888
|
+
logger.info(`š” Stale client on ${channel} - last seen ${new Date(lastTimestamp).toISOString()}, oldest buffered ${new Date(oldestEvent.timestamp).toISOString()}, full sync needed`);
|
|
882
889
|
return { replayed: 0, refreshNeeded: true };
|
|
883
890
|
}
|
|
884
|
-
//
|
|
885
|
-
const eventsToReplay = buffer.events.filter((e) => e.
|
|
891
|
+
// Delta sync: replay events after client's last timestamp
|
|
892
|
+
const eventsToReplay = buffer.events.filter((e) => e.timestamp > lastTimestamp);
|
|
886
893
|
if (eventsToReplay.length === 0) {
|
|
887
894
|
return { replayed: 0, refreshNeeded: false };
|
|
888
895
|
}
|
|
889
|
-
// Replay each missed event to this session
|
|
890
896
|
for (const event of eventsToReplay) {
|
|
891
|
-
sendToSession(sessionId, event.method, { ...event.params, _eventId: event.
|
|
897
|
+
sendToSession(sessionId, event.method, { ...event.params, _eventId: event.timestamp });
|
|
892
898
|
}
|
|
893
|
-
logger.info(`š”
|
|
899
|
+
logger.info(`š” Delta sync: ${channel} - replayed ${eventsToReplay.length} events`);
|
|
894
900
|
return { replayed: eventsToReplay.length, refreshNeeded: false };
|
|
895
901
|
}
|
|
896
902
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
@@ -964,8 +970,8 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
964
970
|
// Called when a client starts viewing a board (from MCP notification)
|
|
965
971
|
// photonId: hash of photon path (unique across servers)
|
|
966
972
|
// itemId: whatever the photon uses to identify the item (e.g., board name)
|
|
967
|
-
//
|
|
968
|
-
function onClientViewingBoard(sessionId, photonId, itemId,
|
|
973
|
+
// lastTimestamp: optional - if provided, delta sync missed events or signal full sync needed
|
|
974
|
+
function onClientViewingBoard(sessionId, photonId, itemId, lastTimestamp) {
|
|
969
975
|
const prevState = sessionViewState.get(sessionId);
|
|
970
976
|
// Unsubscribe from previous item if different
|
|
971
977
|
if (prevState?.itemId && (prevState.photonId !== photonId || prevState.itemId !== itemId)) {
|
|
@@ -976,9 +982,9 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
976
982
|
const channel = `${photonId}:${itemId}`;
|
|
977
983
|
sessionViewState.set(sessionId, { photonId, itemId });
|
|
978
984
|
subscribeToChannel(channel);
|
|
979
|
-
//
|
|
980
|
-
if (
|
|
981
|
-
replayEventsToSession(sessionId, channel,
|
|
985
|
+
// Delta sync missed events if lastTimestamp is provided
|
|
986
|
+
if (lastTimestamp !== undefined) {
|
|
987
|
+
replayEventsToSession(sessionId, channel, lastTimestamp);
|
|
982
988
|
}
|
|
983
989
|
}
|
|
984
990
|
// Called when a client disconnects
|
|
@@ -2265,6 +2271,26 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2265
2271
|
logger.info(isNewPhoton
|
|
2266
2272
|
? `⨠New photon detected: ${photonName}`
|
|
2267
2273
|
: `š File change detected, reloading ${photonName}...`);
|
|
2274
|
+
// Auto-scaffold empty photon files with a starter template
|
|
2275
|
+
if (isNewPhoton) {
|
|
2276
|
+
try {
|
|
2277
|
+
const rawContent = await fs.readFile(photonPath, 'utf-8');
|
|
2278
|
+
if (rawContent.trim().length === 0) {
|
|
2279
|
+
const className = photonName
|
|
2280
|
+
.split(/[-_]/)
|
|
2281
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
2282
|
+
.join('');
|
|
2283
|
+
const scaffold = `/**\n * ${className} Photon\n */\n\nexport default class ${className} {\n /**\n * Example tool\n * @param message Message to echo\n */\n async echo(params: { message: string }) {\n return \`Echo: \${params.message}\`;\n }\n}\n`;
|
|
2284
|
+
await fs.writeFile(photonPath, scaffold, 'utf-8');
|
|
2285
|
+
logger.info(`š Scaffolded empty file: ${photonName}.photon.ts`);
|
|
2286
|
+
// The write triggers another watcher event which will load the scaffolded photon
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
catch {
|
|
2291
|
+
// File read failed, continue with normal load attempt
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2268
2294
|
// For new photons, check if configuration is needed first
|
|
2269
2295
|
if (isNewPhoton) {
|
|
2270
2296
|
const extractor = new SchemaExtractor();
|
|
@@ -2375,6 +2401,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2375
2401
|
// Can't extract params
|
|
2376
2402
|
}
|
|
2377
2403
|
backfillEnvDefaults(mcp.instance, reloadConstructorParams);
|
|
2404
|
+
const isStateful = /@stateful\b/.test(reloadSource);
|
|
2378
2405
|
const reloadedPhoton = {
|
|
2379
2406
|
id: generatePhotonId(photonPath),
|
|
2380
2407
|
name: photonName,
|
|
@@ -2386,6 +2413,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2386
2413
|
description: reloadClassMeta.description,
|
|
2387
2414
|
icon: reloadClassMeta.icon,
|
|
2388
2415
|
internal: reloadClassMeta.internal,
|
|
2416
|
+
...(isStateful && { stateful: true }),
|
|
2389
2417
|
...(reloadConstructorParams.length > 0 && { requiredParams: reloadConstructorParams }),
|
|
2390
2418
|
...(mcp.injectedPhotons &&
|
|
2391
2419
|
mcp.injectedPhotons.length > 0 && { injectedPhotons: mcp.injectedPhotons }),
|
|
@@ -2541,6 +2569,14 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2541
2569
|
console.log(`\nā” Photon Beam ā ${url} (loading photons...)\n`);
|
|
2542
2570
|
resolve();
|
|
2543
2571
|
});
|
|
2572
|
+
// Configure server and socket timeouts to prevent premature disconnections
|
|
2573
|
+
// Disable server timeout for long-lived SSE connections (0 = no timeout)
|
|
2574
|
+
server.setTimeout(0);
|
|
2575
|
+
// Enable TCP keepalive on all connections to prevent intermediary timeouts
|
|
2576
|
+
server.on('connection', (socket) => {
|
|
2577
|
+
socket.setKeepAlive(true, 60000); // Send keepalive probe every 60s
|
|
2578
|
+
socket.setTimeout(0); // Disable socket inactivity timeout
|
|
2579
|
+
});
|
|
2544
2580
|
};
|
|
2545
2581
|
tryListen();
|
|
2546
2582
|
});
|
|
@@ -2569,6 +2605,45 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2569
2605
|
console.log(`ā” Photon Beam ready (${photonStatus}${mcpStatus})`);
|
|
2570
2606
|
// Notify connected clients that photon list is now available
|
|
2571
2607
|
broadcastPhotonChange();
|
|
2608
|
+
// Auto-start daemon and subscribe to state-changed events for stateful photons
|
|
2609
|
+
// Uses reconnect: true so subscriptions survive daemon restarts
|
|
2610
|
+
const statefulPhotons = photons.filter((p) => p.stateful && p.configured);
|
|
2611
|
+
if (statefulPhotons.length > 0) {
|
|
2612
|
+
try {
|
|
2613
|
+
await ensureDaemon();
|
|
2614
|
+
for (const photon of statefulPhotons) {
|
|
2615
|
+
const photonName = photon.name;
|
|
2616
|
+
const channel = `${photonName}:state-changed`;
|
|
2617
|
+
subscribeChannel(photonName, channel, (message) => {
|
|
2618
|
+
broadcastToBeam('photon/state-changed', {
|
|
2619
|
+
photon: photonName,
|
|
2620
|
+
method: message?.method,
|
|
2621
|
+
data: message?.data,
|
|
2622
|
+
});
|
|
2623
|
+
}, {
|
|
2624
|
+
reconnect: true,
|
|
2625
|
+
onReconnect: () => logger.info(`š” Reconnected ${channel} subscription`),
|
|
2626
|
+
onRefreshNeeded: () => {
|
|
2627
|
+
logger.info(`š” Refresh needed for ${channel} (events lost during daemon restart)`);
|
|
2628
|
+
broadcastToBeam('photon/state-changed', {
|
|
2629
|
+
photon: photonName,
|
|
2630
|
+
method: '_refresh',
|
|
2631
|
+
data: {},
|
|
2632
|
+
});
|
|
2633
|
+
},
|
|
2634
|
+
})
|
|
2635
|
+
.then(() => {
|
|
2636
|
+
logger.info(`š” Subscribed to ${channel} for cross-client sync`);
|
|
2637
|
+
})
|
|
2638
|
+
.catch((err) => {
|
|
2639
|
+
logger.warn(`Failed to subscribe to ${channel}: ${getErrorMessage(err)}`);
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
catch (err) {
|
|
2644
|
+
logger.warn(`Failed to start daemon for stateful photons: ${getErrorMessage(err)}`);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2572
2647
|
// Set up file watchers for symlinked and bundled photon assets (now that photons are loaded)
|
|
2573
2648
|
for (const photon of photons) {
|
|
2574
2649
|
if (!photon.path) {
|