@portel/photon 1.6.1 → 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.
Files changed (113) hide show
  1. package/README.md +111 -160
  2. package/dist/auto-ui/beam.d.ts.map +1 -1
  3. package/dist/auto-ui/beam.js +218 -106
  4. package/dist/auto-ui/beam.js.map +1 -1
  5. package/dist/auto-ui/design-system/tokens.d.ts +1 -1
  6. package/dist/auto-ui/design-system/tokens.d.ts.map +1 -1
  7. package/dist/auto-ui/design-system/tokens.js +2 -2
  8. package/dist/auto-ui/design-system/tokens.js.map +1 -1
  9. package/dist/auto-ui/frontend/index.html +1 -1
  10. package/dist/auto-ui/platform-compat.d.ts.map +1 -1
  11. package/dist/auto-ui/platform-compat.js +12 -2
  12. package/dist/auto-ui/platform-compat.js.map +1 -1
  13. package/dist/auto-ui/playground-html.js +5 -5
  14. package/dist/auto-ui/rendering/components.d.ts.map +1 -1
  15. package/dist/auto-ui/rendering/components.js +568 -0
  16. package/dist/auto-ui/rendering/components.js.map +1 -1
  17. package/dist/auto-ui/rendering/field-analyzer.d.ts +56 -0
  18. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -1
  19. package/dist/auto-ui/rendering/field-analyzer.js +177 -0
  20. package/dist/auto-ui/rendering/field-analyzer.js.map +1 -1
  21. package/dist/auto-ui/rendering/layout-selector.d.ts +14 -2
  22. package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -1
  23. package/dist/auto-ui/rendering/layout-selector.js +125 -1
  24. package/dist/auto-ui/rendering/layout-selector.js.map +1 -1
  25. package/dist/auto-ui/streamable-http-transport.d.ts +1 -1
  26. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  27. package/dist/auto-ui/streamable-http-transport.js +370 -26
  28. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  29. package/dist/auto-ui/types.d.ts +7 -1
  30. package/dist/auto-ui/types.d.ts.map +1 -1
  31. package/dist/auto-ui/types.js.map +1 -1
  32. package/dist/beam.bundle.js +21932 -3307
  33. package/dist/beam.bundle.js.map +4 -4
  34. package/dist/cli/commands/info.d.ts.map +1 -1
  35. package/dist/cli/commands/info.js +37 -0
  36. package/dist/cli/commands/info.js.map +1 -1
  37. package/dist/cli/commands/package.d.ts.map +1 -1
  38. package/dist/cli/commands/package.js +16 -0
  39. package/dist/cli/commands/package.js.map +1 -1
  40. package/dist/cli.d.ts.map +1 -1
  41. package/dist/cli.js +640 -17
  42. package/dist/cli.js.map +1 -1
  43. package/dist/context-store.d.ts +79 -0
  44. package/dist/context-store.d.ts.map +1 -0
  45. package/dist/context-store.js +210 -0
  46. package/dist/context-store.js.map +1 -0
  47. package/dist/daemon/client.d.ts +13 -4
  48. package/dist/daemon/client.d.ts.map +1 -1
  49. package/dist/daemon/client.js +138 -77
  50. package/dist/daemon/client.js.map +1 -1
  51. package/dist/daemon/manager.d.ts +0 -25
  52. package/dist/daemon/manager.d.ts.map +1 -1
  53. package/dist/daemon/manager.js +10 -38
  54. package/dist/daemon/manager.js.map +1 -1
  55. package/dist/daemon/protocol.d.ts +7 -2
  56. package/dist/daemon/protocol.d.ts.map +1 -1
  57. package/dist/daemon/protocol.js.map +1 -1
  58. package/dist/daemon/server.js +317 -83
  59. package/dist/daemon/server.js.map +1 -1
  60. package/dist/daemon/session-manager.d.ts +24 -4
  61. package/dist/daemon/session-manager.d.ts.map +1 -1
  62. package/dist/daemon/session-manager.js +62 -12
  63. package/dist/daemon/session-manager.js.map +1 -1
  64. package/dist/index.d.ts +0 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +0 -3
  67. package/dist/index.js.map +1 -1
  68. package/dist/loader.d.ts +3 -20
  69. package/dist/loader.d.ts.map +1 -1
  70. package/dist/loader.js +87 -77
  71. package/dist/loader.js.map +1 -1
  72. package/dist/markdown-utils.d.ts.map +1 -1
  73. package/dist/markdown-utils.js +2 -1
  74. package/dist/markdown-utils.js.map +1 -1
  75. package/dist/marketplace-manager.d.ts.map +1 -1
  76. package/dist/marketplace-manager.js +20 -3
  77. package/dist/marketplace-manager.js.map +1 -1
  78. package/dist/photon-cli-runner.d.ts.map +1 -1
  79. package/dist/photon-cli-runner.js +258 -218
  80. package/dist/photon-cli-runner.js.map +1 -1
  81. package/dist/photon-doc-extractor.d.ts +2 -0
  82. package/dist/photon-doc-extractor.d.ts.map +1 -1
  83. package/dist/photon-doc-extractor.js +45 -7
  84. package/dist/photon-doc-extractor.js.map +1 -1
  85. package/dist/photons/maker.photon.d.ts.map +1 -1
  86. package/dist/photons/maker.photon.js +22 -4
  87. package/dist/photons/maker.photon.js.map +1 -1
  88. package/dist/photons/maker.photon.ts +47 -11
  89. package/dist/security-scanner.d.ts.map +1 -1
  90. package/dist/security-scanner.js +8 -2
  91. package/dist/security-scanner.js.map +1 -1
  92. package/dist/serv/index.d.ts +1 -1
  93. package/dist/serv/index.d.ts.map +1 -1
  94. package/dist/serv/index.js +6 -4
  95. package/dist/serv/index.js.map +1 -1
  96. package/dist/server.d.ts +32 -15
  97. package/dist/server.d.ts.map +1 -1
  98. package/dist/server.js +525 -483
  99. package/dist/server.js.map +1 -1
  100. package/dist/shared/security.d.ts +79 -0
  101. package/dist/shared/security.d.ts.map +1 -0
  102. package/dist/shared/security.js +251 -0
  103. package/dist/shared/security.js.map +1 -0
  104. package/dist/shell-completions.d.ts +21 -0
  105. package/dist/shell-completions.d.ts.map +1 -0
  106. package/dist/shell-completions.js +102 -0
  107. package/dist/shell-completions.js.map +1 -0
  108. package/dist/template-manager.d.ts.map +1 -1
  109. package/dist/template-manager.js +10 -3
  110. package/dist/template-manager.js.map +1 -1
  111. package/dist/version.d.ts.map +1 -1
  112. package/dist/version.js.map +1 -1
  113. package/package.json +12 -7
@@ -16,6 +16,8 @@ export interface DaemonRequest {
16
16
  photonPath?: string;
17
17
  sessionId?: string;
18
18
  clientType?: 'cli' | 'mcp' | 'code-mode' | 'beam';
19
+ /** Instance name hint for auto-recovery from session drift */
20
+ instanceName?: string;
19
21
  method?: string;
20
22
  args?: Record<string, unknown>;
21
23
  /** Response to a prompt request */
@@ -32,7 +34,7 @@ export interface DaemonRequest {
32
34
  jobId?: string;
33
35
  /** Cron expression for scheduled jobs */
34
36
  cron?: string;
35
- /** Last event ID received by client (for replay on reconnect) */
37
+ /** Last event timestamp received by client (for delta sync on reconnect) */
36
38
  lastEventId?: string;
37
39
  }
38
40
  /**
@@ -44,6 +46,8 @@ export interface DaemonResponse {
44
46
  success?: boolean;
45
47
  data?: unknown;
46
48
  error?: string;
49
+ /** Actionable hint for the caller when type === 'error' */
50
+ suggestion?: string;
47
51
  /** Prompt request details (when type === 'prompt') */
48
52
  prompt?: {
49
53
  type: 'text' | 'password' | 'confirm' | 'select';
@@ -58,7 +62,7 @@ export interface DaemonResponse {
58
62
  channel?: string;
59
63
  /** Message payload for channel_message type */
60
64
  message?: unknown;
61
- /** Event ID for tracking (for replay support) */
65
+ /** Event timestamp for tracking (for delta sync support) */
62
66
  eventId?: string;
63
67
  }
64
68
  /**
@@ -78,6 +82,7 @@ export interface DaemonStatus {
78
82
  export interface PhotonSession {
79
83
  id: string;
80
84
  instance: PhotonMCPClass;
85
+ instanceName: string;
81
86
  createdAt: number;
82
87
  lastActivity: number;
83
88
  clientType?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/daemon/protocol.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/daemon/protocol.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/daemon/protocol.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2HH;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAY;IAC/C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,GAAG,GAAG,GAA6B,CAAC;IAE1C,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE7C,MAAM,UAAU,GAAG;QACjB,SAAS;QACT,MAAM;QACN,UAAU;QACV,QAAQ;QACR,iBAAiB;QACjB,WAAW;QACX,aAAa;QACb,SAAS;QACT,MAAM;QACN,QAAQ;QACR,UAAU;QACV,YAAY;QACZ,WAAW;QACX,YAAY;QACZ,kBAAkB;KACnB,CAAC;IACF,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAc,CAAC;QAAE,OAAO,KAAK,CAAC;IAE3D,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC3B,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACnD,CAAC;IAED,4CAA4C;IAC5C,IAAI,CAAC,WAAW,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAc,CAAC,EAAE,CAAC;QACzE,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACpD,CAAC;IAED,sCAAsC;IACtC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAc,CAAC,EAAE,CAAC;QACpD,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACrD,CAAC;IAED,sDAAsD;IACtD,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC5B,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAChD,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QACjD,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACjD,CAAC;IAED,4BAA4B;IAC5B,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAC9B,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IAClD,CAAC;IAED,6BAA6B;IAC7B,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC1B,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACvD,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAY;IAChD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,GAAG,GAAG,GAA8B,CAAC;IAE3C,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC7C,IACE,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,EAAE,gBAAgB,CAAC,CAAC,QAAQ,CAClF,GAAG,CAAC,IAAc,CACnB;QAED,OAAO,KAAK,CAAC;IAEf,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/daemon/protocol.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgIH;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAY;IAC/C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,GAAG,GAAG,GAA6B,CAAC;IAE1C,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE7C,MAAM,UAAU,GAAG;QACjB,SAAS;QACT,MAAM;QACN,UAAU;QACV,QAAQ;QACR,iBAAiB;QACjB,WAAW;QACX,aAAa;QACb,SAAS;QACT,MAAM;QACN,QAAQ;QACR,UAAU;QACV,YAAY;QACZ,WAAW;QACX,YAAY;QACZ,kBAAkB;KACnB,CAAC;IACF,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAc,CAAC;QAAE,OAAO,KAAK,CAAC;IAE3D,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC3B,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACnD,CAAC;IAED,4CAA4C;IAC5C,IAAI,CAAC,WAAW,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAc,CAAC,EAAE,CAAC;QACzE,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACpD,CAAC;IAED,sCAAsC;IACtC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAc,CAAC,EAAE,CAAC;QACpD,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACrD,CAAC;IAED,sDAAsD;IACtD,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC5B,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAChD,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QACjD,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACjD,CAAC;IAED,4BAA4B;IAC5B,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAC9B,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IAClD,CAAC;IAED,6BAA6B;IAC7B,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC1B,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACvD,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAY;IAChD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,GAAG,GAAG,GAA8B,CAAC;IAE3C,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC7C,IACE,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,EAAE,gBAAgB,CAAC,CAAC,QAAQ,CAClF,GAAG,CAAC,IAAc,CACnB;QAED,OAAO,KAAK,CAAC;IAEf,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -16,6 +16,7 @@ import { isValidDaemonRequest, } from './protocol.js';
16
16
  import { setPromptHandler } from '@portel/photon-core';
17
17
  import { createLogger } from '../shared/logger.js';
18
18
  import { getErrorMessage } from '../shared/error-handler.js';
19
+ import { timingSafeEqual, readBody, SimpleRateLimiter } from '../shared/security.js';
19
20
  // Command line args: socketPath (global daemon only needs socket path)
20
21
  const socketPath = process.argv[2];
21
22
  const logger = createLogger({
@@ -30,46 +31,52 @@ if (!socketPath) {
30
31
  // Map of photonName -> SessionManager (lazy initialized)
31
32
  const sessionManagers = new Map();
32
33
  const photonPaths = new Map(); // photonName -> photonPath
33
- let idleTimeout = 600000; // 10 minutes default
34
+ const fileWatchers = new Map();
35
+ const watchDebounce = new Map();
36
+ let idleTimeout = 0; // Daemon stays alive — it manages persistent stateful data
34
37
  let idleTimer = null;
35
38
  // Track pending prompts waiting for user input
36
39
  const pendingPrompts = new Map();
37
40
  // Channel subscriptions for pub/sub
38
41
  const channelSubscriptions = new Map();
39
- const EVENT_BUFFER_SIZE = 30;
42
+ /** Buffer retention window — events older than this are purged */
43
+ const EVENT_BUFFER_DURATION_MS = 5 * 60 * 1000; // 5 minutes
40
44
  const channelEventBuffers = new Map();
41
45
  function bufferEvent(channel, message) {
42
46
  let buffer = channelEventBuffers.get(channel);
43
47
  if (!buffer) {
44
- buffer = { events: [], nextId: 1 };
48
+ buffer = { events: [] };
45
49
  channelEventBuffers.set(channel, buffer);
46
50
  }
47
- const eventId = buffer.nextId++;
51
+ const now = Date.now();
48
52
  const event = {
49
- id: eventId,
53
+ id: now,
50
54
  channel,
51
55
  message,
52
- timestamp: Date.now(),
56
+ timestamp: now,
53
57
  };
54
58
  buffer.events.push(event);
55
- // Keep only last N events (circular buffer)
56
- if (buffer.events.length > EVENT_BUFFER_SIZE) {
59
+ // Purge events older than retention window
60
+ const cutoff = now - EVENT_BUFFER_DURATION_MS;
61
+ while (buffer.events.length > 0 && buffer.events[0].timestamp < cutoff) {
57
62
  buffer.events.shift();
58
63
  }
59
- return eventId;
64
+ return now;
60
65
  }
61
- function getEventsSince(channel, lastEventId) {
66
+ function getEventsSince(channel, lastTimestamp) {
62
67
  const buffer = channelEventBuffers.get(channel);
63
68
  if (!buffer || buffer.events.length === 0) {
64
- return { events: [], refreshNeeded: false };
69
+ // If client has a lastEventId but buffer is empty (e.g. daemon restarted),
70
+ // signal that a full refresh is needed — events were lost.
71
+ return { events: [], refreshNeeded: lastTimestamp > 0 };
65
72
  }
66
73
  const oldestEvent = buffer.events[0];
67
- // If lastEventId is older than our oldest buffered event, refresh needed
68
- if (lastEventId < oldestEvent.id) {
74
+ // Client's last timestamp is older than our oldest buffered event → stale, full sync needed
75
+ if (lastTimestamp < oldestEvent.timestamp) {
69
76
  return { events: [], refreshNeeded: true };
70
77
  }
71
- // Find events to replay
72
- const events = buffer.events.filter((e) => e.id > lastEventId);
78
+ // Delta sync: return events after the client's last timestamp
79
+ const events = buffer.events.filter((e) => e.timestamp > lastTimestamp);
73
80
  return { events, refreshNeeded: false };
74
81
  }
75
82
  // ════════════════════════════════════════════════════════════════════════════════
@@ -233,6 +240,8 @@ function unscheduleJob(jobId) {
233
240
  // ════════════════════════════════════════════════════════════════════════════════
234
241
  let webhookServer = null;
235
242
  const WEBHOOK_PORT = parseInt(process.env.PHOTON_WEBHOOK_PORT || '0');
243
+ // Security: rate limiter for webhook endpoint
244
+ const webhookRateLimiter = new SimpleRateLimiter(30, 60_000);
236
245
  function startWebhookServer(port) {
237
246
  if (port <= 0)
238
247
  return;
@@ -245,6 +254,13 @@ function startWebhookServer(port) {
245
254
  res.end();
246
255
  return;
247
256
  }
257
+ // Security: rate limiting
258
+ const clientKey = req.socket?.remoteAddress || 'unknown';
259
+ if (!webhookRateLimiter.isAllowed(clientKey)) {
260
+ res.writeHead(429, { 'Content-Type': 'application/json' });
261
+ res.end(JSON.stringify({ error: 'Too many requests' }));
262
+ return;
263
+ }
248
264
  // Parse URL: /webhook/{photonName}/{method}
249
265
  const url = new URL(req.url || '/', `http://localhost:${port}`);
250
266
  const pathParts = url.pathname.split('/').filter(Boolean);
@@ -258,62 +274,68 @@ function startWebhookServer(port) {
258
274
  const expectedSecret = process.env.PHOTON_WEBHOOK_SECRET;
259
275
  if (expectedSecret) {
260
276
  const providedSecret = req.headers['x-webhook-secret'];
261
- if (providedSecret !== expectedSecret) {
277
+ if (!providedSecret ||
278
+ typeof providedSecret !== 'string' ||
279
+ !timingSafeEqual(providedSecret, expectedSecret)) {
262
280
  res.writeHead(401, { 'Content-Type': 'application/json' });
263
281
  res.end(JSON.stringify({ error: 'Invalid webhook secret' }));
264
282
  return;
265
283
  }
266
284
  }
267
- let body = '';
268
- req.on('data', (chunk) => {
269
- body += chunk;
270
- });
271
- req.on('end', async () => {
272
- let args = {};
273
- try {
274
- if (body) {
275
- args = JSON.parse(body);
276
- }
277
- args._webhook = {
278
- method: req.method,
279
- headers: req.headers,
280
- query: Object.fromEntries(url.searchParams),
281
- timestamp: Date.now(),
282
- };
283
- }
284
- catch {
285
- res.writeHead(400, { 'Content-Type': 'application/json' });
286
- res.end(JSON.stringify({ error: 'Invalid JSON body' }));
287
- return;
288
- }
289
- const sessionManager = sessionManagers.get(photonName);
290
- if (!sessionManager) {
291
- res.writeHead(503, { 'Content-Type': 'application/json' });
292
- res.end(JSON.stringify({ error: `Photon '${photonName}' not initialized` }));
293
- return;
294
- }
295
- try {
296
- const session = await sessionManager.getOrCreateSession('webhook', 'webhook');
297
- const result = await sessionManager.loader.executeTool(session.instance, method, args);
298
- logger.info('Webhook executed', { photon: photonName, method });
299
- publishToChannel(`webhooks:${photonName}`, {
300
- event: 'webhook-received',
301
- method,
302
- timestamp: Date.now(),
303
- });
304
- res.writeHead(200, { 'Content-Type': 'application/json' });
305
- res.end(JSON.stringify({ success: true, data: result }));
306
- }
307
- catch (error) {
308
- logger.error('Webhook execution failed', {
309
- photon: photonName,
310
- method,
311
- error: getErrorMessage(error),
312
- });
313
- res.writeHead(500, { 'Content-Type': 'application/json' });
314
- res.end(JSON.stringify({ error: getErrorMessage(error) }));
285
+ else if (!process.env.PHOTON_WEBHOOK_ALLOW_UNAUTHENTICATED) {
286
+ // Security: require explicit opt-in for unauthenticated webhooks
287
+ res.writeHead(403, { 'Content-Type': 'application/json' });
288
+ res.end(JSON.stringify({
289
+ error: 'Webhook secret not configured. Set PHOTON_WEBHOOK_SECRET or PHOTON_WEBHOOK_ALLOW_UNAUTHENTICATED=true',
290
+ }));
291
+ return;
292
+ }
293
+ let args = {};
294
+ try {
295
+ const body = await readBody(req);
296
+ if (body) {
297
+ args = JSON.parse(body);
315
298
  }
316
- });
299
+ args._webhook = {
300
+ method: req.method,
301
+ headers: req.headers,
302
+ query: Object.fromEntries(url.searchParams),
303
+ timestamp: Date.now(),
304
+ };
305
+ }
306
+ catch (err) {
307
+ const status = err.message?.includes('too large') ? 413 : 400;
308
+ res.writeHead(status, { 'Content-Type': 'application/json' });
309
+ res.end(JSON.stringify({ error: status === 413 ? 'Request body too large' : 'Invalid JSON body' }));
310
+ return;
311
+ }
312
+ const sessionManager = sessionManagers.get(photonName);
313
+ if (!sessionManager) {
314
+ res.writeHead(503, { 'Content-Type': 'application/json' });
315
+ res.end(JSON.stringify({ error: `Photon '${photonName}' not initialized` }));
316
+ return;
317
+ }
318
+ try {
319
+ const session = await sessionManager.getOrCreateSession('webhook', 'webhook');
320
+ const result = await sessionManager.loader.executeTool(session.instance, method, args);
321
+ logger.info('Webhook executed', { photon: photonName, method });
322
+ publishToChannel(`webhooks:${photonName}`, {
323
+ event: 'webhook-received',
324
+ method,
325
+ timestamp: Date.now(),
326
+ });
327
+ res.writeHead(200, { 'Content-Type': 'application/json' });
328
+ res.end(JSON.stringify({ success: true, data: result }));
329
+ }
330
+ catch (error) {
331
+ logger.error('Webhook execution failed', {
332
+ photon: photonName,
333
+ method,
334
+ error: getErrorMessage(error),
335
+ });
336
+ res.writeHead(500, { 'Content-Type': 'application/json' });
337
+ res.end(JSON.stringify({ error: getErrorMessage(error) }));
338
+ }
317
339
  });
318
340
  webhookServer.listen(port, () => {
319
341
  logger.info('Webhook server started', { port });
@@ -354,7 +376,8 @@ function publishToChannel(channel, message, excludeSocket) {
354
376
  sentSockets.add(socket);
355
377
  }
356
378
  catch {
357
- // Socket write failed
379
+ // Dead socket — remove from subscribers
380
+ exactSubscribers.delete(socket);
358
381
  }
359
382
  }
360
383
  }
@@ -372,7 +395,8 @@ function publishToChannel(channel, message, excludeSocket) {
372
395
  sentSockets.add(socket);
373
396
  }
374
397
  catch {
375
- // Socket write failed
398
+ // Dead socket — remove from subscribers
399
+ wildcardSubscribers.delete(socket);
376
400
  }
377
401
  }
378
402
  }
@@ -406,6 +430,7 @@ async function getOrCreateSessionManager(photonName, photonPath) {
406
430
  manager = new SessionManager(pathToUse, photonName, idleTimeout, logger.child({ scope: photonName }));
407
431
  sessionManagers.set(photonName, manager);
408
432
  photonPaths.set(photonName, pathToUse);
433
+ watchPhotonFile(photonName, pathToUse);
409
434
  logger.info('Session manager initialized', { photonName });
410
435
  return manager;
411
436
  }
@@ -493,32 +518,32 @@ async function handleRequest(request, socket) {
493
518
  }
494
519
  subs.add(socket);
495
520
  logger.info('Client subscribed to channel', { channel, subscribers: subs.size });
496
- // Replay missed events if lastEventId provided
521
+ // Replay missed events if lastEventId (timestamp) provided
497
522
  if (lastEventId !== undefined) {
498
- const parsedLastEventId = parseInt(String(lastEventId), 10) || 0;
499
- const { events, refreshNeeded } = getEventsSince(channel, parsedLastEventId);
523
+ const lastTimestamp = parseInt(String(lastEventId), 10) || 0;
524
+ const { events, refreshNeeded } = getEventsSince(channel, lastTimestamp);
500
525
  if (refreshNeeded) {
501
- // Send refresh-needed signal
526
+ // Stale: client's timestamp is older than buffer window → full sync needed
502
527
  socket.write(JSON.stringify({
503
528
  type: 'refresh_needed',
504
529
  id: request.id,
505
530
  channel,
506
531
  }) + '\n');
507
- logger.info('Replay: refresh needed', { channel, lastEventId });
532
+ logger.info('Stale client, full sync needed', { channel, lastTimestamp });
508
533
  }
509
534
  else if (events.length > 0) {
510
- // Replay events
535
+ // Delta sync: replay missed events
511
536
  for (const event of events) {
512
537
  socket.write(JSON.stringify({
513
538
  type: 'channel_message',
514
- id: `replay_${event.id}`,
515
- eventId: event.id,
539
+ id: `replay_${event.timestamp}`,
540
+ eventId: event.timestamp,
516
541
  channel: event.channel,
517
542
  message: event.message,
518
543
  replay: true,
519
544
  }) + '\n');
520
545
  }
521
- logger.info('Replayed events', { channel, count: events.length });
546
+ logger.info('Delta sync: replayed events', { channel, count: events.length });
522
547
  }
523
548
  }
524
549
  return {
@@ -553,11 +578,11 @@ async function handleRequest(request, socket) {
553
578
  data: { published: true, channel, eventId },
554
579
  };
555
580
  }
556
- // Handle get_events_since (for event replay)
581
+ // Handle get_events_since (for delta sync / full sync detection)
557
582
  if (request.type === 'get_events_since') {
558
583
  const channel = request.channel;
559
- const parsedLastEventId = parseInt(String(request.lastEventId || '0'), 10) || 0;
560
- const { events, refreshNeeded } = getEventsSince(channel, parsedLastEventId);
584
+ const lastTimestamp = parseInt(String(request.lastEventId || '0'), 10) || 0;
585
+ const { events, refreshNeeded } = getEventsSince(channel, lastTimestamp);
561
586
  return {
562
587
  type: 'result',
563
588
  id: request.id,
@@ -608,7 +633,12 @@ async function handleRequest(request, socket) {
608
633
  if (request.type === 'schedule') {
609
634
  const photonName = request.photonName;
610
635
  if (!photonName) {
611
- return { type: 'error', id: request.id, error: 'photonName required for scheduling' };
636
+ return {
637
+ type: 'error',
638
+ id: request.id,
639
+ error: 'photonName required for scheduling',
640
+ suggestion: 'Include photonName in the request payload',
641
+ };
612
642
  }
613
643
  const job = {
614
644
  id: request.jobId,
@@ -644,11 +674,21 @@ async function handleRequest(request, socket) {
644
674
  // Handle command execution
645
675
  if (request.type === 'command') {
646
676
  if (!request.method) {
647
- return { type: 'error', id: request.id, error: 'Method name required' };
677
+ return {
678
+ type: 'error',
679
+ id: request.id,
680
+ error: 'Method name required',
681
+ suggestion: 'Specify the method to call: { method: "methodName" }',
682
+ };
648
683
  }
649
684
  const photonName = request.photonName;
650
685
  if (!photonName) {
651
- return { type: 'error', id: request.id, error: 'photonName required for commands' };
686
+ return {
687
+ type: 'error',
688
+ id: request.id,
689
+ error: 'photonName required for commands',
690
+ suggestion: 'Include photonName in the request payload',
691
+ };
652
692
  }
653
693
  const sessionManager = await getOrCreateSessionManager(photonName, request.photonPath);
654
694
  if (!sessionManager) {
@@ -660,10 +700,53 @@ async function handleRequest(request, socket) {
660
700
  }
661
701
  try {
662
702
  const session = await sessionManager.getOrCreateSession(request.sessionId, request.clientType);
703
+ // ── Auto-recover from instance drift ─────────────────────────
704
+ // If the client tells us which instance it expects but the daemon
705
+ // session has drifted (e.g. session expired and was recreated as
706
+ // "default"), silently switch back to the correct instance.
707
+ if (request.instanceName && request.instanceName !== session.instanceName) {
708
+ logger.info('Instance drift detected, auto-switching', {
709
+ from: session.instanceName || 'default',
710
+ to: request.instanceName,
711
+ sessionId: session.id,
712
+ });
713
+ await sessionManager.switchInstance(session.id, request.instanceName);
714
+ }
715
+ // ── Runtime-injected instance tools ──────────────────────────
716
+ if (request.method === '_use') {
717
+ const instanceName = String(request.args?.name ?? '');
718
+ await sessionManager.switchInstance(session.id, instanceName);
719
+ const label = instanceName || 'default';
720
+ return {
721
+ type: 'result',
722
+ id: request.id,
723
+ success: true,
724
+ data: { instance: label, message: `Switched to instance: ${label}` },
725
+ };
726
+ }
727
+ if (request.method === '_instances') {
728
+ const { InstanceStore } = await import('../context-store.js');
729
+ const store = new InstanceStore();
730
+ const instances = store.listInstances(photonName);
731
+ const current = session.instanceName || 'default';
732
+ // Ensure current instance is always in the list (may not have a state file yet)
733
+ if (!instances.includes(current)) {
734
+ instances.push(current);
735
+ instances.sort();
736
+ }
737
+ return {
738
+ type: 'result',
739
+ id: request.id,
740
+ success: true,
741
+ data: { instances, current },
742
+ };
743
+ }
744
+ // ─────────────────────────────────────────────────────────────
663
745
  logger.info('Executing request', {
664
746
  method: request.method,
665
747
  photon: photonName,
666
748
  sessionId: session.id,
749
+ instance: session.instanceName || 'default',
667
750
  });
668
751
  setPromptHandler(createSocketPromptHandler(socket, request.id));
669
752
  const outputHandler = (emit) => {
@@ -674,6 +757,14 @@ async function handleRequest(request, socket) {
674
757
  };
675
758
  const result = await sessionManager.loader.executeTool(session.instance, request.method, request.args || {}, { outputHandler });
676
759
  setPromptHandler(null);
760
+ // Persist reactive state after each tool call
761
+ await persistInstanceState(session.instance, photonName, session.instanceName);
762
+ // Notify subscribers that state may have changed
763
+ publishToChannel(`${photonName}:state-changed`, {
764
+ event: 'state-changed',
765
+ method: request.method,
766
+ data: result,
767
+ }, socket);
677
768
  return { type: 'result', id: request.id, success: true, data: result };
678
769
  }
679
770
  catch (error) {
@@ -688,6 +779,144 @@ async function handleRequest(request, socket) {
688
779
  return { type: 'error', id: request.id, error: `Unknown request type: ${request.type}` };
689
780
  }
690
781
  // ════════════════════════════════════════════════════════════════════════════════
782
+ // STATE PERSISTENCE
783
+ // ════════════════════════════════════════════════════════════════════════════════
784
+ /**
785
+ * Persist reactive collection state to disk after each tool call.
786
+ * Scans the instance for properties with _propertyName (ReactiveArray/Map/Set markers)
787
+ * and serializes them to the instance-specific state file.
788
+ */
789
+ /** Cache of state keys per photon (extracted once from source) */
790
+ const stateKeysCache = new Map();
791
+ /**
792
+ * Get the state property keys for a photon by extracting constructor params.
793
+ * State params: non-primitive with default on @stateful photon.
794
+ */
795
+ async function getStateKeys(photonName, photonPath) {
796
+ if (stateKeysCache.has(photonName)) {
797
+ return stateKeysCache.get(photonName);
798
+ }
799
+ try {
800
+ const { SchemaExtractor } = await import('@portel/photon-core');
801
+ const fsPromises = await import('fs/promises');
802
+ const source = await fsPromises.readFile(photonPath, 'utf-8');
803
+ const extractor = new SchemaExtractor();
804
+ const injections = extractor.resolveInjections(source, photonName);
805
+ const keys = injections
806
+ .filter((inj) => inj.injectionType === 'state')
807
+ .map((inj) => inj.stateKey);
808
+ stateKeysCache.set(photonName, keys);
809
+ logger.debug('State keys extracted', { photon: photonName, keys });
810
+ return keys;
811
+ }
812
+ catch (error) {
813
+ logger.error('Failed to extract state keys', {
814
+ photon: photonName,
815
+ error: getErrorMessage(error),
816
+ });
817
+ return [];
818
+ }
819
+ }
820
+ /**
821
+ * Persist reactive collection state to disk after each tool call.
822
+ * Only persists properties identified as 'state' injection type
823
+ * (non-primitive constructor params with defaults on @stateful photons).
824
+ */
825
+ async function persistInstanceState(instance, photonName, instanceName) {
826
+ try {
827
+ const photonPath = photonPaths.get(photonName);
828
+ if (!photonPath)
829
+ return;
830
+ const keys = await getStateKeys(photonName, photonPath);
831
+ if (keys.length === 0)
832
+ return;
833
+ // instance is PhotonMCPClass wrapper — actual user class is instance.instance
834
+ const target = instance?.instance ?? instance;
835
+ const snapshot = {};
836
+ for (const key of keys) {
837
+ const value = target[key];
838
+ if (value === undefined)
839
+ continue;
840
+ if (value && typeof value === 'object' && value._propertyName) {
841
+ // ReactiveArray/Map/Set — serialize underlying data
842
+ snapshot[key] = value.toJSON ? value.toJSON() : globalThis.Array.from(value);
843
+ }
844
+ else {
845
+ // Plain value (array, object, etc.)
846
+ snapshot[key] = value;
847
+ }
848
+ }
849
+ if (Object.keys(snapshot).length > 0) {
850
+ const { getInstanceStatePath } = await import('../context-store.js');
851
+ const statePath = getInstanceStatePath(photonName, instanceName);
852
+ const fsPromises = await import('fs/promises');
853
+ const path = await import('path');
854
+ await fsPromises.mkdir(path.dirname(statePath), { recursive: true });
855
+ await fsPromises.writeFile(statePath, JSON.stringify(snapshot, null, 2));
856
+ logger.debug('Persisted state', { photon: photonName, instance: instanceName || 'default' });
857
+ }
858
+ }
859
+ catch (error) {
860
+ logger.error('Failed to persist state', {
861
+ photon: photonName,
862
+ error: getErrorMessage(error),
863
+ });
864
+ }
865
+ }
866
+ // ════════════════════════════════════════════════════════════════════════════════
867
+ // FILE WATCHING (Auto Hot-Reload)
868
+ // ════════════════════════════════════════════════════════════════════════════════
869
+ function watchPhotonFile(photonName, photonPath) {
870
+ if (fileWatchers.has(photonPath))
871
+ return;
872
+ try {
873
+ const watcher = fs.watch(photonPath, (eventType) => {
874
+ // Debounce: 100ms (same as Beam)
875
+ const existing = watchDebounce.get(photonPath);
876
+ if (existing)
877
+ clearTimeout(existing);
878
+ watchDebounce.set(photonPath, setTimeout(async () => {
879
+ watchDebounce.delete(photonPath);
880
+ // On macOS, editors like sed -i and some IDEs replace the file (new inode),
881
+ // which kills the watcher. Re-watch if file still exists.
882
+ if (eventType === 'rename') {
883
+ unwatchPhotonFile(photonPath);
884
+ if (fs.existsSync(photonPath)) {
885
+ watchPhotonFile(photonName, photonPath);
886
+ }
887
+ }
888
+ if (!fs.existsSync(photonPath))
889
+ return;
890
+ logger.info('File changed, auto-reloading', { photonName, path: photonPath });
891
+ // Invalidate cached state keys so they're re-extracted from fresh source
892
+ stateKeysCache.delete(photonName);
893
+ await reloadPhoton(photonName, photonPath);
894
+ }, 100));
895
+ });
896
+ watcher.on('error', (err) => {
897
+ logger.warn('File watcher error', { photonName, error: getErrorMessage(err) });
898
+ unwatchPhotonFile(photonPath);
899
+ });
900
+ fileWatchers.set(photonPath, watcher);
901
+ logger.info('Watching photon file', { photonName, path: photonPath });
902
+ }
903
+ catch (err) {
904
+ logger.warn('Failed to watch photon file', { photonName, error: getErrorMessage(err) });
905
+ }
906
+ }
907
+ function unwatchPhotonFile(photonPath) {
908
+ const watcher = fileWatchers.get(photonPath);
909
+ if (watcher) {
910
+ watcher.close();
911
+ fileWatchers.delete(photonPath);
912
+ }
913
+ const timer = watchDebounce.get(photonPath);
914
+ if (timer) {
915
+ clearTimeout(timer);
916
+ watchDebounce.delete(photonPath);
917
+ }
918
+ }
919
+ // ════════════════════════════════════════════════════════════════════════════════
691
920
  // HOT RELOAD
692
921
  // ════════════════════════════════════════════════════════════════════════════════
693
922
  async function reloadPhoton(photonName, newPhotonPath) {
@@ -695,8 +924,9 @@ async function reloadPhoton(photonName, newPhotonPath) {
695
924
  logger.info('Hot-reloading photon', { photonName, path: newPhotonPath });
696
925
  const sessionManager = sessionManagers.get(photonName);
697
926
  if (!sessionManager) {
698
- // First time - just register the path
927
+ // First time - just register the path and start watching
699
928
  photonPaths.set(photonName, newPhotonPath);
929
+ watchPhotonFile(photonName, newPhotonPath);
700
930
  return { success: true, sessionsUpdated: 0 };
701
931
  }
702
932
  await sessionManager.loader.reloadFile(newPhotonPath);
@@ -850,6 +1080,10 @@ function shutdown() {
850
1080
  jobTimers.clear();
851
1081
  scheduledJobs.clear();
852
1082
  activeLocks.clear();
1083
+ // Close file watchers and debounce timers
1084
+ for (const photonPath of fileWatchers.keys()) {
1085
+ unwatchPhotonFile(photonPath);
1086
+ }
853
1087
  for (const manager of sessionManagers.values()) {
854
1088
  manager.destroy();
855
1089
  }