@portel/photon 1.19.0 → 1.20.1

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 (146) hide show
  1. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  2. package/dist/auto-ui/beam/routes/api-browse.js +16 -4
  3. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  4. package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
  5. package/dist/auto-ui/beam/routes/api-config.js +165 -24
  6. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  7. package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
  8. package/dist/auto-ui/beam/routes/api-marketplace.js +14 -1
  9. package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
  10. package/dist/auto-ui/beam.d.ts.map +1 -1
  11. package/dist/auto-ui/beam.js +187 -77
  12. package/dist/auto-ui/beam.js.map +1 -1
  13. package/dist/auto-ui/bridge/index.d.ts.map +1 -1
  14. package/dist/auto-ui/bridge/index.js +17 -0
  15. package/dist/auto-ui/bridge/index.js.map +1 -1
  16. package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
  17. package/dist/auto-ui/bridge/renderers.js +12 -4
  18. package/dist/auto-ui/bridge/renderers.js.map +1 -1
  19. package/dist/auto-ui/streamable-http-transport.d.ts +1 -0
  20. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  21. package/dist/auto-ui/streamable-http-transport.js +179 -44
  22. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  23. package/dist/auto-ui/types.d.ts +12 -0
  24. package/dist/auto-ui/types.d.ts.map +1 -1
  25. package/dist/auto-ui/types.js.map +1 -1
  26. package/dist/beam-form.bundle.js +63 -185
  27. package/dist/beam-form.bundle.js.map +4 -4
  28. package/dist/beam.bundle.js +2115 -761
  29. package/dist/beam.bundle.js.map +4 -4
  30. package/dist/capability-negotiator.d.ts +67 -0
  31. package/dist/capability-negotiator.d.ts.map +1 -0
  32. package/dist/capability-negotiator.js +104 -0
  33. package/dist/capability-negotiator.js.map +1 -0
  34. package/dist/channel-manager.d.ts +122 -0
  35. package/dist/channel-manager.d.ts.map +1 -0
  36. package/dist/channel-manager.js +266 -0
  37. package/dist/channel-manager.js.map +1 -0
  38. package/dist/cli/commands/beam.d.ts.map +1 -1
  39. package/dist/cli/commands/beam.js +47 -30
  40. package/dist/cli/commands/beam.js.map +1 -1
  41. package/dist/cli/commands/build.d.ts.map +1 -1
  42. package/dist/cli/commands/build.js +27 -2
  43. package/dist/cli/commands/build.js.map +1 -1
  44. package/dist/cli/commands/daemon.d.ts.map +1 -1
  45. package/dist/cli/commands/daemon.js +12 -6
  46. package/dist/cli/commands/daemon.js.map +1 -1
  47. package/dist/cli/commands/mcp.d.ts.map +1 -1
  48. package/dist/cli/commands/mcp.js +18 -6
  49. package/dist/cli/commands/mcp.js.map +1 -1
  50. package/dist/cli/commands/package.d.ts.map +1 -1
  51. package/dist/cli/commands/package.js +25 -7
  52. package/dist/cli/commands/package.js.map +1 -1
  53. package/dist/cli/commands/serve.d.ts.map +1 -1
  54. package/dist/cli/commands/serve.js +14 -2
  55. package/dist/cli/commands/serve.js.map +1 -1
  56. package/dist/cli-alias.d.ts.map +1 -1
  57. package/dist/cli-alias.js +2 -3
  58. package/dist/cli-alias.js.map +1 -1
  59. package/dist/context-store.d.ts +4 -4
  60. package/dist/context-store.d.ts.map +1 -1
  61. package/dist/context-store.js +18 -15
  62. package/dist/context-store.js.map +1 -1
  63. package/dist/context.d.ts +25 -2
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +69 -4
  66. package/dist/context.js.map +1 -1
  67. package/dist/daemon/client.d.ts.map +1 -1
  68. package/dist/daemon/client.js +16 -1
  69. package/dist/daemon/client.js.map +1 -1
  70. package/dist/daemon/manager.d.ts +2 -0
  71. package/dist/daemon/manager.d.ts.map +1 -1
  72. package/dist/daemon/manager.js +40 -8
  73. package/dist/daemon/manager.js.map +1 -1
  74. package/dist/daemon/server.js +89 -64
  75. package/dist/daemon/server.js.map +1 -1
  76. package/dist/daemon/worker-host.js +7 -0
  77. package/dist/daemon/worker-host.js.map +1 -1
  78. package/dist/daemon/worker-manager.d.ts.map +1 -1
  79. package/dist/daemon/worker-manager.js +79 -17
  80. package/dist/daemon/worker-manager.js.map +1 -1
  81. package/dist/daemon/worker-protocol.d.ts +3 -0
  82. package/dist/daemon/worker-protocol.d.ts.map +1 -1
  83. package/dist/deploy/cloudflare.d.ts.map +1 -1
  84. package/dist/deploy/cloudflare.js +2 -4
  85. package/dist/deploy/cloudflare.js.map +1 -1
  86. package/dist/loader.d.ts +11 -1
  87. package/dist/loader.d.ts.map +1 -1
  88. package/dist/loader.js +129 -13
  89. package/dist/loader.js.map +1 -1
  90. package/dist/marketplace-manager.d.ts +7 -1
  91. package/dist/marketplace-manager.d.ts.map +1 -1
  92. package/dist/marketplace-manager.js +165 -61
  93. package/dist/marketplace-manager.js.map +1 -1
  94. package/dist/namespace-migration.d.ts +1 -0
  95. package/dist/namespace-migration.d.ts.map +1 -1
  96. package/dist/namespace-migration.js +86 -0
  97. package/dist/namespace-migration.js.map +1 -1
  98. package/dist/photon-cli-runner.d.ts.map +1 -1
  99. package/dist/photon-cli-runner.js +40 -21
  100. package/dist/photon-cli-runner.js.map +1 -1
  101. package/dist/photon-doc-extractor.d.ts.map +1 -1
  102. package/dist/photon-doc-extractor.js +59 -15
  103. package/dist/photon-doc-extractor.js.map +1 -1
  104. package/dist/resource-server.d.ts +105 -0
  105. package/dist/resource-server.d.ts.map +1 -0
  106. package/dist/resource-server.js +723 -0
  107. package/dist/resource-server.js.map +1 -0
  108. package/dist/serv/auth/jwt.d.ts +2 -0
  109. package/dist/serv/auth/jwt.d.ts.map +1 -1
  110. package/dist/serv/auth/jwt.js +11 -5
  111. package/dist/serv/auth/jwt.js.map +1 -1
  112. package/dist/serv/vault/token-vault.d.ts +2 -0
  113. package/dist/serv/vault/token-vault.d.ts.map +1 -1
  114. package/dist/serv/vault/token-vault.js +6 -0
  115. package/dist/serv/vault/token-vault.js.map +1 -1
  116. package/dist/server.d.ts +20 -149
  117. package/dist/server.d.ts.map +1 -1
  118. package/dist/server.js +246 -1233
  119. package/dist/server.js.map +1 -1
  120. package/dist/shared/audit.d.ts.map +1 -1
  121. package/dist/shared/audit.js +7 -0
  122. package/dist/shared/audit.js.map +1 -1
  123. package/dist/shared/security.d.ts +10 -0
  124. package/dist/shared/security.d.ts.map +1 -1
  125. package/dist/shared/security.js +27 -0
  126. package/dist/shared/security.js.map +1 -1
  127. package/dist/shared-utils.d.ts +4 -0
  128. package/dist/shared-utils.d.ts.map +1 -1
  129. package/dist/shared-utils.js +22 -0
  130. package/dist/shared-utils.js.map +1 -1
  131. package/dist/task-executor.d.ts +69 -0
  132. package/dist/task-executor.d.ts.map +1 -0
  133. package/dist/task-executor.js +182 -0
  134. package/dist/task-executor.js.map +1 -0
  135. package/dist/template-manager.d.ts.map +1 -1
  136. package/dist/template-manager.js +56 -234
  137. package/dist/template-manager.js.map +1 -1
  138. package/dist/types/photon-instance.d.ts +50 -0
  139. package/dist/types/photon-instance.d.ts.map +1 -0
  140. package/dist/types/photon-instance.js +9 -0
  141. package/dist/types/photon-instance.js.map +1 -0
  142. package/dist/types/server-types.d.ts +61 -0
  143. package/dist/types/server-types.d.ts.map +1 -0
  144. package/dist/types/server-types.js +8 -0
  145. package/dist/types/server-types.js.map +1 -0
  146. package/package.json +3 -3
@@ -267,7 +267,7 @@ function cleanupStaleMaps() {
267
267
  }
268
268
  // Remove empty channel subscription sets and prune destroyed sockets
269
269
  for (const [channel, subs] of channelSubscriptions.entries()) {
270
- for (const socket of subs) {
270
+ for (const socket of [...subs]) {
271
271
  if (socket.destroyed)
272
272
  subs.delete(socket);
273
273
  }
@@ -288,8 +288,10 @@ function cleanupStaleMaps() {
288
288
  }
289
289
  }
290
290
  }
291
- setInterval(cleanupExpiredLocks, 10000);
292
- setInterval(cleanupStaleMaps, 60000);
291
+ const lockCleanupInterval = setInterval(cleanupExpiredLocks, 10000);
292
+ const staleMapCleanupInterval = setInterval(cleanupStaleMaps, 60000);
293
+ lockCleanupInterval.unref();
294
+ staleMapCleanupInterval.unref();
293
295
  // ════════════════════════════════════════════════════════════════════════════════
294
296
  // SCHEDULED JOBS
295
297
  // ════════════════════════════════════════════════════════════════════════════════
@@ -711,7 +713,7 @@ function publishToChannel(channel, message, excludeSocket) {
711
713
  // Send to exact channel subscribers
712
714
  const exactSubscribers = channelSubscriptions.get(channel);
713
715
  if (exactSubscribers) {
714
- for (const socket of exactSubscribers) {
716
+ for (const socket of [...exactSubscribers]) {
715
717
  if (socket !== excludeSocket && !socket.destroyed && !sentSockets.has(socket)) {
716
718
  try {
717
719
  socket.write(payload);
@@ -730,7 +732,7 @@ function publishToChannel(channel, message, excludeSocket) {
730
732
  const wildcardChannel = `${channelPrefix}:*`;
731
733
  const wildcardSubscribers = channelSubscriptions.get(wildcardChannel);
732
734
  if (wildcardSubscribers) {
733
- for (const socket of wildcardSubscribers) {
735
+ for (const socket of [...wildcardSubscribers]) {
734
736
  if (socket !== excludeSocket && !socket.destroyed && !sentSockets.has(socket)) {
735
737
  try {
736
738
  socket.write(payload);
@@ -803,15 +805,8 @@ async function getOrCreateSessionManager(photonName, photonPath, workingDir) {
803
805
  // If @worker tagged, spawn in a worker thread instead of in-process
804
806
  if (shouldRunInWorker(pathToUse) && !workerManager.has(key)) {
805
807
  try {
806
- logger.info('Spawning worker thread for @worker photon', { photonName, key });
807
- const info = await workerManager.spawn(key, photonName, pathToUse, workingDir);
808
- photonPaths.set(key, pathToUse);
809
- if (!photonPaths.has(photonName))
810
- photonPaths.set(photonName, pathToUse);
811
- if (workingDir)
812
- workingDirs.set(key, workingDir);
813
- watchPhotonFile(photonName, pathToUse);
814
- // Wire dep resolver to go through main thread's getOrCreateSessionManager
808
+ // Wire dep resolver BEFORE spawn so the worker can resolve @photon deps
809
+ // during initialization (loadFile triggers dep resolution before 'ready').
815
810
  if (!workerManager.depResolver) {
816
811
  workerManager.depResolver = async (depName, depPath, _consumerPhoton) => {
817
812
  const depManager = await getOrCreateSessionManager(depName, depPath, workingDir);
@@ -830,6 +825,14 @@ async function getOrCreateSessionManager(photonName, photonPath, workingDir) {
830
825
  return depManager.loader.executeTool(loaded, method, args);
831
826
  };
832
827
  }
828
+ logger.info('Spawning worker thread for @worker photon', { photonName, key });
829
+ const info = await workerManager.spawn(key, photonName, pathToUse, workingDir);
830
+ photonPaths.set(key, pathToUse);
831
+ if (!photonPaths.has(photonName))
832
+ photonPaths.set(photonName, pathToUse);
833
+ if (workingDir)
834
+ workingDirs.set(key, workingDir);
835
+ watchPhotonFile(photonName, pathToUse);
833
836
  // Return null — caller should check workerManager.has(key) for routing
834
837
  // We store a marker so callers know this is a worker photon
835
838
  logger.info('Worker photon ready', { photonName, tools: info.tools.length });
@@ -938,39 +941,6 @@ async function getOrCreateSessionManager(photonName, photonPath, workingDir) {
938
941
  // Auto-register scheduled jobs and webhook routes from tool metadata
939
942
  // Do this lazily on first session creation
940
943
  void autoRegisterFromMetadata(key, manager);
941
- // Auto-symlink: if this photon was resolved from a @photon dependency path
942
- // (e.g. ./chat.photon.ts relative to consumer), ensure it's accessible by bare
943
- // name from ~/.photon/local/ so CLI can reach it without manual symlinking.
944
- try {
945
- const localDir = path.join(os.homedir(), '.photon', 'local');
946
- const symlinkName = `${photonName}.photon.ts`;
947
- const symlinkPath = path.join(localDir, symlinkName);
948
- const resolvedSource = fs.realpathSync(pathToUse);
949
- if (!fs.existsSync(symlinkPath)) {
950
- fs.mkdirSync(localDir, { recursive: true });
951
- fs.symlinkSync(resolvedSource, symlinkPath);
952
- logger.info('Auto-created symlink for CLI access', {
953
- photonName,
954
- symlink: symlinkPath,
955
- target: resolvedSource,
956
- });
957
- }
958
- // Also symlink the UI directory if it exists alongside the photon
959
- const photonDir = path.dirname(resolvedSource);
960
- const uiDir = path.join(photonDir, photonName);
961
- const symlinkUiDir = path.join(localDir, photonName);
962
- if (fs.existsSync(uiDir) && !fs.existsSync(symlinkUiDir)) {
963
- fs.symlinkSync(uiDir, symlinkUiDir);
964
- logger.info('Auto-created UI directory symlink', {
965
- photonName,
966
- symlink: symlinkUiDir,
967
- target: uiDir,
968
- });
969
- }
970
- }
971
- catch {
972
- // Non-critical — CLI access via full path still works
973
- }
974
944
  return manager;
975
945
  }
976
946
  catch (error) {
@@ -1433,13 +1403,14 @@ async function handleRequest(request, socket) {
1433
1403
  suggestion: 'Include photonName in the request payload',
1434
1404
  };
1435
1405
  }
1406
+ const existing = scheduledJobs.get(request.jobId);
1436
1407
  const job = {
1437
1408
  id: request.jobId,
1438
1409
  method: request.method,
1439
1410
  args: request.args,
1440
1411
  cron: request.cron,
1441
- runCount: 0,
1442
- createdAt: Date.now(),
1412
+ runCount: existing?.runCount ?? 0,
1413
+ createdAt: existing?.createdAt ?? Date.now(),
1443
1414
  createdBy: request.sessionId,
1444
1415
  photonName,
1445
1416
  workingDir: request.workingDir,
@@ -1488,7 +1459,8 @@ async function handleRequest(request, socket) {
1488
1459
  // not forwarded to worker threads (workers don't know about _use etc.)
1489
1460
  const runtimeTools = ['_use', '_instances', '_undo', '_redo'];
1490
1461
  const cmdKey = compositeKey(photonName, request.workingDir);
1491
- if (workerManager.has(cmdKey) && !runtimeTools.includes(request.method)) {
1462
+ const readyWorker = workerManager.get(cmdKey);
1463
+ if (readyWorker?.ready && !runtimeTools.includes(request.method)) {
1492
1464
  const startMs = Date.now();
1493
1465
  const result = await workerManager.call(cmdKey, request.method, request.args || {}, request.sessionId || 'default', request.instanceName || '');
1494
1466
  return {
@@ -1500,10 +1472,28 @@ async function handleRequest(request, socket) {
1500
1472
  durationMs: result.durationMs ?? Date.now() - startMs,
1501
1473
  };
1502
1474
  }
1503
- // Trigger worker spawn if needed (getOrCreateSessionManager returns null for workers)
1504
- const sessionManager = await getOrCreateSessionManager(photonName, request.photonPath, request.workingDir);
1475
+ // Trigger worker spawn if needed (getOrCreateSessionManager returns null for workers).
1476
+ // Bound the wait so a slow worker spawn (cascading deps) doesn't block the CLI indefinitely.
1477
+ let sessionManager = null;
1478
+ try {
1479
+ const initPromise = getOrCreateSessionManager(photonName, request.photonPath, request.workingDir);
1480
+ const INIT_TIMEOUT_MS = 60_000;
1481
+ sessionManager = await Promise.race([
1482
+ initPromise,
1483
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Photon ${photonName} initialization timed out (${INIT_TIMEOUT_MS / 1000}s)`)), INIT_TIMEOUT_MS)),
1484
+ ]);
1485
+ }
1486
+ catch (initErr) {
1487
+ return {
1488
+ type: 'error',
1489
+ id: request.id,
1490
+ error: getErrorMessage(initErr),
1491
+ suggestion: `The photon may still be loading. Try again shortly, or check: cat ${getDefaultContext().logFile}`,
1492
+ };
1493
+ }
1505
1494
  // Re-check: might have spawned a worker
1506
- if (workerManager.has(cmdKey) && !runtimeTools.includes(request.method)) {
1495
+ const readyWorkerAfterInit = workerManager.get(cmdKey);
1496
+ if (readyWorkerAfterInit?.ready && !runtimeTools.includes(request.method)) {
1507
1497
  const startMs = Date.now();
1508
1498
  const result = await workerManager.call(cmdKey, request.method, request.args || {}, request.sessionId || 'default', request.instanceName || '');
1509
1499
  return {
@@ -2294,9 +2284,11 @@ function watchPhotonFile(photonName, photonPath) {
2294
2284
  const existing = watchDebounce.get(watchPath);
2295
2285
  if (existing)
2296
2286
  clearTimeout(existing);
2297
- watchDebounce.set(watchPath, setTimeout(() => {
2287
+ const timer = setTimeout(() => {
2298
2288
  void (async () => {
2299
- watchDebounce.delete(watchPath);
2289
+ const currentTimer = watchDebounce.get(watchPath);
2290
+ if (currentTimer === timer)
2291
+ watchDebounce.delete(watchPath);
2300
2292
  // On macOS, editors like sed -i and some IDEs replace the file (new inode),
2301
2293
  // which kills the watcher. Re-watch via original path (symlink) so we
2302
2294
  // re-resolve to the new real path. Don't return — fall through to reload,
@@ -2345,7 +2337,8 @@ function watchPhotonFile(photonName, photonPath) {
2345
2337
  });
2346
2338
  }
2347
2339
  })();
2348
- }, 100));
2340
+ }, 100);
2341
+ watchDebounce.set(watchPath, timer);
2349
2342
  });
2350
2343
  if ('on' in watcher && typeof watcher.on === 'function') {
2351
2344
  watcher.on('error', (err) => {
@@ -2451,9 +2444,11 @@ function watchWorkingDir(workingDir) {
2451
2444
  const existing = watchDebounce.get(debounceKey);
2452
2445
  if (existing)
2453
2446
  clearTimeout(existing);
2454
- watchDebounce.set(debounceKey, setTimeout(() => {
2447
+ const timer = setTimeout(() => {
2455
2448
  void (async () => {
2456
- watchDebounce.delete(debounceKey);
2449
+ const currentTimer = watchDebounce.get(debounceKey);
2450
+ if (currentTimer === timer)
2451
+ watchDebounce.delete(debounceKey);
2457
2452
  if (fs.existsSync(workingDir)) {
2458
2453
  // Still exists — record updated inode in case it was recreated
2459
2454
  try {
@@ -2506,7 +2501,8 @@ function watchWorkingDir(workingDir) {
2506
2501
  workingDirInodes.delete(workingDir);
2507
2502
  }
2508
2503
  })();
2509
- }, 150));
2504
+ }, 150);
2505
+ watchDebounce.set(debounceKey, timer);
2510
2506
  });
2511
2507
  if ('on' in watcher && typeof watcher.on === 'function') {
2512
2508
  watcher.on('error', (err) => {
@@ -2548,9 +2544,11 @@ function watchStateDir(workingDir) {
2548
2544
  const existing = watchDebounce.get(debounceKey);
2549
2545
  if (existing)
2550
2546
  clearTimeout(existing);
2551
- watchDebounce.set(debounceKey, setTimeout(() => {
2547
+ const timer = setTimeout(() => {
2552
2548
  void (async () => {
2553
- watchDebounce.delete(debounceKey);
2549
+ const currentTimer = watchDebounce.get(debounceKey);
2550
+ if (currentTimer === timer)
2551
+ watchDebounce.delete(debounceKey);
2554
2552
  const photonStateDir = path.join(stateDir, filename);
2555
2553
  if (fs.existsSync(photonStateDir))
2556
2554
  return; // Still there — not a deletion
@@ -2565,7 +2563,8 @@ function watchStateDir(workingDir) {
2565
2563
  await manager.clearInstances();
2566
2564
  }
2567
2565
  })();
2568
- }, 150));
2566
+ }, 150);
2567
+ watchDebounce.set(debounceKey, timer);
2569
2568
  });
2570
2569
  if ('on' in watcher && typeof watcher.on === 'function') {
2571
2570
  watcher.on('error', (err) => {
@@ -3014,6 +3013,8 @@ function shutdown() {
3014
3013
  if (idleTimer) {
3015
3014
  clearTimeout(idleTimer);
3016
3015
  }
3016
+ clearInterval(lockCleanupInterval);
3017
+ clearInterval(staleMapCleanupInterval);
3017
3018
  for (const timer of jobTimers.values()) {
3018
3019
  clearTimeout(timer);
3019
3020
  }
@@ -3049,12 +3050,36 @@ function shutdown() {
3049
3050
  if (webhookServer) {
3050
3051
  webhookServer.close();
3051
3052
  }
3053
+ // Only delete the socket if we still own it.
3054
+ // After `daemon stop` + `daemon start`, a new daemon may have already created
3055
+ // a new socket at this path. Deleting it would orphan the new daemon's listener.
3052
3056
  if (fs.existsSync(socketPath) && process.platform !== 'win32') {
3057
+ let weOwnSocket = true;
3053
3058
  try {
3054
- fs.unlinkSync(socketPath);
3059
+ const pidFile = getDefaultContext().pidFile;
3060
+ const pidContent = fs.readFileSync(pidFile, 'utf-8').trim();
3061
+ const filePid = parseInt(pidContent, 10);
3062
+ if (!isNaN(filePid) && filePid !== process.pid) {
3063
+ // PID file points to a different process — new daemon already started
3064
+ weOwnSocket = false;
3065
+ logger.info('Socket belongs to new daemon, skipping cleanup', {
3066
+ ourPid: process.pid,
3067
+ newPid: filePid,
3068
+ });
3069
+ }
3055
3070
  }
3056
3071
  catch {
3057
- // Ignore cleanup errors
3072
+ // PID file missing (deleted by stop) — another process may own the socket now.
3073
+ // If the socket still exists, it likely belongs to a new daemon. Don't delete.
3074
+ weOwnSocket = false;
3075
+ }
3076
+ if (weOwnSocket) {
3077
+ try {
3078
+ fs.unlinkSync(socketPath);
3079
+ }
3080
+ catch {
3081
+ // Ignore cleanup errors
3082
+ }
3058
3083
  }
3059
3084
  }
3060
3085
  process.exit(0);