@nodetool-ai/websocket 0.7.0-rc.11
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/dist/agent/agent-runtime.d.ts +47 -0
- package/dist/agent/agent-runtime.d.ts.map +1 -0
- package/dist/agent/agent-runtime.js +577 -0
- package/dist/agent/agent-runtime.js.map +1 -0
- package/dist/agent/codex-agent.d.ts +41 -0
- package/dist/agent/codex-agent.d.ts.map +1 -0
- package/dist/agent/codex-agent.js +334 -0
- package/dist/agent/codex-agent.js.map +1 -0
- package/dist/agent/index.d.ts +6 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +4 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/llm-agent.d.ts +51 -0
- package/dist/agent/llm-agent.d.ts.map +1 -0
- package/dist/agent/llm-agent.js +526 -0
- package/dist/agent/llm-agent.js.map +1 -0
- package/dist/agent/mcp-tool-server.d.ts +33 -0
- package/dist/agent/mcp-tool-server.d.ts.map +1 -0
- package/dist/agent/mcp-tool-server.js +292 -0
- package/dist/agent/mcp-tool-server.js.map +1 -0
- package/dist/agent/opencode-agent.d.ts +32 -0
- package/dist/agent/opencode-agent.d.ts.map +1 -0
- package/dist/agent/opencode-agent.js +332 -0
- package/dist/agent/opencode-agent.js.map +1 -0
- package/dist/agent/pi-agent.d.ts +42 -0
- package/dist/agent/pi-agent.d.ts.map +1 -0
- package/dist/agent/pi-agent.js +429 -0
- package/dist/agent/pi-agent.js.map +1 -0
- package/dist/agent/sdk-provider.d.ts +46 -0
- package/dist/agent/sdk-provider.d.ts.map +1 -0
- package/dist/agent/sdk-provider.js +50 -0
- package/dist/agent/sdk-provider.js.map +1 -0
- package/dist/agent/socket-route.d.ts +11 -0
- package/dist/agent/socket-route.d.ts.map +1 -0
- package/dist/agent/socket-route.js +262 -0
- package/dist/agent/socket-route.js.map +1 -0
- package/dist/agent/transport.d.ts +39 -0
- package/dist/agent/transport.d.ts.map +1 -0
- package/dist/agent/transport.js +17 -0
- package/dist/agent/transport.js.map +1 -0
- package/dist/agent/types.d.ts +11 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +10 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/collection-api.d.ts +14 -0
- package/dist/collection-api.d.ts.map +1 -0
- package/dist/collection-api.js +75 -0
- package/dist/collection-api.js.map +1 -0
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +2 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/file-api.d.ts +12 -0
- package/dist/file-api.d.ts.map +1 -0
- package/dist/file-api.js +76 -0
- package/dist/file-api.js.map +1 -0
- package/dist/http-api.d.ts +94 -0
- package/dist/http-api.d.ts.map +1 -0
- package/dist/http-api.js +1472 -0
- package/dist/http-api.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/asset-paths.d.ts +10 -0
- package/dist/lib/asset-paths.d.ts.map +1 -0
- package/dist/lib/asset-paths.js +45 -0
- package/dist/lib/asset-paths.js.map +1 -0
- package/dist/lib/bridge.d.ts +8 -0
- package/dist/lib/bridge.d.ts.map +1 -0
- package/dist/lib/bridge.js +70 -0
- package/dist/lib/bridge.js.map +1 -0
- package/dist/mcp-server.d.ts +32 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +842 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/models-api.d.ts +12 -0
- package/dist/models-api.d.ts.map +1 -0
- package/dist/models-api.js +730 -0
- package/dist/models-api.js.map +1 -0
- package/dist/oauth-api.d.ts +28 -0
- package/dist/oauth-api.d.ts.map +1 -0
- package/dist/oauth-api.js +681 -0
- package/dist/oauth-api.js.map +1 -0
- package/dist/openai-api.d.ts +63 -0
- package/dist/openai-api.d.ts.map +1 -0
- package/dist/openai-api.js +359 -0
- package/dist/openai-api.js.map +1 -0
- package/dist/plugins/websocket.d.ts +14 -0
- package/dist/plugins/websocket.d.ts.map +1 -0
- package/dist/plugins/websocket.js +431 -0
- package/dist/plugins/websocket.js.map +1 -0
- package/dist/resolve-media-urls.d.ts +19 -0
- package/dist/resolve-media-urls.d.ts.map +1 -0
- package/dist/resolve-media-urls.js +109 -0
- package/dist/resolve-media-urls.js.map +1 -0
- package/dist/routes/assets.d.ts +22 -0
- package/dist/routes/assets.d.ts.map +1 -0
- package/dist/routes/assets.js +168 -0
- package/dist/routes/assets.js.map +1 -0
- package/dist/routes/collections.d.ts +16 -0
- package/dist/routes/collections.d.ts.map +1 -0
- package/dist/routes/collections.js +32 -0
- package/dist/routes/collections.js.map +1 -0
- package/dist/routes/files.d.ts +12 -0
- package/dist/routes/files.d.ts.map +1 -0
- package/dist/routes/files.js +11 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/health.d.ts +4 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +41 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/jobs.d.ts +16 -0
- package/dist/routes/jobs.d.ts.map +1 -0
- package/dist/routes/jobs.js +25 -0
- package/dist/routes/jobs.js.map +1 -0
- package/dist/routes/nodes.d.ts +8 -0
- package/dist/routes/nodes.d.ts.map +1 -0
- package/dist/routes/nodes.js +124 -0
- package/dist/routes/nodes.js.map +1 -0
- package/dist/routes/oauth.d.ts +8 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +19 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/openai.d.ts +8 -0
- package/dist/routes/openai.d.ts.map +1 -0
- package/dist/routes/openai.js +20 -0
- package/dist/routes/openai.js.map +1 -0
- package/dist/routes/storage.d.ts +12 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +13 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/workflows.d.ts +18 -0
- package/dist/routes/workflows.d.ts.map +1 -0
- package/dist/routes/workflows.js +25 -0
- package/dist/routes/workflows.js.map +1 -0
- package/dist/routes/workspace.d.ts +15 -0
- package/dist/routes/workspace.d.ts.map +1 -0
- package/dist/routes/workspace.js +24 -0
- package/dist/routes/workspace.js.map +1 -0
- package/dist/screenshot-server.d.ts +21 -0
- package/dist/screenshot-server.d.ts.map +1 -0
- package/dist/screenshot-server.js +222 -0
- package/dist/screenshot-server.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +783 -0
- package/dist/server.js.map +1 -0
- package/dist/settings-registry.d.ts +29 -0
- package/dist/settings-registry.d.ts.map +1 -0
- package/dist/settings-registry.js +117 -0
- package/dist/settings-registry.js.map +1 -0
- package/dist/storage-api.d.ts +5 -0
- package/dist/storage-api.d.ts.map +1 -0
- package/dist/storage-api.js +221 -0
- package/dist/storage-api.js.map +1 -0
- package/dist/test-ui-server.d.ts +19 -0
- package/dist/test-ui-server.d.ts.map +1 -0
- package/dist/test-ui-server.js +1256 -0
- package/dist/test-ui-server.js.map +1 -0
- package/dist/trpc/context.d.ts +21 -0
- package/dist/trpc/context.d.ts.map +1 -0
- package/dist/trpc/context.js +10 -0
- package/dist/trpc/context.js.map +1 -0
- package/dist/trpc/error-formatter.d.ts +18 -0
- package/dist/trpc/error-formatter.d.ts.map +1 -0
- package/dist/trpc/error-formatter.js +41 -0
- package/dist/trpc/error-formatter.js.map +1 -0
- package/dist/trpc/index.d.ts +18 -0
- package/dist/trpc/index.d.ts.map +1 -0
- package/dist/trpc/index.js +13 -0
- package/dist/trpc/index.js.map +1 -0
- package/dist/trpc/middleware.d.ts +8 -0
- package/dist/trpc/middleware.d.ts.map +1 -0
- package/dist/trpc/middleware.js +12 -0
- package/dist/trpc/middleware.js.map +1 -0
- package/dist/trpc/router.d.ts +2543 -0
- package/dist/trpc/router.d.ts.map +1 -0
- package/dist/trpc/router.js +43 -0
- package/dist/trpc/router.js.map +1 -0
- package/dist/trpc/routers/assets.d.ts +229 -0
- package/dist/trpc/routers/assets.d.ts.map +1 -0
- package/dist/trpc/routers/assets.js +263 -0
- package/dist/trpc/routers/assets.js.map +1 -0
- package/dist/trpc/routers/collections.d.ts +88 -0
- package/dist/trpc/routers/collections.d.ts.map +1 -0
- package/dist/trpc/routers/collections.js +166 -0
- package/dist/trpc/routers/collections.js.map +1 -0
- package/dist/trpc/routers/costs.d.ts +129 -0
- package/dist/trpc/routers/costs.d.ts.map +1 -0
- package/dist/trpc/routers/costs.js +95 -0
- package/dist/trpc/routers/costs.js.map +1 -0
- package/dist/trpc/routers/files.d.ts +34 -0
- package/dist/trpc/routers/files.d.ts.map +1 -0
- package/dist/trpc/routers/files.js +92 -0
- package/dist/trpc/routers/files.js.map +1 -0
- package/dist/trpc/routers/jobs.d.ts +92 -0
- package/dist/trpc/routers/jobs.d.ts.map +1 -0
- package/dist/trpc/routers/jobs.js +100 -0
- package/dist/trpc/routers/jobs.js.map +1 -0
- package/dist/trpc/routers/mcp-config.d.ts +69 -0
- package/dist/trpc/routers/mcp-config.d.ts.map +1 -0
- package/dist/trpc/routers/mcp-config.js +290 -0
- package/dist/trpc/routers/mcp-config.js.map +1 -0
- package/dist/trpc/routers/messages.d.ts +121 -0
- package/dist/trpc/routers/messages.d.ts.map +1 -0
- package/dist/trpc/routers/messages.js +113 -0
- package/dist/trpc/routers/messages.js.map +1 -0
- package/dist/trpc/routers/models.d.ts +748 -0
- package/dist/trpc/routers/models.d.ts.map +1 -0
- package/dist/trpc/routers/models.js +1107 -0
- package/dist/trpc/routers/models.js.map +1 -0
- package/dist/trpc/routers/nodes.d.ts +48 -0
- package/dist/trpc/routers/nodes.d.ts.map +1 -0
- package/dist/trpc/routers/nodes.js +57 -0
- package/dist/trpc/routers/nodes.js.map +1 -0
- package/dist/trpc/routers/sandboxes.d.ts +64 -0
- package/dist/trpc/routers/sandboxes.d.ts.map +1 -0
- package/dist/trpc/routers/sandboxes.js +327 -0
- package/dist/trpc/routers/sandboxes.js.map +1 -0
- package/dist/trpc/routers/settings.d.ts +113 -0
- package/dist/trpc/routers/settings.d.ts.map +1 -0
- package/dist/trpc/routers/settings.js +193 -0
- package/dist/trpc/routers/settings.js.map +1 -0
- package/dist/trpc/routers/skills.d.ts +41 -0
- package/dist/trpc/routers/skills.d.ts.map +1 -0
- package/dist/trpc/routers/skills.js +194 -0
- package/dist/trpc/routers/skills.js.map +1 -0
- package/dist/trpc/routers/storage.d.ts +44 -0
- package/dist/trpc/routers/storage.d.ts.map +1 -0
- package/dist/trpc/routers/storage.js +153 -0
- package/dist/trpc/routers/storage.js.map +1 -0
- package/dist/trpc/routers/threads.d.ts +95 -0
- package/dist/trpc/routers/threads.d.ts.map +1 -0
- package/dist/trpc/routers/threads.js +150 -0
- package/dist/trpc/routers/threads.js.map +1 -0
- package/dist/trpc/routers/users.d.ts +79 -0
- package/dist/trpc/routers/users.d.ts.map +1 -0
- package/dist/trpc/routers/users.js +125 -0
- package/dist/trpc/routers/users.js.map +1 -0
- package/dist/trpc/routers/workflows.d.ts +681 -0
- package/dist/trpc/routers/workflows.d.ts.map +1 -0
- package/dist/trpc/routers/workflows.js +725 -0
- package/dist/trpc/routers/workflows.js.map +1 -0
- package/dist/trpc/routers/workspace.d.ts +127 -0
- package/dist/trpc/routers/workspace.d.ts.map +1 -0
- package/dist/trpc/routers/workspace.js +203 -0
- package/dist/trpc/routers/workspace.js.map +1 -0
- package/dist/unified-websocket-runner.d.ts +280 -0
- package/dist/unified-websocket-runner.d.ts.map +1 -0
- package/dist/unified-websocket-runner.js +3840 -0
- package/dist/unified-websocket-runner.js.map +1 -0
- package/dist/workspace-api.d.ts +14 -0
- package/dist/workspace-api.d.ts.map +1 -0
- package/dist/workspace-api.js +104 -0
- package/dist/workspace-api.js.map +1 -0
- package/dist/ws-adapter.d.ts +25 -0
- package/dist/ws-adapter.d.ts.map +1 -0
- package/dist/ws-adapter.js +63 -0
- package/dist/ws-adapter.js.map +1 -0
- package/package.json +92 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* NodeTool WebSocket + HTTP server entry point — Fastify edition.
|
|
4
|
+
*
|
|
5
|
+
* TLS: set TLS_CERT and TLS_KEY to paths of cert.pem / key.pem.
|
|
6
|
+
* An HTTP→HTTPS redirect server starts on port 80 when TLS is active.
|
|
7
|
+
* Connect the CLI with: npm run chat -- --url ws://localhost:7777/ws
|
|
8
|
+
*/
|
|
9
|
+
import { join, resolve, dirname } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { createServer as createHttpServer } from "node:http";
|
|
14
|
+
import crypto from "node:crypto";
|
|
15
|
+
import { createLogger, getDefaultDbPath } from "@nodetool-ai/config";
|
|
16
|
+
import { NodeRegistry } from "@nodetool-ai/node-sdk";
|
|
17
|
+
import { registerBaseNodes } from "@nodetool-ai/base-nodes";
|
|
18
|
+
import { registerElevenLabsNodes } from "@nodetool-ai/elevenlabs-nodes";
|
|
19
|
+
import { registerTransformersJsNodes } from "@nodetool-ai/transformers-js-nodes";
|
|
20
|
+
import { registerTransformersJsProvider } from "@nodetool-ai/transformers-js-provider";
|
|
21
|
+
import { registerFalNodes } from "@nodetool-ai/fal-nodes";
|
|
22
|
+
import { registerKieNodes } from "@nodetool-ai/kie-nodes";
|
|
23
|
+
import { registerReplicateNodes } from "@nodetool-ai/replicate-nodes";
|
|
24
|
+
import { setSecretResolver, PythonStdioBridge } from "@nodetool-ai/runtime";
|
|
25
|
+
import { initMasterKey } from "@nodetool-ai/security";
|
|
26
|
+
import { initDb, getSecret } from "@nodetool-ai/models";
|
|
27
|
+
import { GoogleSearchTool, GoogleNewsTool, GoogleImagesTool, GoogleGroundedSearchTool, GoogleImageGenerationTool, OpenAIWebSearchTool, OpenAIImageGenerationTool, OpenAITextToSpeechTool, BrowserTool, ScreenshotTool, HttpRequestTool, CalculatorTool, RunCodeTool, StatisticsTool, GeometryTool, ConversionTool, SearchEmailTool, ArchiveEmailTool, AddLabelToEmailTool, DataForSEOSearchTool, DataForSEONewsTool, DataForSEOImagesTool } from "@nodetool-ai/agents";
|
|
28
|
+
import { registerPythonProviders } from "./models-api.js";
|
|
29
|
+
import { handleMcpHttpRequest } from "./mcp-server.js";
|
|
30
|
+
import Fastify from "fastify";
|
|
31
|
+
import fastifyWebSocket from "@fastify/websocket";
|
|
32
|
+
import fastifyCors from "@fastify/cors";
|
|
33
|
+
import fastifyStatic from "@fastify/static";
|
|
34
|
+
import { encode } from "@msgpack/msgpack";
|
|
35
|
+
import { SupabaseAuthProvider, LocalAuthProvider } from "@nodetool-ai/auth";
|
|
36
|
+
import { fastifyTRPCPlugin } from "@trpc/server/adapters/fastify";
|
|
37
|
+
import { appRouter } from "./trpc/router.js";
|
|
38
|
+
import { createContextFactory } from "./trpc/context.js";
|
|
39
|
+
import websocketPlugin from "./plugins/websocket.js";
|
|
40
|
+
import healthRoute from "./routes/health.js";
|
|
41
|
+
import assetsRoutes from "./routes/assets.js";
|
|
42
|
+
import workflowsRoutes from "./routes/workflows.js";
|
|
43
|
+
import jobsRoutes from "./routes/jobs.js";
|
|
44
|
+
import nodesRoutes from "./routes/nodes.js";
|
|
45
|
+
import storageRoutes from "./routes/storage.js";
|
|
46
|
+
import openaiRoutes from "./routes/openai.js";
|
|
47
|
+
import oauthRoutes from "./routes/oauth.js";
|
|
48
|
+
import workspaceRoutes from "./routes/workspace.js";
|
|
49
|
+
import filesRoutes from "./routes/files.js";
|
|
50
|
+
import collectionsRoutes from "./routes/collections.js";
|
|
51
|
+
import { agentSocketRoute, getAgentRuntime } from "./agent/index.js";
|
|
52
|
+
// @llamaindex/liteparse bundles a webpack pdf.js whose `isNodeJS` heuristic
|
|
53
|
+
// resolves to false inside Electron utilityProcess (process.type === "utility"),
|
|
54
|
+
// causing it to take browser-only code paths that reference the global `document`
|
|
55
|
+
// and throw `ReferenceError: document is not defined` on parse(). Reporting
|
|
56
|
+
// process.type as "browser" — the value Electron's main process uses — flips
|
|
57
|
+
// the heuristic back to Node mode so NodeBinaryDataFactory / fs-based asset
|
|
58
|
+
// loading is selected. No-op outside Electron.
|
|
59
|
+
{
|
|
60
|
+
const proc = process;
|
|
61
|
+
if (proc.versions?.electron && proc.type && proc.type !== "browser") {
|
|
62
|
+
try {
|
|
63
|
+
Object.defineProperty(process, "type", {
|
|
64
|
+
value: "browser",
|
|
65
|
+
writable: true,
|
|
66
|
+
configurable: true
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Property is non-configurable in some Electron builds; ignore.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const log = createLogger("nodetool.websocket.server");
|
|
75
|
+
const startupT0 = performance.now();
|
|
76
|
+
function startupMs() {
|
|
77
|
+
return `${(performance.now() - startupT0).toFixed(0)}ms`;
|
|
78
|
+
}
|
|
79
|
+
async function broadcastResourceChange(app, change) {
|
|
80
|
+
const message = encode({
|
|
81
|
+
type: "resource_change",
|
|
82
|
+
...change
|
|
83
|
+
});
|
|
84
|
+
for (const client of app.websocketServer.clients) {
|
|
85
|
+
if (client.readyState === 1) {
|
|
86
|
+
client.send(message);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function notifyPythonBridgeResourceChanges(app, pythonBridge) {
|
|
91
|
+
await broadcastResourceChange(app, {
|
|
92
|
+
event: "updated",
|
|
93
|
+
resource_type: "metadata",
|
|
94
|
+
resource: { id: "nodes", etag: String(Date.now()) }
|
|
95
|
+
});
|
|
96
|
+
const registered = await registerPythonProviders(pythonBridge);
|
|
97
|
+
if (registered.length > 0) {
|
|
98
|
+
log.info(`Registered Python providers: ${registered.join(", ")}`);
|
|
99
|
+
}
|
|
100
|
+
const discoveredProviders = await pythonBridge.listProviders();
|
|
101
|
+
const registeredProviders = new Set(registered);
|
|
102
|
+
for (const provider of discoveredProviders) {
|
|
103
|
+
await broadcastResourceChange(app, {
|
|
104
|
+
event: registeredProviders.has(provider.id) ? "created" : "updated",
|
|
105
|
+
resource_type: "provider",
|
|
106
|
+
resource: {
|
|
107
|
+
id: provider.id,
|
|
108
|
+
provider: provider.id,
|
|
109
|
+
capabilities: provider.capabilities,
|
|
110
|
+
etag: String(Date.now())
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
await broadcastResourceChange(app, {
|
|
114
|
+
event: "updated",
|
|
115
|
+
resource_type: "model",
|
|
116
|
+
resource: {
|
|
117
|
+
id: provider.id,
|
|
118
|
+
provider: provider.id,
|
|
119
|
+
etag: String(Date.now())
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Database setup
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
const dbPath = getDefaultDbPath();
|
|
128
|
+
try {
|
|
129
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
130
|
+
initDb(dbPath);
|
|
131
|
+
log.info(`Database ready [${startupMs()}]`, { path: dbPath });
|
|
132
|
+
// Initialize master key from keychain before any secret access.
|
|
133
|
+
// This must happen before setSecretResolver so that getMasterKey() (sync)
|
|
134
|
+
// returns the keychain key rather than auto-generating a new one.
|
|
135
|
+
await initMasterKey();
|
|
136
|
+
setSecretResolver((key, userId) => getSecret(key, userId).then((v) => v ?? undefined));
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
log.error("Database setup failed", err instanceof Error ? err : new Error(String(err)));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Python pip metadata root detection
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
function detectPipMetadataRoots() {
|
|
146
|
+
const script = `
|
|
147
|
+
import json, pathlib, subprocess, sys
|
|
148
|
+
roots = set()
|
|
149
|
+
try:
|
|
150
|
+
# Discover all nodetool-* packages
|
|
151
|
+
list_proc = subprocess.run(
|
|
152
|
+
[sys.executable, "-m", "pip", "list", "--format=json"],
|
|
153
|
+
capture_output=True, text=True, check=False,
|
|
154
|
+
)
|
|
155
|
+
pkg_names = [
|
|
156
|
+
p["name"] for p in json.loads(list_proc.stdout or "[]")
|
|
157
|
+
if p["name"].startswith("nodetool-")
|
|
158
|
+
] or ["nodetool-core", "nodetool-base"]
|
|
159
|
+
proc = subprocess.run(
|
|
160
|
+
[sys.executable, "-m", "pip", "show", "-f"] + pkg_names,
|
|
161
|
+
capture_output=True, text=True, check=False,
|
|
162
|
+
)
|
|
163
|
+
output = proc.stdout or ""
|
|
164
|
+
except Exception:
|
|
165
|
+
output = ""
|
|
166
|
+
location = None
|
|
167
|
+
in_files = False
|
|
168
|
+
for raw in output.splitlines():
|
|
169
|
+
line = raw.rstrip("\\n")
|
|
170
|
+
if line.startswith("Name: "):
|
|
171
|
+
location = None; in_files = False; continue
|
|
172
|
+
if line.startswith("Location: "):
|
|
173
|
+
location = line.split(":", 1)[1].strip(); continue
|
|
174
|
+
if line.startswith("Editable project location: "):
|
|
175
|
+
editable = line.split(":", 1)[1].strip()
|
|
176
|
+
if editable: roots.add(editable)
|
|
177
|
+
continue
|
|
178
|
+
if line.startswith("Files:"): in_files = True; continue
|
|
179
|
+
if line.startswith("---"):
|
|
180
|
+
location = None; in_files = False; continue
|
|
181
|
+
if not in_files or not location or not line.startswith(" "): continue
|
|
182
|
+
rel = line.strip().replace("\\\\", "/")
|
|
183
|
+
if "package_metadata" not in rel: continue
|
|
184
|
+
abs_path = (pathlib.Path(location) / rel).resolve()
|
|
185
|
+
metadata_dir = abs_path if abs_path.is_dir() else abs_path.parent
|
|
186
|
+
roots.add(str(metadata_dir))
|
|
187
|
+
print(json.dumps(sorted(roots)))
|
|
188
|
+
`;
|
|
189
|
+
for (const python of ["python3", "python"]) {
|
|
190
|
+
const proc = spawnSync(python, ["-c", script], {
|
|
191
|
+
encoding: "utf8",
|
|
192
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
193
|
+
});
|
|
194
|
+
if (proc.status !== 0 || !proc.stdout)
|
|
195
|
+
continue;
|
|
196
|
+
try {
|
|
197
|
+
const roots = JSON.parse(proc.stdout.trim());
|
|
198
|
+
if (Array.isArray(roots)) {
|
|
199
|
+
return roots.filter((p) => typeof p === "string" && p.length > 0 && existsSync(p));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// try next python executable
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
const metadataRoots = detectPipMetadataRoots();
|
|
209
|
+
// Also scan local TS node packages that have nodetool/package_metadata
|
|
210
|
+
const localPackagesDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../");
|
|
211
|
+
if (existsSync(localPackagesDir)) {
|
|
212
|
+
try {
|
|
213
|
+
for (const entry of readdirSync(localPackagesDir, {
|
|
214
|
+
withFileTypes: true
|
|
215
|
+
})) {
|
|
216
|
+
if (!entry.isDirectory())
|
|
217
|
+
continue;
|
|
218
|
+
const metaDir = join(localPackagesDir, entry.name, "nodetool", "package_metadata");
|
|
219
|
+
if (existsSync(metaDir)) {
|
|
220
|
+
const resolved = resolve(localPackagesDir, entry.name);
|
|
221
|
+
if (!metadataRoots.includes(resolved)) {
|
|
222
|
+
metadataRoots.push(resolved);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// ignore scan errors
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
log.info(`Metadata roots detected [${startupMs()}]`, {
|
|
232
|
+
roots: metadataRoots
|
|
233
|
+
});
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Node registry
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
const registry = new NodeRegistry();
|
|
238
|
+
registry.loadPythonMetadata({ roots: metadataRoots, maxDepth: 8 });
|
|
239
|
+
log.info(`Python metadata loaded [${startupMs()}]`);
|
|
240
|
+
registerBaseNodes(registry);
|
|
241
|
+
registerElevenLabsNodes(registry);
|
|
242
|
+
registerTransformersJsNodes(registry);
|
|
243
|
+
registerFalNodes(registry);
|
|
244
|
+
registerKieNodes(registry);
|
|
245
|
+
registerReplicateNodes(registry);
|
|
246
|
+
registerTransformersJsProvider();
|
|
247
|
+
log.info(`Node registry ready [${startupMs()}]`);
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Python bridge
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
const pythonBridge = new PythonStdioBridge({
|
|
252
|
+
workerArgs: process.env["NODETOOL_WORKER_NAMESPACES"]
|
|
253
|
+
? ["--namespaces", process.env["NODETOOL_WORKER_NAMESPACES"]]
|
|
254
|
+
: []
|
|
255
|
+
});
|
|
256
|
+
let pythonBridgeReady = false;
|
|
257
|
+
function logPythonBridgeDiagnostics(context) {
|
|
258
|
+
const loadErrors = pythonBridge.getLoadErrors?.() ?? [];
|
|
259
|
+
if (loadErrors.length === 0)
|
|
260
|
+
return;
|
|
261
|
+
log.warn(`Python bridge ${context} with ${loadErrors.length} load error(s)`);
|
|
262
|
+
for (const entry of loadErrors.slice(0, 10)) {
|
|
263
|
+
log.warn(`[python-worker][load-error] ${entry.module} (${entry.phase}): ${entry.error}`);
|
|
264
|
+
}
|
|
265
|
+
if (loadErrors.length > 10) {
|
|
266
|
+
log.warn(`[python-worker][load-error] ... ${loadErrors.length - 10} additional load error(s) omitted`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
pythonBridge.on("stderr", (msg) => {
|
|
270
|
+
for (const line of msg.split("\n")) {
|
|
271
|
+
if (line.trim())
|
|
272
|
+
log.debug(`[python-worker] ${line}`);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
pythonBridge.on("exit", (code) => {
|
|
276
|
+
log.warn(`Python worker exited with code ${code}`);
|
|
277
|
+
pythonBridgeReady = false;
|
|
278
|
+
});
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Tool registry
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
const builtinToolClasses = [
|
|
283
|
+
GoogleSearchTool,
|
|
284
|
+
GoogleNewsTool,
|
|
285
|
+
GoogleImagesTool,
|
|
286
|
+
GoogleGroundedSearchTool,
|
|
287
|
+
GoogleImageGenerationTool,
|
|
288
|
+
OpenAIWebSearchTool,
|
|
289
|
+
OpenAIImageGenerationTool,
|
|
290
|
+
OpenAITextToSpeechTool,
|
|
291
|
+
BrowserTool,
|
|
292
|
+
ScreenshotTool,
|
|
293
|
+
HttpRequestTool,
|
|
294
|
+
CalculatorTool,
|
|
295
|
+
RunCodeTool,
|
|
296
|
+
StatisticsTool,
|
|
297
|
+
GeometryTool,
|
|
298
|
+
ConversionTool,
|
|
299
|
+
SearchEmailTool,
|
|
300
|
+
ArchiveEmailTool,
|
|
301
|
+
AddLabelToEmailTool,
|
|
302
|
+
DataForSEOSearchTool,
|
|
303
|
+
DataForSEONewsTool,
|
|
304
|
+
DataForSEOImagesTool
|
|
305
|
+
];
|
|
306
|
+
// Lazy tool class map — defers instantiation until first access.
|
|
307
|
+
let _toolClassMap = null;
|
|
308
|
+
function getToolClassMap() {
|
|
309
|
+
if (!_toolClassMap) {
|
|
310
|
+
_toolClassMap = new Map();
|
|
311
|
+
for (const cls of builtinToolClasses) {
|
|
312
|
+
const instance = new cls();
|
|
313
|
+
_toolClassMap.set(instance.name, cls);
|
|
314
|
+
}
|
|
315
|
+
log.info(`Tool class map built (${_toolClassMap.size} tools) [${startupMs()}]`);
|
|
316
|
+
}
|
|
317
|
+
return _toolClassMap;
|
|
318
|
+
}
|
|
319
|
+
// Expose as a getter-backed object so existing code using toolClassMap works unchanged.
|
|
320
|
+
const toolClassMap = new Proxy(new Map(), {
|
|
321
|
+
get(_target, prop, _receiver) {
|
|
322
|
+
const map = getToolClassMap();
|
|
323
|
+
const value = map[prop];
|
|
324
|
+
if (typeof value === "function") {
|
|
325
|
+
return value.bind(map);
|
|
326
|
+
}
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// TLS configuration
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
function findCert(envVar, filename) {
|
|
334
|
+
const fromEnv = process.env[envVar];
|
|
335
|
+
if (fromEnv && existsSync(fromEnv))
|
|
336
|
+
return fromEnv;
|
|
337
|
+
// Walk up from cwd looking for the cert file
|
|
338
|
+
let dir = resolve(process.cwd());
|
|
339
|
+
for (let i = 0; i < 5; i++) {
|
|
340
|
+
const candidate = join(dir, filename);
|
|
341
|
+
if (existsSync(candidate))
|
|
342
|
+
return candidate;
|
|
343
|
+
const parent = dirname(dir);
|
|
344
|
+
if (parent === dir)
|
|
345
|
+
break;
|
|
346
|
+
dir = parent;
|
|
347
|
+
}
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
const tlsCertPath = findCert("TLS_CERT", "cert.pem");
|
|
351
|
+
const tlsKeyPath = findCert("TLS_KEY", "key.pem");
|
|
352
|
+
const tlsEnabled = Boolean(tlsCertPath && tlsKeyPath);
|
|
353
|
+
let httpsOptions;
|
|
354
|
+
if (tlsEnabled && tlsCertPath && tlsKeyPath) {
|
|
355
|
+
httpsOptions = {
|
|
356
|
+
cert: readFileSync(tlsCertPath),
|
|
357
|
+
key: readFileSync(tlsKeyPath)
|
|
358
|
+
};
|
|
359
|
+
log.info("TLS enabled", { cert: tlsCertPath, key: tlsKeyPath });
|
|
360
|
+
}
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// Host / port
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
const port = Number(process.env["PORT"] ?? 7777);
|
|
365
|
+
const isProduction = process.env["NODETOOL_ENV"] === "production";
|
|
366
|
+
// In production, bind to all interfaces unless HOST is explicitly set
|
|
367
|
+
const host = process.env["HOST"] ?? (isProduction ? "0.0.0.0" : "127.0.0.1");
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// Fastify app
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
const app = Fastify({
|
|
372
|
+
...(httpsOptions ? { https: httpsOptions } : {}),
|
|
373
|
+
trustProxy: true,
|
|
374
|
+
bodyLimit: 100 * 1024 * 1024, // 100 MB
|
|
375
|
+
logger: false,
|
|
376
|
+
// tRPC's httpBatchLink encodes all batched procedure names into a single
|
|
377
|
+
// URL path segment joined with commas (e.g. `/trpc/foo,bar,baz`). Fastify's
|
|
378
|
+
// default `maxParamLength` of 100 rejects larger batches with a 404 — bump
|
|
379
|
+
// it so batches up to ~50 procedures route correctly.
|
|
380
|
+
routerOptions: {
|
|
381
|
+
ignoreTrailingSlash: true,
|
|
382
|
+
maxParamLength: 2000
|
|
383
|
+
},
|
|
384
|
+
genReqId: (req) => {
|
|
385
|
+
return req.headers["x-request-id"] || crypto.randomUUID();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Request ID correlation
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
app.addHook("onRequest", async (request) => {
|
|
392
|
+
request.log.info({ reqId: request.id, method: request.method, url: request.url }, "incoming request");
|
|
393
|
+
});
|
|
394
|
+
app.addHook("onSend", async (request, reply) => {
|
|
395
|
+
reply.header("X-Request-Id", request.id);
|
|
396
|
+
});
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Auth
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
const supabaseUrl = process.env["SUPABASE_URL"];
|
|
401
|
+
const supabaseKey = process.env["SUPABASE_KEY"];
|
|
402
|
+
const supabaseMode = Boolean(supabaseUrl && supabaseKey);
|
|
403
|
+
const supabaseProvider = supabaseMode
|
|
404
|
+
? new SupabaseAuthProvider({
|
|
405
|
+
supabaseUrl: supabaseUrl,
|
|
406
|
+
supabaseKey: supabaseKey
|
|
407
|
+
})
|
|
408
|
+
: null;
|
|
409
|
+
const localProvider = supabaseProvider ? null : new LocalAuthProvider();
|
|
410
|
+
app.decorateRequest("userId", null);
|
|
411
|
+
app.addHook("onRequest", async (req, reply) => {
|
|
412
|
+
// Let CORS preflight through — the @fastify/cors plugin handles OPTIONS responses
|
|
413
|
+
if (req.method === "OPTIONS")
|
|
414
|
+
return;
|
|
415
|
+
// Public routes — no auth required
|
|
416
|
+
const pathname = req.url.split("?")[0];
|
|
417
|
+
if (pathname === "/health" ||
|
|
418
|
+
pathname === "/ready" ||
|
|
419
|
+
pathname.startsWith("/api/oauth/") ||
|
|
420
|
+
pathname === "/api/assets/packages" ||
|
|
421
|
+
pathname.startsWith("/api/assets/packages/") ||
|
|
422
|
+
pathname === "/api/nodes/metadata") {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Static frontend assets don't require auth (served by fastifyStatic)
|
|
426
|
+
if (hasStaticApp &&
|
|
427
|
+
req.method === "GET" &&
|
|
428
|
+
!pathname.startsWith("/api") &&
|
|
429
|
+
!pathname.startsWith("/ws") &&
|
|
430
|
+
!pathname.startsWith("/v1") &&
|
|
431
|
+
!pathname.startsWith("/trpc")) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// Extract token from the appropriate source
|
|
435
|
+
const isWs = req.headers["upgrade"]?.toLowerCase() === "websocket";
|
|
436
|
+
const searchParams = new URLSearchParams(req.url.split("?")[1] ?? "");
|
|
437
|
+
const provider = supabaseProvider ?? localProvider;
|
|
438
|
+
const token = isWs
|
|
439
|
+
? provider.extractTokenFromWs(req.headers, searchParams)
|
|
440
|
+
: provider.extractTokenFromHeaders(req.headers);
|
|
441
|
+
if (supabaseMode) {
|
|
442
|
+
if (!token) {
|
|
443
|
+
reply.status(401).send({ error: "Unauthorized" });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const result = await supabaseProvider.verifyToken(token);
|
|
447
|
+
if (!result.ok) {
|
|
448
|
+
reply.status(401).send({ error: result.error ?? "Unauthorized" });
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
req.userId = result.userId ?? null;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
// Dev mode: localhost only
|
|
455
|
+
// Use req.socket.remoteAddress rather than req.ip because trustProxy: true
|
|
456
|
+
// makes req.ip reflect x-forwarded-for (spoofable).
|
|
457
|
+
const remoteAddr = req.socket.remoteAddress ?? "";
|
|
458
|
+
const isLocalhost = remoteAddr === "127.0.0.1" || remoteAddr === "::1";
|
|
459
|
+
if (!isLocalhost) {
|
|
460
|
+
reply.status(401).send({ error: "Remote access requires authentication" });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
req.userId = "1";
|
|
464
|
+
});
|
|
465
|
+
// CORS
|
|
466
|
+
await app.register(fastifyCors, { origin: true });
|
|
467
|
+
// WebSocket support
|
|
468
|
+
await app.register(fastifyWebSocket);
|
|
469
|
+
// Parse all request bodies as raw Buffer — the Web API handlers do their own parsing
|
|
470
|
+
app.removeAllContentTypeParsers();
|
|
471
|
+
app.addContentTypeParser("*", { parseAs: "buffer" }, (_req, body, done) => {
|
|
472
|
+
done(null, body);
|
|
473
|
+
});
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// API options for HTTP route handlers
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
const apiOptions = { metadataRoots, registry };
|
|
478
|
+
const staticFolder = process.env["STATIC_FOLDER"];
|
|
479
|
+
const hasStaticApp = Boolean(staticFolder && existsSync(staticFolder));
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// Register route plugins
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
const createContext = createContextFactory({
|
|
484
|
+
registry,
|
|
485
|
+
apiOptions,
|
|
486
|
+
pythonBridge,
|
|
487
|
+
getPythonBridgeReady: () => pythonBridgeReady
|
|
488
|
+
});
|
|
489
|
+
await app.register(fastifyTRPCPlugin, {
|
|
490
|
+
prefix: "/trpc",
|
|
491
|
+
trpcOptions: {
|
|
492
|
+
router: appRouter,
|
|
493
|
+
createContext,
|
|
494
|
+
onError({ path, error }) {
|
|
495
|
+
log.error(`tRPC error on ${path}`, error instanceof Error ? error : new Error(String(error)));
|
|
496
|
+
// Surface inner ZodError details (output validation failures) so callers
|
|
497
|
+
// can diagnose the offending fields instead of seeing only "Output
|
|
498
|
+
// validation failed".
|
|
499
|
+
const cause = error.cause;
|
|
500
|
+
if (cause && typeof cause === "object" && "issues" in cause) {
|
|
501
|
+
log.error(`tRPC error cause on ${path}: ${JSON.stringify(cause.issues)}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
await app.register(websocketPlugin, {
|
|
507
|
+
registry,
|
|
508
|
+
pythonBridge,
|
|
509
|
+
getPythonBridgeReady: () => pythonBridgeReady,
|
|
510
|
+
ensurePythonBridge: async () => {
|
|
511
|
+
if (pythonBridgeReady)
|
|
512
|
+
return;
|
|
513
|
+
log.info(`Lazily starting Python bridge [${startupMs()}]`);
|
|
514
|
+
try {
|
|
515
|
+
await pythonBridge.ensureConnected();
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
log.warn(`Python bridge lazy start failed [${startupMs()}]`, err instanceof Error ? err : new Error(String(err)));
|
|
519
|
+
throw err;
|
|
520
|
+
}
|
|
521
|
+
pythonBridgeReady = true;
|
|
522
|
+
log.info(`Python bridge lazy start completed [${startupMs()}]`);
|
|
523
|
+
const meta = pythonBridge.getNodeMetadata();
|
|
524
|
+
// Register Python bridge nodes — skip those already loaded from JSON metadata
|
|
525
|
+
let bridgeOnly = 0;
|
|
526
|
+
for (const nodeMeta of meta) {
|
|
527
|
+
if (!nodeMeta.node_type)
|
|
528
|
+
continue;
|
|
529
|
+
if (registry.getMetadata(nodeMeta.node_type))
|
|
530
|
+
continue;
|
|
531
|
+
bridgeOnly++;
|
|
532
|
+
registry.loadMetadata(nodeMeta.node_type, {
|
|
533
|
+
...nodeMeta,
|
|
534
|
+
namespace: nodeMeta.node_type.split(".").slice(0, -1).join("."),
|
|
535
|
+
layout: "default",
|
|
536
|
+
recommended_models: [],
|
|
537
|
+
basic_fields: [],
|
|
538
|
+
required_settings: nodeMeta.required_settings ?? [],
|
|
539
|
+
is_dynamic: nodeMeta.is_dynamic ?? false,
|
|
540
|
+
is_streaming_output: nodeMeta.is_streaming_output ?? false,
|
|
541
|
+
expose_as_tool: false,
|
|
542
|
+
supports_dynamic_outputs: false
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
log.info(`Python bridge connected [${startupMs()}] — ${meta.length} Python nodes (${bridgeOnly} bridge-only, ${meta.length - bridgeOnly} from JSON)`);
|
|
546
|
+
pythonBridge
|
|
547
|
+
.getWorkerStatus?.()
|
|
548
|
+
?.then((status) => {
|
|
549
|
+
log.info(`Python bridge status [${startupMs()}]`, {
|
|
550
|
+
protocol_version: status.protocol_version,
|
|
551
|
+
node_count: status.node_count,
|
|
552
|
+
provider_count: status.provider_count,
|
|
553
|
+
namespaces: status.namespaces,
|
|
554
|
+
transport: status.transport,
|
|
555
|
+
max_frame_size: status.max_frame_size,
|
|
556
|
+
load_error_count: status.load_errors.length
|
|
557
|
+
});
|
|
558
|
+
})
|
|
559
|
+
.catch((err) => {
|
|
560
|
+
log.warn(`Failed to fetch Python bridge status [${startupMs()}]`, err instanceof Error ? err : new Error(String(err)));
|
|
561
|
+
});
|
|
562
|
+
logPythonBridgeDiagnostics("connected");
|
|
563
|
+
notifyPythonBridgeResourceChanges(app, pythonBridge).catch((err) => {
|
|
564
|
+
log.warn("Failed to notify Python bridge resource changes", err instanceof Error ? err : new Error(String(err)));
|
|
565
|
+
});
|
|
566
|
+
},
|
|
567
|
+
toolClassMap
|
|
568
|
+
});
|
|
569
|
+
await app.register(healthRoute);
|
|
570
|
+
// All HTTP API routes receive apiOptions
|
|
571
|
+
const routeOpts = { apiOptions };
|
|
572
|
+
await app.register(assetsRoutes, routeOpts);
|
|
573
|
+
await app.register(workflowsRoutes, routeOpts);
|
|
574
|
+
await app.register(jobsRoutes, routeOpts);
|
|
575
|
+
await app.register(nodesRoutes, routeOpts);
|
|
576
|
+
await app.register(storageRoutes, routeOpts);
|
|
577
|
+
await app.register(openaiRoutes, routeOpts);
|
|
578
|
+
await app.register(oauthRoutes, routeOpts);
|
|
579
|
+
await app.register(workspaceRoutes, routeOpts);
|
|
580
|
+
await app.register(filesRoutes, routeOpts);
|
|
581
|
+
await app.register(collectionsRoutes, routeOpts);
|
|
582
|
+
// MCP endpoints are only available in local/dev mode — not in production.
|
|
583
|
+
// The configuration endpoints moved to the tRPC `mcpConfig` router; the
|
|
584
|
+
// `/mcp` proxy below is a bare MCP over-HTTP transport and stays on REST.
|
|
585
|
+
if (!isProduction) {
|
|
586
|
+
app.all("/mcp", async (req, reply) => {
|
|
587
|
+
const protocol = httpsOptions ? "https" : "http";
|
|
588
|
+
const url = `${protocol}://${req.headers.host ?? `127.0.0.1:${port}`}${req.url}`;
|
|
589
|
+
const headers = new Headers();
|
|
590
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
591
|
+
if (Array.isArray(value)) {
|
|
592
|
+
for (const item of value) {
|
|
593
|
+
headers.append(key, item);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
else if (typeof value === "string") {
|
|
597
|
+
headers.set(key, value);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const requestBody = req.method === "GET" || req.method === "DELETE"
|
|
601
|
+
? undefined
|
|
602
|
+
: Buffer.isBuffer(req.body)
|
|
603
|
+
? req.body
|
|
604
|
+
: typeof req.body === "string"
|
|
605
|
+
? req.body
|
|
606
|
+
: req.body == null
|
|
607
|
+
? Buffer.alloc(0)
|
|
608
|
+
: JSON.stringify(req.body);
|
|
609
|
+
const request = new Request(url, {
|
|
610
|
+
method: req.method,
|
|
611
|
+
headers,
|
|
612
|
+
body: requestBody,
|
|
613
|
+
duplex: "half"
|
|
614
|
+
});
|
|
615
|
+
const response = await handleMcpHttpRequest(request, { metadataRoots });
|
|
616
|
+
if (!response) {
|
|
617
|
+
reply.status(404).send({ error: "Not found" });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
reply.status(response.status);
|
|
621
|
+
response.headers.forEach((value, key) => {
|
|
622
|
+
reply.header(key, value);
|
|
623
|
+
});
|
|
624
|
+
const payload = Buffer.from(await response.arrayBuffer());
|
|
625
|
+
reply.send(payload);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
await app.register(agentSocketRoute);
|
|
629
|
+
log.info(`Routes registered [${startupMs()}]`);
|
|
630
|
+
if (hasStaticApp && staticFolder) {
|
|
631
|
+
log.info("Serving static app", { root: staticFolder });
|
|
632
|
+
await app.register(fastifyStatic, {
|
|
633
|
+
root: staticFolder,
|
|
634
|
+
prefix: "/"
|
|
635
|
+
});
|
|
636
|
+
app.get("/", async (_req, reply) => {
|
|
637
|
+
return reply.sendFile("index.html");
|
|
638
|
+
});
|
|
639
|
+
app.get("/apps/index.html", async (_req, reply) => {
|
|
640
|
+
return reply.sendFile("index.html");
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
// Log all 404s so we can identify missing routes
|
|
644
|
+
app.setNotFoundHandler((req, reply) => {
|
|
645
|
+
const pathname = req.url.split("?")[0] ?? "/";
|
|
646
|
+
if (hasStaticApp &&
|
|
647
|
+
req.method === "GET" &&
|
|
648
|
+
!pathname.startsWith("/api") &&
|
|
649
|
+
!pathname.startsWith("/health") &&
|
|
650
|
+
!pathname.startsWith("/ws") &&
|
|
651
|
+
!pathname.startsWith("/v1") &&
|
|
652
|
+
!pathname.startsWith("/oauth") &&
|
|
653
|
+
!pathname.startsWith("/trpc") &&
|
|
654
|
+
!pathname.includes(".")) {
|
|
655
|
+
return reply.sendFile("index.html");
|
|
656
|
+
}
|
|
657
|
+
log.warn(`404 Not Found: ${req.method} ${req.url}`);
|
|
658
|
+
return reply.status(404).send({ detail: "Not found" });
|
|
659
|
+
});
|
|
660
|
+
// ---------------------------------------------------------------------------
|
|
661
|
+
// HTTP → HTTPS redirect server (when TLS is active)
|
|
662
|
+
// ---------------------------------------------------------------------------
|
|
663
|
+
if (tlsEnabled) {
|
|
664
|
+
const redirectPort = Number(process.env["REDIRECT_PORT"] ?? 80);
|
|
665
|
+
const redirectServer = createHttpServer((req, res) => {
|
|
666
|
+
const reqHost = req.headers.host?.split(":")[0] ?? "localhost";
|
|
667
|
+
const location = `https://${reqHost}:${port}${req.url ?? "/"}`;
|
|
668
|
+
res.statusCode = 301;
|
|
669
|
+
res.setHeader("Location", location);
|
|
670
|
+
res.end();
|
|
671
|
+
});
|
|
672
|
+
redirectServer.on("error", (err) => {
|
|
673
|
+
if (err.code === "EACCES") {
|
|
674
|
+
log.warn(`HTTP redirect server skipped — port ${redirectPort} requires elevated privileges`);
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
log.error("HTTP redirect server error", err);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
redirectServer.listen(redirectPort, "0.0.0.0", () => {
|
|
681
|
+
log.info(`HTTP → HTTPS redirect server listening on :${redirectPort}`);
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
// ---------------------------------------------------------------------------
|
|
685
|
+
// Start Fastify server immediately, Python bridge connects in background
|
|
686
|
+
// ---------------------------------------------------------------------------
|
|
687
|
+
const proto = tlsEnabled ? "https" : "http";
|
|
688
|
+
app.listen({ port, host }, (err) => {
|
|
689
|
+
if (err) {
|
|
690
|
+
log.error("Failed to start server", err);
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
log.info(`Server listening on ${proto}://${host}:${port} [${startupMs()}]`);
|
|
694
|
+
log.info(`WebSocket endpoint: ${tlsEnabled ? "wss" : "ws"}://${host}:${port}/ws`);
|
|
695
|
+
});
|
|
696
|
+
// ---------------------------------------------------------------------------
|
|
697
|
+
// Graceful shutdown — ensure child processes (Python worker, etc.) are killed
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
async function shutdown(signal) {
|
|
700
|
+
log.info(`${signal} received — shutting down`);
|
|
701
|
+
log.info("Closing Python bridge");
|
|
702
|
+
try {
|
|
703
|
+
getAgentRuntime().closeAllSessions();
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
// best-effort cleanup
|
|
707
|
+
}
|
|
708
|
+
pythonBridge.close();
|
|
709
|
+
try {
|
|
710
|
+
await app.close();
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
// ignore close errors
|
|
714
|
+
}
|
|
715
|
+
process.exit(0);
|
|
716
|
+
}
|
|
717
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
718
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
719
|
+
// On Windows, Ctrl+C in some terminals fires SIGBREAK instead of SIGINT
|
|
720
|
+
if (process.platform === "win32") {
|
|
721
|
+
process.on("SIGBREAK", () => void shutdown("SIGBREAK"));
|
|
722
|
+
}
|
|
723
|
+
// Start Python bridge eagerly if Python is installed.
|
|
724
|
+
if (pythonBridge.hasPython()) {
|
|
725
|
+
log.info(`Starting Python bridge eagerly [${startupMs()}]`);
|
|
726
|
+
pythonBridge
|
|
727
|
+
.ensureConnected()
|
|
728
|
+
.then(() => {
|
|
729
|
+
pythonBridgeReady = true;
|
|
730
|
+
log.info(`Python bridge eager start completed [${startupMs()}]`);
|
|
731
|
+
const meta = pythonBridge.getNodeMetadata();
|
|
732
|
+
let bridgeOnly = 0;
|
|
733
|
+
for (const nodeMeta of meta) {
|
|
734
|
+
if (!nodeMeta.node_type)
|
|
735
|
+
continue;
|
|
736
|
+
if (registry.getMetadata(nodeMeta.node_type))
|
|
737
|
+
continue;
|
|
738
|
+
bridgeOnly++;
|
|
739
|
+
registry.loadMetadata(nodeMeta.node_type, {
|
|
740
|
+
...nodeMeta,
|
|
741
|
+
namespace: nodeMeta.node_type.split(".").slice(0, -1).join("."),
|
|
742
|
+
layout: "default",
|
|
743
|
+
recommended_models: [],
|
|
744
|
+
basic_fields: [],
|
|
745
|
+
required_settings: nodeMeta.required_settings ?? [],
|
|
746
|
+
is_dynamic: nodeMeta.is_dynamic ?? false,
|
|
747
|
+
is_streaming_output: nodeMeta.is_streaming_output ?? false,
|
|
748
|
+
expose_as_tool: false,
|
|
749
|
+
supports_dynamic_outputs: false
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
log.info(`Python bridge connected [${startupMs()}] — ${meta.length} Python nodes (${bridgeOnly} bridge-only, ${meta.length - bridgeOnly} from JSON)`);
|
|
753
|
+
pythonBridge
|
|
754
|
+
.getWorkerStatus?.()
|
|
755
|
+
?.then((status) => {
|
|
756
|
+
log.info(`Python bridge status [${startupMs()}]`, {
|
|
757
|
+
protocol_version: status.protocol_version,
|
|
758
|
+
node_count: status.node_count,
|
|
759
|
+
provider_count: status.provider_count,
|
|
760
|
+
namespaces: status.namespaces,
|
|
761
|
+
transport: status.transport,
|
|
762
|
+
max_frame_size: status.max_frame_size,
|
|
763
|
+
load_error_count: status.load_errors.length
|
|
764
|
+
});
|
|
765
|
+
})
|
|
766
|
+
.catch((err) => {
|
|
767
|
+
log.warn(`Failed to fetch Python bridge status [${startupMs()}]`, err instanceof Error ? err : new Error(String(err)));
|
|
768
|
+
});
|
|
769
|
+
logPythonBridgeDiagnostics("connected");
|
|
770
|
+
notifyPythonBridgeResourceChanges(app, pythonBridge).catch((err) => {
|
|
771
|
+
log.warn("Failed to notify Python bridge resource changes", err instanceof Error ? err : new Error(String(err)));
|
|
772
|
+
});
|
|
773
|
+
})
|
|
774
|
+
.catch((err) => {
|
|
775
|
+
log.warn("Python bridge failed to start (Python nodes will not be available)", err instanceof Error ? err : new Error(String(err)));
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
log.info(isProduction
|
|
780
|
+
? "Python bridge disabled in production — Python nodes will not be available"
|
|
781
|
+
: "Python not found — Python nodes will not be available");
|
|
782
|
+
}
|
|
783
|
+
//# sourceMappingURL=server.js.map
|