@portel/photon 1.4.0 → 1.5.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.
- package/README.md +287 -1160
- package/dist/auto-ui/beam.d.ts +9 -0
- package/dist/auto-ui/beam.d.ts.map +1 -0
- package/dist/auto-ui/beam.js +2381 -0
- package/dist/auto-ui/beam.js.map +1 -0
- package/dist/auto-ui/components/card.d.ts +13 -0
- package/dist/auto-ui/components/card.d.ts.map +1 -0
- package/dist/auto-ui/components/card.js +64 -0
- package/dist/auto-ui/components/card.js.map +1 -0
- package/dist/auto-ui/components/form.d.ts +15 -0
- package/dist/auto-ui/components/form.d.ts.map +1 -0
- package/dist/auto-ui/components/form.js +72 -0
- package/dist/auto-ui/components/form.js.map +1 -0
- package/dist/auto-ui/components/list.d.ts +13 -0
- package/dist/auto-ui/components/list.d.ts.map +1 -0
- package/dist/auto-ui/components/list.js +58 -0
- package/dist/auto-ui/components/list.js.map +1 -0
- package/dist/auto-ui/components/progress.d.ts +18 -0
- package/dist/auto-ui/components/progress.d.ts.map +1 -0
- package/dist/auto-ui/components/progress.js +125 -0
- package/dist/auto-ui/components/progress.js.map +1 -0
- package/dist/auto-ui/components/table.d.ts +13 -0
- package/dist/auto-ui/components/table.d.ts.map +1 -0
- package/dist/auto-ui/components/table.js +82 -0
- package/dist/auto-ui/components/table.js.map +1 -0
- package/dist/auto-ui/components/tree.d.ts +13 -0
- package/dist/auto-ui/components/tree.d.ts.map +1 -0
- package/dist/auto-ui/components/tree.js +61 -0
- package/dist/auto-ui/components/tree.js.map +1 -0
- package/dist/auto-ui/daemon-tools.d.ts +45 -0
- package/dist/auto-ui/daemon-tools.d.ts.map +1 -0
- package/dist/auto-ui/daemon-tools.js +580 -0
- package/dist/auto-ui/daemon-tools.js.map +1 -0
- package/dist/auto-ui/design-system/index.d.ts +21 -0
- package/dist/auto-ui/design-system/index.d.ts.map +1 -0
- package/dist/auto-ui/design-system/index.js +27 -0
- package/dist/auto-ui/design-system/index.js.map +1 -0
- package/dist/auto-ui/design-system/tokens.d.ts +9 -0
- package/dist/auto-ui/design-system/tokens.d.ts.map +1 -0
- package/dist/auto-ui/design-system/tokens.js +27 -0
- package/dist/auto-ui/design-system/tokens.js.map +1 -0
- package/dist/auto-ui/design-system/transaction-ui.d.ts +70 -0
- package/dist/auto-ui/design-system/transaction-ui.d.ts.map +1 -0
- package/dist/auto-ui/design-system/transaction-ui.js +982 -0
- package/dist/auto-ui/design-system/transaction-ui.js.map +1 -0
- package/dist/auto-ui/frontend/index.html +84 -0
- package/dist/auto-ui/index.d.ts +21 -0
- package/dist/auto-ui/index.d.ts.map +1 -0
- package/dist/auto-ui/index.js +25 -0
- package/dist/auto-ui/index.js.map +1 -0
- package/dist/auto-ui/openapi-generator.d.ts +71 -0
- package/dist/auto-ui/openapi-generator.d.ts.map +1 -0
- package/dist/auto-ui/openapi-generator.js +223 -0
- package/dist/auto-ui/openapi-generator.js.map +1 -0
- package/dist/auto-ui/photon-bridge.d.ts +159 -0
- package/dist/auto-ui/photon-bridge.d.ts.map +1 -0
- package/dist/auto-ui/photon-bridge.js +262 -0
- package/dist/auto-ui/photon-bridge.js.map +1 -0
- package/dist/auto-ui/photon-host.d.ts +113 -0
- package/dist/auto-ui/photon-host.d.ts.map +1 -0
- package/dist/auto-ui/photon-host.js +284 -0
- package/dist/auto-ui/photon-host.js.map +1 -0
- package/dist/auto-ui/platform-compat.d.ts +71 -0
- package/dist/auto-ui/platform-compat.d.ts.map +1 -0
- package/dist/auto-ui/platform-compat.js +574 -0
- package/dist/auto-ui/platform-compat.js.map +1 -0
- package/dist/auto-ui/playground-html.d.ts +15 -0
- package/dist/auto-ui/playground-html.d.ts.map +1 -0
- package/dist/auto-ui/playground-html.js +1113 -0
- package/dist/auto-ui/playground-html.js.map +1 -0
- package/dist/auto-ui/playground-server.d.ts +7 -0
- package/dist/auto-ui/playground-server.d.ts.map +1 -0
- package/dist/auto-ui/playground-server.js +840 -0
- package/dist/auto-ui/playground-server.js.map +1 -0
- package/dist/auto-ui/registry.d.ts +13 -0
- package/dist/auto-ui/registry.d.ts.map +1 -0
- package/dist/auto-ui/registry.js +62 -0
- package/dist/auto-ui/registry.js.map +1 -0
- package/dist/auto-ui/renderer.d.ts +14 -0
- package/dist/auto-ui/renderer.d.ts.map +1 -0
- package/dist/auto-ui/renderer.js +88 -0
- package/dist/auto-ui/renderer.js.map +1 -0
- package/dist/auto-ui/rendering/components.d.ts +29 -0
- package/dist/auto-ui/rendering/components.d.ts.map +1 -0
- package/dist/auto-ui/rendering/components.js +773 -0
- package/dist/auto-ui/rendering/components.js.map +1 -0
- package/dist/auto-ui/rendering/field-analyzer.d.ts +48 -0
- package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -0
- package/dist/auto-ui/rendering/field-analyzer.js +270 -0
- package/dist/auto-ui/rendering/field-analyzer.js.map +1 -0
- package/dist/auto-ui/rendering/field-renderers.d.ts +64 -0
- package/dist/auto-ui/rendering/field-renderers.d.ts.map +1 -0
- package/dist/auto-ui/rendering/field-renderers.js +317 -0
- package/dist/auto-ui/rendering/field-renderers.js.map +1 -0
- package/dist/auto-ui/rendering/index.d.ts +28 -0
- package/dist/auto-ui/rendering/index.d.ts.map +1 -0
- package/dist/auto-ui/rendering/index.js +60 -0
- package/dist/auto-ui/rendering/index.js.map +1 -0
- package/dist/auto-ui/rendering/layout-selector.d.ts +48 -0
- package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -0
- package/dist/auto-ui/rendering/layout-selector.js +352 -0
- package/dist/auto-ui/rendering/layout-selector.js.map +1 -0
- package/dist/auto-ui/rendering/template-engine.d.ts +41 -0
- package/dist/auto-ui/rendering/template-engine.d.ts.map +1 -0
- package/dist/auto-ui/rendering/template-engine.js +238 -0
- package/dist/auto-ui/rendering/template-engine.js.map +1 -0
- package/dist/auto-ui/streamable-http-transport.d.ts +79 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -0
- package/dist/auto-ui/streamable-http-transport.js +1314 -0
- package/dist/auto-ui/streamable-http-transport.js.map +1 -0
- package/dist/auto-ui/types.d.ts +310 -0
- package/dist/auto-ui/types.d.ts.map +1 -0
- package/dist/auto-ui/types.js +71 -0
- package/dist/auto-ui/types.js.map +1 -0
- package/dist/beam.bundle.js +13506 -0
- package/dist/beam.bundle.js.map +7 -0
- package/dist/claude-code-plugin.d.ts.map +1 -1
- package/dist/claude-code-plugin.js +30 -30
- package/dist/claude-code-plugin.js.map +1 -1
- package/dist/cli/commands/info.d.ts +11 -0
- package/dist/cli/commands/info.d.ts.map +1 -0
- package/dist/cli/commands/info.js +313 -0
- package/dist/cli/commands/info.js.map +1 -0
- package/dist/cli/commands/marketplace.d.ts +11 -0
- package/dist/cli/commands/marketplace.d.ts.map +1 -0
- package/dist/cli/commands/marketplace.js +198 -0
- package/dist/cli/commands/marketplace.js.map +1 -0
- package/dist/cli/commands/package-app.d.ts +9 -0
- package/dist/cli/commands/package-app.d.ts.map +1 -0
- package/dist/cli/commands/package-app.js +191 -0
- package/dist/cli/commands/package-app.js.map +1 -0
- package/dist/cli/commands/package.d.ts +11 -0
- package/dist/cli/commands/package.d.ts.map +1 -0
- package/dist/cli/commands/package.js +573 -0
- package/dist/cli/commands/package.js.map +1 -0
- package/dist/cli-alias.d.ts.map +1 -1
- package/dist/cli-alias.js +30 -28
- package/dist/cli-alias.js.map +1 -1
- package/dist/cli-formatter.d.ts +8 -24
- package/dist/cli-formatter.d.ts.map +1 -1
- package/dist/cli-formatter.js +8 -325
- package/dist/cli-formatter.js.map +1 -1
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1157 -1132
- package/dist/cli.js.map +1 -1
- package/dist/daemon/client.d.ts +81 -0
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +583 -13
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +46 -12
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +102 -61
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/protocol.d.ts +74 -6
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js +76 -1
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/server.d.ts +6 -6
- package/dist/daemon/server.js +778 -117
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +8 -1
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +32 -9
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts +12 -0
- package/dist/deploy/cloudflare.d.ts.map +1 -0
- package/dist/deploy/cloudflare.js +216 -0
- package/dist/deploy/cloudflare.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +172 -15
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +1132 -267
- package/dist/loader.js.map +1 -1
- package/dist/markdown-utils.d.ts +8 -0
- package/dist/markdown-utils.d.ts.map +1 -0
- package/dist/markdown-utils.js +63 -0
- package/dist/markdown-utils.js.map +1 -0
- package/dist/marketplace-manager.d.ts +10 -0
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +112 -28
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/mcp-client.d.ts +9 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +11 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp-elicitation.d.ts +32 -0
- package/dist/mcp-elicitation.d.ts.map +1 -0
- package/dist/mcp-elicitation.js +26 -0
- package/dist/mcp-elicitation.js.map +1 -0
- package/dist/path-resolver.d.ts +9 -12
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +13 -43
- package/dist/path-resolver.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +216 -73
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +88 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +536 -27
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/maker.photon.d.ts +182 -0
- package/dist/photons/maker.photon.d.ts.map +1 -0
- package/dist/photons/maker.photon.js +504 -0
- package/dist/photons/maker.photon.js.map +1 -0
- package/dist/photons/maker.photon.ts +626 -0
- package/dist/photons/marketplace.photon.d.ts +110 -0
- package/dist/photons/marketplace.photon.d.ts.map +1 -0
- package/dist/photons/marketplace.photon.js +260 -0
- package/dist/photons/marketplace.photon.js.map +1 -0
- package/dist/photons/marketplace.photon.ts +378 -0
- package/dist/photons/tunnel.photon.d.ts +80 -0
- package/dist/photons/tunnel.photon.d.ts.map +1 -0
- package/dist/photons/tunnel.photon.js +269 -0
- package/dist/photons/tunnel.photon.js.map +1 -0
- package/dist/photons/tunnel.photon.ts +345 -0
- package/dist/security-scanner.d.ts.map +1 -1
- package/dist/security-scanner.js +18 -15
- package/dist/security-scanner.js.map +1 -1
- package/dist/serv/auth/jwt.d.ts +89 -0
- package/dist/serv/auth/jwt.d.ts.map +1 -0
- package/dist/serv/auth/jwt.js +239 -0
- package/dist/serv/auth/jwt.js.map +1 -0
- package/dist/serv/auth/oauth.d.ts +117 -0
- package/dist/serv/auth/oauth.d.ts.map +1 -0
- package/dist/serv/auth/oauth.js +395 -0
- package/dist/serv/auth/oauth.js.map +1 -0
- package/dist/serv/auth/well-known.d.ts +60 -0
- package/dist/serv/auth/well-known.d.ts.map +1 -0
- package/dist/serv/auth/well-known.js +154 -0
- package/dist/serv/auth/well-known.js.map +1 -0
- package/dist/serv/db/d1-client.d.ts +65 -0
- package/dist/serv/db/d1-client.d.ts.map +1 -0
- package/dist/serv/db/d1-client.js +137 -0
- package/dist/serv/db/d1-client.js.map +1 -0
- package/dist/serv/db/d1-stores.d.ts +62 -0
- package/dist/serv/db/d1-stores.d.ts.map +1 -0
- package/dist/serv/db/d1-stores.js +307 -0
- package/dist/serv/db/d1-stores.js.map +1 -0
- package/dist/serv/index.d.ts +114 -0
- package/dist/serv/index.d.ts.map +1 -0
- package/dist/serv/index.js +172 -0
- package/dist/serv/index.js.map +1 -0
- package/dist/serv/local.d.ts +118 -0
- package/dist/serv/local.d.ts.map +1 -0
- package/dist/serv/local.js +392 -0
- package/dist/serv/local.js.map +1 -0
- package/dist/serv/middleware/auth.d.ts +66 -0
- package/dist/serv/middleware/auth.d.ts.map +1 -0
- package/dist/serv/middleware/auth.js +178 -0
- package/dist/serv/middleware/auth.js.map +1 -0
- package/dist/serv/middleware/tenant.d.ts +94 -0
- package/dist/serv/middleware/tenant.d.ts.map +1 -0
- package/dist/serv/middleware/tenant.js +152 -0
- package/dist/serv/middleware/tenant.js.map +1 -0
- package/dist/serv/runtime/executor.d.ts +76 -0
- package/dist/serv/runtime/executor.d.ts.map +1 -0
- package/dist/serv/runtime/executor.js +105 -0
- package/dist/serv/runtime/executor.js.map +1 -0
- package/dist/serv/runtime/index.d.ts +8 -0
- package/dist/serv/runtime/index.d.ts.map +1 -0
- package/dist/serv/runtime/index.js +10 -0
- package/dist/serv/runtime/index.js.map +1 -0
- package/dist/serv/runtime/oauth-context.d.ts +121 -0
- package/dist/serv/runtime/oauth-context.d.ts.map +1 -0
- package/dist/serv/runtime/oauth-context.js +153 -0
- package/dist/serv/runtime/oauth-context.js.map +1 -0
- package/dist/serv/session/kv-store.d.ts +54 -0
- package/dist/serv/session/kv-store.d.ts.map +1 -0
- package/dist/serv/session/kv-store.js +149 -0
- package/dist/serv/session/kv-store.js.map +1 -0
- package/dist/serv/session/store.d.ts +113 -0
- package/dist/serv/session/store.d.ts.map +1 -0
- package/dist/serv/session/store.js +284 -0
- package/dist/serv/session/store.js.map +1 -0
- package/dist/serv/types/index.d.ts +147 -0
- package/dist/serv/types/index.d.ts.map +1 -0
- package/dist/serv/types/index.js +8 -0
- package/dist/serv/types/index.js.map +1 -0
- package/dist/serv/vault/token-vault.d.ts +102 -0
- package/dist/serv/vault/token-vault.d.ts.map +1 -0
- package/dist/serv/vault/token-vault.js +177 -0
- package/dist/serv/vault/token-vault.js.map +1 -0
- package/dist/server.d.ts +173 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1622 -86
- package/dist/server.js.map +1 -1
- package/dist/shared/cli-sections.d.ts +6 -0
- package/dist/shared/cli-sections.d.ts.map +1 -0
- package/dist/shared/cli-sections.js +16 -0
- package/dist/shared/cli-sections.js.map +1 -0
- package/dist/shared/cli-utils.d.ts +81 -0
- package/dist/shared/cli-utils.d.ts.map +1 -0
- package/dist/shared/cli-utils.js +174 -0
- package/dist/shared/cli-utils.js.map +1 -0
- package/dist/shared/config-docs.d.ts +6 -0
- package/dist/shared/config-docs.d.ts.map +1 -0
- package/dist/shared/config-docs.js +6 -0
- package/dist/shared/config-docs.js.map +1 -0
- package/dist/shared/error-handler.d.ts +128 -0
- package/dist/shared/error-handler.d.ts.map +1 -0
- package/dist/shared/error-handler.js +342 -0
- package/dist/shared/error-handler.js.map +1 -0
- package/dist/shared/logger.d.ts +42 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/logger.js +123 -0
- package/dist/shared/logger.js.map +1 -0
- package/dist/shared/performance.d.ts +65 -0
- package/dist/shared/performance.d.ts.map +1 -0
- package/dist/shared/performance.js +136 -0
- package/dist/shared/performance.js.map +1 -0
- package/dist/shared/task-runner.d.ts +2 -0
- package/dist/shared/task-runner.d.ts.map +1 -0
- package/dist/shared/task-runner.js +16 -0
- package/dist/shared/task-runner.js.map +1 -0
- package/dist/shared/validation.d.ts +6 -0
- package/dist/shared/validation.d.ts.map +1 -0
- package/dist/shared/validation.js +6 -0
- package/dist/shared/validation.js.map +1 -0
- package/dist/shared-utils.d.ts +63 -0
- package/dist/shared-utils.d.ts.map +1 -0
- package/dist/shared-utils.js +123 -0
- package/dist/shared-utils.js.map +1 -0
- package/dist/template-manager.d.ts +23 -2
- package/dist/template-manager.d.ts.map +1 -1
- package/dist/template-manager.js +177 -88
- package/dist/template-manager.js.map +1 -1
- package/dist/test-client.d.ts.map +1 -1
- package/dist/test-client.js +10 -8
- package/dist/test-client.js.map +1 -1
- package/dist/test-runner.d.ts +52 -0
- package/dist/test-runner.d.ts.map +1 -0
- package/dist/test-runner.js +785 -0
- package/dist/test-runner.js.map +1 -0
- package/dist/testing.d.ts +103 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +163 -0
- package/dist/testing.js.map +1 -0
- package/dist/version-checker.d.ts.map +1 -1
- package/dist/version-checker.js +2 -2
- package/dist/version-checker.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/watcher.d.ts +6 -3
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +49 -10
- package/dist/watcher.js.map +1 -1
- package/package.json +47 -7
- package/templates/cloudflare/worker.ts.template +381 -0
- package/templates/cloudflare/wrangler.toml.template +9 -0
- package/dist/base.d.ts +0 -58
- package/dist/base.d.ts.map +0 -1
- package/dist/base.js +0 -92
- package/dist/base.js.map +0 -1
- package/dist/dependency-manager.d.ts +0 -49
- package/dist/dependency-manager.d.ts.map +0 -1
- package/dist/dependency-manager.js +0 -165
- package/dist/dependency-manager.js.map +0 -1
- package/dist/registry-manager.d.ts +0 -76
- package/dist/registry-manager.d.ts.map +0 -1
- package/dist/registry-manager.js +0 -220
- package/dist/registry-manager.js.map +0 -1
- package/dist/schema-extractor.d.ts +0 -110
- package/dist/schema-extractor.d.ts.map +0 -1
- package/dist/schema-extractor.js +0 -727
- package/dist/schema-extractor.js.map +0 -1
- package/dist/test-marketplace-sources.d.ts +0 -5
- package/dist/test-marketplace-sources.d.ts.map +0 -1
- package/dist/test-marketplace-sources.js +0 -53
- package/dist/test-marketplace-sources.js.map +0 -1
- package/dist/types.d.ts +0 -109
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -12
- package/dist/types.js.map +0 -1
package/dist/daemon/server.js
CHANGED
|
@@ -1,215 +1,876 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Daemon Server
|
|
3
|
+
* Global Photon Daemon Server
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
5
|
+
* Single daemon process for all photons.
|
|
6
|
+
* - Listens on global Unix socket / named pipe (~/.photon/daemon.sock)
|
|
7
|
+
* - Handles requests for multiple photons via photonName field
|
|
8
|
+
* - Lazy-initializes SessionManagers per photon on first request
|
|
9
|
+
* - Provides pub/sub, locks, scheduled jobs, and webhooks
|
|
10
10
|
*/
|
|
11
11
|
import * as net from 'net';
|
|
12
|
+
import * as http from 'http';
|
|
12
13
|
import * as fs from 'fs';
|
|
13
14
|
import { SessionManager } from './session-manager.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
import { isValidDaemonRequest, } from './protocol.js';
|
|
16
|
+
import { setPromptHandler } from '@portel/photon-core';
|
|
17
|
+
import { createLogger } from '../shared/logger.js';
|
|
18
|
+
import { getErrorMessage } from '../shared/error-handler.js';
|
|
19
|
+
// Command line args: socketPath (global daemon only needs socket path)
|
|
20
|
+
const socketPath = process.argv[2];
|
|
21
|
+
const logger = createLogger({
|
|
22
|
+
component: 'daemon-server',
|
|
23
|
+
scope: 'global',
|
|
24
|
+
minimal: true,
|
|
25
|
+
});
|
|
26
|
+
if (!socketPath) {
|
|
27
|
+
logger.error('Missing required argument: socketPath');
|
|
20
28
|
process.exit(1);
|
|
21
29
|
}
|
|
22
|
-
|
|
30
|
+
// Map of photonName -> SessionManager (lazy initialized)
|
|
31
|
+
const sessionManagers = new Map();
|
|
32
|
+
const photonPaths = new Map(); // photonName -> photonPath
|
|
23
33
|
let idleTimeout = 600000; // 10 minutes default
|
|
24
34
|
let idleTimer = null;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// Track pending prompts waiting for user input
|
|
36
|
+
const pendingPrompts = new Map();
|
|
37
|
+
// Channel subscriptions for pub/sub
|
|
38
|
+
const channelSubscriptions = new Map();
|
|
39
|
+
const EVENT_BUFFER_SIZE = 30;
|
|
40
|
+
const channelEventBuffers = new Map();
|
|
41
|
+
function bufferEvent(channel, message) {
|
|
42
|
+
let buffer = channelEventBuffers.get(channel);
|
|
43
|
+
if (!buffer) {
|
|
44
|
+
buffer = { events: [], nextId: 1 };
|
|
45
|
+
channelEventBuffers.set(channel, buffer);
|
|
46
|
+
}
|
|
47
|
+
const eventId = buffer.nextId++;
|
|
48
|
+
const event = {
|
|
49
|
+
id: eventId,
|
|
50
|
+
channel,
|
|
51
|
+
message,
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
};
|
|
54
|
+
buffer.events.push(event);
|
|
55
|
+
// Keep only last N events (circular buffer)
|
|
56
|
+
if (buffer.events.length > EVENT_BUFFER_SIZE) {
|
|
57
|
+
buffer.events.shift();
|
|
58
|
+
}
|
|
59
|
+
return eventId;
|
|
60
|
+
}
|
|
61
|
+
function getEventsSince(channel, lastEventId) {
|
|
62
|
+
const buffer = channelEventBuffers.get(channel);
|
|
63
|
+
if (!buffer || buffer.events.length === 0) {
|
|
64
|
+
return { events: [], refreshNeeded: false };
|
|
65
|
+
}
|
|
66
|
+
const oldestEvent = buffer.events[0];
|
|
67
|
+
// If lastEventId is older than our oldest buffered event, refresh needed
|
|
68
|
+
if (lastEventId < oldestEvent.id) {
|
|
69
|
+
return { events: [], refreshNeeded: true };
|
|
70
|
+
}
|
|
71
|
+
// Find events to replay
|
|
72
|
+
const events = buffer.events.filter((e) => e.id > lastEventId);
|
|
73
|
+
return { events, refreshNeeded: false };
|
|
74
|
+
}
|
|
75
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
76
|
+
// DISTRIBUTED LOCKS
|
|
77
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
78
|
+
const activeLocks = new Map();
|
|
79
|
+
const DEFAULT_LOCK_TIMEOUT = 30000;
|
|
80
|
+
function acquireLock(lockName, holder, timeout = DEFAULT_LOCK_TIMEOUT) {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const existing = activeLocks.get(lockName);
|
|
83
|
+
if (existing && existing.expiresAt > now) {
|
|
84
|
+
if (existing.holder !== holder) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
existing.expiresAt = now + timeout;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
activeLocks.set(lockName, {
|
|
91
|
+
name: lockName,
|
|
92
|
+
holder,
|
|
93
|
+
acquiredAt: now,
|
|
94
|
+
expiresAt: now + timeout,
|
|
95
|
+
});
|
|
96
|
+
logger.info('Lock acquired', { lockName, holder, timeout });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
function releaseLock(lockName, holder) {
|
|
100
|
+
const existing = activeLocks.get(lockName);
|
|
101
|
+
if (!existing)
|
|
102
|
+
return true;
|
|
103
|
+
if (existing.holder !== holder)
|
|
104
|
+
return false;
|
|
105
|
+
activeLocks.delete(lockName);
|
|
106
|
+
logger.info('Lock released', { lockName, holder });
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
function cleanupExpiredLocks() {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
for (const [name, lock] of activeLocks.entries()) {
|
|
112
|
+
if (lock.expiresAt <= now) {
|
|
113
|
+
activeLocks.delete(name);
|
|
114
|
+
logger.info('Lock expired', { lockName: name, holder: lock.holder });
|
|
37
115
|
}
|
|
38
116
|
}
|
|
117
|
+
}
|
|
118
|
+
setInterval(cleanupExpiredLocks, 10000);
|
|
119
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
120
|
+
// SCHEDULED JOBS
|
|
121
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
122
|
+
const scheduledJobs = new Map();
|
|
123
|
+
const jobTimers = new Map();
|
|
124
|
+
function parseCron(cron) {
|
|
125
|
+
const parts = cron.trim().split(/\s+/);
|
|
126
|
+
if (parts.length !== 5) {
|
|
127
|
+
return { isValid: false, nextRun: 0 };
|
|
128
|
+
}
|
|
129
|
+
const [minute, hour] = parts;
|
|
130
|
+
const now = new Date();
|
|
131
|
+
const nextDate = new Date(now);
|
|
132
|
+
nextDate.setSeconds(0);
|
|
133
|
+
nextDate.setMilliseconds(0);
|
|
134
|
+
if (minute === '*' && hour === '*') {
|
|
135
|
+
nextDate.setMinutes(nextDate.getMinutes() + 1);
|
|
136
|
+
}
|
|
137
|
+
else if (minute.startsWith('*/')) {
|
|
138
|
+
const interval = parseInt(minute.slice(2));
|
|
139
|
+
const currentMinute = nextDate.getMinutes();
|
|
140
|
+
const nextMinute = Math.ceil((currentMinute + 1) / interval) * interval;
|
|
141
|
+
nextDate.setMinutes(nextMinute);
|
|
142
|
+
}
|
|
143
|
+
else if (hour === '*') {
|
|
144
|
+
const targetMinute = parseInt(minute);
|
|
145
|
+
if (nextDate.getMinutes() >= targetMinute) {
|
|
146
|
+
nextDate.setHours(nextDate.getHours() + 1);
|
|
147
|
+
}
|
|
148
|
+
nextDate.setMinutes(targetMinute);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const targetMinute = parseInt(minute);
|
|
152
|
+
const targetHour = parseInt(hour);
|
|
153
|
+
nextDate.setMinutes(targetMinute);
|
|
154
|
+
nextDate.setHours(targetHour);
|
|
155
|
+
if (nextDate <= now) {
|
|
156
|
+
nextDate.setDate(nextDate.getDate() + 1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { isValid: true, nextRun: nextDate.getTime() };
|
|
160
|
+
}
|
|
161
|
+
function scheduleJob(job) {
|
|
162
|
+
const { isValid, nextRun } = parseCron(job.cron);
|
|
163
|
+
if (!isValid) {
|
|
164
|
+
logger.error('Invalid cron expression', { jobId: job.id, cron: job.cron });
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
job.nextRun = nextRun;
|
|
168
|
+
scheduledJobs.set(job.id, job);
|
|
169
|
+
const existingTimer = jobTimers.get(job.id);
|
|
170
|
+
if (existingTimer) {
|
|
171
|
+
clearTimeout(existingTimer);
|
|
172
|
+
}
|
|
173
|
+
const delay = nextRun - Date.now();
|
|
174
|
+
const timer = setTimeout(() => runJob(job.id), delay);
|
|
175
|
+
jobTimers.set(job.id, timer);
|
|
176
|
+
logger.info('Job scheduled', {
|
|
177
|
+
jobId: job.id,
|
|
178
|
+
method: job.method,
|
|
179
|
+
photon: job.photonName,
|
|
180
|
+
nextRun: new Date(nextRun).toISOString(),
|
|
181
|
+
});
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
async function runJob(jobId) {
|
|
185
|
+
const job = scheduledJobs.get(jobId);
|
|
186
|
+
if (!job)
|
|
187
|
+
return;
|
|
188
|
+
const sessionManager = sessionManagers.get(job.photonName);
|
|
189
|
+
if (!sessionManager) {
|
|
190
|
+
logger.warn('Cannot run job - photon not initialized', { jobId, photon: job.photonName });
|
|
191
|
+
scheduleJob(job); // Reschedule anyway
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
logger.info('Running scheduled job', { jobId, method: job.method, photon: job.photonName });
|
|
195
|
+
try {
|
|
196
|
+
const session = await sessionManager.getOrCreateSession('scheduler', 'scheduler');
|
|
197
|
+
await sessionManager.loader.executeTool(session.instance, job.method, job.args || {});
|
|
198
|
+
job.lastRun = Date.now();
|
|
199
|
+
job.runCount++;
|
|
200
|
+
publishToChannel(`jobs:${job.photonName}`, {
|
|
201
|
+
event: 'job-completed',
|
|
202
|
+
jobId,
|
|
203
|
+
method: job.method,
|
|
204
|
+
runCount: job.runCount,
|
|
205
|
+
});
|
|
206
|
+
logger.info('Job completed', { jobId, method: job.method, runCount: job.runCount });
|
|
207
|
+
}
|
|
39
208
|
catch (error) {
|
|
40
|
-
|
|
41
|
-
|
|
209
|
+
logger.error('Job failed', { jobId, method: job.method, error: getErrorMessage(error) });
|
|
210
|
+
publishToChannel(`jobs:${job.photonName}`, {
|
|
211
|
+
event: 'job-failed',
|
|
212
|
+
jobId,
|
|
213
|
+
method: job.method,
|
|
214
|
+
error: getErrorMessage(error),
|
|
215
|
+
});
|
|
42
216
|
}
|
|
217
|
+
scheduleJob(job);
|
|
43
218
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
clearTimeout(idleTimer);
|
|
219
|
+
function unscheduleJob(jobId) {
|
|
220
|
+
const timer = jobTimers.get(jobId);
|
|
221
|
+
if (timer) {
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
jobTimers.delete(jobId);
|
|
50
224
|
}
|
|
51
|
-
|
|
52
|
-
|
|
225
|
+
const existed = scheduledJobs.delete(jobId);
|
|
226
|
+
if (existed) {
|
|
227
|
+
logger.info('Job unscheduled', { jobId });
|
|
53
228
|
}
|
|
54
|
-
|
|
55
|
-
|
|
229
|
+
return existed;
|
|
230
|
+
}
|
|
231
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
232
|
+
// WEBHOOK HTTP SERVER
|
|
233
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
234
|
+
let webhookServer = null;
|
|
235
|
+
const WEBHOOK_PORT = parseInt(process.env.PHOTON_WEBHOOK_PORT || '0');
|
|
236
|
+
function startWebhookServer(port) {
|
|
237
|
+
if (port <= 0)
|
|
238
|
+
return;
|
|
239
|
+
webhookServer = http.createServer(async (req, res) => {
|
|
240
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
241
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
242
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Webhook-Secret, X-Photon-Name');
|
|
243
|
+
if (req.method === 'OPTIONS') {
|
|
244
|
+
res.writeHead(204);
|
|
245
|
+
res.end();
|
|
56
246
|
return;
|
|
57
|
-
const lastActivity = sessionManager.getLastActivity();
|
|
58
|
-
const idleTime = Date.now() - lastActivity;
|
|
59
|
-
if (idleTime >= idleTimeout) {
|
|
60
|
-
console.error(`[daemon-server] Idle timeout reached (${idleTime}ms). Shutting down.`);
|
|
61
|
-
shutdown();
|
|
62
247
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
248
|
+
// Parse URL: /webhook/{photonName}/{method}
|
|
249
|
+
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
250
|
+
const pathParts = url.pathname.split('/').filter(Boolean);
|
|
251
|
+
if (pathParts[0] !== 'webhook' || !pathParts[1] || !pathParts[2]) {
|
|
252
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
253
|
+
res.end(JSON.stringify({ error: 'Not found. Use /webhook/{photonName}/{method}' }));
|
|
254
|
+
return;
|
|
66
255
|
}
|
|
67
|
-
|
|
256
|
+
const photonName = pathParts[1];
|
|
257
|
+
const method = pathParts[2];
|
|
258
|
+
const expectedSecret = process.env.PHOTON_WEBHOOK_SECRET;
|
|
259
|
+
if (expectedSecret) {
|
|
260
|
+
const providedSecret = req.headers['x-webhook-secret'];
|
|
261
|
+
if (providedSecret !== expectedSecret) {
|
|
262
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
263
|
+
res.end(JSON.stringify({ error: 'Invalid webhook secret' }));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
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) }));
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
webhookServer.listen(port, () => {
|
|
319
|
+
logger.info('Webhook server started', { port });
|
|
320
|
+
});
|
|
321
|
+
webhookServer.on('error', (error) => {
|
|
322
|
+
logger.error('Webhook server error', { error: getErrorMessage(error) });
|
|
323
|
+
});
|
|
68
324
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
function
|
|
73
|
-
|
|
325
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
326
|
+
// PUB/SUB
|
|
327
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
328
|
+
function cleanupSocketSubscriptions(socket) {
|
|
329
|
+
for (const [channel, subs] of channelSubscriptions.entries()) {
|
|
330
|
+
subs.delete(socket);
|
|
331
|
+
if (subs.size === 0) {
|
|
332
|
+
channelSubscriptions.delete(channel);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
74
335
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
336
|
+
function publishToChannel(channel, message, excludeSocket) {
|
|
337
|
+
// Buffer the event for replay
|
|
338
|
+
const eventId = bufferEvent(channel, message);
|
|
339
|
+
const payload = JSON.stringify({
|
|
340
|
+
type: 'channel_message',
|
|
341
|
+
id: `ch_${eventId}`,
|
|
342
|
+
eventId,
|
|
343
|
+
channel,
|
|
344
|
+
message,
|
|
345
|
+
}) + '\n';
|
|
346
|
+
const sentSockets = new Set();
|
|
347
|
+
// Send to exact channel subscribers
|
|
348
|
+
const exactSubscribers = channelSubscriptions.get(channel);
|
|
349
|
+
if (exactSubscribers) {
|
|
350
|
+
for (const socket of exactSubscribers) {
|
|
351
|
+
if (socket !== excludeSocket && !socket.destroyed && !sentSockets.has(socket)) {
|
|
352
|
+
try {
|
|
353
|
+
socket.write(payload);
|
|
354
|
+
sentSockets.add(socket);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Socket write failed
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Send to wildcard subscribers
|
|
363
|
+
const channelPrefix = channel.split(':')[0];
|
|
364
|
+
if (channelPrefix) {
|
|
365
|
+
const wildcardChannel = `${channelPrefix}:*`;
|
|
366
|
+
const wildcardSubscribers = channelSubscriptions.get(wildcardChannel);
|
|
367
|
+
if (wildcardSubscribers) {
|
|
368
|
+
for (const socket of wildcardSubscribers) {
|
|
369
|
+
if (socket !== excludeSocket && !socket.destroyed && !sentSockets.has(socket)) {
|
|
370
|
+
try {
|
|
371
|
+
socket.write(payload);
|
|
372
|
+
sentSockets.add(socket);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// Socket write failed
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
logger.debug('Published to channel', {
|
|
382
|
+
channel,
|
|
383
|
+
eventId,
|
|
384
|
+
exactSubs: exactSubscribers?.size || 0,
|
|
385
|
+
wildcardSubs: channelSubscriptions.get(`${channelPrefix}:*`)?.size || 0,
|
|
386
|
+
});
|
|
387
|
+
return eventId;
|
|
388
|
+
}
|
|
389
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
390
|
+
// SESSION MANAGER (Lazy Initialization)
|
|
391
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
392
|
+
async function getOrCreateSessionManager(photonName, photonPath) {
|
|
393
|
+
let manager = sessionManagers.get(photonName);
|
|
394
|
+
if (manager) {
|
|
395
|
+
return manager;
|
|
396
|
+
}
|
|
397
|
+
// Need photonPath to initialize
|
|
398
|
+
const storedPath = photonPaths.get(photonName);
|
|
399
|
+
const pathToUse = photonPath || storedPath;
|
|
400
|
+
if (!pathToUse) {
|
|
401
|
+
logger.warn('Cannot initialize photon - no path provided', { photonName });
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
logger.info('Initializing session manager', { photonName, photonPath: pathToUse });
|
|
406
|
+
manager = new SessionManager(pathToUse, photonName, idleTimeout, logger.child({ scope: photonName }));
|
|
407
|
+
sessionManagers.set(photonName, manager);
|
|
408
|
+
photonPaths.set(photonName, pathToUse);
|
|
409
|
+
logger.info('Session manager initialized', { photonName });
|
|
410
|
+
return manager;
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
logger.error('Failed to initialize session manager', {
|
|
414
|
+
photonName,
|
|
415
|
+
error: getErrorMessage(error),
|
|
416
|
+
});
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
421
|
+
// PROMPT HANDLER
|
|
422
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
423
|
+
function createSocketPromptHandler(socket, requestId) {
|
|
424
|
+
return async (message, defaultValue) => {
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
pendingPrompts.set(requestId, {
|
|
427
|
+
resolve: (value) => resolve(value),
|
|
428
|
+
reject,
|
|
429
|
+
});
|
|
430
|
+
const promptResponse = {
|
|
431
|
+
type: 'prompt',
|
|
432
|
+
id: requestId,
|
|
433
|
+
prompt: {
|
|
434
|
+
type: 'text',
|
|
435
|
+
message,
|
|
436
|
+
default: defaultValue,
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
socket.write(JSON.stringify(promptResponse) + '\n');
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
444
|
+
// REQUEST HANDLER
|
|
445
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
446
|
+
async function handleRequest(request, socket) {
|
|
79
447
|
resetIdleTimer();
|
|
80
448
|
if (request.type === 'ping') {
|
|
449
|
+
return { type: 'pong', id: request.id };
|
|
450
|
+
}
|
|
451
|
+
if (request.type === 'shutdown') {
|
|
452
|
+
shutdown();
|
|
453
|
+
return { type: 'result', id: request.id, success: true, data: { message: 'Shutting down' } };
|
|
454
|
+
}
|
|
455
|
+
// Handle hot-reload request
|
|
456
|
+
if (request.type === 'reload') {
|
|
457
|
+
const photonName = request.photonName;
|
|
458
|
+
const photonPath = request.photonPath;
|
|
459
|
+
if (!photonName || !photonPath) {
|
|
460
|
+
return {
|
|
461
|
+
type: 'error',
|
|
462
|
+
id: request.id,
|
|
463
|
+
error: 'photonName and photonPath required for reload',
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const result = await reloadPhoton(photonName, photonPath);
|
|
81
467
|
return {
|
|
82
|
-
type: '
|
|
468
|
+
type: 'result',
|
|
83
469
|
id: request.id,
|
|
470
|
+
success: result.success,
|
|
471
|
+
data: result.success
|
|
472
|
+
? { message: 'Reload complete', sessionsUpdated: result.sessionsUpdated }
|
|
473
|
+
: { error: result.error },
|
|
84
474
|
};
|
|
85
475
|
}
|
|
86
|
-
|
|
87
|
-
|
|
476
|
+
// Handle prompt response
|
|
477
|
+
if (request.type === 'prompt_response') {
|
|
478
|
+
const pending = pendingPrompts.get(request.id);
|
|
479
|
+
if (pending) {
|
|
480
|
+
pendingPrompts.delete(request.id);
|
|
481
|
+
pending.resolve(request.promptValue ?? null);
|
|
482
|
+
}
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
// Handle channel subscribe
|
|
486
|
+
if (request.type === 'subscribe') {
|
|
487
|
+
const channel = request.channel;
|
|
488
|
+
const lastEventId = request.lastEventId;
|
|
489
|
+
let subs = channelSubscriptions.get(channel);
|
|
490
|
+
if (!subs) {
|
|
491
|
+
subs = new Set();
|
|
492
|
+
channelSubscriptions.set(channel, subs);
|
|
493
|
+
}
|
|
494
|
+
subs.add(socket);
|
|
495
|
+
logger.info('Client subscribed to channel', { channel, subscribers: subs.size });
|
|
496
|
+
// Replay missed events if lastEventId provided
|
|
497
|
+
if (lastEventId !== undefined) {
|
|
498
|
+
const parsedLastEventId = parseInt(String(lastEventId), 10) || 0;
|
|
499
|
+
const { events, refreshNeeded } = getEventsSince(channel, parsedLastEventId);
|
|
500
|
+
if (refreshNeeded) {
|
|
501
|
+
// Send refresh-needed signal
|
|
502
|
+
socket.write(JSON.stringify({
|
|
503
|
+
type: 'refresh_needed',
|
|
504
|
+
id: request.id,
|
|
505
|
+
channel,
|
|
506
|
+
}) + '\n');
|
|
507
|
+
logger.info('Replay: refresh needed', { channel, lastEventId });
|
|
508
|
+
}
|
|
509
|
+
else if (events.length > 0) {
|
|
510
|
+
// Replay events
|
|
511
|
+
for (const event of events) {
|
|
512
|
+
socket.write(JSON.stringify({
|
|
513
|
+
type: 'channel_message',
|
|
514
|
+
id: `replay_${event.id}`,
|
|
515
|
+
eventId: event.id,
|
|
516
|
+
channel: event.channel,
|
|
517
|
+
message: event.message,
|
|
518
|
+
replay: true,
|
|
519
|
+
}) + '\n');
|
|
520
|
+
}
|
|
521
|
+
logger.info('Replayed events', { channel, count: events.length });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
type: 'result',
|
|
526
|
+
id: request.id,
|
|
527
|
+
success: true,
|
|
528
|
+
data: { subscribed: true, channel },
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
// Handle channel unsubscribe
|
|
532
|
+
if (request.type === 'unsubscribe') {
|
|
533
|
+
const channel = request.channel;
|
|
534
|
+
const subs = channelSubscriptions.get(channel);
|
|
535
|
+
if (subs) {
|
|
536
|
+
subs.delete(socket);
|
|
537
|
+
if (subs.size === 0) {
|
|
538
|
+
channelSubscriptions.delete(channel);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
logger.info('Client unsubscribed from channel', { channel });
|
|
542
|
+
return { type: 'result', id: request.id, success: true, data: { unsubscribed: true, channel } };
|
|
543
|
+
}
|
|
544
|
+
// Handle channel publish
|
|
545
|
+
if (request.type === 'publish') {
|
|
546
|
+
const channel = request.channel;
|
|
547
|
+
const message = request.message;
|
|
548
|
+
const eventId = publishToChannel(channel, message, socket);
|
|
88
549
|
return {
|
|
89
550
|
type: 'result',
|
|
90
551
|
id: request.id,
|
|
91
552
|
success: true,
|
|
92
|
-
data: {
|
|
553
|
+
data: { published: true, channel, eventId },
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
// Handle get_events_since (for event replay)
|
|
557
|
+
if (request.type === 'get_events_since') {
|
|
558
|
+
const channel = request.channel;
|
|
559
|
+
const parsedLastEventId = parseInt(String(request.lastEventId || '0'), 10) || 0;
|
|
560
|
+
const { events, refreshNeeded } = getEventsSince(channel, parsedLastEventId);
|
|
561
|
+
return {
|
|
562
|
+
type: 'result',
|
|
563
|
+
id: request.id,
|
|
564
|
+
success: true,
|
|
565
|
+
data: { events, refreshNeeded },
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
// Handle lock acquisition
|
|
569
|
+
if (request.type === 'lock') {
|
|
570
|
+
const lockName = request.lockName;
|
|
571
|
+
const holder = request.sessionId || request.id;
|
|
572
|
+
const timeout = request.lockTimeout || DEFAULT_LOCK_TIMEOUT;
|
|
573
|
+
const acquired = acquireLock(lockName, holder, timeout);
|
|
574
|
+
return {
|
|
575
|
+
type: 'result',
|
|
576
|
+
id: request.id,
|
|
577
|
+
success: acquired,
|
|
578
|
+
data: {
|
|
579
|
+
acquired,
|
|
580
|
+
lockName,
|
|
581
|
+
holder,
|
|
582
|
+
...(acquired ? {} : { reason: 'Lock held by another client' }),
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
// Handle lock release
|
|
587
|
+
if (request.type === 'unlock') {
|
|
588
|
+
const lockName = request.lockName;
|
|
589
|
+
const holder = request.sessionId || request.id;
|
|
590
|
+
const released = releaseLock(lockName, holder);
|
|
591
|
+
return {
|
|
592
|
+
type: 'result',
|
|
593
|
+
id: request.id,
|
|
594
|
+
success: released,
|
|
595
|
+
data: {
|
|
596
|
+
released,
|
|
597
|
+
lockName,
|
|
598
|
+
...(released ? {} : { reason: 'Cannot release lock held by another client' }),
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
// Handle list locks
|
|
603
|
+
if (request.type === 'list_locks') {
|
|
604
|
+
const locks = Array.from(activeLocks.values());
|
|
605
|
+
return { type: 'result', id: request.id, success: true, data: { locks } };
|
|
606
|
+
}
|
|
607
|
+
// Handle job scheduling
|
|
608
|
+
if (request.type === 'schedule') {
|
|
609
|
+
const photonName = request.photonName;
|
|
610
|
+
if (!photonName) {
|
|
611
|
+
return { type: 'error', id: request.id, error: 'photonName required for scheduling' };
|
|
612
|
+
}
|
|
613
|
+
const job = {
|
|
614
|
+
id: request.jobId,
|
|
615
|
+
method: request.method,
|
|
616
|
+
args: request.args,
|
|
617
|
+
cron: request.cron,
|
|
618
|
+
runCount: 0,
|
|
619
|
+
createdAt: Date.now(),
|
|
620
|
+
createdBy: request.sessionId,
|
|
621
|
+
photonName,
|
|
622
|
+
};
|
|
623
|
+
const scheduled = scheduleJob(job);
|
|
624
|
+
return {
|
|
625
|
+
type: 'result',
|
|
626
|
+
id: request.id,
|
|
627
|
+
success: scheduled,
|
|
628
|
+
data: scheduled
|
|
629
|
+
? { scheduled: true, jobId: job.id, nextRun: job.nextRun }
|
|
630
|
+
: { scheduled: false, reason: 'Invalid cron expression' },
|
|
93
631
|
};
|
|
94
632
|
}
|
|
633
|
+
// Handle job unscheduling
|
|
634
|
+
if (request.type === 'unschedule') {
|
|
635
|
+
const jobId = request.jobId;
|
|
636
|
+
const unscheduled = unscheduleJob(jobId);
|
|
637
|
+
return { type: 'result', id: request.id, success: true, data: { unscheduled, jobId } };
|
|
638
|
+
}
|
|
639
|
+
// Handle list jobs
|
|
640
|
+
if (request.type === 'list_jobs') {
|
|
641
|
+
const jobs = Array.from(scheduledJobs.values());
|
|
642
|
+
return { type: 'result', id: request.id, success: true, data: { jobs } };
|
|
643
|
+
}
|
|
644
|
+
// Handle command execution
|
|
95
645
|
if (request.type === 'command') {
|
|
96
646
|
if (!request.method) {
|
|
97
|
-
return {
|
|
98
|
-
type: 'error',
|
|
99
|
-
id: request.id,
|
|
100
|
-
error: 'Method name required',
|
|
101
|
-
};
|
|
647
|
+
return { type: 'error', id: request.id, error: 'Method name required' };
|
|
102
648
|
}
|
|
649
|
+
const photonName = request.photonName;
|
|
650
|
+
if (!photonName) {
|
|
651
|
+
return { type: 'error', id: request.id, error: 'photonName required for commands' };
|
|
652
|
+
}
|
|
653
|
+
const sessionManager = await getOrCreateSessionManager(photonName, request.photonPath);
|
|
103
654
|
if (!sessionManager) {
|
|
104
655
|
return {
|
|
105
656
|
type: 'error',
|
|
106
657
|
id: request.id,
|
|
107
|
-
error: '
|
|
658
|
+
error: `Cannot initialize photon '${photonName}'. Provide photonPath in request.`,
|
|
108
659
|
};
|
|
109
660
|
}
|
|
110
661
|
try {
|
|
111
|
-
// Get or create session for this client
|
|
112
662
|
const session = await sessionManager.getOrCreateSession(request.sessionId, request.clientType);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
663
|
+
logger.info('Executing request', {
|
|
664
|
+
method: request.method,
|
|
665
|
+
photon: photonName,
|
|
666
|
+
sessionId: session.id,
|
|
667
|
+
});
|
|
668
|
+
setPromptHandler(createSocketPromptHandler(socket, request.id));
|
|
669
|
+
const outputHandler = (emit) => {
|
|
670
|
+
if (emit && typeof emit === 'object' && emit.channel) {
|
|
671
|
+
publishToChannel(emit.channel, emit, socket);
|
|
672
|
+
logger.debug('Published to channel', { channel: emit.channel });
|
|
673
|
+
}
|
|
121
674
|
};
|
|
675
|
+
const result = await sessionManager.loader.executeTool(session.instance, request.method, request.args || {}, { outputHandler });
|
|
676
|
+
setPromptHandler(null);
|
|
677
|
+
return { type: 'result', id: request.id, success: true, data: result };
|
|
122
678
|
}
|
|
123
679
|
catch (error) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
};
|
|
680
|
+
logger.error('Error executing request', {
|
|
681
|
+
method: request.method,
|
|
682
|
+
error: getErrorMessage(error),
|
|
683
|
+
});
|
|
684
|
+
setPromptHandler(null);
|
|
685
|
+
return { type: 'error', id: request.id, error: getErrorMessage(error) };
|
|
130
686
|
}
|
|
131
687
|
}
|
|
132
|
-
return {
|
|
133
|
-
type: 'error',
|
|
134
|
-
id: request.id,
|
|
135
|
-
error: `Unknown request type: ${request.type}`,
|
|
136
|
-
};
|
|
688
|
+
return { type: 'error', id: request.id, error: `Unknown request type: ${request.type}` };
|
|
137
689
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
690
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
691
|
+
// HOT RELOAD
|
|
692
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
693
|
+
async function reloadPhoton(photonName, newPhotonPath) {
|
|
694
|
+
try {
|
|
695
|
+
logger.info('Hot-reloading photon', { photonName, path: newPhotonPath });
|
|
696
|
+
const sessionManager = sessionManagers.get(photonName);
|
|
697
|
+
if (!sessionManager) {
|
|
698
|
+
// First time - just register the path
|
|
699
|
+
photonPaths.set(photonName, newPhotonPath);
|
|
700
|
+
return { success: true, sessionsUpdated: 0 };
|
|
701
|
+
}
|
|
702
|
+
await sessionManager.loader.reloadFile(newPhotonPath);
|
|
703
|
+
const sessions = sessionManager.getSessions();
|
|
704
|
+
let updatedCount = 0;
|
|
705
|
+
for (const session of sessions) {
|
|
706
|
+
try {
|
|
707
|
+
const newInstance = await sessionManager.loader.loadFile(newPhotonPath);
|
|
708
|
+
const oldInstance = session.instance;
|
|
709
|
+
if (oldInstance && typeof oldInstance === 'object') {
|
|
710
|
+
for (const key of Object.keys(oldInstance)) {
|
|
711
|
+
const value = oldInstance[key];
|
|
712
|
+
if (typeof value !== 'function' && key !== 'constructor') {
|
|
713
|
+
try {
|
|
714
|
+
newInstance[key] = value;
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
// Some properties may be read-only
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (sessionManager.updateSessionInstance(session.id, newInstance)) {
|
|
723
|
+
updatedCount++;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
catch (err) {
|
|
727
|
+
logger.error('Failed to update session instance', {
|
|
728
|
+
sessionId: session.id,
|
|
729
|
+
error: getErrorMessage(err),
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
publishToChannel(`system:${photonName}`, {
|
|
734
|
+
event: 'photon-reloaded',
|
|
735
|
+
timestamp: Date.now(),
|
|
736
|
+
sessionsUpdated: updatedCount,
|
|
737
|
+
});
|
|
738
|
+
logger.info('Photon reloaded successfully', { photonName, sessionsUpdated: updatedCount });
|
|
739
|
+
return { success: true, sessionsUpdated: updatedCount };
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
const errorMessage = getErrorMessage(error);
|
|
743
|
+
logger.error('Photon reload failed', { photonName, error: errorMessage });
|
|
744
|
+
return { success: false, error: errorMessage };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
748
|
+
// IDLE TIMER
|
|
749
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
750
|
+
function startIdleTimer() {
|
|
751
|
+
if (idleTimer) {
|
|
752
|
+
clearTimeout(idleTimer);
|
|
753
|
+
}
|
|
754
|
+
if (idleTimeout <= 0)
|
|
755
|
+
return;
|
|
756
|
+
idleTimer = setTimeout(() => {
|
|
757
|
+
let activeSubscribers = 0;
|
|
758
|
+
for (const subs of channelSubscriptions.values()) {
|
|
759
|
+
activeSubscribers += subs.size;
|
|
760
|
+
}
|
|
761
|
+
if (activeSubscribers > 0) {
|
|
762
|
+
logger.debug('Active channel subscribers, staying alive', { activeSubscribers });
|
|
763
|
+
startIdleTimer();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
// Check if any session manager has recent activity
|
|
767
|
+
let lastActivity = 0;
|
|
768
|
+
for (const manager of sessionManagers.values()) {
|
|
769
|
+
const activity = manager.getLastActivity();
|
|
770
|
+
if (activity > lastActivity) {
|
|
771
|
+
lastActivity = activity;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const idleTime = Date.now() - lastActivity;
|
|
775
|
+
if (idleTime >= idleTimeout && sessionManagers.size > 0) {
|
|
776
|
+
logger.warn('Idle timeout reached, shutting down', { idleTime });
|
|
777
|
+
shutdown();
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
startIdleTimer();
|
|
781
|
+
}
|
|
782
|
+
}, idleTimeout);
|
|
783
|
+
}
|
|
784
|
+
function resetIdleTimer() {
|
|
785
|
+
startIdleTimer();
|
|
786
|
+
}
|
|
787
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
788
|
+
// SERVER
|
|
789
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
141
790
|
function startServer() {
|
|
142
791
|
const server = net.createServer((socket) => {
|
|
143
|
-
|
|
792
|
+
logger.info('Client connected');
|
|
144
793
|
let buffer = '';
|
|
145
794
|
socket.on('data', async (chunk) => {
|
|
146
795
|
buffer += chunk.toString();
|
|
147
|
-
// Process complete JSON messages (newline-delimited)
|
|
148
796
|
const lines = buffer.split('\n');
|
|
149
|
-
buffer = lines.pop() || '';
|
|
797
|
+
buffer = lines.pop() || '';
|
|
150
798
|
for (const line of lines) {
|
|
151
799
|
if (!line.trim())
|
|
152
800
|
continue;
|
|
153
801
|
try {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
802
|
+
const parsed = JSON.parse(line);
|
|
803
|
+
if (!isValidDaemonRequest(parsed)) {
|
|
804
|
+
socket.write(JSON.stringify({ type: 'error', id: 'unknown', error: 'Invalid request format' }) +
|
|
805
|
+
'\n');
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
const request = parsed;
|
|
809
|
+
const response = await handleRequest(request, socket);
|
|
810
|
+
if (response !== null) {
|
|
811
|
+
socket.write(JSON.stringify(response) + '\n');
|
|
812
|
+
}
|
|
157
813
|
}
|
|
158
814
|
catch (error) {
|
|
159
|
-
|
|
160
|
-
socket.write(JSON.stringify({
|
|
161
|
-
type: 'error',
|
|
162
|
-
id: 'unknown',
|
|
163
|
-
error: error.message,
|
|
164
|
-
}) + '\n');
|
|
815
|
+
logger.error('Error processing request', { error: getErrorMessage(error) });
|
|
816
|
+
socket.write(JSON.stringify({ type: 'error', id: 'unknown', error: getErrorMessage(error) }) + '\n');
|
|
165
817
|
}
|
|
166
818
|
}
|
|
167
819
|
});
|
|
168
820
|
socket.on('end', () => {
|
|
169
|
-
|
|
821
|
+
logger.info('Client disconnected');
|
|
822
|
+
cleanupSocketSubscriptions(socket);
|
|
170
823
|
});
|
|
171
824
|
socket.on('error', (error) => {
|
|
172
|
-
|
|
825
|
+
logger.warn('Socket error', { error: getErrorMessage(error) });
|
|
826
|
+
cleanupSocketSubscriptions(socket);
|
|
827
|
+
});
|
|
828
|
+
socket.on('close', () => {
|
|
829
|
+
cleanupSocketSubscriptions(socket);
|
|
173
830
|
});
|
|
174
831
|
});
|
|
175
832
|
server.listen(socketPath, () => {
|
|
176
|
-
|
|
177
|
-
console.error(`[daemon-server] PID: ${process.pid}`);
|
|
833
|
+
logger.info('Global Photon daemon listening', { socketPath, pid: process.pid });
|
|
178
834
|
});
|
|
179
835
|
server.on('error', (error) => {
|
|
180
|
-
|
|
836
|
+
logger.error('Server error', { error: getErrorMessage(error) });
|
|
181
837
|
process.exit(1);
|
|
182
838
|
});
|
|
183
|
-
// Graceful shutdown handlers
|
|
184
839
|
process.on('SIGTERM', shutdown);
|
|
185
840
|
process.on('SIGINT', shutdown);
|
|
186
841
|
}
|
|
187
|
-
/**
|
|
188
|
-
* Shutdown daemon
|
|
189
|
-
*/
|
|
190
842
|
function shutdown() {
|
|
191
|
-
|
|
843
|
+
logger.info('Shutting down global daemon');
|
|
192
844
|
if (idleTimer) {
|
|
193
845
|
clearTimeout(idleTimer);
|
|
194
846
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
847
|
+
for (const timer of jobTimers.values()) {
|
|
848
|
+
clearTimeout(timer);
|
|
849
|
+
}
|
|
850
|
+
jobTimers.clear();
|
|
851
|
+
scheduledJobs.clear();
|
|
852
|
+
activeLocks.clear();
|
|
853
|
+
for (const manager of sessionManagers.values()) {
|
|
854
|
+
manager.destroy();
|
|
855
|
+
}
|
|
856
|
+
sessionManagers.clear();
|
|
857
|
+
if (webhookServer) {
|
|
858
|
+
webhookServer.close();
|
|
198
859
|
}
|
|
199
|
-
// Clean up socket file (Unix only)
|
|
200
860
|
if (fs.existsSync(socketPath) && process.platform !== 'win32') {
|
|
201
861
|
try {
|
|
202
862
|
fs.unlinkSync(socketPath);
|
|
203
863
|
}
|
|
204
|
-
catch
|
|
864
|
+
catch {
|
|
205
865
|
// Ignore cleanup errors
|
|
206
866
|
}
|
|
207
867
|
}
|
|
208
868
|
process.exit(0);
|
|
209
869
|
}
|
|
210
870
|
// Main execution
|
|
211
|
-
(
|
|
212
|
-
await initializeSessionManager();
|
|
871
|
+
(() => {
|
|
213
872
|
startServer();
|
|
873
|
+
startWebhookServer(WEBHOOK_PORT);
|
|
874
|
+
startIdleTimer();
|
|
214
875
|
})();
|
|
215
876
|
//# sourceMappingURL=server.js.map
|