diff --git a/.gitignore b/.gitignore index 6deefde..35584a6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ count.txt # SpecStory development history .specstory/ + +pilot/data* \ No newline at end of file diff --git a/bun.lock b/bun.lock index f852f76..8ee3d41 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "@decocms/mcps", @@ -330,6 +329,19 @@ "wrangler": "^4.28.0", }, }, + "pilot": { + "name": "mcp-pilot", + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.1", + "zod": "^3.24.3", + "zod-to-json-schema": "^3.24.5", + }, + "devDependencies": { + "@types/bun": "^1.1.14", + "typescript": "^5.7.2", + }, + }, "pinecone": { "name": "pinecone", "version": "1.0.0", @@ -588,7 +600,7 @@ "@ai-sdk/anthropic-v5": ["@ai-sdk/anthropic@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.12" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-egqr9PHqqX2Am5mn/Xs1C3+1/wphVKiAjpsVpW85eLc2WpW7AgiAg52DCBr4By9bw3UVVuMeR4uEO1X0dKDUDA=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.11", "", { "dependencies": { "@ai-sdk/provider": "3.0.2", "@ai-sdk/provider-utils": "4.0.4", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLrgNXA95wdo/zAlA0miX/SJEYKSYCHg+e0Y/uQeABLScZAMjPw3jWaeANta/Db1T4xfi8cBvY3nnV8Pa27z+w=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.13", "", { "dependencies": { "@ai-sdk/provider": "3.0.2", "@ai-sdk/provider-utils": "4.0.5", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-g7nE4PFtngOZNZSy1lOPpkC+FAiHxqBJXqyRMEG7NUrEVZlz5goBdtHg1YgWRJIX776JTXAmbOI5JreAKVAsVA=="], "@ai-sdk/google-v5": ["@ai-sdk/google@2.0.40", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-E7MTVE6vhWXQJzXQDvojwA9t5xlhWpxttCH3R/kUyiE6y0tT8Ay2dmZLO+bLpFBQ5qrvBMrjKWpDVQMoo6TJZg=="], @@ -602,7 +614,7 @@ "@ai-sdk/provider": ["@ai-sdk/provider@3.0.2", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-HrEmNt/BH/hkQ7zpi2o6N3k1ZR1QTb7z85WYhYygiTxOQuaml4CMtHCWRbric5WPU+RNsYI7r1EpyVQMKO1pYw=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.4", "", { "dependencies": { "@ai-sdk/provider": "3.0.2", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VxhX0B/dWGbpNHxrKCWUAJKXIXV015J4e7qYjdIU9lLWeptk0KMLGcqkB4wFxff5Njqur8dt8wRi1MN9lZtDqg=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.5", "", { "dependencies": { "@ai-sdk/provider": "3.0.2", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ow/X/SEkeExTTc1x+nYLB9ZHK2WUId8+9TlkamAx7Tl9vxU+cKzWx2dwjgMHeCN6twrgwkLrrtqckQeO4mxgVA=="], "@ai-sdk/provider-utils-v5": ["@ai-sdk/provider-utils@3.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZtbdvYxdMoria+2SlNarEk6Hlgyf+zzcznlD55EAl+7VZvJaSg2sqPvwArY7L6TfDEDJsnCq0fdhBSkYo0Xqdg=="], @@ -630,35 +642,35 @@ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.966.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.966.0", "@aws-sdk/credential-provider-node": "3.966.0", "@aws-sdk/middleware-bucket-endpoint": "3.966.0", "@aws-sdk/middleware-expect-continue": "3.965.0", "@aws-sdk/middleware-flexible-checksums": "3.966.0", "@aws-sdk/middleware-host-header": "3.965.0", "@aws-sdk/middleware-location-constraint": "3.965.0", "@aws-sdk/middleware-logger": "3.965.0", "@aws-sdk/middleware-recursion-detection": "3.965.0", "@aws-sdk/middleware-sdk-s3": "3.966.0", "@aws-sdk/middleware-ssec": "3.965.0", "@aws-sdk/middleware-user-agent": "3.966.0", "@aws-sdk/region-config-resolver": "3.965.0", "@aws-sdk/signature-v4-multi-region": "3.966.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@aws-sdk/util-user-agent-browser": "3.965.0", "@aws-sdk/util-user-agent-node": "3.966.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.1", "@smithy/eventstream-serde-browser": "^4.2.7", "@smithy/eventstream-serde-config-resolver": "^4.3.7", "@smithy/eventstream-serde-node": "^4.2.7", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-blob-browser": "^4.2.8", "@smithy/hash-node": "^4.2.7", "@smithy/hash-stream-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/md5-js": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.2", "@smithy/middleware-retry": "^4.4.18", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.17", "@smithy/util-defaults-mode-node": "^4.2.20", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-IckVv+A6irQyXTiJrNpfi63ZtPuk6/Iu70TnMq2DTRFK/4bD2bOvqL1IHZ2WGmZMoeWd5LI8Fn6pIwdK6g4QJQ=="], + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.967.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.967.0", "@aws-sdk/credential-provider-node": "3.967.0", "@aws-sdk/middleware-bucket-endpoint": "3.966.0", "@aws-sdk/middleware-expect-continue": "3.965.0", "@aws-sdk/middleware-flexible-checksums": "3.967.0", "@aws-sdk/middleware-host-header": "3.965.0", "@aws-sdk/middleware-location-constraint": "3.965.0", "@aws-sdk/middleware-logger": "3.965.0", "@aws-sdk/middleware-recursion-detection": "3.965.0", "@aws-sdk/middleware-sdk-s3": "3.967.0", "@aws-sdk/middleware-ssec": "3.965.0", "@aws-sdk/middleware-user-agent": "3.967.0", "@aws-sdk/region-config-resolver": "3.965.0", "@aws-sdk/signature-v4-multi-region": "3.967.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@aws-sdk/util-user-agent-browser": "3.965.0", "@aws-sdk/util-user-agent-node": "3.967.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.2", "@smithy/eventstream-serde-browser": "^4.2.7", "@smithy/eventstream-serde-config-resolver": "^4.3.7", "@smithy/eventstream-serde-node": "^4.2.7", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-blob-browser": "^4.2.8", "@smithy/hash-node": "^4.2.7", "@smithy/hash-stream-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/md5-js": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.3", "@smithy/middleware-retry": "^4.4.19", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.4", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.18", "@smithy/util-defaults-mode-node": "^4.2.21", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-7vDlsBqd9y0dJDjCy84WMN+1r60El97IKMGlegU+l9K2+t8+Wf8bYj/J2xfm+6Ayemje6P4nkKS9tubxBLqg+A=="], - "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.966.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.966.0", "@aws-sdk/middleware-host-header": "3.965.0", "@aws-sdk/middleware-logger": "3.965.0", "@aws-sdk/middleware-recursion-detection": "3.965.0", "@aws-sdk/middleware-user-agent": "3.966.0", "@aws-sdk/region-config-resolver": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@aws-sdk/util-user-agent-browser": "3.965.0", "@aws-sdk/util-user-agent-node": "3.966.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.1", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.2", "@smithy/middleware-retry": "^4.4.18", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.17", "@smithy/util-defaults-mode-node": "^4.2.20", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-hQZDQgqRJclALDo9wK+bb5O+VpO8JcjImp52w9KPSz9XveNRgE9AYfklRJd8qT2Bwhxe6IbnqYEino2wqUMA1w=="], + "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.967.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.967.0", "@aws-sdk/middleware-host-header": "3.965.0", "@aws-sdk/middleware-logger": "3.965.0", "@aws-sdk/middleware-recursion-detection": "3.965.0", "@aws-sdk/middleware-user-agent": "3.967.0", "@aws-sdk/region-config-resolver": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@aws-sdk/util-user-agent-browser": "3.965.0", "@aws-sdk/util-user-agent-node": "3.967.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.2", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.3", "@smithy/middleware-retry": "^4.4.19", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.4", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.18", "@smithy/util-defaults-mode-node": "^4.2.21", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7RgUwHcRMJtWme6kCHGUVT+Rn9GmNH+FHm34N9UgMXzUqQlzFMweE7T5E9O8nv3wIp7xFNB20ADaCw9Xdnox1Q=="], - "@aws-sdk/core": ["@aws-sdk/core@3.966.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@aws-sdk/xml-builder": "3.965.0", "@smithy/core": "^3.20.1", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-QaRVBHD1prdrFXIeFAY/1w4b4S0EFyo/ytzU+rCklEjMRT7DKGXGoHXTWLGz+HD7ovlS5u+9cf8a/LeSOEMzww=="], + "@aws-sdk/core": ["@aws-sdk/core@3.967.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@aws-sdk/xml-builder": "3.965.0", "@smithy/core": "^3.20.2", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.4", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-sJmuP7GrVmlbO6DpXkuf9Mbn6jGNNvy6PLawvaxVF150c8bpNk3w39rerRls6q1dot1dBFV2D29hBXMY1agNMg=="], "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-9FbIyJ/Zz1AdEIrb0+Pn7wRi+F/0Y566ooepg0hDyHUzRV3ZXKjOlu3wJH3YwTz2UkdwQmldfUos2yDJps7RyA=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.966.0", "", { "dependencies": { "@aws-sdk/core": "3.966.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-sxVKc9PY0SH7jgN/8WxhbKQ7MWDIgaJv1AoAKJkhJ+GM5r09G5Vb2Vl8ALYpsy+r8b+iYpq5dGJj8k2VqxoQMg=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.967.0", "", { "dependencies": { "@aws-sdk/core": "3.967.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-+XWw0+f/txeMbEVRtTFZhgSw1ymH1ffaVKkdMBSnw48rfSohJElKmitCqdihagRTZpzh7m8qI6tIQ5t3OUqugw=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.966.0", "", { "dependencies": { "@aws-sdk/core": "3.966.0", "@aws-sdk/types": "3.965.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-VTJDP1jOibVtc5pn5TNE12rhqOO/n10IjkoJi8fFp9BMfmh3iqo70Ppvphz/Pe/R9LcK5Z3h0Z4EB9IXDR6kag=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.967.0", "", { "dependencies": { "@aws-sdk/core": "3.967.0", "@aws-sdk/types": "3.965.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.4", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-0/GIAEv5pY5htg6IBMuYccBgzz3oS2DqHjHi396ziTrwlhbrCNX96AbNhQhzAx3LBZUk13sPfeapjyQ7G57Ekg=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.966.0", "", { "dependencies": { "@aws-sdk/core": "3.966.0", "@aws-sdk/credential-provider-env": "3.966.0", "@aws-sdk/credential-provider-http": "3.966.0", "@aws-sdk/credential-provider-login": "3.966.0", "@aws-sdk/credential-provider-process": "3.966.0", "@aws-sdk/credential-provider-sso": "3.966.0", "@aws-sdk/credential-provider-web-identity": "3.966.0", "@aws-sdk/nested-clients": "3.966.0", "@aws-sdk/types": "3.965.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-4oQKkYMCUx0mffKuH8LQag1M4Fo5daKVmsLAnjrIqKh91xmCrcWlAFNMgeEYvI1Yy125XeNSaFMfir6oNc2ODA=="], + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.967.0", "", { "dependencies": { "@aws-sdk/core": "3.967.0", "@aws-sdk/credential-provider-env": "3.967.0", "@aws-sdk/credential-provider-http": "3.967.0", "@aws-sdk/credential-provider-login": "3.967.0", "@aws-sdk/credential-provider-process": "3.967.0", "@aws-sdk/credential-provider-sso": "3.967.0", "@aws-sdk/credential-provider-web-identity": "3.967.0", "@aws-sdk/nested-clients": "3.967.0", "@aws-sdk/types": "3.965.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-U8dMpaM6Qf6+2Qvp1uG6OcWv1RlrZW7tQkpmzEVWH8HZTGrVHIXXju64NMtIOr7yOnNwd0CKcytuD1QG+phCwQ=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.966.0", "", { "dependencies": { "@aws-sdk/core": "3.966.0", "@aws-sdk/nested-clients": "3.966.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-wD1KlqLyh23Xfns/ZAPxebwXixoJJCuDbeJHFrLDpP4D4h3vA2S8nSFgBSFR15q9FhgRfHleClycf6g5K4Ww6w=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.967.0", "", { "dependencies": { "@aws-sdk/core": "3.967.0", "@aws-sdk/nested-clients": "3.967.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-kbvZsZL6CBlfnb71zuJdJmBUFZN5utNrcziZr/DZ2olEOkA9vlmizE8i9BUIbmS7ptjgvRnmcY1A966yfhiblw=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.966.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.966.0", "@aws-sdk/credential-provider-http": "3.966.0", "@aws-sdk/credential-provider-ini": "3.966.0", "@aws-sdk/credential-provider-process": "3.966.0", "@aws-sdk/credential-provider-sso": "3.966.0", "@aws-sdk/credential-provider-web-identity": "3.966.0", "@aws-sdk/types": "3.965.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-7QCOERGddMw7QbjE+LSAFgwOBpPv4px2ty0GCK7ZiPJGsni2EYmM4TtYnQb9u1WNHmHqIPWMbZR0pKDbyRyHlQ=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.967.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.967.0", "@aws-sdk/credential-provider-http": "3.967.0", "@aws-sdk/credential-provider-ini": "3.967.0", "@aws-sdk/credential-provider-process": "3.967.0", "@aws-sdk/credential-provider-sso": "3.967.0", "@aws-sdk/credential-provider-web-identity": "3.967.0", "@aws-sdk/types": "3.965.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-WuNbHs9rfKKSVok4+OBrZf0AHfzDgFYYMxN2G/q6ZfUmY4QmiPyxV5HkNFh1rqDxS9VV6kAZPo0EBmry10idSg=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.966.0", "", { "dependencies": { "@aws-sdk/core": "3.966.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-q5kCo+xHXisNbbPAh/DiCd+LZX4wdby77t7GLk0b2U0/mrel4lgy6o79CApe+0emakpOS1nPZS7voXA7vGPz4w=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.967.0", "", { "dependencies": { "@aws-sdk/core": "3.967.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-sNCY5JDV0whsfsZ6c2+6eUwH33H7UhKbqvCPbEYlIIa8wkGjCtCyFI3zZIJHVcMKJJ3117vSUFHEkNA7g+8rtw=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.966.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.966.0", "@aws-sdk/core": "3.966.0", "@aws-sdk/token-providers": "3.966.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-Rv5aEfbpqsQZzxpX2x+FbSyVFOE3Dngome+exNA8jGzc00rrMZEUnm3J3yAsLp/I2l7wnTfI0r2zMe+T9/nZAQ=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.967.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.967.0", "@aws-sdk/core": "3.967.0", "@aws-sdk/token-providers": "3.967.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-0K6kITKNytFjk1UYabYUsTThgU6TQkyW6Wmt8S5zd1A/up7NSQGpp58Rpg9GIf4amQDQwb+p9FGG7emmV8FEeA=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.966.0", "", { "dependencies": { "@aws-sdk/core": "3.966.0", "@aws-sdk/nested-clients": "3.966.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-Yv1lc9iic9xg3ywMmIAeXN1YwuvfcClLVdiF2y71LqUgIOupW8B8my84XJr6pmOQuKzZa++c2znNhC9lGsbKyw=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.967.0", "", { "dependencies": { "@aws-sdk/core": "3.967.0", "@aws-sdk/nested-clients": "3.967.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-Vkr7S2ec7q/v8i/MzkHcBEdqqfWz3lyb8FDjb+NjslEwdxC3f6XwADRZzWwV1pChfx6SbsvJXKfkcF/pKAelhA=="], "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.966.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@aws-sdk/util-arn-parser": "3.966.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KMPZ7gtFXErd9pMpXJMBwFlxxlGIaIQrUBfj3ea7rlrNtoVHnSI4qsoldLq5l9/Ho64KoCiICH4+qXjze8JTDQ=="], "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-UBxVytsmhEmFwkBnt+aV0eAJ7uc+ouNokCqMBrQ7Oc5A77qhlcHfOgXIKz2SxqsiYTsDq+a0lWFM/XpyRWraqA=="], - "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.966.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.966.0", "@aws-sdk/crc64-nvme": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-0/ofXeceTH/flKhg4EGGYr4cDtaLVkR/2RI05J/hxrHIls+iM6j8++GO0TocxmZYK+8B+7XKSaV9LU26nboTUQ=="], + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.967.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.967.0", "@aws-sdk/crc64-nvme": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-RuOan0fknnAep2pTSjmJ+Heomowxg3M3s+pcs0JEW/SYnvdwYhFOTcFg2VBvGv3V1kwXxXHMlC57zoGn6pNcqg=="], "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-SfpSYqoPOAmdb3DBsnNsZ0vix+1VAtkUkzXM79JL3R5IfacpyKE2zytOgVAQx/FjhhlpSTwuXd+LRhUEVb3MaA=="], @@ -668,21 +680,21 @@ "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-6dvD+18Ni14KCRu+tfEoNxq1sIGVp9tvoZDZ7aMvpnA7mDXuRLrOjRQ/TAZqXwr9ENKVGyxcPl0cRK8jk1YWjA=="], - "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.966.0", "", { "dependencies": { "@aws-sdk/core": "3.966.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-arn-parser": "3.966.0", "@smithy/core": "^3.20.1", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-9N9zncsY5ydDCRatKdrPZcdCwNWt7TdHmqgwQM52PuA5gs1HXWwLLNDy/51H+9RTHi7v6oly+x9utJ/qypCh2g=="], + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.967.0", "", { "dependencies": { "@aws-sdk/core": "3.967.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-arn-parser": "3.966.0", "@smithy/core": "^3.20.2", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.4", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Kkd6xGwTqbg7Spq1SI3ZX6PPYKdGLxdRGlXGNE3lnEPzNueQZQJKLZFpOY2aVdcAT+ytAY96N5szeeeAsFdUaA=="], "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-dke++CTw26y+a2D1DdVuZ4+2TkgItdx6TeuE0zOl4lsqXGvTBUG4eaIZalt7ZOAW5ys2pbDOk1bPuh4opoD3pQ=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.966.0", "", { "dependencies": { "@aws-sdk/core": "3.966.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@smithy/core": "^3.20.1", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-MvGoy0vhMluVpSB5GaGJbYLqwbZfZjwEZhneDHdPhgCgQqmCtugnYIIjpUw7kKqWGsmaMQmNEgSFf1zYYmwOyg=="], + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.967.0", "", { "dependencies": { "@aws-sdk/core": "3.967.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@smithy/core": "^3.20.2", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-2qzJzZj5u+cZiG7kz3XJPaTH4ssUY/aet1kwJsUTFKrWeHUf7mZZkDFfkXP5cOffgiOyR5ZkrmJoLKAde9hshg=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.966.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.966.0", "@aws-sdk/middleware-host-header": "3.965.0", "@aws-sdk/middleware-logger": "3.965.0", "@aws-sdk/middleware-recursion-detection": "3.965.0", "@aws-sdk/middleware-user-agent": "3.966.0", "@aws-sdk/region-config-resolver": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@aws-sdk/util-user-agent-browser": "3.965.0", "@aws-sdk/util-user-agent-node": "3.966.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.1", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.2", "@smithy/middleware-retry": "^4.4.18", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.17", "@smithy/util-defaults-mode-node": "^4.2.20", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-FRzAWwLNoKiaEWbYhnpnfartIdOgiaBLnPcd3uG1Io+vvxQUeRPhQIy4EfKnT3AuA+g7gzSCjMG2JKoJOplDtQ=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.967.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.967.0", "@aws-sdk/middleware-host-header": "3.965.0", "@aws-sdk/middleware-logger": "3.965.0", "@aws-sdk/middleware-recursion-detection": "3.965.0", "@aws-sdk/middleware-user-agent": "3.967.0", "@aws-sdk/region-config-resolver": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@aws-sdk/util-user-agent-browser": "3.965.0", "@aws-sdk/util-user-agent-node": "3.967.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.2", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.3", "@smithy/middleware-retry": "^4.4.19", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.4", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.18", "@smithy/util-defaults-mode-node": "^4.2.21", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PYa7V8w0gaNux6Sz/Z7zrHmPloEE+EKpRxQIOG/D0askTr5Yd4oO2KGgcInf65uHK3f0Z9U4CTUGHZvQvABypA=="], "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/config-resolver": "^4.4.5", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-RoMhu9ly2B0coxn8ctXosPP2WmDD0MkQlZGLjoYHQUOCBmty5qmCxOqBmBDa6wbWbB8xKtMQ/4VXloQOgzjHXg=="], - "@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.966.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.966.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-format-url": "3.965.0", "@smithy/middleware-endpoint": "^4.4.2", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-RUxg33fhT7qOJuqcxuLMs6vS8Fy84KF6lL+G6JHO769AnJkHqGW5iVaiq+/fhkU38tGHviljyx8uIl6dBUM9KA=="], + "@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.967.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.967.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-format-url": "3.965.0", "@smithy/middleware-endpoint": "^4.4.3", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.4", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-7AD5wWly5zAT8Vo3TuZJDHbStdnLVGRZaeO79NKKGQR8HdqtqtjH0AdMQkKBH0+CeFwWqZ1X1N6cY+X0x8Zn6w=="], - "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.966.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.966.0", "@aws-sdk/types": "3.965.0", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-VNSpyfKtDiBg/nPwSXDvnjISaDE9mI8zhOK3C4/obqh8lK1V6j04xDlwyIWbbIM0f6VgV1FVixlghtJB79eBqA=="], + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.967.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.967.0", "@aws-sdk/types": "3.965.0", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-LfpCEqe/BliiwBtNImz/Txx6MQZkDqjP2bbk+Q4Km6mYhFU9pyPlKo3AYEHfGWn92Smt1nS3S8SzIK0nL6J2Fg=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.966.0", "", { "dependencies": { "@aws-sdk/core": "3.966.0", "@aws-sdk/nested-clients": "3.966.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-8k5cBTicTGYJHhKaweO4gL4fud1KDnLS5fByT6/Xbiu59AxYM4E/h3ds+3jxDMnniCE3gIWpEnyfM9khtmw2lA=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.967.0", "", { "dependencies": { "@aws-sdk/core": "3.967.0", "@aws-sdk/nested-clients": "3.967.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-Qnd/nJ0CgeUa7zQczgmdQm0vYUF7pD1G0C+dR1T7huHQHRIsgCWIsCV9wNKzOFluqtcr6YAeuTwvY0+l8XWxnA=="], "@aws-sdk/types": ["@aws-sdk/types@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ=="], @@ -696,29 +708,29 @@ "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/types": "^4.11.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-Xiza/zMntQGpkd2dETQeAK8So1pg5+STTzpcdGWxj5q0jGO5ayjqT/q1Q7BrsX5KIr6PvRkl9/V7lLCv04wGjQ=="], - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.966.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.966.0", "@aws-sdk/types": "3.965.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-vPPe8V0GLj+jVS5EqFz2NUBgWH35favqxliUOvhp8xBdNRkEjiZm5TqitVtFlxS4RrLY3HOndrWbrP5ejbwl1Q=="], + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.967.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.967.0", "@aws-sdk/types": "3.965.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-yUz6pCGxyG4+QaDg0dkdIBphjQp8A9rrbZa/+U3RJgRrW47hy64clFQUROzj5Poy1Ur8ICVXEUpBsSqRuYEU2g=="], "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-Tcod25/BTupraQwtb+Q+GX8bmEZfxIFjjJ/AvkhUZsZlkPeVluzq1uu3Oeqf145DCdMjzLIN6vab5MrykbDP+g=="], "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], - "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], + "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], - "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], - "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -726,19 +738,19 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], - "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.1", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg=="], @@ -756,7 +768,7 @@ "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260107.1", "", { "os": "win32", "cpu": "x64" }, "sha512-gmBMqs606Gd/IhBEBPSL/hJAqy2L8IyPUjKtoqd/Ccy7GQxbSc0rYlRkxbQ9YzmqnuhrTVYvXuLscyWrpmAJkw=="], - "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260111.0", "", {}, "sha512-NFA2U+AqEWHkAmw6oRzNWJyc14rIvBlF/OlK3lixokunRKwyziuON07nWUZ0w0kKWlW4fJ/muA09tEUaQY07tA=="], + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260113.0", "", {}, "sha512-CS2tUdGn1EMAV5GoFYYUfsZ4vwwXiYxwrUiI8ZRkxrJGqkHNGily/5Zf+vt/wh1HSoiCIChNYiuLEoCA/XUybw=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], @@ -938,7 +950,7 @@ "@jsr/deno__graph": ["@jsr/deno__graph@0.73.1", "https://npm.jsr.io/~/11/@jsr/deno__graph/0.73.1.tgz", {}, "sha512-8MrXym1aHkOttvGSKZGAjayW3hV5b2PADfE5Q7MGAMMA7lClBXC73ZiQNlbmhzyebZAb0UDd6sUXoKEJLDCYtg=="], - "@jsr/hono__hono": ["@jsr/hono__hono@4.11.3", "https://npm.jsr.io/~/11/@jsr/hono__hono/4.11.3.tgz", {}, "sha512-1K5jN5tabn9NzylJUQBdYuz25Nv3WarXRXfkSZeiCZK05ahzGZW2aXtKx1odkE1ztTIdkVGDkfht1CQHdGh4iA=="], + "@jsr/hono__hono": ["@jsr/hono__hono@4.11.4", "https://npm.jsr.io/~/11/@jsr/hono__hono/4.11.4.tgz", {}, "sha512-GdHrXgX+q2Q9LCse3RVXo5vmg8wJDlQoavZVXW3eXN9bczlzdkWBiy712FELfRdLZ0ij16BDQM5nkcc86O/fwg=="], "@jsr/std__assert": ["@jsr/std__assert@1.0.16", "https://npm.jsr.io/~/11/@jsr/std__assert/1.0.16.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12" } }, "sha512-bX9ih0nR1kQ12/cnQRCQU0ppTCV7MFkP0qjyWxJRoDI8RC5cpTAmLFH/KcFgxmdN4flKkRbub8VtLuyKq+4OxA=="], @@ -1176,21 +1188,21 @@ "@oxfmt/win32-x64": ["@oxfmt/win32-x64@0.9.0", "", { "os": "win32", "cpu": "x64" }, "sha512-77OiFJ9lpc7ICmHMSN+belxHPDMOu9U7N/LEp40YuC219QWClt6E5Ved6GwNV5bsDCTxTrpH1/3LhxBNKC66Xg=="], - "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.38.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9rN3047QTyA4i73FKikDUBdczRcLtOsIwZ5TsEx5Q7jr5nBjolhYQOFQf9QdhBLdInxw1iX4+lgdMCf1g74zjg=="], + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.39.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lT3hNhIa02xCujI6YGgjmYGg3Ht/X9ag5ipUVETaMpx5Rd4BbTNWUPif1WN1YZHxt3KLCIqaAe7zVhatv83HOQ=="], - "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.38.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Y1UHW4KOlg5NvyrSn/bVBQP8/LRuid7Pnu+BWGbAVVsFcK0b565YgMSO3Eu9nU3w8ke91dr7NFpUmS+bVkdkbw=="], + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.39.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UT+rfTWd+Yr7iJeSLd/7nF8X4gTYssKh+n77hxl6Oilp3NnG1CKRHxZDy3o3lIBnwgzJkdyUAiYWO1bTMXQ1lA=="], - "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.38.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZiVxPZizlXSnAMdkEFWX/mAj7U3bNiku8p6I9UgLrXzgGSSAhFobx8CaFGwVoKyWOd+gQgZ/ogCrunvx2k0CFg=="], + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qocBkvS2V6rH0t9AT3DfQunMnj3xkM7srs5/Ycj2j5ZqMoaWd/FxHNVJDFP++35roKSvsRJoS0mtA8/77jqm6Q=="], - "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.38.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ELtlCIGZ72A65ATZZHFxHMFrkRtY+DYDCKiNKg6v7u5PdeOFey+OlqRXgXtXlxWjCL+g7nivwI2FPVsWqf05Qw=="], + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-arZzAc1PPcz9epvGBBCMHICeyQloKtHX3eoOe62B3Dskn7gf6Q14wnDHr1r9Vp4vtcBATNq6HlKV14smdlC/qA=="], - "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.38.0", "", { "os": "linux", "cpu": "x64" }, "sha512-E1OcDh30qyng1m0EIlsOuapYkqk5QB6o6IMBjvDKqIoo6IrjlVAasoJfS/CmSH998gXRL3BcAJa6Qg9IxPFZnQ=="], + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZVt5qsECpuNprdWxAPpDBwoixr1VTcZ4qAEQA2l/wmFyVPDYFD3oBY/SWACNnWBddMrswjTg9O8ALxYWoEpmXw=="], - "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.38.0", "", { "os": "linux", "cpu": "x64" }, "sha512-4AfpbM/4sQnr6S1dMijEPfsq4stQbN5vJ2jsahSy/QTcvIVbFkgY+RIhrA5UWlC6eb0rD5CdaPQoKGMJGeXpYw=="], + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pB0hlGyKPbxr9NMIV783lD6cWL3MpaqnZRM9MWni4yBdHPTKyFNYdg5hGD0Bwg+UP4S2rOevq/+OO9x9Bi7E6g=="], - "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.38.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-OvUVYdI68OwXh3d1RjH9N/okCxb6PrOGtEtzXyqGA7Gk+IxyZcX0/QCTBwV8FNbSSzDePSSEHOKpoIB+VXdtvg=="], + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.39.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gg2SFaJohI9+tIQVKXlPw3FsPQFi/eCSWiCgwPtPn5uzQxHRTeQEZKuluz1fuzR5U70TXubb2liZi4Dgl8LJQA=="], - "@oxlint/win32-x64": ["@oxlint/win32-x64@1.38.0", "", { "os": "win32", "cpu": "x64" }, "sha512-7IuZMYiZiOcgg5zHvpJY6jRlEwh8EB/uq7GsoQJO9hANq96TIjyntGByhIjFSsL4asyZmhTEki+MO/u5Fb/WQA=="], + "@oxlint/win32-x64": ["@oxlint/win32-x64@1.39.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sbi25lfj74hH+6qQtb7s1wEvd1j8OQbTaH8v3xTcDjrwm579Cyh0HBv1YSZ2+gsnVwfVDiCTL1D0JsNqYXszVA=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], @@ -1338,7 +1350,7 @@ "@smithy/config-resolver": ["@smithy/config-resolver@4.4.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg=="], - "@smithy/core": ["@smithy/core@3.20.2", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.8", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-nc99TseyTwL1bg+T21cyEA5oItNy1XN4aUeyOlXJnvyRW5VSK1oRKRoSM/Iq0KFPuqZMxjBemSZHZCOZbSyBMw=="], + "@smithy/core": ["@smithy/core@3.20.3", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.8", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-iwF1e0+H9vX+4reUA0WjKnc5ueg0Leinl5kI7wsie5bVXoYdzkpINz6NPYhpr/5InOv332a7wNV5AxJyFoVUsQ=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA=="], @@ -1368,9 +1380,9 @@ "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.3", "", { "dependencies": { "@smithy/core": "^3.20.2", "@smithy/middleware-serde": "^4.2.8", "@smithy/node-config-provider": "^4.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-Zb8R35hjBhp1oFhiaAZ9QhClpPHdEDmNDC2UrrB2fqV0oNDUUPH12ovZHB5xi/Rd+pg/BJHOR1q+SfsieSKPQg=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.4", "", { "dependencies": { "@smithy/core": "^3.20.3", "@smithy/middleware-serde": "^4.2.8", "@smithy/node-config-provider": "^4.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-TFxS6C5bGSc4djD1SLVmstCpfYDjmMnBR4KRDge5HEEtgSINGPKuxLvaAGfSPx5FFoMaTJkj4jJLNFggeWpRoQ=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.19", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/service-error-classification": "^4.2.7", "@smithy/smithy-client": "^4.10.4", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-QtisFIjIw2tjMm/ESatjWFVIQb5Xd093z8xhxq/SijLg7Mgo2C2wod47Ib/AHpBLFhwYXPzd7Hp2+JVXfeZyMQ=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.20", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/service-error-classification": "^4.2.7", "@smithy/smithy-client": "^4.10.5", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-+UvEn/8HGzh/6zpe9xFGZe7go4/fzflggfeRG/TvdGLoUY7Gw+4RgzKJEPU2NvPo0k/j/o7vvx25ZWyOXeGoxw=="], "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w=="], @@ -1394,7 +1406,7 @@ "@smithy/signature-v4": ["@smithy/signature-v4@5.3.7", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.10.4", "", { "dependencies": { "@smithy/core": "^3.20.2", "@smithy/middleware-endpoint": "^4.4.3", "@smithy/middleware-stack": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-rHig+BWjhjlHlah67ryaW9DECYixiJo5pQCTEwsJyarRBAwHMMC3iYz5MXXAHXe64ZAMn1NhTUSTFIu1T6n6jg=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.10.5", "", { "dependencies": { "@smithy/core": "^3.20.3", "@smithy/middleware-endpoint": "^4.4.4", "@smithy/middleware-stack": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-uotYm3WDne01R0DxBqF9J8WZc8gSgdj+uC7Lv/R+GinH4rxcgRLxLDayYkyGAboZlYszly6maQA+NGQ5N4gLhQ=="], "@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], @@ -1410,9 +1422,9 @@ "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.18", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.4", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-Ao1oLH37YmLyHnKdteMp6l4KMCGBeZEAN68YYe00KAaKFijFELDbRQRm3CNplz7bez1HifuBV0l5uR6eVJLhIg=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.19", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.5", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-5fkC/yE5aepnzcF9dywKefGlJUMM7JEYUOv97TRDLTtGiiAqf7YG80HJWIBR0qWQPQW3dlQ5eFlUsySvt0rGEA=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.21", "", { "dependencies": { "@smithy/config-resolver": "^4.4.5", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.4", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-e21ASJDirE96kKXZLcYcnn4Zt0WGOvMYc1P8EK0gQeQ3I8PbJWqBKx9AUr/YeFpDkpYwEu1RsPe4UXk2+QL7IA=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.22", "", { "dependencies": { "@smithy/config-resolver": "^4.4.5", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.5", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-f0KNaSK192+kv6GFkUDA0Tvr5B8eU2bFh1EO+cUdlzZ2jap5Zv7KZXa0B/7r/M1+xiYPSIuroxlxQVP1ua9kxg=="], "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg=="], @@ -1566,7 +1578,7 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], @@ -1596,7 +1608,7 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ai": ["ai@6.0.27", "", { "dependencies": { "@ai-sdk/gateway": "3.0.11", "@ai-sdk/provider": "3.0.2", "@ai-sdk/provider-utils": "4.0.4", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-a4ToMt5+4xOzHuYdoTy2GQXuNa2/Tpuhxfs5QESHtEV/Vf2HAzSwmTwikrIs8CDKPuMWV1I9YqAvaN5xexMCiQ=="], + "ai": ["ai@6.0.31", "", { "dependencies": { "@ai-sdk/gateway": "3.0.13", "@ai-sdk/provider": "3.0.2", "@ai-sdk/provider-utils": "4.0.5", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-aAn62jsDueAK7oiY4jeqJcA4zFctDqVHGyEaUDaWxEXzz4kbMdoByfYlYZhO1V3nhkeVoI8qNyFfiZusAubQLQ=="], "ai-v5": ["ai@5.0.97", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="], @@ -1918,7 +1930,7 @@ "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], - "hono": ["hono@4.11.3", "", {}, "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w=="], + "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], "hono-openapi": ["hono-openapi@0.4.8", "", { "dependencies": { "json-schema-walker": "^2.0.0" }, "peerDependencies": { "@hono/arktype-validator": "^2.0.0", "@hono/effect-validator": "^1.2.0", "@hono/typebox-validator": "^0.2.0 || ^0.3.0", "@hono/valibot-validator": "^0.5.1", "@hono/zod-validator": "^0.4.1", "@sinclair/typebox": "^0.34.9", "@valibot/to-json-schema": "^1.0.0-beta.3", "arktype": "^2.0.0", "effect": "^3.11.3", "hono": "^4.6.13", "openapi-types": "^12.1.3", "valibot": "^1.0.0-beta.9", "zod": "^3.23.8", "zod-openapi": "^4.0.0" }, "optionalPeers": ["@hono/arktype-validator", "@hono/effect-validator", "@hono/typebox-validator", "@hono/valibot-validator", "@hono/zod-validator", "@sinclair/typebox", "@valibot/to-json-schema", "arktype", "effect", "hono", "valibot", "zod", "zod-openapi"] }, "sha512-LYr5xdtD49M7hEAduV1PftOMzuT8ZNvkyWfh1DThkLsIr4RkvDb12UxgIiFbwrJB6FLtFXLoOZL9x4IeDk2+VA=="], @@ -2054,6 +2066,8 @@ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mcp-pilot": ["mcp-pilot@workspace:pilot"], + "mcp-studio": ["mcp-studio@workspace:mcp-studio"], "mcp-template-minimal": ["mcp-template-minimal@workspace:template-minimal"], @@ -2136,7 +2150,7 @@ "oxfmt": ["oxfmt@0.9.0", "", { "optionalDependencies": { "@oxfmt/darwin-arm64": "0.9.0", "@oxfmt/darwin-x64": "0.9.0", "@oxfmt/linux-arm64-gnu": "0.9.0", "@oxfmt/linux-arm64-musl": "0.9.0", "@oxfmt/linux-x64-gnu": "0.9.0", "@oxfmt/linux-x64-musl": "0.9.0", "@oxfmt/win32-arm64": "0.9.0", "@oxfmt/win32-x64": "0.9.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-RVMw8kqZjCDCFxBZyDK4VW8DHxmSHV0pRky7LoLq9JL3ge4kelT0UB8GS0nVTZIteqOJ9rfwPxSZRUVXSX/n0w=="], - "oxlint": ["oxlint@1.38.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.38.0", "@oxlint/darwin-x64": "1.38.0", "@oxlint/linux-arm64-gnu": "1.38.0", "@oxlint/linux-arm64-musl": "1.38.0", "@oxlint/linux-x64-gnu": "1.38.0", "@oxlint/linux-x64-musl": "1.38.0", "@oxlint/win32-arm64": "1.38.0", "@oxlint/win32-x64": "1.38.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-XT7tBinQS+hVLxtfJOnokJ9qVBiQvZqng40tDgR6qEJMRMnpVq/JwYfbYyGntSq8MO+Y+N9M1NG4bAMFUtCJiw=="], + "oxlint": ["oxlint@1.39.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.39.0", "@oxlint/darwin-x64": "1.39.0", "@oxlint/linux-arm64-gnu": "1.39.0", "@oxlint/linux-arm64-musl": "1.39.0", "@oxlint/linux-x64-gnu": "1.39.0", "@oxlint/linux-x64-musl": "1.39.0", "@oxlint/win32-arm64": "1.39.0", "@oxlint/win32-x64": "1.39.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA=="], "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], @@ -2788,6 +2802,8 @@ "@ts-morph/common/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + "@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "ai-v5/@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W+cB1sOWvPcz9qiIsNtD+HxUrBUva2vWv2K1EFukuImX+HA0uZx3EyyOjhYQ9gtf/teqEG80M6OvJ7xx/VLV2A=="], "ai-v5/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], @@ -2856,6 +2872,8 @@ "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "mcp-pilot/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "mcp-template-minimal/@decocms/runtime": ["@decocms/runtime@0.25.1", "", { "dependencies": { "@cloudflare/workers-types": "^4.20250617.0", "@deco/mcp": "npm:@jsr/deco__mcp@0.5.5", "@mastra/cloudflare-d1": "^0.13.4", "@mastra/core": "^0.20.2", "@modelcontextprotocol/sdk": "^1.19.1", "bidc": "0.0.3", "drizzle-orm": "^0.44.5", "jose": "^6.0.11", "mime-db": "1.52.0", "zod": "^3.25.76", "zod-from-json-schema": "^0.0.5", "zod-to-json-schema": "^3.24.4" } }, "sha512-G1J09NpHkuOcBQMPDi7zJDwtNweH33/39sOsR/mpA+sRWn2W3CX51FXeB5dp06oAmCe9BoBpYnyvb896hSQ+Jg=="], "mcp-template-minimal/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.20.2", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg=="], diff --git a/mcp-studio/README.md b/mcp-studio/README.md index 5431bf8..196e780 100644 --- a/mcp-studio/README.md +++ b/mcp-studio/README.md @@ -1,3 +1,519 @@ # MCP Studio -Your MCP server description goes here. +MCP server for managing workflows, executions, assistants, and prompts. Supports both HTTP and stdio transports. + +## Features + +- **Workflows**: Create, update, delete, and list workflow definitions +- **Executions**: View and manage workflow execution history +- **Assistants**: Manage AI assistant configurations +- **Prompts**: Store and retrieve prompt templates + +## Workflow Scripting Language + +MCP Studio provides a declarative JSON-based language for defining multi-step tool call workflows. Workflows automatically handle dependency resolution, parallel execution, and data flow between steps. + +### Core Concepts + +#### Steps + +A workflow is a sequence of **steps**. Each step has: + +- `name`: Unique identifier (other steps reference it as `@name.field`) +- `action`: What the step does (tool call, code transform, or wait for signal) +- `input`: Data passed to the action (can include `@ref` references) +- `outputSchema`: Optional JSON Schema for output validation +- `config`: Optional retry/timeout settings + +```json +{ + "name": "fetch_user", + "action": { "toolName": "GET_USER" }, + "input": { "userId": "@input.user_id" } +} +``` + +#### Automatic Parallel Execution + +Steps run **in parallel** unless they reference each other. The execution order is auto-determined from `@ref` dependencies: + +```json +{ + "title": "Parallel Fetch", + "steps": [ + { "name": "fetch_users", "action": { "toolName": "GET_USERS" } }, + { "name": "fetch_orders", "action": { "toolName": "GET_ORDERS" } }, + { + "name": "merge", + "action": { "code": "..." }, + "input": { + "users": "@fetch_users.data", + "orders": "@fetch_orders.data" + } + } + ] +} +``` + +In this example: +- `fetch_users` and `fetch_orders` run **in parallel** (no dependencies) +- `merge` waits for **both** to complete (references both via `@ref`) + +### The `@ref` Syntax + +The `@ref` syntax wires data between steps: + +| Reference | Description | +|-----------|-------------| +| `@input.field` | Workflow input data | +| `@stepName.field` | Output from a previous step | +| `@stepName.nested.path` | Nested path into step output | +| `@item` | Current item in forEach loop | +| `@index` | Current index in forEach loop | + +#### Examples + +```jsonc +// Direct reference - entire value +{ "user": "@fetch_user" } + +// Nested path +{ "userName": "@fetch_user.profile.name" } + +// String interpolation +{ "message": "Hello @fetch_user.name, your order @fetch_order.id is ready" } + +// Array access +{ "firstItem": "@fetch_list.items.0" } +``` + +### Action Types + +#### 1. Tool Call Action + +Invokes an MCP tool through the configured gateway: + +```json +{ + "name": "get_weather", + "action": { + "toolName": "WEATHER_GET_FORECAST" + }, + "input": { + "city": "@input.city", + "units": "celsius" + } +} +``` + +With optional result transformation: + +```json +{ + "name": "get_weather", + "action": { + "toolName": "WEATHER_GET_FORECAST", + "transformCode": "interface Output { temp: number } export default function(input) { return { temp: input.temperature.current } }" + }, + "input": { "city": "@input.city" } +} +``` + +#### 2. Code Action + +Pure TypeScript for data transformation. Runs in a sandboxed QuickJS environment: + +```json +{ + "name": "merge_data", + "action": { + "code": "interface Input { users: User[]; orders: Order[] } interface Output { combined: Array<{ user: User; orderCount: number }> } export default function(input: Input): Output { return { combined: input.users.map(u => ({ user: u, orderCount: input.orders.filter(o => o.userId === u.id).length })) } }" + }, + "input": { + "users": "@fetch_users.data", + "orders": "@fetch_orders.data" + } +} +``` + +Code requirements: +- Must export a `default` function +- Optionally declare `Input` and `Output` interfaces for type extraction +- Runs in isolated sandbox (no network, filesystem, or non-deterministic APIs) + +#### 3. Wait for Signal Action (Human-in-the-Loop) + +Pauses execution until an external signal is received: + +```json +{ + "name": "await_approval", + "action": { + "signalName": "approval" + }, + "config": { + "timeoutMs": 86400000 + } +} +``` + +Use `SEND_SIGNAL` tool to resume: +```json +{ "executionId": "...", "signalName": "approval", "payload": { "approved": true } } +``` + +### Step Configuration + +Optional retry and timeout settings: + +```json +{ + "name": "flaky_api_call", + "action": { "toolName": "EXTERNAL_API" }, + "config": { + "maxAttempts": 3, + "backoffMs": 1000, + "timeoutMs": 30000 + } +} +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `maxAttempts` | 1 | Maximum retry attempts on failure | +| `backoffMs` | - | Initial delay between retries (doubles each attempt) | +| `timeoutMs` | 30000 | Maximum execution time before timeout | + +### Output Schema + +Define expected output structure with JSON Schema: + +```json +{ + "name": "extract_info", + "action": { "toolName": "LLM_EXTRACT" }, + "outputSchema": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "number" } + }, + "required": ["name"] + } +} +``` + +When `outputSchema` is provided (without `transformCode`), the step output is automatically filtered to only include properties defined in the schema. + +### Complete Workflow Example + +```json +{ + "id": "enrich-contact", + "title": "Enrich Contact Information", + "description": "Fetches contact data from multiple sources and merges into unified profile", + "steps": [ + { + "name": "lookup_email", + "description": "Find email by name", + "action": { "toolName": "CLEARBIT_LOOKUP" }, + "input": { "name": "@input.contact_name" } + }, + { + "name": "lookup_linkedin", + "description": "Find LinkedIn profile", + "action": { "toolName": "LINKEDIN_SEARCH" }, + "input": { "query": "@input.contact_name @input.company" } + }, + { + "name": "get_company_info", + "description": "Fetch company details", + "action": { "toolName": "CRUNCHBASE_COMPANY" }, + "input": { "name": "@input.company" } + }, + { + "name": "merge_profile", + "description": "Combine all data sources", + "action": { + "code": "interface Input { email: { address: string }; linkedin: { url: string; title: string }; company: { size: string; funding: string } } interface Output { profile: { email: string; linkedinUrl: string; title: string; companySize: string; funding: string } } export default function(input: Input): Output { return { profile: { email: input.email.address, linkedinUrl: input.linkedin.url, title: input.linkedin.title, companySize: input.company.size, funding: input.company.funding } } }" + }, + "input": { + "email": "@lookup_email", + "linkedin": "@lookup_linkedin", + "company": "@get_company_info" + }, + "outputSchema": { + "type": "object", + "properties": { + "profile": { + "type": "object", + "properties": { + "email": { "type": "string" }, + "linkedinUrl": { "type": "string" }, + "title": { "type": "string" }, + "companySize": { "type": "string" }, + "funding": { "type": "string" } + } + } + } + } + } + ] +} +``` + +Execution flow: +1. `lookup_email`, `lookup_linkedin`, and `get_company_info` run **in parallel** +2. `merge_profile` waits for all three, then combines results + +### DAG Visualization + +The workflow engine builds a **Directed Acyclic Graph (DAG)** from step dependencies. Functions available for visualization: + +- `computeStepLevels(steps)` - Get execution level for each step +- `groupStepsByLevel(steps)` - Group steps by parallel execution level +- `buildDependencyEdges(steps)` - Get `[from, to]` edges for graph visualization +- `validateNoCycles(steps)` - Check for circular dependencies + +--- + +## Filesystem Workflow Loading + +Load workflows from JSON files instead of (or alongside) the database. This enables version-controlled workflows, MCP packaging, and database-free operation. + +### Configuration + +```bash +# Set workflow directory (scans recursively for *.json files) +WORKFLOW_DIR=/path/to/workflows bun run stdio + +# Or specify individual files +WORKFLOW_FILES=./workflows/enrich.json,./workflows/notify.json bun run stdio + +# Combine with database (workflows from both sources are merged) +WORKFLOW_DIR=/path/to/workflows DATABASE_URL=... bun run stdio +``` + +### Directory Structure + +``` +workflows/ +├── enrich-contact.json # Single workflow file +├── notify-team.workflow.json # Alternative naming convention +└── my-mcp/ # MCPs can package workflows + ├── workflow-a.json + └── bundled.json # Can contain multiple workflows +``` + +### File Formats + +**Single workflow:** +```json +{ + "id": "enrich-contact", + "title": "Enrich Contact", + "description": "Fetch and merge contact data", + "steps": [ + { "name": "lookup", "action": { "toolName": "LOOKUP_CONTACT" } } + ] +} +``` + +**Multiple workflows in one file:** +```json +{ + "workflows": [ + { "id": "workflow-a", "title": "...", "steps": [...] }, + { "id": "workflow-b", "title": "...", "steps": [...] } + ] +} +``` + +**Array format:** +```json +[ + { "id": "workflow-a", "title": "...", "steps": [...] }, + { "id": "workflow-b", "title": "...", "steps": [...] } +] +``` + +### Filesystem-Specific Tools + +When filesystem mode is enabled, additional tools become available: + +- `WORKFLOW_RELOAD` - Reload all workflows from disk (after editing files) +- `WORKFLOW_SOURCE_INFO` - Show where workflows are loaded from + +### Hot Reload + +When `WORKFLOW_DIR` is set, file changes are automatically detected and workflows are reloaded. Edit a JSON file and the changes are immediately available. + +### Source Filtering + +The `COLLECTION_WORKFLOW_LIST` tool accepts a `source` parameter: + +```json +{ "source": "filesystem" } // Only filesystem workflows +{ "source": "database" } // Only database workflows +{ "source": "all" } // Both (default) +``` + +Each workflow in the response includes `_source: "filesystem" | "database"` to identify its origin. + +### Use Cases + +1. **Version Control**: Store workflows in git alongside code +2. **MCP Packaging**: MCPs can ship pre-built workflows in their package +3. **Local Development**: Edit JSON files with hot-reload +4. **Database-Free**: Run without PostgreSQL for simple setups +5. **CI/CD**: Deploy workflows from repository as code + +### Example: MCP with Bundled Workflows + +An MCP package can include workflows that are automatically available: + +``` +my-mcp/ +├── package.json +├── src/ +│ └── index.ts +└── workflows/ + ├── enrich.json + └── notify.json +``` + +Start with: `WORKFLOW_DIR=./workflows bun run src/index.ts` + +--- + +## Usage + +### HTTP Transport (Mesh Web Connection) + +```bash +# Development with hot reload +bun run dev + +# Production +bun run build:server +node dist/server/main.js +``` + +### Stdio Transport (Mesh Custom Command) + +```bash +# Run directly +bun run stdio + +# Development with hot reload +bun run dev:stdio +``` + +#### Adding to Mesh as Custom Command + +In Mesh, add a new custom command connection: + +1. Go to **MCPs** → **Add MCP** → **Custom Command** +2. Configure the command: + - **Command**: `bun` + - **Args**: `--watch /path/to/mcp-studio/server/stdio.ts` +3. Click **Save** - Mesh will spawn the process and fetch the tools +4. Go to the **Settings** tab to configure the database binding +5. Select a PostgreSQL connection from the **Database** dropdown +6. Click **Save Changes** + +This enables: +- Live reloading during development +- Mesh bindings UI for database configuration (same dropdowns as HTTP connections) +- Automatic migrations when bindings are configured + +## Bindings + +The stdio transport supports Mesh bindings via TWO mechanisms: + +### 1. Environment Variables (Primary for STDIO) + +Mesh passes bindings to stdio processes via environment variables: + +| Variable | Description | +|----------|-------------| +| `MESH_URL` | Base URL of the Mesh instance (e.g., `https://mesh.example.com`) | +| `MESH_TOKEN` | JWT token for authenticating with Mesh API | +| `MESH_STATE` | JSON with binding connection IDs | + +The `MESH_STATE` format: +```json +{ + "DATABASE": { "__type": "@deco/postgres", "value": "connection-id" }, + "EVENT_BUS": { "__type": "@deco/event-bus", "value": "connection-id" }, + "CONNECTION": { "__type": "@deco/connection", "value": "connection-id" } +} +``` + +When these env vars are set, the server: +1. Parses bindings at startup +2. Runs database migrations automatically +3. Uses Mesh's proxy API to execute database operations + +### 2. MCP Tools (Dynamic Configuration) + +- `MCP_CONFIGURATION` - Returns the state schema for the bindings UI (uses `BindingOf` format) +- `ON_MCP_CONFIGURATION` - Receives configured bindings dynamically (used for UI-based configuration) + +The binding schema uses the same format as HTTP mode: +```typescript +const StdioStateSchema = z.object({ + DATABASE: BindingOf("@deco/postgres").describe("PostgreSQL database binding"), +}); +``` + +This renders as a dropdown in Mesh UI showing all connections that implement `@deco/postgres`. + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `MESH_URL` | Yes* | Base URL of the Mesh instance | +| `MESH_TOKEN` | Yes* | JWT token for Mesh API authentication | +| `MESH_STATE` | Yes* | JSON with binding connection IDs | +| `WORKFLOW_DIR` | No | Directory to scan for workflow JSON files (recursive) | +| `WORKFLOW_FILES` | No | Comma-separated list of specific workflow JSON file paths | + +*Required for database operations. Mesh passes these automatically when spawning stdio connections. + +## Available Tools + +### Workflow Collection +- `COLLECTION_WORKFLOW_LIST` - List all workflows +- `COLLECTION_WORKFLOW_GET` - Get a single workflow by ID +- `COLLECTION_WORKFLOW_CREATE` - Create a new workflow +- `COLLECTION_WORKFLOW_UPDATE` - Update an existing workflow +- `COLLECTION_WORKFLOW_DELETE` - Delete a workflow + +### Execution Collection +- `COLLECTION_WORKFLOW_EXECUTION_LIST` - List workflow executions +- `COLLECTION_WORKFLOW_EXECUTION_GET` - Get execution details with step results + +### Assistant Collection +- `COLLECTION_ASSISTANT_LIST` - List all assistants +- `COLLECTION_ASSISTANT_GET` - Get a single assistant by ID +- `COLLECTION_ASSISTANT_CREATE` - Create a new assistant +- `COLLECTION_ASSISTANT_UPDATE` - Update an existing assistant +- `COLLECTION_ASSISTANT_DELETE` - Delete an assistant + +### Prompt Collection +- `COLLECTION_PROMPT_LIST` - List all prompts +- `COLLECTION_PROMPT_GET` - Get a single prompt by ID + +## Development + +```bash +# Install dependencies +bun install + +# Type check +bun run check + +# Format code +bun run fmt +``` diff --git a/mcp-studio/package.json b/mcp-studio/package.json index db7434a..f3ad62a 100644 --- a/mcp-studio/package.json +++ b/mcp-studio/package.json @@ -6,6 +6,8 @@ "type": "module", "scripts": { "dev": "bun run --hot server/main.ts", + "dev:stdio": "bun --watch server/stdio.ts", + "stdio": "bun server/stdio.ts", "configure": "deco configure", "gen": "deco gen --output=shared/deco.gen.ts", "check": "tsc --noEmit", diff --git a/mcp-studio/server/db/file-workflows.ts b/mcp-studio/server/db/file-workflows.ts new file mode 100644 index 0000000..1db36b7 --- /dev/null +++ b/mcp-studio/server/db/file-workflows.ts @@ -0,0 +1,233 @@ +/** + * File-based Workflows Loader + * + * Loads workflow JSON files from directories specified in WORKFLOWS_DIRS env var. + * These workflows are read-only and can be duplicated to PostgreSQL. + * + * Features: + * - Supports multiple directories (comma-separated) + * - Supports ~ for home directory expansion + * - Watches for file changes (optional, for dev mode) + * - Caches workflows in memory with TTL + */ + +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as os from "node:os"; +import type { Workflow } from "@decocms/bindings/workflow"; + +// ============================================================================ +// Types +// ============================================================================ + +export interface FileWorkflow extends Workflow { + /** Mark as read-only (comes from file, not DB) */ + readonly: true; + /** Source file path */ + source_file: string; + /** Source directory */ + source_dir: string; +} + +interface CacheEntry { + workflows: FileWorkflow[]; + loadedAt: number; +} + +// ============================================================================ +// Configuration +// ============================================================================ + +const CACHE_TTL_MS = 60 * 1000; // 1 minute cache + +let cache: CacheEntry | null = null; + +// ============================================================================ +// Path Helpers +// ============================================================================ + +function expandPath(p: string): string { + if (p.startsWith("~/")) { + return path.join(os.homedir(), p.slice(2)); + } + return p; +} + +function getWorkflowDirs(): string[] { + const envVar = process.env.WORKFLOWS_DIRS; + if (!envVar) return []; + + return envVar + .split(",") + .map((d) => d.trim()) + .filter(Boolean) + .map(expandPath); +} + +// ============================================================================ +// File Loading +// ============================================================================ + +function loadWorkflowFromFile( + filePath: string, + sourceDir: string, +): FileWorkflow | null { + try { + const content = fs.readFileSync(filePath, "utf-8"); + const parsed = JSON.parse(content); + + // Validate basic structure + if (!parsed.id || !parsed.title) { + console.error( + `[file-workflows] Invalid workflow (missing id or title): ${filePath}`, + ); + return null; + } + + // Ensure steps array exists + if (!Array.isArray(parsed.steps)) { + parsed.steps = []; + } + + return { + ...parsed, + readonly: true, + source_file: filePath, + source_dir: sourceDir, + // Ensure dates exist + created_at: parsed.created_at || new Date().toISOString(), + updated_at: parsed.updated_at || new Date().toISOString(), + } as FileWorkflow; + } catch (error) { + console.error(`[file-workflows] Error loading ${filePath}:`, error); + return null; + } +} + +function loadWorkflowsFromDir(dir: string): FileWorkflow[] { + const workflows: FileWorkflow[] = []; + + if (!fs.existsSync(dir)) { + console.warn(`[file-workflows] Directory not found: ${dir}`); + return workflows; + } + + try { + const files = fs.readdirSync(dir); + + for (const file of files) { + if (!file.endsWith(".json")) continue; + + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (!stat.isFile()) continue; + + const workflow = loadWorkflowFromFile(filePath, dir); + if (workflow) { + workflows.push(workflow); + } + } + } catch (error) { + console.error(`[file-workflows] Error reading directory ${dir}:`, error); + } + + return workflows; +} + +// ============================================================================ +// Public API +// ============================================================================ + +/** + * Get all file-based workflows. + * Uses caching with TTL for performance. + */ +export function getFileWorkflows(forceRefresh = false): FileWorkflow[] { + const now = Date.now(); + + // Return cached if valid + if (cache && !forceRefresh && now - cache.loadedAt < CACHE_TTL_MS) { + return cache.workflows; + } + + // Load from all directories + const dirs = getWorkflowDirs(); + const workflows: FileWorkflow[] = []; + + for (const dir of dirs) { + const dirWorkflows = loadWorkflowsFromDir(dir); + workflows.push(...dirWorkflows); + } + + // Deduplicate by ID (first one wins) + const seen = new Set(); + const deduped = workflows.filter((w) => { + if (seen.has(w.id)) { + console.warn( + `[file-workflows] Duplicate workflow ID "${w.id}" found, using first occurrence`, + ); + return false; + } + seen.add(w.id); + return true; + }); + + // Update cache + cache = { + workflows: deduped, + loadedAt: now, + }; + + if (deduped.length > 0) { + console.error( + `[file-workflows] Loaded ${deduped.length} workflows from ${dirs.length} directories`, + ); + } + + return deduped; +} + +/** + * Get a specific file-based workflow by ID. + */ +export function getFileWorkflow(id: string): FileWorkflow | null { + const workflows = getFileWorkflows(); + return workflows.find((w) => w.id === id) || null; +} + +/** + * Check if a workflow ID exists in file-based workflows. + */ +export function isFileWorkflow(id: string): boolean { + return getFileWorkflow(id) !== null; +} + +/** + * Clear the cache (for testing or hot-reload scenarios). + */ +export function clearFileWorkflowsCache(): void { + cache = null; +} + +/** + * Initialize and log status. + */ +export function initFileWorkflows(): void { + const dirs = getWorkflowDirs(); + + if (dirs.length === 0) { + console.error( + "[file-workflows] No WORKFLOWS_DIRS configured - only PostgreSQL workflows available", + ); + return; + } + + console.error(`[file-workflows] Configured directories: ${dirs.join(", ")}`); + + // Pre-load to validate + const workflows = getFileWorkflows(true); + console.error( + `[file-workflows] Loaded ${workflows.length} file-based workflows`, + ); +} diff --git a/mcp-studio/server/main.ts b/mcp-studio/server/main.ts index da856de..4509023 100644 --- a/mcp-studio/server/main.ts +++ b/mcp-studio/server/main.ts @@ -8,7 +8,11 @@ import { serve } from "@decocms/mcps-shared/serve"; import { withRuntime } from "@decocms/runtime"; import { ensureCollections, ensureIndexes } from "./db/index.ts"; -import { ensurePromptsTable } from "./db/schemas/agents.ts"; +import { initFileWorkflows } from "./db/file-workflows.ts"; +import { + ensureAssistantsTable, + ensurePromptsTable, +} from "./db/schemas/agents.ts"; import { handleWorkflowEvents, WORKFLOW_EVENTS } from "./events/handler.ts"; import { tools } from "./tools/index.ts"; import { type Env, type Registry, StateSchema } from "./types/env.ts"; @@ -59,10 +63,13 @@ const runtime = withRuntime({ }, configuration: { onChange: async (env) => { - // Create tables first, then indexes + // Initialize file-based workflows (from WORKFLOWS_DIRS env var) + initFileWorkflows(); + + await ensureIndexes(env); await ensureCollections(env); + await ensureAssistantsTable(env); await ensurePromptsTable(env); - await ensureIndexes(env); }, scopes: [ "DATABASE::DATABASES_RUN_SQL", @@ -73,7 +80,7 @@ const runtime = withRuntime({ state: StateSchema, }, tools, - prompts: [], // removed because this was making a call to the database for every request to the MCP server + prompts: [], }); serve(runtime.fetch); diff --git a/mcp-studio/server/stdio-tools.ts b/mcp-studio/server/stdio-tools.ts new file mode 100644 index 0000000..beee85a --- /dev/null +++ b/mcp-studio/server/stdio-tools.ts @@ -0,0 +1,1513 @@ +/** + * MCP Studio - Stdio Tool Registration + * + * Adapts the runtime-based tools for standalone stdio transport. + * Uses Mesh bindings to connect to database via Mesh's proxy API. + * + * Supports Mesh bindings via TWO mechanisms: + * 1. Environment variables (primary for STDIO): + * - MESH_STATE: JSON with binding connection IDs + * - MESH_URL: Base URL of the Mesh instance + * - MESH_TOKEN: JWT token for authenticating with Mesh API + * + * 2. MCP tools (for dynamic configuration): + * - MCP_CONFIGURATION: Returns the state schema for the bindings UI + * - ON_MCP_CONFIGURATION: Receives configured bindings dynamically + * + * When bindings are configured, calls Mesh's API to run SQL queries. + * The mesh token provides authentication and the binding's connection ID + * routes the query to the correct database. + */ + +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; +import { + isFilesystemMode, + loadWorkflows, + getCachedWorkflows, + getWorkflowById, + startWatching, + getWorkflowSource, +} from "./workflow-loader.ts"; + +// ============================================================================ +// Configuration State (Bindings) +// ============================================================================ + +/** + * Creates a binding schema compatible with Mesh UI. + * Uses @deco/ prefix for app-name based matching. + */ +const BindingOf = (bindingType: string) => + z.object({ + __type: z.literal(bindingType).default(bindingType), + value: z.string().describe("Connection ID"), + }); + +/** + * Creates a binding schema with tool-based matching. + * Uses non-@deco/ prefix to enable tool-based filtering in Mesh. + */ +const BindingWithTools = (bindingType: string, _tools: { name: string }[]) => + z.object({ + __type: z.literal(bindingType).default(bindingType), + value: z.string().describe("Connection ID"), + }); + +/** + * Tool definitions for tool-based connection matching. + */ +const DATABASE_BINDING_TOOLS = [{ name: "DATABASES_RUN_SQL" }]; + +const EVENT_BUS_BINDING_TOOLS = [ + { name: "EVENT_PUBLISH" }, + { name: "EVENT_SUBSCRIBE" }, +]; + +/** + * State schema for stdio mode bindings. + * Uses tool-based matching for DATABASE and EVENT_BUS, + * and app-name matching for CONNECTION. + */ +const StdioStateSchema = z.object({ + // Tool-based matching: finds connections with DATABASES_RUN_SQL tool + DATABASE: BindingWithTools("DATABASE", DATABASE_BINDING_TOOLS) + .optional() + .describe("PostgreSQL database binding"), + // Tool-based matching: finds connections with EVENT_PUBLISH/EVENT_SUBSCRIBE tools + EVENT_BUS: BindingWithTools("EVENT_BUS", EVENT_BUS_BINDING_TOOLS) + .optional() + .describe("Event bus for workflow events"), + // App-name matching: finds connections with connection management tools + CONNECTION: BindingOf("@deco/connection") + .optional() + .describe("Connection management"), +}); + +/** + * Post-process the state schema to inject binding tool definitions. + * This tells Mesh to match connections by the tools they provide. + */ +function injectBindingSchema( + schema: Record, +): Record { + const props = schema.properties as Record>; + + // Inject DATABASE binding tools + if (props?.DATABASE?.properties) { + const dbProps = props.DATABASE.properties as Record< + string, + Record + >; + dbProps.__binding = { + const: DATABASE_BINDING_TOOLS, + }; + } + + // Inject EVENT_BUS binding tools + if (props?.EVENT_BUS?.properties) { + const ebProps = props.EVENT_BUS.properties as Record< + string, + Record + >; + ebProps.__binding = { + const: EVENT_BUS_BINDING_TOOLS, + }; + } + + return schema; +} + +// ============================================================================ +// Environment Variable Parsing (for STDIO mode) +// ============================================================================ + +interface BindingValue { + __type: string; + value: string; +} + +interface EnvBindings { + database?: string; + eventBus?: string; + connection?: string; +} + +/** + * Parse bindings from MESH_STATE environment variable. + * Mesh passes this when spawning STDIO processes. + */ +function parseBindingsFromEnv(): EnvBindings { + const meshStateJson = process.env.MESH_STATE; + if (!meshStateJson) return {}; + + try { + const state = JSON.parse(meshStateJson) as Record; + return { + database: state.DATABASE?.value, + eventBus: state.EVENT_BUS?.value, + connection: state.CONNECTION?.value, + }; + } catch (e) { + console.error("[mcp-studio] Failed to parse MESH_STATE:", e); + return {}; + } +} + +// ============================================================================ +// Mesh Configuration +// ============================================================================ + +interface MeshConfig { + meshUrl: string; + meshToken: string; + databaseConnectionId: string; +} + +// Parse env bindings at module load +const envBindings = parseBindingsFromEnv(); + +// Initialize meshConfig from env vars if available +let meshConfig: MeshConfig | null = null; +let migrationsRan = false; + +// Initialize from env vars (primary path for STDIO connections) +const meshUrl = process.env.MESH_URL; +const meshToken = process.env.MESH_TOKEN; + +if (meshUrl && meshToken && envBindings.database) { + meshConfig = { + meshUrl, + meshToken, + databaseConnectionId: envBindings.database, + }; + console.error( + "[mcp-studio] ✅ Bindings initialized from MESH_STATE env var:", + ); + console.error(`[mcp-studio] MESH_URL: ${meshUrl}`); + console.error(`[mcp-studio] DATABASE: ${envBindings.database}`); + if (envBindings.eventBus) { + console.error(`[mcp-studio] EVENT_BUS: ${envBindings.eventBus}`); + } + if (envBindings.connection) { + console.error(`[mcp-studio] CONNECTION: ${envBindings.connection}`); + } +} else if (envBindings.database) { + console.error( + "[mcp-studio] ⚠️ DATABASE binding found but MESH_URL or MESH_TOKEN missing", + ); + console.error( + "[mcp-studio] Database operations will fail until ON_MCP_CONFIGURATION is called", + ); +} + +// ============================================================================ +// Database Connection via Mesh API +// ============================================================================ + +/** + * Call a tool on a Mesh connection via the proxy API. + * This allows STDIO MCPs to use bindings just like HTTP MCPs. + */ +async function callMeshTool( + connectionId: string, + toolName: string, + args: Record, +): Promise { + if (!meshConfig) { + throw new Error( + "Database not configured. Configure bindings in Mesh UI first.", + ); + } + + const endpoint = `${meshConfig.meshUrl}/mcp/${connectionId}`; + + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + Authorization: `Bearer ${meshConfig.meshToken}`, + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: Date.now(), + method: "tools/call", + params: { + name: toolName, + arguments: args, + }, + }), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Mesh API error (${response.status}): ${text}`); + } + + // Handle both JSON and SSE responses + const contentType = response.headers.get("Content-Type") || ""; + let json: { + result?: { structuredContent?: T; content?: { text: string }[] }; + error?: { message: string }; + }; + + if (contentType.includes("text/event-stream")) { + // Parse SSE response - extract JSON from data lines + const text = await response.text(); + const lines = text.split("\n"); + const dataLines = lines.filter((line) => line.startsWith("data: ")); + const lastData = dataLines[dataLines.length - 1]; + if (!lastData) { + throw new Error("Empty SSE response from Mesh API"); + } + json = JSON.parse(lastData.slice(6)); // Remove "data: " prefix + } else { + json = await response.json(); + } + + if (json.error) { + throw new Error(`Mesh tool error: ${json.error.message}`); + } + + return (json.result?.structuredContent ?? + JSON.parse(json.result?.content?.[0]?.text ?? "null")) as T; +} + +/** + * Run SQL query via Mesh's database binding proxy. + * Uses DATABASES_RUN_SQL tool on the configured database connection. + */ +async function runSQL( + query: string, + params: unknown[] = [], +): Promise { + if (!meshConfig) { + throw new Error( + "Database not configured. Configure bindings in Mesh UI first.", + ); + } + + const result = await callMeshTool<{ + result: { results?: T[] }[]; + }>(meshConfig.databaseConnectionId, "DATABASES_RUN_SQL", { + sql: query, + params, + }); + + return result.result?.[0]?.results ?? []; +} + +// ============================================================================ +// Database Migrations +// ============================================================================ + +/** + * Run migrations to ensure all tables exist. + * This mirrors the `configuration.onChange` behavior from HTTP mode. + */ +async function runMigrations(): Promise { + console.error("[mcp-studio] Running migrations..."); + + // workflow_collection table + await runSQL(` + CREATE TABLE IF NOT EXISTS workflow_collection ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + input JSONB, + gateway_id TEXT NOT NULL, + description TEXT, + steps JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by TEXT, + updated_by TEXT + ) + `); + + await runSQL(` + CREATE INDEX IF NOT EXISTS idx_workflow_collection_created_at ON workflow_collection(created_at DESC); + CREATE INDEX IF NOT EXISTS idx_workflow_collection_updated_at ON workflow_collection(updated_at DESC); + CREATE INDEX IF NOT EXISTS idx_workflow_collection_title ON workflow_collection(title); + `); + + // workflow table + await runSQL(` + CREATE TABLE IF NOT EXISTS workflow ( + id TEXT PRIMARY KEY, + workflow_collection_id TEXT, + steps JSONB NOT NULL DEFAULT '{}', + input JSONB, + gateway_id TEXT NOT NULL, + created_at_epoch_ms BIGINT NOT NULL, + created_by TEXT + ) + `); + + await runSQL(` + CREATE INDEX IF NOT EXISTS idx_workflow_created_at_epoch ON workflow(created_at_epoch_ms DESC); + CREATE INDEX IF NOT EXISTS idx_workflow_collection_id ON workflow(workflow_collection_id); + CREATE INDEX IF NOT EXISTS idx_workflow_gateway_id ON workflow(gateway_id); + `); + + // workflow_execution table + await runSQL(` + CREATE TABLE IF NOT EXISTS workflow_execution ( + id TEXT PRIMARY KEY, + workflow_id TEXT NOT NULL, + status TEXT NOT NULL CHECK(status IN ('enqueued', 'cancelled', 'success', 'error', 'running')), + input JSONB, + output JSONB, + created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM now())*1000)::bigint, + updated_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM now())*1000)::bigint, + start_at_epoch_ms BIGINT, + started_at_epoch_ms BIGINT, + completed_at_epoch_ms BIGINT, + timeout_ms BIGINT, + deadline_at_epoch_ms BIGINT, + error JSONB, + created_by TEXT + ) + `); + + await runSQL(` + CREATE INDEX IF NOT EXISTS idx_workflow_execution_status ON workflow_execution(status); + CREATE INDEX IF NOT EXISTS idx_workflow_execution_created_at ON workflow_execution(created_at DESC); + CREATE INDEX IF NOT EXISTS idx_workflow_execution_start_at ON workflow_execution(start_at_epoch_ms); + `); + + // workflow_execution_step_result table + await runSQL(` + CREATE TABLE IF NOT EXISTS workflow_execution_step_result ( + execution_id TEXT NOT NULL, + step_id TEXT NOT NULL, + started_at_epoch_ms BIGINT, + completed_at_epoch_ms BIGINT, + output JSONB, + error JSONB, + PRIMARY KEY (execution_id, step_id), + FOREIGN KEY (execution_id) REFERENCES workflow_execution(id) + ) + `); + + await runSQL(` + CREATE INDEX IF NOT EXISTS idx_workflow_execution_step_result_execution ON workflow_execution_step_result(execution_id); + CREATE INDEX IF NOT EXISTS idx_workflow_execution_step_result_started ON workflow_execution_step_result(started_at_epoch_ms DESC); + CREATE INDEX IF NOT EXISTS idx_workflow_execution_step_result_completed ON workflow_execution_step_result(completed_at_epoch_ms DESC); + `); + + // assistants table + await runSQL(` + CREATE TABLE IF NOT EXISTS assistants ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by TEXT, + updated_by TEXT, + description TEXT NOT NULL, + instructions TEXT NOT NULL, + tool_set JSONB NOT NULL DEFAULT '{}', + avatar TEXT NOT NULL DEFAULT '', + system_prompt TEXT NOT NULL DEFAULT '', + gateway_id TEXT NOT NULL DEFAULT '', + model JSONB NOT NULL DEFAULT '{"id":"","connectionId":""}'::jsonb + ) + `); + + await runSQL(` + CREATE INDEX IF NOT EXISTS idx_assistants_created_at ON assistants(created_at DESC); + CREATE INDEX IF NOT EXISTS idx_assistants_updated_at ON assistants(updated_at DESC); + CREATE INDEX IF NOT EXISTS idx_assistants_title ON assistants(title); + `); + + // prompts table + await runSQL(` + CREATE TABLE IF NOT EXISTS prompts ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by TEXT, + updated_by TEXT, + description TEXT, + arguments JSONB NOT NULL DEFAULT '[]', + icons JSONB NOT NULL DEFAULT '[]', + messages JSONB NOT NULL DEFAULT '[]' + ) + `); + + await runSQL(` + CREATE INDEX IF NOT EXISTS idx_prompts_created_at ON prompts(created_at DESC); + CREATE INDEX IF NOT EXISTS idx_prompts_updated_at ON prompts(updated_at DESC); + CREATE INDEX IF NOT EXISTS idx_prompts_title ON prompts(title); + `); + + console.error("[mcp-studio] Migrations complete"); +} + +// ============================================================================ +// Tool Logging +// ============================================================================ + +function logTool(name: string, args: Record) { + const argStr = Object.entries(args) + .map(([k, v]) => `${k}=${JSON.stringify(v)?.slice(0, 50)}`) + .join(" "); + console.error(`[mcp-studio] ${name}${argStr ? ` ${argStr}` : ""}`); +} + +function withLogging>( + toolName: string, + handler: (args: T) => Promise, +): (args: T) => Promise { + return async (args: T) => { + logTool(toolName, args as Record); + return handler(args); + }; +} + +// ============================================================================ +// Tool Registration +// ============================================================================ + +export async function registerStdioTools(server: McpServer): Promise { + // ========================================================================= + // Initialize Filesystem Workflow Loading (if configured) + // ========================================================================= + + const filesystemMode = isFilesystemMode(); + if (filesystemMode) { + console.error("[mcp-studio] Filesystem workflow mode enabled"); + await loadWorkflows(); + + // Start watching for changes + const source = getWorkflowSource(); + if (source.workflowDir) { + await startWatching({ + ...source, + watch: true, + onChange: (workflows) => { + console.error( + `[mcp-studio] Workflows reloaded: ${workflows.length} workflow(s)`, + ); + }, + }); + } + } + + // ========================================================================= + // Run Migrations at Startup (if we have meshConfig from env vars) + // ========================================================================= + + if (meshConfig && !migrationsRan) { + try { + await runMigrations(); + migrationsRan = true; + console.error( + "[mcp-studio] ✅ Migrations completed at startup via Mesh API", + ); + } catch (error) { + console.error("[mcp-studio] ⚠️ Migration error at startup:", error); + // Don't fail startup, let tools fail gracefully if needed + } + } + + // ========================================================================= + // MCP Configuration Tools (for Mesh bindings UI) + // ========================================================================= + + server.registerTool( + "MCP_CONFIGURATION", + { + title: "MCP Configuration", + description: + "Returns the configuration schema for this MCP server. Used by Mesh to show the bindings UI.", + inputSchema: {}, + annotations: { readOnlyHint: true }, + }, + withLogging("MCP_CONFIGURATION", async () => { + // Use Zod v4's built-in JSON schema conversion + const rawStateSchema = z.toJSONSchema(StdioStateSchema); + + // Inject binding tool definitions for tool-based connection matching + const stateSchema = injectBindingSchema( + rawStateSchema as Record, + ); + + const result = { + stateSchema, + scopes: [ + "DATABASE::DATABASES_RUN_SQL", + "EVENT_BUS::*", + "CONNECTION::*", + ], + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + // Binding schema for ON_MCP_CONFIGURATION input + const BindingInputSchema = z + .object({ + __type: z.string(), + value: z.string(), + }) + .optional(); + + server.registerTool( + "ON_MCP_CONFIGURATION", + { + title: "On MCP Configuration", + description: + "Called by Mesh when the user saves binding configuration. Applies the configured state and mesh credentials.", + inputSchema: { + state: z + .object({ + DATABASE: BindingInputSchema, + EVENT_BUS: BindingInputSchema, + CONNECTION: BindingInputSchema, + }) + .passthrough() + .describe("The configured state from the bindings UI"), + scopes: z.array(z.string()).describe("List of authorized scopes"), + // Mesh credentials for STDIO connections to call back to Mesh API + meshToken: z + .string() + .optional() + .describe("JWT token for authenticating with Mesh API"), + meshUrl: z + .string() + .optional() + .describe("Base URL of the Mesh instance"), + }, + annotations: { readOnlyHint: false }, + }, + withLogging("ON_MCP_CONFIGURATION", async (args) => { + console.error("[mcp-studio] Received configuration"); + + const state = args.state || {}; + const databaseConnectionId = state.DATABASE?.value; + + // Store mesh configuration if provided + if (args.meshToken && args.meshUrl && databaseConnectionId) { + meshConfig = { + meshToken: args.meshToken, + meshUrl: args.meshUrl, + databaseConnectionId, + }; + console.error( + `[mcp-studio] Mesh binding configured: ${args.meshUrl} -> ${databaseConnectionId}`, + ); + + // Run migrations via Mesh API + if (!migrationsRan) { + try { + await runMigrations(); + migrationsRan = true; + console.error("[mcp-studio] Migrations completed via Mesh API"); + } catch (error) { + console.error("[mcp-studio] Migration error:", error); + } + } + } else if (databaseConnectionId) { + console.error( + `[mcp-studio] Database binding configured to: ${databaseConnectionId}`, + ); + console.error( + "[mcp-studio] Warning: No meshToken/meshUrl provided - database operations will fail", + ); + } + + if (state.EVENT_BUS?.value) { + console.error( + `[mcp-studio] Event bus binding: ${state.EVENT_BUS.value}`, + ); + } + if (state.CONNECTION?.value) { + console.error( + `[mcp-studio] Connection binding: ${state.CONNECTION.value}`, + ); + } + + const result = { success: true, configured: !!meshConfig }; + return { + content: [{ type: "text", text: JSON.stringify(result) }], + structuredContent: result, + }; + }), + ); + + // ========================================================================= + // Workflow Collection Tools + // ========================================================================= + + server.registerTool( + "COLLECTION_WORKFLOW_LIST", + { + title: "List Workflows", + description: "List all workflows with optional pagination", + inputSchema: { + limit: z.number().default(50), + offset: z.number().default(0), + source: z + .enum(["all", "filesystem", "database"]) + .default("all") + .describe( + "Filter by source: all (both), filesystem (from files), database (from PostgreSQL)", + ), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("COLLECTION_WORKFLOW_LIST", async (args) => { + const includeFilesystem = + args.source === "all" || args.source === "filesystem"; + const includeDatabase = + args.source === "all" || args.source === "database"; + + let allItems: Record[] = []; + + // Get filesystem workflows + if (includeFilesystem && filesystemMode) { + const fsWorkflows = getCachedWorkflows().map((w) => ({ + ...w, + _source: "filesystem", + })); + allItems.push(...fsWorkflows); + } + + // Get database workflows (only if we have mesh config) + if (includeDatabase && meshConfig) { + try { + const dbItems = await runSQL>( + "SELECT * FROM workflow_collection ORDER BY updated_at DESC", + [], + ); + const transformed = dbItems.map((item) => ({ + ...transformWorkflow(item), + _source: "database", + })); + allItems.push(...transformed); + } catch (error) { + // Database not available, skip silently + console.error( + "[mcp-studio] Database query failed, using filesystem only", + ); + } + } + + // Apply pagination + const totalCount = allItems.length; + const paginatedItems = allItems.slice( + args.offset, + args.offset + args.limit, + ); + + const result = { + items: paginatedItems, + totalCount, + hasMore: args.offset + paginatedItems.length < totalCount, + mode: filesystemMode ? "filesystem" : "database", + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + server.registerTool( + "COLLECTION_WORKFLOW_GET", + { + title: "Get Workflow", + description: "Get a single workflow by ID", + inputSchema: { + id: z.string().describe("Workflow ID"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("COLLECTION_WORKFLOW_GET", async (args) => { + // Try filesystem first + if (filesystemMode) { + const fsWorkflow = getWorkflowById(args.id); + if (fsWorkflow) { + const result = { + item: { ...fsWorkflow, _source: "filesystem" }, + }; + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + } + } + + // Fall back to database + if (meshConfig) { + const items = await runSQL>( + "SELECT * FROM workflow_collection WHERE id = ? LIMIT 1", + [args.id], + ); + + const result = { + item: items[0] + ? { ...transformWorkflow(items[0]), _source: "database" } + : null, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + } + + // No workflow found + const result = { item: null }; + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + server.registerTool( + "COLLECTION_WORKFLOW_CREATE", + { + title: "Create Workflow", + description: "Create a new workflow", + inputSchema: { + data: z.object({ + id: z.string().optional(), + title: z.string(), + description: z.string().optional(), + steps: z.array(z.unknown()).optional(), + gateway_id: z.string().optional(), + }), + }, + annotations: { readOnlyHint: false }, + }, + withLogging("COLLECTION_WORKFLOW_CREATE", async (args) => { + const now = new Date().toISOString(); + const id = args.data.id || crypto.randomUUID(); + + await runSQL( + `INSERT INTO workflow_collection (id, title, description, steps, gateway_id, created_at, updated_at, created_by, updated_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + id, + args.data.title, + args.data.description || null, + JSON.stringify(args.data.steps || []), + args.data.gateway_id || "", + now, + now, + "stdio-user", + "stdio-user", + ], + ); + + const items = await runSQL>( + "SELECT * FROM workflow_collection WHERE id = ? LIMIT 1", + [id], + ); + + const result = { + item: items[0] ? transformWorkflow(items[0]) : null, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + server.registerTool( + "COLLECTION_WORKFLOW_UPDATE", + { + title: "Update Workflow", + description: "Update an existing workflow", + inputSchema: { + id: z.string(), + data: z.object({ + title: z.string().optional(), + description: z.string().optional(), + steps: z.array(z.unknown()).optional(), + }), + }, + annotations: { readOnlyHint: false }, + }, + withLogging("COLLECTION_WORKFLOW_UPDATE", async (args) => { + const now = new Date().toISOString(); + const setClauses: string[] = ["updated_at = ?", "updated_by = ?"]; + const params: unknown[] = [now, "stdio-user"]; + + if (args.data.title !== undefined) { + setClauses.push("title = ?"); + params.push(args.data.title); + } + if (args.data.description !== undefined) { + setClauses.push("description = ?"); + params.push(args.data.description); + } + if (args.data.steps !== undefined) { + setClauses.push("steps = ?"); + params.push(JSON.stringify(args.data.steps)); + } + + params.push(args.id); + + await runSQL( + `UPDATE workflow_collection SET ${setClauses.join(", ")} WHERE id = ?`, + params, + ); + + const items = await runSQL>( + "SELECT * FROM workflow_collection WHERE id = ? LIMIT 1", + [args.id], + ); + + const result = { + item: items[0] ? transformWorkflow(items[0]) : null, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + server.registerTool( + "COLLECTION_WORKFLOW_DELETE", + { + title: "Delete Workflow", + description: "Delete a workflow by ID", + inputSchema: { + id: z.string(), + }, + annotations: { readOnlyHint: false, destructiveHint: true }, + }, + withLogging("COLLECTION_WORKFLOW_DELETE", async (args) => { + const items = await runSQL>( + "DELETE FROM workflow_collection WHERE id = ? RETURNING *", + [args.id], + ); + + const result = { + item: items[0] ? transformWorkflow(items[0]) : null, + success: items.length > 0, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + // ========================================================================= + // Workflow Execution Tools + // ========================================================================= + + server.registerTool( + "COLLECTION_WORKFLOW_EXECUTION_LIST", + { + title: "List Executions", + description: "List workflow executions with pagination", + inputSchema: { + limit: z.number().default(50), + offset: z.number().default(0), + workflow_id: z.string().optional(), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("COLLECTION_WORKFLOW_EXECUTION_LIST", async (args) => { + let sql = + "SELECT * FROM workflow_execution ORDER BY created_at DESC LIMIT ? OFFSET ?"; + const params: unknown[] = [args.limit, args.offset]; + + if (args.workflow_id) { + sql = + "SELECT * FROM workflow_execution WHERE workflow_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?"; + params.unshift(args.workflow_id); + } + + const items = await runSQL>(sql, params); + + let countSql = "SELECT COUNT(*) as count FROM workflow_execution"; + const countParams: unknown[] = []; + + if (args.workflow_id) { + countSql = + "SELECT COUNT(*) as count FROM workflow_execution WHERE workflow_id = ?"; + countParams.push(args.workflow_id); + } + + const countResult = await runSQL<{ count: string }>( + countSql, + countParams, + ); + const totalCount = parseInt(countResult[0]?.count || "0", 10); + + const result = { + items: items.map(transformExecution), + totalCount, + hasMore: args.offset + items.length < totalCount, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + server.registerTool( + "COLLECTION_WORKFLOW_EXECUTION_GET", + { + title: "Get Execution", + description: "Get a single workflow execution by ID with step results", + inputSchema: { + id: z.string().describe("Execution ID"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("COLLECTION_WORKFLOW_EXECUTION_GET", async (args) => { + const executions = await runSQL>( + "SELECT * FROM workflow_execution WHERE id = ? LIMIT 1", + [args.id], + ); + + const stepResults = await runSQL>( + "SELECT * FROM workflow_execution_step_result WHERE execution_id = ? ORDER BY started_at_epoch_ms ASC", + [args.id], + ); + + const result = { + item: executions[0] ? transformExecution(executions[0]) : null, + step_results: stepResults.map(transformStepResult), + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + // ========================================================================= + // Assistant Collection Tools + // ========================================================================= + + server.registerTool( + "COLLECTION_ASSISTANT_LIST", + { + title: "List Assistants", + description: "List all assistants with pagination", + inputSchema: { + limit: z.number().default(50), + offset: z.number().default(0), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("COLLECTION_ASSISTANT_LIST", async (args) => { + const items = await runSQL>( + "SELECT * FROM assistants ORDER BY updated_at DESC LIMIT ? OFFSET ?", + [args.limit, args.offset], + ); + + const countResult = await runSQL<{ count: string }>( + "SELECT COUNT(*) as count FROM assistants", + ); + const totalCount = parseInt(countResult[0]?.count || "0", 10); + + const result = { + items: items.map(transformAssistant), + totalCount, + hasMore: args.offset + items.length < totalCount, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + server.registerTool( + "COLLECTION_ASSISTANT_GET", + { + title: "Get Assistant", + description: "Get a single assistant by ID", + inputSchema: { + id: z.string().describe("Assistant ID"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("COLLECTION_ASSISTANT_GET", async (args) => { + const items = await runSQL>( + "SELECT * FROM assistants WHERE id = ? LIMIT 1", + [args.id], + ); + + const result = { + item: items[0] ? transformAssistant(items[0]) : null, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + server.registerTool( + "COLLECTION_ASSISTANT_CREATE", + { + title: "Create Assistant", + description: "Create a new assistant", + inputSchema: { + data: z.object({ + id: z.string().optional(), + title: z.string(), + description: z.string().optional(), + avatar: z.string().optional(), + system_prompt: z.string().optional(), + gateway_id: z.string().optional(), + model: z + .object({ + id: z.string(), + connectionId: z.string(), + }) + .optional(), + }), + }, + annotations: { readOnlyHint: false }, + }, + withLogging("COLLECTION_ASSISTANT_CREATE", async (args) => { + const now = new Date().toISOString(); + const id = args.data.id || crypto.randomUUID(); + const defaultAvatar = + "https://assets.decocache.com/decocms/fd07a578-6b1c-40f1-bc05-88a3b981695d/f7fc4ffa81aec04e37ae670c3cd4936643a7b269.png"; + + await runSQL( + `INSERT INTO assistants (id, title, description, avatar, system_prompt, gateway_id, model, instructions, created_at, updated_at, created_by, updated_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + id, + args.data.title, + args.data.description || null, + args.data.avatar || defaultAvatar, + args.data.system_prompt || "", + args.data.gateway_id || "", + JSON.stringify(args.data.model || { id: "", connectionId: "" }), + "", // instructions - default to empty string + now, + now, + "stdio-user", + "stdio-user", + ], + ); + + const items = await runSQL>( + "SELECT * FROM assistants WHERE id = ? LIMIT 1", + [id], + ); + + const result = { + item: items[0] ? transformAssistant(items[0]) : null, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + server.registerTool( + "COLLECTION_ASSISTANT_UPDATE", + { + title: "Update Assistant", + description: "Update an existing assistant", + inputSchema: { + id: z.string(), + data: z.object({ + title: z.string().optional(), + description: z.string().optional(), + avatar: z.string().optional(), + system_prompt: z.string().optional(), + gateway_id: z.string().optional(), + model: z + .object({ + id: z.string(), + connectionId: z.string(), + }) + .optional(), + }), + }, + annotations: { readOnlyHint: false }, + }, + withLogging("COLLECTION_ASSISTANT_UPDATE", async (args) => { + const now = new Date().toISOString(); + const setClauses: string[] = ["updated_at = ?", "updated_by = ?"]; + const params: unknown[] = [now, "stdio-user"]; + + if (args.data.title !== undefined) { + setClauses.push("title = ?"); + params.push(args.data.title); + } + if (args.data.description !== undefined) { + setClauses.push("description = ?"); + params.push(args.data.description); + } + if (args.data.avatar !== undefined) { + setClauses.push("avatar = ?"); + params.push(args.data.avatar); + } + if (args.data.system_prompt !== undefined) { + setClauses.push("system_prompt = ?"); + params.push(args.data.system_prompt); + } + if (args.data.gateway_id !== undefined) { + setClauses.push("gateway_id = ?"); + params.push(args.data.gateway_id); + } + if (args.data.model !== undefined) { + setClauses.push("model = ?"); + params.push(JSON.stringify(args.data.model)); + } + + params.push(args.id); + + await runSQL( + `UPDATE assistants SET ${setClauses.join(", ")} WHERE id = ?`, + params, + ); + + const items = await runSQL>( + "SELECT * FROM assistants WHERE id = ? LIMIT 1", + [args.id], + ); + + const result = { + item: items[0] ? transformAssistant(items[0]) : null, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + server.registerTool( + "COLLECTION_ASSISTANT_DELETE", + { + title: "Delete Assistant", + description: "Delete an assistant by ID", + inputSchema: { + id: z.string(), + }, + annotations: { readOnlyHint: false, destructiveHint: true }, + }, + withLogging("COLLECTION_ASSISTANT_DELETE", async (args) => { + const items = await runSQL>( + "DELETE FROM assistants WHERE id = ? RETURNING *", + [args.id], + ); + + const result = { + item: items[0] ? transformAssistant(items[0]) : null, + success: items.length > 0, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + // ========================================================================= + // Prompt Collection Tools + // ========================================================================= + + server.registerTool( + "COLLECTION_PROMPT_LIST", + { + title: "List Prompts", + description: "List all prompts with pagination", + inputSchema: { + limit: z.number().default(50), + offset: z.number().default(0), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("COLLECTION_PROMPT_LIST", async (args) => { + const items = await runSQL>( + "SELECT * FROM prompts ORDER BY updated_at DESC LIMIT ? OFFSET ?", + [args.limit, args.offset], + ); + + const countResult = await runSQL<{ count: string }>( + "SELECT COUNT(*) as count FROM prompts", + ); + const totalCount = parseInt(countResult[0]?.count || "0", 10); + + const result = { + items: items.map(transformPrompt), + totalCount, + hasMore: args.offset + items.length < totalCount, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + server.registerTool( + "COLLECTION_PROMPT_GET", + { + title: "Get Prompt", + description: "Get a single prompt by ID", + inputSchema: { + id: z.string().describe("Prompt ID"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("COLLECTION_PROMPT_GET", async (args) => { + const items = await runSQL>( + "SELECT * FROM prompts WHERE id = ? LIMIT 1", + [args.id], + ); + + const result = { + item: items[0] ? transformPrompt(items[0]) : null, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + // ========================================================================= + // Filesystem Workflow Tools + // ========================================================================= + + if (filesystemMode) { + server.registerTool( + "WORKFLOW_RELOAD", + { + title: "Reload Workflows", + description: + "Reload all workflows from the filesystem. Use this after editing workflow JSON files.", + inputSchema: {}, + annotations: { readOnlyHint: true }, + }, + withLogging("WORKFLOW_RELOAD", async () => { + const workflows = await loadWorkflows(); + + const result = { + success: true, + count: workflows.length, + workflows: workflows.map((w) => ({ + id: w.id, + title: w.title, + sourceFile: w._sourceFile, + stepCount: w.steps.length, + })), + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + server.registerTool( + "WORKFLOW_SOURCE_INFO", + { + title: "Workflow Source Info", + description: + "Get information about where workflows are loaded from (filesystem paths, file counts)", + inputSchema: {}, + annotations: { readOnlyHint: true }, + }, + withLogging("WORKFLOW_SOURCE_INFO", async () => { + const source = getWorkflowSource(); + const workflows = getCachedWorkflows(); + + // Group by source file + const byFile = new Map(); + for (const w of workflows) { + const file = w._sourceFile; + if (!byFile.has(file)) { + byFile.set(file, []); + } + byFile.get(file)!.push(w.id); + } + + const result = { + mode: "filesystem", + workflowDir: source.workflowDir || null, + workflowFiles: source.workflowFiles || [], + totalWorkflows: workflows.length, + files: Array.from(byFile.entries()).map(([file, ids]) => ({ + path: file, + workflows: ids, + })), + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + } + + console.error("[mcp-studio] All stdio tools registered"); + if (filesystemMode) { + console.error( + "[mcp-studio] Filesystem mode: WORKFLOW_RELOAD and WORKFLOW_SOURCE_INFO available", + ); + } +} + +// ============================================================================ +// Transform Functions +// ============================================================================ + +function transformWorkflow(row: Record) { + let steps: unknown[] = []; + if (row.steps) { + const parsed = + typeof row.steps === "string" ? JSON.parse(row.steps) : row.steps; + // Handle legacy { phases: [...] } format + if (parsed && typeof parsed === "object" && "phases" in parsed) { + steps = (parsed as { phases: unknown[] }).phases; + } else if (Array.isArray(parsed)) { + steps = parsed; + } + } + + // Ensure each step has required properties (action, name) to prevent UI crashes + const normalizedSteps = steps.map((step, index) => { + const s = step as Record; + return { + name: s.name || `Step_${index + 1}`, + description: s.description, + action: s.action || { toolName: "" }, // Default to empty tool step if missing + input: s.input || {}, + outputSchema: s.outputSchema || {}, + config: s.config, + }; + }); + + return { + id: row.id, + title: row.title, + description: row.description, + steps: normalizedSteps, + gateway_id: row.gateway_id, + created_at: row.created_at, + updated_at: row.updated_at, + created_by: row.created_by, + updated_by: row.updated_by, + }; +} + +function transformExecution(row: Record) { + return { + id: row.id, + workflow_id: row.workflow_id, + status: row.status, + input: typeof row.input === "string" ? JSON.parse(row.input) : row.input, + output: row.output + ? typeof row.output === "string" + ? JSON.parse(row.output) + : row.output + : null, + error: row.error, + created_at: row.created_at, + updated_at: row.updated_at, + started_at: row.started_at, + completed_at: row.completed_at, + }; +} + +function transformStepResult(row: Record) { + return { + id: row.id, + execution_id: row.execution_id, + step_name: row.step_name, + status: row.status, + input: row.input + ? typeof row.input === "string" + ? JSON.parse(row.input) + : row.input + : null, + output: row.output + ? typeof row.output === "string" + ? JSON.parse(row.output) + : row.output + : null, + error: row.error, + created_at: row.created_at, + completed_at: row.completed_at, + }; +} + +function transformAssistant(row: Record) { + const defaultAvatar = + "https://assets.decocache.com/decocms/fd07a578-6b1c-40f1-bc05-88a3b981695d/f7fc4ffa81aec04e37ae670c3cd4936643a7b269.png"; + const model = row.model + ? typeof row.model === "string" + ? JSON.parse(row.model) + : row.model + : { id: "", connectionId: "" }; + + return { + id: row.id, + title: row.title, + description: row.description, + avatar: row.avatar || defaultAvatar, + system_prompt: row.system_prompt || "", + gateway_id: row.gateway_id || "", + model, + created_at: row.created_at, + updated_at: row.updated_at, + created_by: row.created_by, + updated_by: row.updated_by, + }; +} + +function transformPrompt(row: Record) { + return { + id: row.id, + title: row.title, + description: row.description, + content: row.content, + variables: row.variables + ? typeof row.variables === "string" + ? JSON.parse(row.variables) + : row.variables + : [], + created_at: row.created_at, + updated_at: row.updated_at, + created_by: row.created_by, + updated_by: row.updated_by, + }; +} diff --git a/mcp-studio/server/stdio.ts b/mcp-studio/server/stdio.ts new file mode 100644 index 0000000..b84608f --- /dev/null +++ b/mcp-studio/server/stdio.ts @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/** + * MCP Studio - Stdio Entry Point + * + * This is the main entry point for running the MCP server via stdio, + * which is the standard transport for CLI-based MCP servers. + * + * Usage: + * bun run stdio # Run directly + * bun run dev:stdio # Run with hot reload + * + * In Mesh, add as STDIO connection: + * Command: bun + * Args: /path/to/mcp-studio/server/stdio.ts + * + * Environment variables (passed by Mesh automatically): + * MESH_URL - Base URL of the Mesh instance (e.g., https://mesh.example.com) + * MESH_TOKEN - JWT token for authenticating with Mesh API + * MESH_STATE - JSON with binding connection IDs: + * {"DATABASE":{"__type":"@deco/postgres","value":"conn-id"}, ...} + * + * Optional environment variables: + * WORKFLOWS_DIR - Directory to load workflows from (enables filesystem mode) + * WORKFLOW_FILES - Comma-separated list of workflow JSON files + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { registerStdioTools } from "./stdio-tools.ts"; + +/** + * Create and start the MCP server with stdio transport + */ +async function main() { + console.error("[mcp-studio] Starting MCP Studio via stdio transport..."); + + // Create MCP server + const server = new McpServer({ + name: "mcp-studio", + version: "1.0.0", + }); + + // Register all tools (this also initializes bindings from env vars) + await registerStdioTools(server); + + // Connect to stdio transport + const transport = new StdioServerTransport(); + await server.connect(transport); + + // Log startup summary + console.error("[mcp-studio] ✅ MCP server running via stdio"); + console.error( + "[mcp-studio] Available: Workflow, Execution, Assistant, and Prompt tools", + ); + + // Log binding status + const hasMeshConfig = + process.env.MESH_URL && process.env.MESH_TOKEN && process.env.MESH_STATE; + if (hasMeshConfig) { + console.error("[mcp-studio] ✅ Mesh bindings configured from environment"); + } else { + console.error("[mcp-studio] ⚠️ No Mesh bindings in environment"); + console.error( + "[mcp-studio] Waiting for ON_MCP_CONFIGURATION or configure bindings in Mesh UI", + ); + if (!process.env.MESH_URL) + console.error("[mcp-studio] Missing: MESH_URL"); + if (!process.env.MESH_TOKEN) + console.error("[mcp-studio] Missing: MESH_TOKEN"); + if (!process.env.MESH_STATE) + console.error("[mcp-studio] Missing: MESH_STATE"); + } +} + +main().catch((error) => { + console.error("[mcp-studio] Fatal error:", error); + process.exit(1); +}); diff --git a/mcp-studio/server/tools/workflow.ts b/mcp-studio/server/tools/workflow.ts index 7203d34..fa3b40b 100644 --- a/mcp-studio/server/tools/workflow.ts +++ b/mcp-studio/server/tools/workflow.ts @@ -1,10 +1,11 @@ /** biome-ignore-all lint/suspicious/noExplicitAny: complicated types */ import { createCollectionListOutputSchema } from "@decocms/bindings/collections"; import { - StepSchema, - type Workflow, + createDefaultWorkflow, WORKFLOW_BINDING, + type Workflow, WorkflowSchema, + StepSchema, } from "@decocms/bindings/workflow"; import { createPrivateTool } from "@decocms/runtime/tools"; import { z } from "zod"; @@ -12,6 +13,12 @@ import { runSQL } from "../db/postgres.ts"; import type { Env } from "../types/env.ts"; import { validateWorkflow } from "../utils/validator.ts"; import { buildOrderByClause, buildWhereClause } from "./_helpers.ts"; +import { + getFileWorkflows, + getFileWorkflow, + isFileWorkflow, + type FileWorkflow, +} from "../db/file-workflows.ts"; const LIST_BINDING = WORKFLOW_BINDING.find( (b) => b.name === "COLLECTION_WORKFLOW_LIST", @@ -56,14 +63,27 @@ if (!DELETE_BINDING?.inputSchema || !DELETE_BINDING?.outputSchema) { ); } -function transformDbRowToWorkflowCollectionItem(row: unknown): Workflow { +/** Extended workflow with readonly flag */ +interface WorkflowWithMeta extends Workflow { + readonly?: boolean; + source_file?: string; +} + +function transformDbRowToWorkflowCollectionItem( + row: unknown, +): WorkflowWithMeta { const r = row as Record; // Parse steps - handle both old { phases: [...] } format and new direct array format let steps: unknown = []; if (r.steps) { const parsed = typeof r.steps === "string" ? JSON.parse(r.steps) : r.steps; - steps = parsed; + // Handle legacy { phases: [...] } format + if (parsed && typeof parsed === "object" && "phases" in parsed) { + steps = (parsed as { phases: unknown }).phases; + } else { + steps = parsed; + } } return { @@ -75,6 +95,7 @@ function transformDbRowToWorkflowCollectionItem(row: unknown): Workflow { updated_at: r.updated_at as string, created_by: r.created_by as string | undefined, updated_by: r.updated_by as string | undefined, + readonly: false, // DB workflows are editable }; } @@ -82,7 +103,7 @@ export const createListTool = (env: Env) => createPrivateTool({ id: "COLLECTION_WORKFLOW_LIST", description: - "List workflows with filtering, sorting, and pagination. This does not include the steps of the workflows, use the GET tool to check the list of steps.", + "List workflows with filtering, sorting, and pagination. Includes file-based workflows (readonly) from WORKFLOWS_DIRS.", inputSchema: LIST_BINDING.inputSchema, outputSchema: createCollectionListOutputSchema(WorkflowSchema), execute: async ({ @@ -109,27 +130,53 @@ export const createListTool = (env: Env) => LIMIT ? OFFSET ? `; - const itemsResult = await runSQL>(env, sql, [ - ...params, - limit, - offset, - ]); + const itemsResult: any = + await env.MESH_REQUEST_CONTEXT?.state?.DATABASE?.DATABASES_RUN_SQL({ + sql, + params: [...params, limit, offset], + }); + + if (!itemsResult?.result?.[0]?.results) { + throw new Error("Database query failed or returned invalid result"); + } const countQuery = `SELECT COUNT(*) as count FROM workflow_collection ${whereClause}`; - const countResult = await runSQL<{ count: string }>( - env, - countQuery, - params, + const countResult = + await env.MESH_REQUEST_CONTEXT?.state?.DATABASE?.DATABASES_RUN_SQL({ + sql: countQuery, + params, + }); + const dbTotalCount = parseInt( + (countResult?.result?.[0]?.results?.[0] as { count: string })?.count || + "0", + 10, + ); + + // Get DB workflows + const dbWorkflows: WorkflowWithMeta[] = itemsResult.result[0].results.map( + (item: Record) => + transformDbRowToWorkflowCollectionItem(item), ); - const totalCount = parseInt(countResult[0]?.count || "0", 10); + + // Get file-based workflows (always included, marked readonly) + const fileWorkflows = getFileWorkflows(); + + // Get IDs of DB workflows to avoid duplicates + const dbIds = new Set(dbWorkflows.map((w) => w.id)); + + // Filter file workflows to exclude those with same ID as DB (DB takes precedence) + const uniqueFileWorkflows = fileWorkflows.filter( + (fw) => !dbIds.has(fw.id), + ); + + // Merge: DB workflows first, then file workflows + const allWorkflows = [...dbWorkflows, ...uniqueFileWorkflows]; + const totalCount = dbTotalCount + uniqueFileWorkflows.length; return { - items: itemsResult?.map((item: Record) => ({ - ...transformDbRowToWorkflowCollectionItem(item), - steps: [], - })), + items: allWorkflows, totalCount, - hasMore: offset + (itemsResult?.length || 0) < totalCount, + hasMore: offset + dbWorkflows.length < dbTotalCount, }; }, }); @@ -137,13 +184,28 @@ export const createListTool = (env: Env) => export async function getWorkflowCollection( env: Env, id: string, -): Promise { - const result = await runSQL>( - env, - "SELECT * FROM workflow_collection WHERE id = ? LIMIT 1", - [id], - ); - return result[0] ? transformDbRowToWorkflowCollectionItem(result[0]) : null; +): Promise { + // First check DB + const result = + await env.MESH_REQUEST_CONTEXT?.state?.DATABASE?.DATABASES_RUN_SQL({ + sql: "SELECT * FROM workflow_collection WHERE id = ? LIMIT 1", + params: [id], + }); + const item = result?.result?.[0]?.results?.[0] || null; + + if (item) { + return transformDbRowToWorkflowCollectionItem( + item as Record, + ); + } + + // Fall back to file-based workflows + const fileWorkflow = getFileWorkflow(id); + if (fileWorkflow) { + return fileWorkflow as WorkflowWithMeta; + } + + return null; } export const createGetTool = (env: Env) => @@ -166,42 +228,51 @@ export const createGetTool = (env: Env) => }, }); -export async function insertWorkflowCollectionItem( - env: Env, - workflow: Workflow & { gateway_id: string }, -) { +export async function insertWorkflowCollection(env: Env, data?: Workflow) { try { + const user = env.MESH_REQUEST_CONTEXT?.ensureAuthenticated(); + const now = new Date().toISOString(); + + const workflow: Workflow = { + ...createDefaultWorkflow(), + ...data, + }; await validateWorkflow(workflow, env); + const stepsJson = JSON.stringify( - workflow.steps?.map((s) => ({ + workflow.steps.map((s) => ({ ...s, name: s.name.trim().replaceAll(/\s+/g, "_"), })) || [], ); - const result = await runSQL<{ id: string }>( + // Note: gateway_id should come from workflow data, not hard-coded + const gatewayId = (workflow as any).gateway_id ?? ""; + + const result = await runSQL>( env, - `INSERT INTO workflow_collection (id, title, gateway_id, description, steps, created_at, updated_at, created_by, updated_by) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`, + `INSERT INTO workflow_collection (id, title, input, gateway_id, description, steps, created_at, updated_at, created_by, updated_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`, [ workflow.id, workflow.title, - workflow.gateway_id, + JSON.stringify((workflow as any).input ?? null), + gatewayId, workflow.description || null, stepsJson, - workflow.created_at, - workflow.updated_at, - workflow.created_by, - workflow.updated_by, + now, + now, + user?.id || null, + user?.id || null, ], ); - if (!result?.length) { - throw new Error("Failed to create workflow collection item"); + if (!result.length) { + throw new Error("Failed to create workflow collection"); } return { - item: WorkflowSchema.parse(result[0]), + item: workflow, }; } catch (error) { console.error("Error creating workflow:", error); @@ -212,15 +283,11 @@ export async function insertWorkflowCollectionItem( export const createInsertTool = (env: Env) => createPrivateTool({ id: CREATE_BINDING.name, - description: `Creates a template/definition for a workflow. This entity is not executable, but can be used to create executions. - This is ideal for storing and reusing workflows. You may also want to use this tool to iterate on a workflow before creating executions. You may start with an empty array of steps and add steps gradually. + description: `Create a workflow: a sequence of steps that execute automatically with data flowing between them. Key concepts: - Steps run in parallel unless they reference each other's outputs via @ref -- Use @ref syntax to wire data: - - @input.field - From the execution input - - @stepName.field - From the output of a step -You can also put many refs inside a single string, for example: "Hello @input.name, your order @input.order_id is ready". +- Use @ref syntax to wire data: @input.field, @stepName.field, @item (in loops) - Execution order is auto-determined from @ref dependencies Example workflow with 2 parallel steps: @@ -228,57 +295,58 @@ Example workflow with 2 parallel steps: { "name": "fetch_users", "action": { "toolName": "GET_USERS" } }, { "name": "fetch_orders", "action": { "toolName": "GET_ORDERS" } }, ]} - + Example workflow with a step that references the output of another step: -{ "title": "Fetch a user by email and then fetch orders", "steps": [ - { "name": "fetch_user", "action": { "toolName": "GET_USER" }, "input": { "email": "@input.user_email" } }, - { "name": "fetch_orders", "action": { "toolName": "GET_USER_ORDERS" }, "input": { "user_id": "@fetch_user.user.id" } }, +{ "title": "Get first user and then fetch orders", "steps": [ + { "name": "fetch_users", "action": { "toolName": "GET_USERS" }, "input": { "all": true }, "transformCode": "export default async (i) => i[0]" }, + { "name": "fetch_orders", "action": { "toolName": "GET_ORDERS" }, "input": { "user": "@fetch_users.user" } }, ]} `, inputSchema: z.object({ - data: z.object({ - title: z.string(), - steps: z - .array(z.object(StepSchema.omit({ outputSchema: true }).shape)) - .optional(), - gateway_id: z - .string() - .default("") - .describe( - "The gateway ID to use for the workflow execution. The execution will only be able to use tools from this gateway.", - ), - description: z - .string() - .optional() - .describe("The description of the workflow"), - }), + data: z + .object({ + title: z.string().optional().describe("The title of the workflow"), + steps: z + .array(z.object(StepSchema.omit({ outputSchema: true }).shape)) + .optional() + .describe( + "The steps to execute - need to provide this or the workflow_collection_id", + ), + input: z + .record(z.string(), z.unknown()) + .optional() + .describe("The input to the workflow"), + gateway_id: z + .string() + .optional() + .describe("The gateway ID to use for the workflow"), + description: z + .string() + .optional() + .describe("The description of the workflow"), + created_by: z + .string() + .optional() + .describe("The created by user of the workflow"), + }) + .optional() + .describe("The data for the workflow"), }), - // outputSchema: CREATE_BINDING.outputSchema, + outputSchema: z + .object({}) + .catchall(z.unknown()) + .describe("The ID of the created workflow"), execute: async ({ context }) => { const { data } = context; - const user = env.MESH_REQUEST_CONTEXT?.ensureAuthenticated(); - const workflow: Workflow & { gateway_id: string } = { + const workflow = { id: crypto.randomUUID(), - title: data.title, - description: data.description, + title: data?.title ?? `Workflow ${Date.now()}`, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), - steps: data.steps ?? [], - created_by: user?.id || undefined, - updated_by: user?.id || undefined, - gateway_id: data.gateway_id, - }; - const { item } = await insertWorkflowCollectionItem(env, workflow); - const result = WorkflowSchema.parse(item); - return { - item: { - ...result, - steps: result.steps.map((s) => ({ - ...s, - outputSchema: undefined, - })), - }, + steps: data?.steps ?? [], + ...data, }; + return await insertWorkflowCollection(env, workflow); }, }); @@ -286,9 +354,17 @@ async function updateWorkflowCollection( env: Env, context: { id: string; data: Workflow }, ) { + const { id, data } = context; + + // Check if this is a file-based workflow (readonly) + if (isFileWorkflow(id)) { + throw new Error( + `Cannot update workflow "${id}" - it is a file-based workflow (readonly). Use COLLECTION_WORKFLOW_DUPLICATE to create an editable copy.`, + ); + } + const user = env.MESH_REQUEST_CONTEXT?.ensureAuthenticated(); const now = new Date().toISOString(); - const { id, data } = context; await validateWorkflow(data, env); const setClauses: string[] = []; @@ -308,9 +384,9 @@ async function updateWorkflowCollection( setClauses.push(`description = ?`); params.push(data.description); } - if (data.steps && data.steps.length > 0) { + if (data.steps !== undefined) { setClauses.push(`steps = ?`); - params.push(JSON.stringify(data.steps)); + params.push(JSON.stringify(data.steps || [])); } params.push(id); @@ -322,15 +398,19 @@ async function updateWorkflowCollection( RETURNING * `; - const result = await runSQL>(env, sql, params); + const result = + await env.MESH_REQUEST_CONTEXT?.state?.DATABASE?.DATABASES_RUN_SQL({ + sql, + params, + }); - if (result?.length === 0) { + if (!result?.result?.[0]?.results || result.result[0].results.length === 0) { throw new Error(`Workflow collection with id ${id} not found`); } return { item: transformDbRowToWorkflowCollectionItem( - result[0] as Record, + result.result[0].results[0] as Record, ), }; } @@ -347,7 +427,13 @@ export const createUpdateTool = (env: Env) => steps: z .array(z.object(StepSchema.omit({ outputSchema: true }).shape)) .optional() - .describe("The steps of the workflow"), + .describe( + "The steps to execute - need to provide this or the workflow_collection_id", + ), + input: z + .record(z.string(), z.unknown()) + .optional() + .describe("The input to the workflow"), gateway_id: z .string() .optional() @@ -356,10 +442,10 @@ export const createUpdateTool = (env: Env) => .string() .optional() .describe("The description of the workflow"), - updated_by: z + created_by: z .string() .optional() - .describe("The updated by user of the workflow"), + .describe("The created by user of the workflow"), }) .optional() .describe("The data for the workflow"), @@ -367,11 +453,10 @@ export const createUpdateTool = (env: Env) => outputSchema: UPDATE_BINDING.outputSchema, execute: async ({ context }) => { try { - const result = await updateWorkflowCollection(env, { + return await updateWorkflowCollection(env, { id: context.id as string, data: context.data as Workflow, }); - return result; } catch (error) { console.error("Error updating workflow:", error); throw new Error( @@ -381,143 +466,95 @@ export const createUpdateTool = (env: Env) => }, }); -export const createAppendStepTool = (env: Env) => +export const createDeleteTool = (env: Env) => createPrivateTool({ - id: "COLLECTION_WORKFLOW_APPEND_STEP", - description: "Append a new step to an existing workflow", - inputSchema: z.object({ - id: z.string().describe("The ID of the workflow to append the step to"), - step: z - .object(StepSchema.omit({ outputSchema: true }).shape) - .describe("The step to append"), - }), - outputSchema: z.object({ - success: z - .boolean() - .describe("Whether the step was appended successfully"), - }), + id: "COLLECTION_WORKFLOW_DELETE", + description: + "Delete a workflow by ID. Cannot delete file-based workflows (readonly).", + inputSchema: DELETE_BINDING.inputSchema, + outputSchema: DELETE_BINDING.outputSchema, execute: async ({ context }) => { - const { id, step } = context; - - const workflow = await getWorkflowCollection(env, id as string); + const { id } = context; - if (!workflow) { - throw new Error(`Workflow with id ${id} not found`); + // Check if this is a file-based workflow (readonly) + if (isFileWorkflow(id)) { + throw new Error( + `Cannot delete workflow "${id}" - it is a file-based workflow (readonly). Remove the JSON file from the WORKFLOWS_DIRS directory to delete it.`, + ); } - await validateWorkflow( - { - ...workflow, - steps: [...workflow.steps, step], - }, + const result = await runSQL>( env, + "DELETE FROM workflow_collection WHERE id = ? RETURNING *", + [id], ); - await updateWorkflowCollection(env, { - id, - data: { - ...workflow, - steps: [...workflow.steps, step], - }, - }); + + const item = result[0]; + if (!item) { + throw new Error(`Workflow collection with id ${id} not found`); + } return { - success: true, + item: transformDbRowToWorkflowCollectionItem(item), }; }, }); -export const createUpdateStepsTool = (env: Env) => +export const createDuplicateTool = (env: Env) => createPrivateTool({ - id: "COLLECTION_WORKFLOW_UPDATE_STEPS", - description: "Update one or more steps of a workflow", + id: "COLLECTION_WORKFLOW_DUPLICATE", + description: + "Duplicate a workflow (file-based or DB) to create an editable copy in PostgreSQL. Use this to customize file-based workflows.", inputSchema: z.object({ - steps: z - .array( - z.object(StepSchema.omit({ outputSchema: true }).shape).partial(), - ) + id: z.string().describe("The ID of the workflow to duplicate"), + new_id: z + .string() .optional() - .describe("The steps to update"), - id: z.string().describe("The ID of the workflow to update"), + .describe("Optional new ID for the duplicate. Defaults to id-copy."), + new_title: z + .string() + .optional() + .describe( + "Optional new title for the duplicate. Defaults to original title + (Copy).", + ), }), outputSchema: z.object({ - success: z - .boolean() - .describe("Whether the step was updated successfully"), + item: WorkflowSchema, }), execute: async ({ context }) => { - const { steps, id } = context; + const { id, new_id, new_title } = context; - if (!steps) { - throw new Error("No steps provided"); + // Get the source workflow (from DB or file) + const sourceWorkflow = await getWorkflowCollection(env, id); + if (!sourceWorkflow) { + throw new Error(`Workflow "${id}" not found`); } - const workflow = await getWorkflowCollection(env, id as string); + // Create a copy with new ID + const copyId = new_id || `${id}-copy`; + const copyTitle = new_title || `${sourceWorkflow.title} (Copy)`; - if (!workflow) { - throw new Error(`Workflow with id ${id} not found`); - } + // Check if the new ID already exists in DB + const existingResult = + await env.MESH_REQUEST_CONTEXT?.state?.DATABASE.DATABASES_RUN_SQL({ + sql: "SELECT id FROM workflow_collection WHERE id = ? LIMIT 1", + params: [copyId], + }); - // Validate that all steps to update exist in the workflow - for (const step of steps) { - const existingStep = workflow.steps?.find((s) => s.name === step.name); - if (!existingStep) { - throw new Error(`Step with name ${step.name} not found in workflow`); - } + if (existingResult.result[0]?.results?.length > 0) { + throw new Error(`Workflow with ID "${copyId}" already exists`); } - // Map over all existing steps, applying updates where names match - const newSteps = workflow.steps.map((existingStep) => { - const stepUpdate = steps.find((s) => s.name === existingStep.name); - if (stepUpdate) { - return { - ...existingStep, - ...stepUpdate, - }; - } - return existingStep; - }); - - await validateWorkflow( - { - ...workflow, - steps: newSteps, - }, - env, - ); - await updateWorkflowCollection(env, { - id, - data: { - ...workflow, - steps: newSteps, - }, - }); - return { - success: true, + // Create the duplicate + const duplicateWorkflow: Workflow = { + id: copyId, + title: copyTitle, + description: sourceWorkflow.description, + steps: sourceWorkflow.steps, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), }; - }, - }); -export const createDeleteTool = (env: Env) => - createPrivateTool({ - id: "COLLECTION_WORKFLOW_DELETE", - description: "Delete a workflow by ID", - inputSchema: DELETE_BINDING.inputSchema, - outputSchema: DELETE_BINDING.outputSchema, - execute: async ({ context }) => { - const { id } = context; - - const result = await runSQL>( - env, - "DELETE FROM workflow_collection WHERE id = ? RETURNING *", - [id], - ); - - const item = result[0]; - if (!item) { - throw new Error(`Workflow collection with id ${id} not found`); - } - return { - item: transformDbRowToWorkflowCollectionItem(item), - }; + return await insertWorkflowCollection(env, duplicateWorkflow); }, }); @@ -526,7 +563,6 @@ export const workflowCollectionTools = [ createGetTool, createInsertTool, createUpdateTool, - createUpdateStepsTool, - createAppendStepTool, createDeleteTool, + createDuplicateTool, ]; diff --git a/mcp-studio/server/workflow-loader.ts b/mcp-studio/server/workflow-loader.ts new file mode 100644 index 0000000..018a496 --- /dev/null +++ b/mcp-studio/server/workflow-loader.ts @@ -0,0 +1,323 @@ +/** + * Filesystem Workflow Loader + * + * Loads workflow definitions from JSON files on the filesystem. + * This enables: + * - Version-controlled workflows (store in git) + * - MCP packaging (MCPs can ship workflows) + * - Local development (edit files, hot-reload) + * - Database-free operation (no PostgreSQL required) + * + * Environment variables: + * - WORKFLOW_DIR: Directory to scan for *.workflow.json or *.json files + * - WORKFLOW_FILES: Comma-separated list of specific workflow files + * + * File formats supported: + * - Single workflow: { "id": "...", "title": "...", "steps": [...] } + * - Multiple workflows: { "workflows": [...] } + * + * Example directory structure: + * workflows/ + * ├── enrich-contact.workflow.json + * ├── notify-team.workflow.json + * └── my-mcp/ + * └── bundled-workflows.json (can contain multiple) + */ + +import { readdir, readFile, stat, watch } from "node:fs/promises"; +import { join, extname, basename, dirname } from "node:path"; +import { WorkflowSchema, type Workflow } from "@decocms/bindings/workflow"; + +export interface LoadedWorkflow extends Workflow { + /** Source file path */ + _sourceFile: string; + /** Whether this is a filesystem workflow (vs database) */ + _isFilesystem: true; +} + +export interface WorkflowLoaderOptions { + /** Directory to scan for workflow files */ + workflowDir?: string; + /** Specific workflow files to load */ + workflowFiles?: string[]; + /** Enable file watching for hot reload */ + watch?: boolean; + /** Callback when workflows change */ + onChange?: (workflows: LoadedWorkflow[]) => void; +} + +/** + * In-memory cache of loaded workflows + */ +let cachedWorkflows: LoadedWorkflow[] = []; +let watchAbortController: AbortController | null = null; + +/** + * Get the configured workflow source from environment + */ +export function getWorkflowSource(): WorkflowLoaderOptions { + const options: WorkflowLoaderOptions = {}; + + if (process.env.WORKFLOW_DIR) { + options.workflowDir = process.env.WORKFLOW_DIR; + } + + if (process.env.WORKFLOW_FILES) { + options.workflowFiles = process.env.WORKFLOW_FILES.split(",").map((f) => + f.trim(), + ); + } + + return options; +} + +/** + * Check if filesystem workflow loading is enabled + */ +export function isFilesystemMode(): boolean { + const source = getWorkflowSource(); + return !!(source.workflowDir || source.workflowFiles?.length); +} + +/** + * Parse a workflow file and extract workflow(s) + */ +async function parseWorkflowFile(filePath: string): Promise { + let content: string; + try { + content = await readFile(filePath, "utf-8"); + } catch (error) { + // Handle file access errors (deleted, permission denied, etc.) gracefully + console.error(`[workflow-loader] Failed to read ${filePath}:`, error); + return []; + } + + let parsed: unknown; + + try { + parsed = JSON.parse(content); + } catch (error) { + console.error(`[workflow-loader] Failed to parse ${filePath}:`, error); + return []; + } + + const workflows: LoadedWorkflow[] = []; + + // Handle array of workflows + if (Array.isArray(parsed)) { + for (const item of parsed) { + const validated = validateWorkflow(item, filePath); + if (validated) workflows.push(validated); + } + return workflows; + } + + // Handle object with "workflows" key + if ( + typeof parsed === "object" && + parsed !== null && + "workflows" in parsed && + Array.isArray((parsed as { workflows: unknown }).workflows) + ) { + for (const item of (parsed as { workflows: unknown[] }).workflows) { + const validated = validateWorkflow(item, filePath); + if (validated) workflows.push(validated); + } + return workflows; + } + + // Handle single workflow + const validated = validateWorkflow(parsed, filePath); + if (validated) workflows.push(validated); + + return workflows; +} + +/** + * Validate a workflow object against the schema + */ +function validateWorkflow( + data: unknown, + sourceFile: string, +): LoadedWorkflow | null { + const result = WorkflowSchema.safeParse(data); + + if (!result.success) { + console.error( + `[workflow-loader] Invalid workflow in ${sourceFile}:`, + result.error.format(), + ); + return null; + } + + // Generate ID from filename if not present + let id = result.data.id; + if (!id) { + const base = basename(sourceFile, extname(sourceFile)); + // Remove .workflow suffix if present + id = base.replace(/\.workflow$/, ""); + } + + return { + ...result.data, + id, + _sourceFile: sourceFile, + _isFilesystem: true, + }; +} + +/** + * Scan a directory for workflow files + */ +async function scanDirectory(dir: string): Promise { + const files: string[] = []; + + try { + const entries = await readdir(dir); + + for (const entry of entries) { + const fullPath = join(dir, entry); + const stats = await stat(fullPath); + + if (stats.isDirectory()) { + // Recursively scan subdirectories + const subFiles = await scanDirectory(fullPath); + files.push(...subFiles); + } else if (stats.isFile()) { + // Include .json and .workflow.json files + if (entry.endsWith(".json")) { + files.push(fullPath); + } + } + } + } catch (error) { + console.error(`[workflow-loader] Failed to scan ${dir}:`, error); + } + + return files; +} + +/** + * Load all workflows from configured sources + */ +export async function loadWorkflows( + options?: WorkflowLoaderOptions, +): Promise { + const source = options || getWorkflowSource(); + const allWorkflows: LoadedWorkflow[] = []; + const filesToLoad: string[] = []; + + // Collect files from directory + if (source.workflowDir) { + const dirFiles = await scanDirectory(source.workflowDir); + filesToLoad.push(...dirFiles); + console.error( + `[workflow-loader] Found ${dirFiles.length} files in ${source.workflowDir}`, + ); + } + + // Add explicitly specified files + if (source.workflowFiles) { + filesToLoad.push(...source.workflowFiles); + } + + // Load each file + for (const file of filesToLoad) { + const workflows = await parseWorkflowFile(file); + allWorkflows.push(...workflows); + } + + // Cache the results + cachedWorkflows = allWorkflows; + + console.error( + `[workflow-loader] Loaded ${allWorkflows.length} workflow(s) from filesystem`, + ); + + // Log workflow IDs for debugging + if (allWorkflows.length > 0) { + console.error( + `[workflow-loader] Workflows: ${allWorkflows.map((w) => w.id).join(", ")}`, + ); + } + + return allWorkflows; +} + +/** + * Get cached workflows (call loadWorkflows first) + */ +export function getCachedWorkflows(): LoadedWorkflow[] { + return cachedWorkflows; +} + +/** + * Get a specific workflow by ID + */ +export function getWorkflowById(id: string): LoadedWorkflow | undefined { + return cachedWorkflows.find((w) => w.id === id); +} + +/** + * Start watching for file changes + */ +export async function startWatching( + options?: WorkflowLoaderOptions, +): Promise { + const source = options || getWorkflowSource(); + + // Stop any existing watcher + stopWatching(); + + watchAbortController = new AbortController(); + + if (source.workflowDir) { + console.error( + `[workflow-loader] Watching ${source.workflowDir} for changes`, + ); + + try { + const watcher = watch(source.workflowDir, { + recursive: true, + signal: watchAbortController.signal, + }); + + (async () => { + try { + for await (const event of watcher) { + if (event.filename?.endsWith(".json")) { + console.error( + `[workflow-loader] File changed: ${event.filename}`, + ); + await loadWorkflows(options); + options.onChange?.(cachedWorkflows); + } + } + } catch (error) { + if ((error as { name?: string }).name !== "AbortError") { + console.error("[workflow-loader] Watch error:", error); + } + } + })(); + } catch (error) { + console.error("[workflow-loader] Failed to start watcher:", error); + } + } +} + +/** + * Stop watching for file changes + */ +export function stopWatching(): void { + if (watchAbortController) { + watchAbortController.abort(); + watchAbortController = null; + } +} + +/** + * Reload workflows from disk + */ +export async function reloadWorkflows(): Promise { + return loadWorkflows(getWorkflowSource()); +} diff --git a/openrouter/package.json b/openrouter/package.json index c5528b6..c59a82e 100644 --- a/openrouter/package.json +++ b/openrouter/package.json @@ -6,6 +6,9 @@ "type": "module", "scripts": { "dev": "bun run --hot server/main.ts", + "stdio": "bun server/stdio.ts", + "stdio:dev": "bun --watch server/stdio.ts", + "start": "bun server/stdio.ts", "build:server": "NODE_ENV=production bun build server/main.ts --target=bun --outfile=dist/server/main.js", "build": "bun run build:server", "publish": "cat app.json | deco registry publish -w /shared/deco -y", diff --git a/openrouter/server/stdio.ts b/openrouter/server/stdio.ts new file mode 100644 index 0000000..2b146a1 --- /dev/null +++ b/openrouter/server/stdio.ts @@ -0,0 +1,424 @@ +#!/usr/bin/env bun +/** + * OpenRouter MCP Server - Stdio Transport + * + * This allows running the OpenRouter MCP locally via stdio, + * without needing to manage an HTTP server. + * + * Usage: + * OPENROUTER_API_KEY=sk-... bun server/stdio.ts + * + * In Mesh, add as STDIO connection: + * Command: bun + * Args: /path/to/openrouter/server/stdio.ts + * Env: OPENROUTER_API_KEY=sk-... + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { createOpenRouter } from "@openrouter/ai-sdk-provider"; +import type { LanguageModelV2CallOptions } from "@ai-sdk/provider"; +import { OpenRouterClient } from "./lib/openrouter-client.ts"; +import { z } from "zod"; +import { type ModelCollectionEntitySchema } from "@decocms/bindings/llm"; +import { WELL_KNOWN_MODEL_IDS } from "./tools/models/well-known.ts"; +import { compareModels, recommendModelsForTask } from "./tools/models/utils.ts"; + +// ============================================================================ +// Environment +// ============================================================================ + +const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; +if (!OPENROUTER_API_KEY) { + console.error("Error: OPENROUTER_API_KEY environment variable is required"); + process.exit(1); +} + +// ============================================================================ +// Constants +// ============================================================================ + +const OPENROUTER_PROVIDER = "openrouter" as const; +const DEFAULT_LOGO = + "https://assets.decocache.com/decocms/bc2ca488-2bae-4aac-8d3e-ead262dad764/agent.png"; +const PROVIDER_LOGOS: Record = { + openai: + "https://assets.decocache.com/webdraw/15dc381c-23b4-4f6b-9ceb-9690f77a7cf5/openai.svg", + anthropic: + "https://assets.decocache.com/webdraw/6ae2b0e1-7b81-48f7-9707-998751698b6f/anthropic.svg", + google: + "https://assets.decocache.com/webdraw/17df85af-1578-42ef-ae07-4300de0d1723/gemini.svg", + "x-ai": + "https://assets.decocache.com/webdraw/7a8003ff-8f2d-4988-8693-3feb20e87eca/xai.svg", +}; + +// ============================================================================ +// Helper Functions (simplified from llm-binding.ts) +// ============================================================================ + +type ListedModel = Awaited>[number]; + +function toNumberOrNull(value?: string): number | null { + if (!value?.length) return null; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +} + +function extractOutputLimit(model: ListedModel): number | null { + const topProviderLimit = model.top_provider?.max_completion_tokens; + if (typeof topProviderLimit === "number") return topProviderLimit; + const perRequestLimit = model.per_request_limits?.completion_tokens; + if (perRequestLimit) { + const parsed = Number(perRequestLimit); + return Number.isFinite(parsed) ? parsed : null; + } + return null; +} + +function extractCapabilities(model: ListedModel): string[] { + const capabilities: string[] = ["text"]; + if (model.architecture?.modality?.includes("image")) + capabilities.push("vision"); + if (model.supported_generation_methods?.includes("tools")) + capabilities.push("tools"); + if (model.supported_generation_methods?.includes("json_mode")) + capabilities.push("json-mode"); + return capabilities; +} + +function extractProviderLogo(modelId: string): string { + const provider = modelId.split("/")[0] || ""; + return PROVIDER_LOGOS[provider] ?? DEFAULT_LOGO; +} + +function transformToLLMEntity( + model: ListedModel, +): z.infer { + const now = new Date().toISOString(); + const inputCost = toNumberOrNull(model.pricing.prompt); + const outputCost = toNumberOrNull(model.pricing.completion); + const contextWindow = model.context_length || 0; + const maxOutputTokens = extractOutputLimit(model) || 0; + + return { + id: model.id, + title: model.name, + created_at: model.created + ? new Date(model.created * 1000).toISOString() + : now, + updated_at: now, + created_by: undefined, + updated_by: undefined, + logo: extractProviderLogo(model.id), + description: model.description ?? null, + capabilities: extractCapabilities(model), + provider: OPENROUTER_PROVIDER, + limits: + contextWindow > 0 || maxOutputTokens > 0 + ? { contextWindow, maxOutputTokens } + : null, + costs: + inputCost !== null || outputCost !== null + ? { input: inputCost ?? 0, output: outputCost ?? 0 } + : null, + }; +} + +function sortModelsByWellKnown(models: ListedModel[]): ListedModel[] { + const modelById = new Map(models.map((model) => [model.id, model])); + const wellKnownModels = WELL_KNOWN_MODEL_IDS.map((id) => + modelById.get(id), + ).filter((model): model is ListedModel => Boolean(model)); + const wellKnownIds = new Set(wellKnownModels.map((model) => model.id)); + const remainingModels = models.filter((model) => !wellKnownIds.has(model.id)); + return [...wellKnownModels, ...remainingModels]; +} + +// ============================================================================ +// MCP Server Setup +// ============================================================================ + +async function main() { + const server = new McpServer({ + name: "openrouter", + version: "1.0.0", + }); + + const client = new OpenRouterClient({ apiKey: OPENROUTER_API_KEY }); + const openrouter = createOpenRouter({ apiKey: OPENROUTER_API_KEY }); + + // ============================================================================ + // COLLECTION_LLM_LIST - List all available models + // ============================================================================ + server.tool( + "COLLECTION_LLM_LIST", + "List all available models from OpenRouter with filtering and pagination", + { + where: z.any().optional().describe("Filter expression"), + orderBy: z.any().optional().describe("Sort order"), + limit: z.number().optional().default(50).describe("Max results"), + offset: z.number().optional().default(0).describe("Pagination offset"), + }, + async ({ limit = 50, offset = 0 }) => { + const models = await client.listModels(); + const sorted = sortModelsByWellKnown(models); + const paginated = sorted.slice(offset, offset + limit); + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + items: paginated.map(transformToLLMEntity), + totalCount: sorted.length, + hasMore: sorted.length > offset + limit, + }), + }, + ], + }; + }, + ); + + // ============================================================================ + // COLLECTION_LLM_GET - Get a single model by ID + // ============================================================================ + server.tool( + "COLLECTION_LLM_GET", + "Get detailed information about a specific OpenRouter model", + { + id: z + .string() + .describe("The model ID (e.g., 'anthropic/claude-3.5-sonnet')"), + }, + async ({ id }) => { + try { + const model = await client.getModel(id); + return { + content: [ + { + type: "text", + text: JSON.stringify({ item: transformToLLMEntity(model) }), + }, + ], + }; + } catch { + return { + content: [{ type: "text", text: JSON.stringify({ item: null }) }], + }; + } + }, + ); + + // ============================================================================ + // LLM_METADATA - Get model metadata + // ============================================================================ + server.tool( + "LLM_METADATA", + "Get metadata about a model's capabilities including supported URL patterns", + { + modelId: z.string().describe("The model ID"), + }, + async ({ modelId }) => { + try { + const model = await client.getModel(modelId); + const supportedUrls: Record = { + "text/*": ["data:*"], + }; + if (model.architecture?.modality?.includes("image")) { + supportedUrls["image/*"] = ["https://*", "data:*"]; + } + return { + content: [{ type: "text", text: JSON.stringify({ supportedUrls }) }], + }; + } catch { + return { + content: [ + { + type: "text", + text: JSON.stringify({ supportedUrls: { "text/*": ["data:*"] } }), + }, + ], + }; + } + }, + ); + + // ============================================================================ + // LLM_DO_GENERATE - Generate a complete response (non-streaming) + // ============================================================================ + server.tool( + "LLM_DO_GENERATE", + "Generate a complete language model response using OpenRouter (non-streaming)", + { + modelId: z.string().describe("The model ID to use"), + callOptions: z + .any() + .optional() + .describe("Language model call options (prompt, messages, etc.)"), + }, + async ({ modelId, callOptions: rawCallOptions }) => { + const { abortSignal: _abortSignal, ...callOptions } = + rawCallOptions ?? {}; + + const model = openrouter.languageModel(modelId); + const result = await model.doGenerate( + callOptions as LanguageModelV2CallOptions, + ); + + // Clean up non-serializable data + const cleanResult = { + ...result, + request: result.request ? { body: undefined } : undefined, + response: result.response + ? { + id: result.response.id, + timestamp: result.response.timestamp, + modelId: result.response.modelId, + headers: result.response.headers, + } + : undefined, + }; + + return { + content: [{ type: "text", text: JSON.stringify(cleanResult) }], + }; + }, + ); + + // ============================================================================ + // LLM_DO_STREAM - Stream a response (simplified for stdio) + // ============================================================================ + server.tool( + "LLM_DO_STREAM", + "Stream a language model response in real-time using OpenRouter. Note: In stdio mode, this returns the full response (streaming not supported via stdio transport).", + { + modelId: z.string().describe("The model ID to use"), + callOptions: z + .any() + .optional() + .describe("Language model call options (prompt, messages, etc.)"), + }, + async ({ modelId, callOptions: rawCallOptions }) => { + // In stdio mode, we can't truly stream, so we use doGenerate instead + const { abortSignal: _abortSignal, ...callOptions } = + rawCallOptions ?? {}; + + const model = openrouter.languageModel(modelId); + const result = await model.doGenerate( + callOptions as LanguageModelV2CallOptions, + ); + + // Clean up non-serializable data + const cleanResult = { + ...result, + request: result.request ? { body: undefined } : undefined, + response: result.response + ? { + id: result.response.id, + timestamp: result.response.timestamp, + modelId: result.response.modelId, + headers: result.response.headers, + } + : undefined, + }; + + return { + content: [{ type: "text", text: JSON.stringify(cleanResult) }], + }; + }, + ); + + // ============================================================================ + // COMPARE_MODELS - Compare multiple models side-by-side + // ============================================================================ + server.tool( + "COMPARE_MODELS", + "Compare multiple OpenRouter models side-by-side to help choose the best model for a specific use case. Compares pricing, context length, capabilities, and performance characteristics.", + { + modelIds: z + .array(z.string()) + .min(2) + .max(5) + .describe( + "Array of 2-5 model IDs to compare (e.g., ['openai/gpt-4o', 'anthropic/claude-3.5-sonnet'])", + ), + criteria: z + .array(z.enum(["price", "context_length", "modality", "moderation"])) + .optional() + .describe("Specific criteria to focus on in comparison"), + }, + async ({ modelIds, criteria }) => { + const allModels = await client.listModels(); + const result = compareModels(modelIds, allModels, criteria); + return { + content: [{ type: "text", text: JSON.stringify(result) }], + }; + }, + ); + + // ============================================================================ + // RECOMMEND_MODEL - Get model recommendations for a task + // ============================================================================ + server.tool( + "RECOMMEND_MODEL", + "Get intelligent model recommendations based on your task description and requirements. Analyzes your task and suggests the most suitable models.", + { + taskDescription: z + .string() + .describe( + "Description of your task (e.g., 'generate Python code', 'analyze documents')", + ), + requirements: z + .object({ + maxCostPer1MTokens: z + .number() + .positive() + .optional() + .describe("Maximum budget per 1M tokens in dollars"), + minContextLength: z + .number() + .positive() + .optional() + .describe("Minimum required context length in tokens"), + requiredModality: z + .enum(["text->text", "text+image->text", "text->image"]) + .optional() + .describe("Required model capability"), + prioritize: z + .enum(["cost", "quality", "speed"]) + .default("quality") + .optional() + .describe("What to prioritize in recommendations"), + }) + .optional() + .describe("Optional requirements and constraints"), + }, + async ({ taskDescription, requirements = {} }) => { + const allModels = await client.listModels(); + const recommendations = recommendModelsForTask( + taskDescription, + requirements, + allModels, + ); + return { + content: [{ type: "text", text: JSON.stringify({ recommendations }) }], + }; + }, + ); + + // ============================================================================ + // Connect to stdio transport + // ============================================================================ + const transport = new StdioServerTransport(); + await server.connect(transport); + + console.error("[openrouter] MCP server running via stdio"); + console.error( + "[openrouter] Available tools: COLLECTION_LLM_LIST, COLLECTION_LLM_GET, LLM_METADATA, LLM_DO_GENERATE, LLM_DO_STREAM, COMPARE_MODELS, RECOMMEND_MODEL", + ); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/openrouter/server/tools/llm-binding.ts b/openrouter/server/tools/llm-binding.ts index 2bc5332..4d4c060 100644 --- a/openrouter/server/tools/llm-binding.ts +++ b/openrouter/server/tools/llm-binding.ts @@ -575,10 +575,12 @@ export const createLLMStreamTool = (usageHooks?: UsageHooks) => (env: Env) => "Returns a streaming response for interactive chat experiences.", inputSchema: STREAM_BINDING.inputSchema, execute: async ({ context }) => { - const { - modelId, - callOptions: { abortSignal: _abortSignal, ...callOptions }, - } = context; + const { modelId, callOptions: rawCallOptions } = context; + + // Handle null/undefined callOptions gracefully + const { abortSignal: _abortSignal, ...callOptions } = + rawCallOptions ?? {}; + env.MESH_REQUEST_CONTEXT.ensureAuthenticated(); const apiKey = getOpenRouterApiKey(env); @@ -735,10 +737,12 @@ export const createLLMGenerateTool = (usageHooks?: UsageHooks) => (env: Env) => inputSchema: GENERATE_BINDING.inputSchema, outputSchema: GENERATE_BINDING.outputSchema, execute: async ({ context }) => { - const { - modelId, - callOptions: { abortSignal: _abortSignal, ...callOptions }, - } = context; + const { modelId, callOptions: rawCallOptions } = context; + + // Handle null/undefined callOptions gracefully + const { abortSignal: _abortSignal, ...callOptions } = + rawCallOptions ?? {}; + env.MESH_REQUEST_CONTEXT.ensureAuthenticated(); const apiKey = getOpenRouterApiKey(env); diff --git a/package.json b/package.json index a1fb0ca..a1e3053 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "object-storage", "openrouter", "perplexity", + "pilot", "pinecone", "readonly-sql", "reddit", diff --git a/pilot/AGENTS.md b/pilot/AGENTS.md new file mode 100644 index 0000000..d83e45a --- /dev/null +++ b/pilot/AGENTS.md @@ -0,0 +1,51 @@ +# Pilot Agent Guidelines + +## Debugging Complex AI Flows + +When debugging multi-step workflows, LLM calls, or async task execution where terminal logs may be truncated or lost: + +**Use temporary file-based logging** to capture the full picture: + +```typescript +const fs = await import("fs"); +const logPath = "/tmp/pilot-debug.log"; +const log = (msg: string) => { + const line = `[${new Date().toISOString()}] ${msg}\n`; + fs.appendFileSync(logPath, line); + console.error(msg); // Also emit to stderr for STDIO capture +}; + +log(`🔍 LLM CALL: model=${modelId}, messages=${messages.length}`); +log(`📝 PROMPT: ${prompt.slice(0, 300)}`); +const result = await callLLM(...); +log(`📤 RESULT: text=${!!result.text} (${result.text?.length || 0} chars)`); +``` + +This technique is essential when: +- Pilot runs as STDIO subprocess (Mesh only captures stderr) +- Terminal output is truncated or scrolling +- Async callbacks (setTimeout) fire after parent logs +- You need timestamps to trace execution order across concurrent flows + +**Always use `console.error` instead of `console.log`** in Pilot - Mesh's STDIO transport only pipes stderr to the main console. + +Remember to clean up debug logging before committing. + +## Common Gotchas + +### Model ID Resolution +When spawning child workflows via `start_task`, always pass the resolved model IDs from the parent config: + +```typescript +// ❌ Wrong - passes literal strings +config: { fastModel: "fast", smartModel: "smart" } + +// ✅ Correct - passes actual model IDs +config: { fastModel: config.fastModel, smartModel: config.smartModel } +``` + +### STDIO Logging +Pilot runs as an STDIO process under Mesh. Only `stderr` is captured: +- Use `console.error()` for debug output +- `console.log()` output will be lost + diff --git a/pilot/ANNOUNCEMENT.md b/pilot/ANNOUNCEMENT.md new file mode 100644 index 0000000..12b0d46 --- /dev/null +++ b/pilot/ANNOUNCEMENT.md @@ -0,0 +1,439 @@ +# Blog Post Draft: Introducing Pilot + +> Technical blog post structure for announcing Pilot. Focus on workflow-driven AI, event-based communication, and composable task execution. + +--- + +## Title Options + +1. "Pilot: A Workflow-Driven AI Agent for the MCP Ecosystem" +2. "From Events to Intelligence: Building an AI Agent with MCP Workflows" +3. "How We Built an AI Agent That Any Interface Can Use" + +--- + +## Hook (150 words) + +The problem with AI agents isn't intelligence—it's integration. + +Every new interface (WhatsApp, Slack, CLI, Raycast) needs its own agent implementation. Every agent duplicates the same tool-calling logic. Every update requires changes in multiple places. + +We built Pilot to solve this. It's a single AI agent that: +- Subscribes to events from any interface +- Executes configurable workflows +- Publishes responses back via events + +The key insight: **separate the AI brain from the interface layer**. Let specialized bridges handle DOM/UI, and let a central agent handle intelligence. + +Pilot runs as an MCP inside your mesh. It has access to all your tools. It persists task history. And any interface can use it just by publishing events. + +--- + +## Section 1: The Problem (300 words) + +### The Interface Fragmentation Problem + +When you want AI in WhatsApp, you build an AI integration for WhatsApp. +When you want AI in Slack, you build another one for Slack. +When you want AI in your CLI, another one. + +Each integration: +- Implements its own LLM-calling logic +- Manages its own conversation state +- Has its own tool definitions +- Needs its own updates when things change + +This doesn't scale. + +### What If the AI Was a Service? + +Imagine instead: +1. Interfaces just publish events: "user said X" +2. A central agent receives all events +3. Agent processes with full tool access +4. Agent publishes response events +5. Interfaces receive and display + +``` +WhatsApp Bridge ─┐ + ├──► Event Bus ──► Pilot Agent ──► Event Bus ──┬──► WhatsApp Bridge +Slack Bot ───────┤ ├──► Slack Bot +CLI ─────────────┘ └──► CLI +``` + +Now you have: +- One agent to update +- One place for tools +- One source of truth for AI behavior +- N interfaces that just handle their specific UI + +--- + +## Section 2: How Pilot Works (400 words) + +### Event-Driven Architecture + +Pilot never knows about WhatsApp or Slack directly. It subscribes to generic events: + +```typescript +// Pilot subscribes to this event type +"user.message.received" { + text: "What's the weather?", + source: "whatsapp", // Just metadata + chatId: "self" +} +``` + +And publishes generic response events: + +```typescript +// Pilot publishes to agent.response.{source} +"agent.response.whatsapp" { + taskId: "task-123", + text: "It's 72°F and sunny!", + isFinal: true +} +``` + +The `source` field determines which interface receives the response. That's the only coupling. + +### Workflow Execution + +Every request is processed by a **workflow**—a JSON configuration that defines execution steps: + +```json +{ + "id": "fast-router", + "steps": [ + { + "name": "route", + "action": { + "type": "llm", + "prompt": "@input.message", + "model": "fast", + "tools": "all" + } + } + ] +} +``` + +Workflows are: +- **Declarative**: Describe what, not how +- **Composable**: One workflow can trigger another +- **Hot-reloadable**: Change JSON, behavior changes + +### The Fast Router Pattern + +The default workflow (`fast-router`) implements a smart routing pattern: + +1. **Direct Response**: For simple queries (greetings, questions) +2. **Single Tool Call**: For specific operations (search, file read) +3. **Async Workflow**: For complex multi-step tasks + +``` +"Hello!" → Direct response (no tools) +"Search for X" → Single perplexity_search call +"Write a blog post" → Start async workflow, return immediately +``` + +This keeps simple requests fast while handling complex tasks properly. + +### Task Management + +Every workflow execution creates a **Task** (MCP Tasks protocol): + +```typescript +interface Task { + taskId: string; + status: "working" | "completed" | "failed"; + workflowId: string; + stepResults: StepResult[]; // Full execution trace + result?: unknown; + createdAt: string; +} +``` + +Tasks are persisted to disk. You can: +- Check status (`TASK_GET`) +- Get results (`TASK_RESULT`) +- List all tasks (`TASK_LIST`) +- Cancel running tasks (`TASK_CANCEL`) + +--- + +## Section 3: Tool Access (300 words) + +### Full Mesh Integration + +Pilot runs inside MCP Mesh and has access to all connected tools: + +``` +Pilot connects to: +├── OpenRouter (LLM) +├── Perplexity (web search) +├── Writing MCP (blog tools) +├── Local FS (file operations) +├── Your custom MCPs... +└── Any tool in your mesh +``` + +The `fast-router` workflow uses `tools: "all"` to give the LLM access to everything: + +```json +{ + "action": { + "type": "llm", + "tools": "all" // All mesh tools available + } +} +``` + +Or you can restrict to specific tools: + +```json +{ + "action": { + "type": "llm", + "tools": ["perplexity_search", "COLLECTION_ARTICLES_CREATE"] + } +} +``` + +### Tool Discovery + +Pilot automatically discovers available tools from the mesh: + +```typescript +const connections = await listConnections(); +// Returns all connections with their tools + +for (const conn of connections) { + console.log(conn.title, conn.tools.length); +} +// OpenRouter: 3 tools +// Perplexity: 4 tools +// Writing: 15 tools +// ... +``` + +The LLM sees a unified tool list across all MCPs. + +--- + +## Section 4: Progress & Real-Time Updates (200 words) + +### Progress Events + +During execution, Pilot publishes progress events: + +```typescript +await publishEvent("agent.task.progress", { + taskId: "task-123", + source: "whatsapp", + chatId: "self", + message: "🔍 Searching the web..." +}); +``` + +Interfaces can display these to users: +- WhatsApp Bridge shows progress messages in chat +- CLI could show a spinner +- Web UI could show a progress bar + +### Completion Events + +When done, Pilot publishes completion: + +```typescript +await publishEvent("agent.task.completed", { + taskId: "task-123", + source: "whatsapp", + chatId: "self", + response: "Here's what I found...", + duration: 3420, + toolsUsed: ["perplexity_search", "COLLECTION_ARTICLES_CREATE"] +}); +``` + +This includes: +- The final response +- How long it took +- Which tools were used +- Whether it can be retried (on failure) + +--- + +## Section 5: Conversations (200 words) + +### Long-Running Conversations + +Sometimes you want back-and-forth dialogue, not just command-response. + +Pilot supports **conversation mode**: + +```typescript +// Start a conversation +await CONVERSATION_START({ text: "Let's brainstorm ideas" }); + +// Follow-up messages automatically route to same conversation +await MESSAGE({ text: "What about marketing?" }); + +// End explicitly or via timeout +await CONVERSATION_END(); +``` + +Conversations: +- Maintain message history +- Route by `source + chatId` +- Auto-expire after configurable timeout + +### Conversation Workflow + +The `conversation` workflow handles this: + +```json +{ + "id": "conversation", + "steps": [ + { + "name": "respond", + "action": { + "type": "llm", + "prompt": "@input.message", + "model": "fast", + "tools": "all", + "systemPrompt": "You are in a conversation. Use history for context." + }, + "input": { + "message": "@input.message", + "history": "@input.history" + } + } + ] +} +``` + +--- + +## Section 6: Demo Walkthrough (200 words) + +### What to Show + +1. **Setup** (30 sec) + - Show Mesh with Pilot connection + - Show Pilot logs showing subscription + +2. **Simple Query** (30 sec) + - Send "Hello" via WhatsApp + - Show instant direct response + - Show task created and completed + +3. **Tool Usage** (60 sec) + - Send "Search for MCP news" + - Show Pilot calling Perplexity + - Show response in WhatsApp + +4. **Complex Task** (90 sec) + - Send "Write a draft about Pilot and publish" + - Show workflow starting async + - Show progress events in chat + - Show article created in blog + +5. **Task Management** (30 sec) + - Show `TASK_LIST` output + - Show task JSON with full execution trace + +### Key Talking Points + +- "One agent serves all interfaces" +- "Workflows are JSON—change behavior without code" +- "Full mesh tool access" +- "Progress updates in real-time" +- "Task history for debugging" + +--- + +## Section 7: Creating Custom Workflows (200 words) + +### The Pattern + +Create a JSON file in your workflows directory: + +```json +{ + "id": "research-and-write", + "title": "Research and Write", + "steps": [ + { + "name": "research", + "action": { + "type": "llm", + "prompt": "Research this topic: @input.topic", + "model": "fast", + "tools": ["perplexity_search"] + } + }, + { + "name": "write", + "action": { + "type": "llm", + "prompt": "Write an article based on this research: @research.output", + "model": "smart", + "tools": ["COLLECTION_ARTICLES_CREATE"] + } + } + ] +} +``` + +### Reference Syntax + +- `@input.topic` - Workflow input +- `@research.output` - Previous step output +- `@config.smartModel` - Configuration value + +### Triggering + +Via event mapping: +```bash +EVENT_WORKFLOW_MAP=custom.research:research-and-write +``` + +Or directly: +```typescript +await WORKFLOW_START({ + workflowId: "research-and-write", + input: { topic: "AI agents" } +}); +``` + +--- + +## Closing (100 words) + +AI agents shouldn't be tied to interfaces. They should be services that any interface can use. + +Pilot implements this pattern: +- Events in, events out +- Workflows define behavior +- Full mesh tool access +- Persistent task tracking + +It runs locally, uses your keys, and connects to your entire MCP ecosystem. + +We're using it with WhatsApp today. Tomorrow: Slack, Raycast, CLI, and whatever else we build. One agent, many interfaces. + +The future of AI isn't siloed bots—it's composable intelligence. + +--- + +## Links + +- **GitHub**: [decolabs/mcps/pilot](https://github.com/decolabs/mcps/tree/main/pilot) +- **MCP Mesh**: [decolabs/mesh](https://github.com/decolabs/mesh) +- **Mesh Bridge**: [decolabs/mesh-bridge](https://github.com/decolabs/mesh-bridge) +- **Event Bus Docs**: [mesh.dev/docs/event-bus](https://mesh.dev/docs/event-bus) + + + diff --git a/pilot/README.md b/pilot/README.md new file mode 100644 index 0000000..ab11205 --- /dev/null +++ b/pilot/README.md @@ -0,0 +1,290 @@ +# Pilot + +**Workflow-driven AI agent for MCP Mesh.** + +Pilot is a local AI agent that executes configurable workflows. It subscribes to events from any interface, processes them with full mesh tool access, and publishes responses back. One agent, many interfaces. + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ MCP MESH │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ EVENT BUS │ │ +│ │ │ │ +│ │ user.message.received ──────► Pilot subscribes │ │ +│ │ agent.response.* ◄────────── Pilot publishes │ │ +│ │ agent.task.progress ◄─────── Pilot publishes │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ ▲ │ +│ │ │ +│ ┌─────────────┐ ┌───────┴───────┐ ┌─────────────────┐ │ +│ │ Pilot │ │ mesh-bridge │ │ Other MCPs │ │ +│ │ │ │ │ │ │ │ +│ │ Workflows │◄───│ WhatsApp │ │ • OpenRouter │ │ +│ │ Tasks │ │ LinkedIn │ │ • Perplexity │ │ +│ │ Events │ │ Any site... │ │ • Your tools │ │ +│ └─────────────┘ └───────────────┘ └─────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────┘ +``` + +## How It Works + +1. **Interface** publishes `user.message.received` event +2. **Pilot** receives via `ON_EVENTS` tool +3. **Pilot** executes workflow (fast-router by default) +4. **Workflow** calls LLM with full tool access +5. **Pilot** publishes `agent.response.{source}` event +6. **Interface** receives and displays response + +## Recent Updates + +### Thread Management + +Messages within 5 minutes are treated as the same "thread" (conversation). This enables: + +- **Workflow chaining**: "draft this" after research continues the flow +- **Natural follow-ups**: "yes", "continue", "go ahead" proceed to next step +- **Fresh starts**: "new thread", "nova conversa" clears context + +### Improved Tool Routing + +The fast-router now explicitly guides LLMs to use the correct local tools: + +| Use This | NOT This | +|----------|----------| +| `list_tasks` | `TASK_LIST`, `task_list` | +| `list_workflows` | `COLLECTION_WORKFLOW_LIST` | +| `start_task` | `WORKFLOW_START`, `TASK_CREATE` | + +This prevents confusion when 192+ tools are available. + +## Quick Start + +### 1. Configure + +```bash +cp env.example .env +# Edit .env with your MESH_TOKEN +``` + +### 2. Add to Mesh + +In MCP Mesh, add Pilot as a **Custom Command** connection: + +| Field | Value | +|-------|-------| +| Name | `Pilot` | +| Type | `Custom Command` | +| Command | `bun` | +| Arguments | `run`, `start` | +| Working Directory | `/path/to/mcps/pilot` | + +### 3. Configure Bindings + +Pilot requires these bindings: +- **LLM**: OpenRouter or compatible (for AI responses) +- **CONNECTION**: Access to mesh connections (for tool discovery) +- **EVENT_BUS**: For pub/sub (optional but recommended) + +### 4. Test + +Send a message via any connected interface (WhatsApp, CLI via mesh-bridge, etc.) and watch Pilot process it. + +> **Note:** For a CLI interface, use [mesh-bridge CLI](../mesh-bridge) which connects to the mesh event bus like any other interface. + +## Workflows + +Every request is processed by a **workflow**—a JSON file defining execution steps. + +### Built-in Workflows + +| ID | Description | +|----|-------------| +| `fast-router` | Routes to direct response, tool call, or async task | +| `conversation` | Long-running conversation with memory | +| `direct-execution` | Execute with all tools, no routing | +| `execute-multi-step` | Complex multi-step tasks | +| `research-first` | Read context before responding | + +### Creating Custom Workflows + +Create a JSON file in `workflows/` or `CUSTOM_WORKFLOWS_DIR`: + +```json +{ + "id": "my-workflow", + "title": "My Custom Workflow", + "steps": [ + { + "name": "process", + "action": { + "type": "llm", + "prompt": "@input.message", + "model": "fast", + "tools": "all" + } + } + ] +} +``` + +### Step Actions + +| Type | Description | +|------|-------------| +| `llm` | Call LLM with prompt, tools, system prompt | +| `tool` | Call a specific MCP tool | +| `code` | Run TypeScript transform (future) | + +### Reference Syntax + +- `@input.message` - Workflow input +- `@step_name.output` - Previous step output +- `@config.fastModel` - Configuration value + +## MCP Tools + +### Execution + +| Tool | Description | +|------|-------------| +| `WORKFLOW_START` | Start workflow synchronously | +| `MESSAGE` | Smart routing (conversation or command) | +| `CONVERSATION_START` | Start long-running conversation | +| `CONVERSATION_END` | End active conversation | + +### Task Management + +| Tool | Description | +|------|-------------| +| `TASK_GET` | Get task status | +| `TASK_RESULT` | Get completed task result | +| `TASK_LIST` | List tasks with filtering | +| `TASK_CANCEL` | Cancel running task | +| `TASK_STATS` | Get statistics | + +### Workflows + +| Tool | Description | +|------|-------------| +| `WORKFLOW_LIST` | List all workflows | +| `WORKFLOW_GET` | Get workflow by ID | +| `WORKFLOW_CREATE` | Create new workflow | + +### Events + +| Tool | Description | +|------|-------------| +| `ON_EVENTS` | Receive events from mesh | + +## Event Types + +### Subscribed (Incoming) + +```typescript +"user.message.received" { + text: string; + source: string; // whatsapp, cli, etc. + chatId?: string; + sender?: { name?: string }; +} +``` + +### Published (Outgoing) + +```typescript +"agent.response.{source}" { + taskId: string; + text: string; + isFinal: boolean; +} + +"agent.task.progress" { + taskId: string; + message: string; +} + +"agent.task.completed" { + taskId: string; + response: string; + duration: number; + toolsUsed: string[]; +} +``` + +## Configuration + +```bash +# Mesh connection +MESH_URL=http://localhost:3000 +MESH_TOKEN=... + +# AI models +FAST_MODEL=google/gemini-2.5-flash +SMART_MODEL=anthropic/claude-sonnet-4 + +# Storage +TASKS_DIR=~/Projects/tasks +CUSTOM_WORKFLOWS_DIR=~/Projects/workflows + +# Defaults +DEFAULT_WORKFLOW=fast-router +CONVERSATION_WORKFLOW=conversation +CONVERSATION_TIMEOUT_MS=300000 + +# Event mapping (optional) +EVENT_WORKFLOW_MAP=custom.event:my-workflow +``` + +## File Structure + +``` +pilot/ +├── server/ +│ ├── main.ts # MCP server +│ ├── events.ts # Event types +│ ├── core/ +│ │ ├── workflow-executor.ts +│ │ ├── workflow-storage.ts +│ │ ├── task-storage.ts +│ │ └── conversation-manager.ts +│ └── types/ +│ ├── task.ts +│ └── workflow.ts +├── workflows/ # Built-in workflows +│ ├── fast-router.json +│ ├── conversation.json +│ └── ... +├── docs/ +│ └── ARCHITECTURE.md +├── env.example +└── README.md +``` + +## Development + +```bash +# Install dependencies +bun install + +# Run MCP server with hot reload +bun run dev + +# Run tests +bun test + +# Type check +bun run check +``` + +## See Also + +- [Architecture](docs/ARCHITECTURE.md) - Detailed architecture overview +- [Mesh Bridge](../mesh-bridge) - Browser interface for Pilot +- [MCP Mesh](https://github.com/decolabs/mesh) - The mesh platform + +## License + +MIT diff --git a/pilot/docs/ARCHITECTURE.md b/pilot/docs/ARCHITECTURE.md new file mode 100644 index 0000000..7214c32 --- /dev/null +++ b/pilot/docs/ARCHITECTURE.md @@ -0,0 +1,203 @@ +# Pilot Architecture + +## Overview + +Pilot is an **event-driven AI agent** that handles messages via the MCP Mesh event bus. It subscribes to user events, processes them with LLM + tool calling, and publishes response events back. + +``` +┌───────────────────────────────────────────────────────────────────────────┐ +│ MCP MESH │ +│ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ EVENT BUS │ │ +│ │ │ │ +│ │ user.message.received ───────► Pilot subscribes │ │ +│ │ agent.response.* ◄─────────── Pilot publishes │ │ +│ │ agent.task.progress ◄──────── Pilot publishes │ │ +│ │ │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ ┌──────────────┐ │ +│ │ Pilot │ │ mesh-bridge │ │ Other MCPs │ │ +│ │ │ │ │ │ │ │ +│ │ Subscribes to: │ │ Publishes: │ │ • OpenRouter │ │ +│ │ user.message.* │ │ user.message.* │ │ • Perplexity │ │ +│ │ │ │ │ │ • MCP Studio │ │ +│ │ Publishes: │ │ Subscribes to: │ │ │ │ +│ │ agent.response.* │ │ agent.response.* │ │ │ │ +│ │ agent.task.* │ │ │ │ │ │ +│ └─────────────────────┘ └─────────────────────┘ └──────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +## Design Principles + +### 1. Simple Message Loop + +Every message follows this flow: +1. Receive `user.message.received` event +2. Get/create thread for conversation continuity +3. Call LLM with available tools +4. Execute tool calls via gateway +5. Loop until final response +6. Publish `agent.response.*` event + +### 2. Event-Driven Communication + +Pilot never calls interfaces directly. It: +- **Subscribes** to `user.message.received` events +- **Publishes** `agent.response.*` events for each interface +- **Publishes** progress events during execution + +### 3. Thread-Based Conversations + +Threads are stored as JSON files in `~/.deco/pilot/threads/`. Each thread: +- Has a 5-minute TTL (configurable) +- Continues if messages arrive within TTL +- Can be closed with `/new` command + +## File Structure + +``` +pilot/ +├── server/ +│ ├── main.ts # MCP server entry (~1100 lines) +│ ├── events.ts # Event type definitions +│ ├── thread-manager.ts # File-based conversation threads +│ ├── types/ +│ │ └── workflow.ts # Workflow type definitions +│ └── tools/ +│ ├── index.ts # Tool exports +│ ├── system.ts # File/shell/clipboard tools +│ └── speech.ts # Text-to-speech tools +├── workflows/ # JSON workflow definitions +├── docs/ +│ └── ARCHITECTURE.md +├── env.example +└── README.md +``` + +## Core Components + +### main.ts + +The main server file contains: +- Configuration parsing from env vars +- Mesh API helpers (`callMeshTool`, `callLLM`, `callAgentTool`) +- Event publishing/subscribing +- Tool cache management +- `handleMessage()` - the core message processing loop +- MCP tool registrations + +### thread-manager.ts + +Manages conversation state: +- `getOrCreateThread()` - get existing or create new thread +- `addMessage()` - add user/assistant messages +- `closeAllThreadsForSource()` - handle `/new` command +- `buildMessageHistory()` - build LLM context + +### events.ts + +Defines CloudEvent types: +- `UserMessageEventSchema` - incoming messages +- `TaskProgressEventSchema` - progress updates +- `AgentResponseEventSchema` - outgoing responses + +## MCP Tools + +| Tool | Description | +|------|-------------| +| `MCP_CONFIGURATION` | Returns binding schema for Mesh UI | +| `ON_MCP_CONFIGURATION` | Receives configuration from Mesh | +| `WORKFLOW_START` | Start a workflow via MCP Studio | +| `MESSAGE` | Handle a message with thread continuation | +| `NEW_THREAD` | Start a fresh conversation | +| `ON_EVENTS` | Receive CloudEvents from mesh | + +## Event Types + +### Subscribed (Incoming) + +```typescript +"user.message.received" { + text: string; + source: string; // whatsapp, cli, etc. + chatId?: string; + sender?: { name?: string }; +} +``` + +### Published (Outgoing) + +```typescript +"agent.response.{source}" { + text: string; + chatId?: string; + isFinal: boolean; +} + +"agent.task.progress" { + taskId: string; + source: string; + message: string; +} +``` + +## Configuration + +```bash +# Mesh connection +MESH_URL=http://localhost:3000 +MESH_TOKEN=... + +# AI models +FAST_MODEL=google/gemini-2.5-flash +SMART_MODEL=anthropic/claude-sonnet-4 + +# Thread settings +THREAD_TTL_MS=300000 # 5 minutes +``` + +## Bindings + +Pilot requires these bindings configured in Mesh: + +| Binding | Type | Description | +|---------|------|-------------| +| LLM | `@deco/openrouter` | LLM for AI responses | +| AGENT | `@deco/agent` | Gateway for tool access | +| EVENT_BUS | `@deco/event-bus` | Event bus for pub/sub | + +## Message Flow + +``` +1. mesh-bridge receives WhatsApp message + ↓ +2. Publishes user.message.received + ↓ +3. Pilot receives via ON_EVENTS + ↓ +4. handleMessage(): + a. Get/create thread + b. Call LLM with tools + c. Execute tool calls via gateway + d. Repeat until response ready + ↓ +5. Pilot publishes agent.response.whatsapp + ↓ +6. mesh-bridge receives, sends to WhatsApp +``` + +## Local Tools + +Pilot includes local system tools: +- `LIST_FILES` - List directory contents +- `READ_FILE` - Read file contents +- `RUN_SHELL` - Execute shell commands +- `LIST_APPS` - List running applications +- `GET_CLIPBOARD` / `SET_CLIPBOARD` - Clipboard access +- `SEND_NOTIFICATION` - System notifications +- `SAY_TEXT` - Text-to-speech +- `STOP_SPEAKING` - Stop TTS diff --git a/pilot/env.example b/pilot/env.example new file mode 100644 index 0000000..228119f --- /dev/null +++ b/pilot/env.example @@ -0,0 +1,71 @@ +# ============================================================================= +# PILOT MCP CONFIGURATION (v3.0 - PostgreSQL-backed) +# ============================================================================= +# Copy this file to .env and customize for your environment. + +# ============================================================================= +# MESH CONNECTION +# ============================================================================= + +# URL of the MCP Mesh server +MESH_URL=http://localhost:3000 + +# Authentication token for mesh API calls +# MESH_TOKEN=your-token-here + +# ============================================================================= +# AI MODELS +# ============================================================================= + +# Model for quick routing/planning (cheap, fast) +FAST_MODEL=google/gemini-2.5-flash + +# Model for complex tasks (capable, may be slower) +# Defaults to FAST_MODEL if not set +SMART_MODEL=anthropic/claude-sonnet-4.5 + +# ============================================================================= +# WORKFLOW STUDIO (PostgreSQL-backed storage) +# ============================================================================= +# +# Pilot uses MCP Studio for workflow and execution storage. +# All workflows and thread history are stored in PostgreSQL. +# +# Setup: +# 1. Deploy mcp-studio with PostgreSQL +# 2. In Mesh UI, add mcp-studio as a connection +# 3. Pilot's WORKFLOW_STUDIO binding will be configured via MCP_CONFIGURATION +# +# Import/Export tools (for publishing workflows): +# - WORKFLOW_IMPORT: Import JSON files → PostgreSQL +# - WORKFLOW_EXPORT: Export PostgreSQL → JSON files + +# ============================================================================= +# THREAD CONFIGURATION +# ============================================================================= + +# Default workflow for conversations (special "thread" type) +THREAD_WORKFLOW=thread + +# Thread timeout - messages within this window continue the same thread +# Default: 300000 (5 minutes) +THREAD_TTL_MS=300000 + +# ============================================================================= +# EVENT → WORKFLOW MAPPING +# ============================================================================= +# Map event types to specific workflows. +# Format: EVENT_WORKFLOW_MAP=event.type:workflow-id,another.event:other-workflow +# +# Example: +# EVENT_WORKFLOW_MAP=whatsapp.message:thread,slack.message:thread +# +# If an event type is not mapped, it uses THREAD_WORKFLOW +EVENT_WORKFLOW_MAP=whatsapp.message:thread + +# ============================================================================= +# DEBUG +# ============================================================================= + +# Enable verbose logging +DEBUG=false diff --git a/pilot/package.json b/pilot/package.json new file mode 100644 index 0000000..a9d28c4 --- /dev/null +++ b/pilot/package.json @@ -0,0 +1,28 @@ +{ + "name": "mcp-pilot", + "version": "1.0.0", + "description": "deco pilot - Your local AI agent that orchestrates tasks across deco MCP mesh", + "private": true, + "type": "module", + "scripts": { + "dev": "bun --watch server/main.ts", + "start": "bun server/main.ts", + "cli": "bun cli/index.ts", + "cli:dev": "bun --watch cli/index.ts", + "build": "bun build server/main.ts --outdir=./dist --target=bun", + "check": "bun build server/main.ts --outdir=.tmp --target=bun && rm -rf .tmp", + "test": "bun test" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.1", + "zod": "^3.24.3", + "zod-to-json-schema": "^3.24.5" + }, + "devDependencies": { + "@types/bun": "^1.1.14", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/pilot/server/events.test.ts b/pilot/server/events.test.ts new file mode 100644 index 0000000..62749f8 --- /dev/null +++ b/pilot/server/events.test.ts @@ -0,0 +1,118 @@ +/** + * Events Tests + */ + +import { describe, it, expect } from "bun:test"; +import { + EVENT_TYPES, + getResponseEventType, + UserMessageEventSchema, + TaskCompletedEventSchema, +} from "./events.ts"; + +describe("Event Types", () => { + describe("EVENT_TYPES", () => { + it("has correct user event types", () => { + expect(EVENT_TYPES.USER_MESSAGE).toBe("user.message.received"); + }); + + it("has correct task event types", () => { + expect(EVENT_TYPES.TASK_CREATED).toBe("agent.task.created"); + expect(EVENT_TYPES.TASK_STARTED).toBe("agent.task.started"); + expect(EVENT_TYPES.TASK_PROGRESS).toBe("agent.task.progress"); + expect(EVENT_TYPES.TASK_COMPLETED).toBe("agent.task.completed"); + expect(EVENT_TYPES.TASK_FAILED).toBe("agent.task.failed"); + }); + }); + + describe("getResponseEventType", () => { + it("builds correct event type for whatsapp", () => { + expect(getResponseEventType("whatsapp")).toBe("agent.response.whatsapp"); + }); + + it("builds correct event type for cli", () => { + expect(getResponseEventType("cli")).toBe("agent.response.cli"); + }); + + it("handles custom sources", () => { + expect(getResponseEventType("raycast")).toBe("agent.response.raycast"); + }); + }); +}); + +describe("Event Schemas", () => { + describe("UserMessageEventSchema", () => { + it("validates minimal message", () => { + const result = UserMessageEventSchema.safeParse({ + text: "Hello", + source: "cli", + }); + + expect(result.success).toBe(true); + expect(result.data?.text).toBe("Hello"); + expect(result.data?.source).toBe("cli"); + }); + + it("validates full message with all fields", () => { + const result = UserMessageEventSchema.safeParse({ + text: "Hello", + source: "whatsapp", + chatId: "chat123", + sender: { id: "user1", name: "John" }, + replyTo: "msg123", + metadata: { isGroup: true }, + }); + + expect(result.success).toBe(true); + expect(result.data?.chatId).toBe("chat123"); + expect(result.data?.sender?.name).toBe("John"); + }); + + it("rejects message without text", () => { + const result = UserMessageEventSchema.safeParse({ + source: "cli", + }); + + expect(result.success).toBe(false); + }); + + it("rejects message without source", () => { + const result = UserMessageEventSchema.safeParse({ + text: "Hello", + }); + + expect(result.success).toBe(false); + }); + }); + + describe("TaskCompletedEventSchema", () => { + it("validates completed task event", () => { + const result = TaskCompletedEventSchema.safeParse({ + taskId: "task_123", + source: "whatsapp", + chatId: "chat123", + response: "Done!", + duration: 1500, + toolsUsed: ["LIST_FILES", "READ_FILE"], + }); + + expect(result.success).toBe(true); + expect(result.data?.taskId).toBe("task_123"); + expect(result.data?.toolsUsed).toContain("LIST_FILES"); + }); + + it("accepts optional summary", () => { + const result = TaskCompletedEventSchema.safeParse({ + taskId: "task_123", + source: "cli", + response: "Done!", + summary: "Listed 5 files and read 2", + duration: 1500, + toolsUsed: [], + }); + + expect(result.success).toBe(true); + expect(result.data?.summary).toBe("Listed 5 files and read 2"); + }); + }); +}); diff --git a/pilot/server/events.ts b/pilot/server/events.ts new file mode 100644 index 0000000..91b3975 --- /dev/null +++ b/pilot/server/events.ts @@ -0,0 +1,206 @@ +/** + * Pilot Event Types + * + * Defines the CloudEvent types used for communication between + * interfaces (WhatsApp, CLI, etc.) and the Pilot agent. + */ + +import { z } from "zod"; + +// ============================================================================ +// Incoming Events (Pilot subscribes to) +// ============================================================================ + +/** + * User message received from any interface + */ +export const UserMessageEventSchema = z.object({ + /** The message text */ + text: z.string(), + + /** Source interface (whatsapp, cli, raycast, etc.) */ + source: z.string(), + + /** Optional chat/conversation ID for context */ + chatId: z.string().optional(), + + /** Optional sender info */ + sender: z + .object({ + id: z.string().optional(), + name: z.string().optional(), + }) + .optional(), + + /** Optional reply-to message ID for threaded conversations */ + replyTo: z.string().optional(), + + /** Interface-specific metadata */ + metadata: z.record(z.unknown()).optional(), +}); + +export type UserMessageEvent = z.infer; + +/** + * Direct command from user (not conversational) + */ +export const UserCommandEventSchema = z.object({ + /** Command name */ + command: z.string(), + + /** Command arguments */ + args: z.record(z.unknown()).optional(), + + /** Source interface */ + source: z.string(), +}); + +export type UserCommandEvent = z.infer; + +// ============================================================================ +// Outgoing Events (Pilot publishes) +// ============================================================================ + +/** + * Task created and acknowledged + */ +export const TaskCreatedEventSchema = z.object({ + /** Task ID */ + taskId: z.string(), + + /** Original user message */ + userMessage: z.string(), + + /** Source interface to reply to */ + source: z.string(), + + /** Chat ID for replies */ + chatId: z.string().optional(), +}); + +export type TaskCreatedEvent = z.infer; + +/** + * Task processing started + */ +export const TaskStartedEventSchema = z.object({ + taskId: z.string(), + source: z.string(), + chatId: z.string().optional(), + mode: z.enum(["FAST", "SMART"]), +}); + +export type TaskStartedEvent = z.infer; + +/** + * Task progress update + */ +export const TaskProgressEventSchema = z.object({ + taskId: z.string(), + source: z.string(), + chatId: z.string().optional(), + message: z.string(), + /** Progress percentage (0-100) */ + percent: z.number().min(0).max(100).optional(), + /** Current step name */ + step: z.string().optional(), +}); + +export type TaskProgressEvent = z.infer; + +/** + * Tool was called + */ +export const TaskToolCalledEventSchema = z.object({ + taskId: z.string(), + source: z.string(), + chatId: z.string().optional(), + tool: z.string(), + status: z.enum(["started", "success", "error"]), + duration: z.number().optional(), + error: z.string().optional(), +}); + +export type TaskToolCalledEvent = z.infer; + +/** + * Task completed successfully + */ +export const TaskCompletedEventSchema = z.object({ + taskId: z.string(), + source: z.string(), + chatId: z.string().optional(), + /** The response to send back to the user */ + response: z.string(), + /** Brief summary of what was done */ + summary: z.string().optional(), + /** Duration in milliseconds */ + duration: z.number(), + /** Tools that were used */ + toolsUsed: z.array(z.string()), +}); + +export type TaskCompletedEvent = z.infer; + +/** + * Task failed + */ +export const TaskFailedEventSchema = z.object({ + taskId: z.string(), + source: z.string(), + chatId: z.string().optional(), + error: z.string(), + /** Whether the task can be retried */ + canRetry: z.boolean(), +}); + +export type TaskFailedEvent = z.infer; + +/** + * Response targeted at a specific interface + * This is published when the agent wants to send a response + */ +export const AgentResponseEventSchema = z.object({ + taskId: z.string(), + source: z.string(), + chatId: z.string().optional(), + /** Response text */ + text: z.string(), + /** Optional image URL */ + imageUrl: z.string().optional(), + /** Whether this is the final response */ + isFinal: z.boolean(), +}); + +export type AgentResponseEvent = z.infer; + +// ============================================================================ +// Event Type Constants +// ============================================================================ + +export const EVENT_TYPES = { + // Incoming + USER_MESSAGE: "user.message.received", + + // Connection lifecycle (from Mesh) + CONNECTION_CREATED: "connection.created", + CONNECTION_DELETED: "connection.deleted", + + // Outgoing + TASK_CREATED: "agent.task.created", + TASK_STARTED: "agent.task.started", + TASK_PROGRESS: "agent.task.progress", + TASK_TOOL_CALLED: "agent.task.tool_called", + TASK_COMPLETED: "agent.task.completed", + TASK_FAILED: "agent.task.failed", + + // Interface-specific responses (dynamically built) + RESPONSE_PREFIX: "agent.response.", +} as const; + +/** + * Build the response event type for a specific interface + */ +export function getResponseEventType(source: string): string { + return `${EVENT_TYPES.RESPONSE_PREFIX}${source}`; +} diff --git a/pilot/server/main.ts b/pilot/server/main.ts new file mode 100644 index 0000000..e8d4b19 --- /dev/null +++ b/pilot/server/main.ts @@ -0,0 +1,1404 @@ +/** + * Pilot MCP Server + * + * An AI agent that handles messages via the MCP Mesh event bus. + * + * Architecture: + * - Fast Router: Inline LLM prompt that decides how to handle each message + * - Threads: File-based JSON conversation tracking (~/.deco/pilot/threads/) + * - Event-driven: Subscribes to user.message.received, publishes responses + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import zodToJsonSchema from "zod-to-json-schema"; + +import { + getOrCreateThread, + addMessage, + closeAllThreadsForSource, + buildMessageHistory, +} from "./thread-manager.ts"; + +import { + EVENT_TYPES, + getResponseEventType, + UserMessageEventSchema, +} from "./events.ts"; + +const PILOT_VERSION = "3.0.0"; + +// ============================================================================ +// Configuration +// ============================================================================ + +const DEFAULT_THREAD_TTL_MS = 5 * 60 * 1000; // 5 minutes + +const config = { + meshUrl: process.env.MESH_URL || "http://localhost:3000", + meshToken: process.env.MESH_TOKEN, + fastModel: process.env.FAST_MODEL || "google/gemini-2.5-flash", + smartModel: + process.env.SMART_MODEL || + process.env.FAST_MODEL || + "google/gemini-2.5-flash", + threadTtlMs: parseInt( + process.env.THREAD_TTL_MS || String(DEFAULT_THREAD_TTL_MS), + 10, + ), +}; + +// Parse MESH_STATE from env (passed by mesh when spawning STDIO process) +interface BindingValue { + __type: string; + value: string; +} + +// User API Key binding has additional fields +interface UserApiKeyBindingValue { + __type: string; + value: string; // The actual API key + keyId?: string; + userId?: string; +} + +function parseBindingsFromEnv(): { + llm?: string; + agent?: string; + eventBus?: string; + userApiKey?: string; +} { + const meshStateJson = process.env.MESH_STATE; + if (!meshStateJson) return {}; + + try { + const state = JSON.parse(meshStateJson) as Record< + string, + BindingValue | UserApiKeyBindingValue + >; + return { + llm: state.LLM?.value, + agent: state.AGENT?.value, + eventBus: state.EVENT_BUS?.value, + userApiKey: state.USER_API_KEY?.value, + }; + } catch (e) { + console.error("[pilot] Failed to parse MESH_STATE:", e); + return {}; + } +} + +const envBindings = parseBindingsFromEnv(); + +// Binding connection IDs (from env or set via ON_MCP_CONFIGURATION) +let llmConnectionId: string | undefined = envBindings.llm; +let agentId: string | undefined = envBindings.agent; +let eventBusConnectionId: string | undefined = envBindings.eventBus; +// User API key for calling gateway with user's permissions +let userApiKey: string | undefined = envBindings.userApiKey; + +if ( + envBindings.llm || + envBindings.agent || + envBindings.eventBus || + envBindings.userApiKey +) { + console.error("[pilot] ✅ Bindings from MESH_STATE env var:"); + if (envBindings.llm) console.error(`[pilot] LLM: ${envBindings.llm}`); + if (envBindings.agent) console.error(`[pilot] AGENT: ${envBindings.agent}`); + if (envBindings.eventBus) + console.error(`[pilot] EVENT_BUS: ${envBindings.eventBus}`); + if (envBindings.userApiKey) + console.error( + `[pilot] USER_API_KEY: ${envBindings.userApiKey ? "set" : "not set"}`, + ); +} + +// ============================================================================ +// Binding Schema +// ============================================================================ + +const BindingOf = (bindingType: string) => + z.object({ + __type: z.literal(bindingType).default(bindingType), + value: z.string().describe("Connection ID"), + }); + +// User API Key binding has additional fields for API key management +const UserApiKeyBindingSchema = z.object({ + __type: z.literal("@deco/user-api-key").default("@deco/user-api-key"), + value: z.string().describe("API key for calling Mesh on behalf of this user"), + keyId: z.string().optional().describe("API key ID for management"), + userId: z.string().optional().describe("User ID who owns this key"), +}); + +const StateSchema = z.object({ + LLM: BindingOf("@deco/openrouter").describe("LLM for AI responses"), + AGENT: BindingOf("@deco/agent").describe( + "Agent (gateway) for tool access (required)", + ), + EVENT_BUS: BindingOf("@deco/event-bus") + .optional() + .describe("Event bus for pub/sub"), + USER_API_KEY: UserApiKeyBindingSchema.optional().describe( + "API key for calling gateway with user's permissions (required for tool access)", + ), +}); + +// ============================================================================ +// Mesh API Helpers +// ============================================================================ + +interface LLMContent { + type: string; + text?: string; + toolName?: string; + args?: Record; + input?: string | Record; +} + +interface LLMResponse { + text?: string; + content?: LLMContent[]; +} + +type LLMCallback = ( + model: string, + messages: Array<{ role: string; content: string }>, + tools: Array<{ name: string; description: string; inputSchema: unknown }>, +) => Promise<{ + text?: string; + toolCalls?: Array<{ name: string; arguments: Record }>; +}>; + +async function callMeshTool( + connectionId: string, + toolName: string, + args: Record, +): Promise { + if (!config.meshToken) { + throw new Error("MESH_TOKEN not configured"); + } + + const response = await fetch(`${config.meshUrl}/mcp/${connectionId}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + Authorization: `Bearer ${config.meshToken}`, + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: Date.now(), + method: "tools/call", + params: { name: toolName, arguments: args }, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error( + `[pilot] Mesh API error ${response.status}: ${errorText.slice(0, 200)}`, + ); + + if (response.status === 401 || response.status === 403) { + console.error(`[pilot] ⚠️ Auth error. Exiting for respawn...`); + setTimeout(() => process.exit(1), 100); + throw new Error(`Auth error (${response.status}). Process will restart.`); + } + + throw new Error(`Mesh API error: ${response.status}`); + } + + const json = (await response.json()) as { + result?: { + structuredContent?: T; + content?: Array<{ text?: string }>; + isError?: boolean; + }; + error?: { message: string }; + }; + + if (json.error) { + throw new Error(json.error.message); + } + + if (json.result?.isError) { + const errorText = json.result.content?.[0]?.text || "Unknown tool error"; + throw new Error(`Tool error from ${toolName}: ${errorText}`); + } + + if (json.result?.structuredContent) { + return json.result.structuredContent; + } + + if (json.result?.content?.[0]?.text) { + try { + return JSON.parse(json.result.content[0].text) as T; + } catch { + return json.result.content[0].text as T; + } + } + + return null as T; +} + +const callLLM: LLMCallback = async (model, messages, tools) => { + if (!llmConnectionId) { + throw new Error("LLM binding not configured"); + } + + const prompt = messages.map((m) => { + if (m.role === "system") { + return { role: "system", content: m.content }; + } + return { role: m.role, content: [{ type: "text", text: m.content }] }; + }); + + const toolsForLLM = tools.map((t) => ({ + type: "function" as const, + name: t.name, + description: t.description, + parameters: t.inputSchema, + })); + + const callOptions: Record = { + prompt, + maxOutputTokens: 2048, + temperature: 0.7, + }; + if (toolsForLLM.length > 0) { + callOptions.tools = toolsForLLM; + callOptions.toolChoice = { type: "auto" }; + } + + const result = await callMeshTool( + llmConnectionId, + "LLM_DO_GENERATE", + { modelId: model, callOptions }, + ); + + let text: string | undefined; + if (result?.content) { + const textPart = result.content.find((c) => c.type === "text"); + if (textPart?.text) text = textPart.text; + } + if (!text && result?.text) text = result.text; + + const toolCalls: Array<{ name: string; arguments: Record }> = + []; + const toolCallParts = + result?.content?.filter((c) => c.type === "tool-call") || []; + + for (const tc of toolCallParts) { + let parsedArgs: Record = {}; + if (tc.args && typeof tc.args === "object") { + parsedArgs = tc.args; + } else if (tc.input) { + if (typeof tc.input === "string") { + try { + parsedArgs = JSON.parse(tc.input); + } catch { + // empty + } + } else { + parsedArgs = tc.input; + } + } + + if (tc.toolName) { + toolCalls.push({ name: tc.toolName, arguments: parsedArgs }); + } + } + + return { text, toolCalls }; +}; + +async function callAgentTool( + toolName: string, + args: Record, +): Promise { + // Use userApiKey if available (has user's permissions), fall back to meshToken + const authToken = userApiKey || config.meshToken; + if (!authToken) { + throw new Error( + "No auth token available (neither USER_API_KEY nor MESH_TOKEN configured)", + ); + } + if (!agentId) { + throw new Error("AGENT not configured"); + } + + const response = await fetch(`${config.meshUrl}/mcp/gateway/${agentId}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: Date.now(), + method: "tools/call", + params: { name: toolName, arguments: args }, + }), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Agent API error (${response.status}): ${text}`); + } + + const contentType = response.headers.get("Content-Type") || ""; + let json: { + result?: { structuredContent?: T; content?: { text: string }[] }; + error?: { message: string }; + }; + + if (contentType.includes("text/event-stream")) { + const text = await response.text(); + const lines = text.split("\n"); + const dataLines = lines.filter((line) => line.startsWith("data: ")); + const lastData = dataLines[dataLines.length - 1]; + if (!lastData) { + throw new Error("Empty SSE response from Agent API"); + } + json = JSON.parse(lastData.slice(6)); + } else { + json = await response.json(); + } + + if (json.error) { + throw new Error(`Agent tool error: ${json.error.message}`); + } + + if (json.result?.structuredContent) { + return json.result.structuredContent; + } + + if (json.result?.content?.[0]?.text) { + try { + return JSON.parse(json.result.content[0].text) as T; + } catch { + return json.result.content[0].text as T; + } + } + + return null as T; +} + +async function publishEvent( + type: string, + data: Record, +): Promise { + if (!eventBusConnectionId) { + console.error(`[pilot] ⚠️ Cannot publish ${type}: no eventBusConnectionId`); + return; + } + + console.error(`[pilot] 📤 Publishing event: ${type}`); + + try { + await callMeshTool(eventBusConnectionId, "EVENT_PUBLISH", { type, data }); + console.error(`[pilot] ✅ Published ${type}`); + } catch (error) { + console.error(`[pilot] ❌ Failed to publish ${type}:`, error); + } +} + +async function subscribeToEvents(): Promise { + if (!eventBusConnectionId) { + console.error("[pilot] Cannot subscribe: EVENT_BUS not configured"); + return; + } + + const subscriberId = process.env.MESH_CONNECTION_ID; + if (!subscriberId) { + console.error("[pilot] ⚠️ MESH_CONNECTION_ID not set"); + } + + const eventsToSubscribe = [ + EVENT_TYPES.USER_MESSAGE, + EVENT_TYPES.CONNECTION_CREATED, + EVENT_TYPES.CONNECTION_DELETED, + "bridge.agent.info.requested", + ]; + + for (const eventType of eventsToSubscribe) { + try { + await callMeshTool(eventBusConnectionId, "EVENT_SUBSCRIBE", { + eventType, + subscriberId, + }); + console.error(`[pilot] ✅ Subscribed to ${eventType}`); + } catch (error) { + console.error(`[pilot] ❌ Failed to subscribe to ${eventType}:`, error); + } + } +} + +// ============================================================================ +// Tool Cache +// ============================================================================ + +let availableToolsCache: { + tools: Array<{ + name: string; + description?: string; + inputSchema?: unknown; + connectionId: string; + connectionTitle: string; + }>; + timestamp: number; +} | null = null; +const TOOL_CACHE_TTL_MS = 5 * 60 * 1000; + +function invalidateToolCache(): void { + availableToolsCache = null; +} + +async function getAvailableTools(): Promise< + Array<{ + name: string; + description?: string; + inputSchema?: unknown; + connectionId: string; + connectionTitle: string; + }> +> { + if ( + availableToolsCache && + Date.now() - availableToolsCache.timestamp < TOOL_CACHE_TTL_MS + ) { + return availableToolsCache.tools; + } + + if (!agentId) { + console.error("[pilot] ⚠️ AGENT not configured - no tools available"); + return []; + } + + try { + // Use userApiKey if available (has user's permissions), fall back to meshToken + const authToken = userApiKey || config.meshToken; + if (!authToken) { + console.error("[pilot] ⚠️ No auth token available for getAvailableTools"); + return []; + } + + const response = await fetch(`${config.meshUrl}/mcp/gateway/${agentId}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: Date.now(), + method: "tools/list", + params: {}, + }), + }); + + if (!response.ok) { + throw new Error(`Agent API error (${response.status})`); + } + + // Handle SSE response if needed + const contentType = response.headers.get("Content-Type") || ""; + let json: { result?: { tools?: unknown[] }; error?: { message: string } }; + + if (contentType.includes("text/event-stream")) { + const text = await response.text(); + const lines = text.split("\n"); + const dataLines = lines.filter((line) => line.startsWith("data: ")); + const lastData = dataLines[dataLines.length - 1]; + if (!lastData) { + throw new Error("Empty SSE response"); + } + json = JSON.parse(lastData.slice(6)); + } else { + json = await response.json(); + } + if (json.error) { + throw new Error(json.error.message || "Agent tools/list failed"); + } + + const toolsList = json.result?.tools || []; + // #region agent log + fetch("http://127.0.0.1:7242/ingest/8397b2ea-9df9-487e-9ffa-b17eb1bfd701", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "main.ts:468", + message: "Gateway tools/list raw response", + data: { + totalToolsFromGateway: toolsList.length, + toolNames: toolsList + .map((t: { name?: string }) => t.name) + .slice(0, 20), + }, + timestamp: Date.now(), + sessionId: "debug-session", + runId: "run1", + hypothesisId: "A", + }), + }).catch(() => {}); + // #endregion + const tools: Array<{ + name: string; + description?: string; + inputSchema?: unknown; + connectionId: string; + connectionTitle: string; + }> = []; + + for (const tool of toolsList) { + if ( + tool.name?.startsWith("COLLECTION_") || + tool.name?.startsWith("EVENT_") || + tool.name === "ON_EVENTS" || + tool.name === "ON_MCP_CONFIGURATION" || + tool.name === "MCP_CONFIGURATION" + ) { + continue; + } + + tools.push({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + connectionId: tool.connectionId || agentId, + connectionTitle: tool.connectionTitle || "Agent", + }); + } + + // #region agent log + fetch("http://127.0.0.1:7242/ingest/8397b2ea-9df9-487e-9ffa-b17eb1bfd701", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "main.ts:497", + message: "Filtered tools after exclusions", + data: { + filteredToolCount: tools.length, + filteredToolNames: tools.map((t) => t.name), + }, + timestamp: Date.now(), + sessionId: "debug-session", + runId: "run1", + hypothesisId: "E", + }), + }).catch(() => {}); + // #endregion + availableToolsCache = { tools, timestamp: Date.now() }; + console.error(`[pilot] 🔧 Loaded ${tools.length} tools from Agent`); + return tools; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + console.error(`[pilot] ❌ Failed to load tools: ${errorMsg}`); + return []; + } +} + +async function executeToolCall( + toolName: string, + args: Record, +): Promise<{ success: boolean; result?: unknown; error?: string }> { + if (!agentId) { + return { success: false, error: "AGENT not configured" }; + } + + console.error(`[pilot] 🔧 Calling ${toolName}`); + + try { + const result = await callAgentTool(toolName, args); + return { success: true, result }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + console.error(`[pilot] ❌ Tool call failed: ${errorMsg}`); + return { success: false, error: errorMsg }; + } +} + +async function publishProgress( + source: string, + chatId: string | undefined, + message: string, +): Promise { + await publishEvent(EVENT_TYPES.TASK_PROGRESS, { + taskId: chatId || "unknown", + source, + chatId, + message, + }); +} + +// ============================================================================ +// Message Handling +// ============================================================================ + +interface HandleMessageResult { + response: string; + action: "reply" | "tool"; +} + +async function handleMessage( + text: string, + source: string, + chatId?: string, + options: { forceNewThread?: boolean } = {}, +): Promise { + console.error(`[pilot] 📨 handleMessage: "${text.slice(0, 50)}..."`); + + try { + const thread = getOrCreateThread( + source, + chatId || `${source}-default`, + options.forceNewThread, + ); + + addMessage(thread.id, "user", text); + thread.messages.push({ + role: "user", + content: text, + timestamp: new Date().toISOString(), + }); + + const availableTools = await getAvailableTools(); + + const toolDefs = availableTools.map((t) => ({ + name: t.name, + description: t.description || `Tool: ${t.name}`, + inputSchema: t.inputSchema || { type: "object", properties: {} }, + })); + + // Detect gateway mode based on available tools + const toolNames = new Set(availableTools.map((t) => t.name)); + const isCodeExecutionMode = + toolNames.has("GATEWAY_SEARCH_TOOLS") && + toolNames.has("GATEWAY_DESCRIBE_TOOLS") && + toolNames.has("GATEWAY_RUN_CODE"); + + const codeExecutionPrompt = `You are Pilot, a helpful AI assistant connected to MCP Mesh. + +## Gateway Tool Discovery Pattern (Code Execution Mode) + +The gateway uses a code-execution strategy. To use external tools, you MUST complete ALL 3 steps: + +### Step 1: SEARCH +Call \`GATEWAY_SEARCH_TOOLS\` with { query: "keyword" } +- Use specific keywords from the user's request (e.g., "perplexity", "weather", "search") +- Returns a list of matching tool names + +### Step 2: DESCRIBE +Call \`GATEWAY_DESCRIBE_TOOLS\` with { tools: ["tool_name"] } +- IMPORTANT: The parameter is called "tools" (not "toolNames") +- Use the EXACT tool names from search results (e.g., "perplexity_ask") +- Returns the input schema for each tool + +### Step 3: EXECUTE (REQUIRED!) +Call \`GATEWAY_RUN_CODE\` with { code: "..." } containing JavaScript: +\`\`\`javascript +export default async (tools) => { + const result = await tools.perplexity_ask({ + messages: [{ role: "user", content: "your query here" }] + }); + return result; +}; +\`\`\` +- IMPORTANT: Use \`(tools)\` NOT \`({ tools })\` - tools is a direct parameter, not destructured +- Use the schema from Step 2 to construct the correct parameters +- ALWAYS complete this step to get actual results for the user +- DO NOT skip this step - the user needs real data + +## CRITICAL RULES + +- You MUST complete all 3 steps (SEARCH → DESCRIBE → EXECUTE) before responding to the user +- DO NOT stop after DESCRIBE - you need to call GATEWAY_RUN_CODE to get actual results +- DO NOT ask the user for clarification mid-workflow - complete all steps first +- If search returns no results, inform the user the tool doesn't exist +- Match user's language (Portuguese/English)`; + + const passthroughPrompt = `You are Pilot, a helpful AI assistant connected to MCP Mesh. + +## Available Tools (Passthrough Mode) + +You have direct access to ${availableTools.length} tools. Use function calling to invoke them directly. + +Available tools: ${availableTools.map((t) => t.name).join(", ")} + +## Rules + +- Use function calling to invoke tools directly +- For greetings/simple questions: respond directly with text +- Match user's language (Portuguese/English)`; + + const systemPrompt = isCodeExecutionMode + ? codeExecutionPrompt + : passthroughPrompt; + + await publishProgress(source, chatId, "🧠 Thinking..."); + + const threadHistory = buildMessageHistory(thread); + const messages: Array<{ role: string; content: string }> = [ + { role: "system", content: systemPrompt }, + ...threadHistory, + ]; + + // #region agent log + fetch("http://127.0.0.1:7242/ingest/8397b2ea-9df9-487e-9ffa-b17eb1bfd701", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "main.ts:610", + message: "Tools passed to LLM", + data: { + toolCount: toolDefs.length, + toolNames: toolDefs.map((t) => t.name), + userMessage: text.slice(0, 100), + }, + timestamp: Date.now(), + sessionId: "debug-session", + runId: "run1", + hypothesisId: "C", + }), + }).catch(() => {}); + // #endregion + + let action: "reply" | "tool" = "reply"; + let response = ""; + + // LLM call with all tools + const result = await callLLM(config.fastModel, messages, toolDefs); + + // #region agent log + fetch("http://127.0.0.1:7242/ingest/8397b2ea-9df9-487e-9ffa-b17eb1bfd701", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "main.ts:620", + message: "LLM response", + data: { + hasText: !!result.text, + textPreview: result.text?.slice(0, 100), + toolCallCount: result.toolCalls?.length || 0, + toolCallNames: result.toolCalls?.map((tc) => tc.name), + }, + timestamp: Date.now(), + sessionId: "debug-session", + runId: "run1", + hypothesisId: "C", + }), + }).catch(() => {}); + // #endregion + + if ( + result.text?.trim() && + (!result.toolCalls || result.toolCalls.length === 0) + ) { + // Direct response + response = result.text.trim(); + console.error(`[pilot] 💬 Direct response: "${response.slice(0, 100)}"`); + } else if (result.toolCalls && result.toolCalls.length > 0) { + // Tool execution loop + action = "tool"; + const executionMessages = [...messages]; + const MAX_ITERATIONS = 20; // Allow enough iterations for SEARCH → DESCRIBE → EXECUTE workflow + + for (let i = 0; i < MAX_ITERATIONS; i++) { + console.error(`[pilot] 🔄 Iteration ${i + 1}/${MAX_ITERATIONS}`); + + const iterResult = + i === 0 + ? result + : await callLLM(config.smartModel, executionMessages, toolDefs); + + if (!iterResult.toolCalls || iterResult.toolCalls.length === 0) { + response = iterResult.text?.replace(/^REPLY\s*/i, "").trim() || ""; + if (response) break; + response = "Desculpe, não consegui processar sua mensagem."; + break; + } + + for (const toolCall of iterResult.toolCalls) { + await publishProgress(source, chatId, `🔧 ${toolCall.name}...`); + console.error(`[pilot] 🔧 ${toolCall.name}`); + + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/8397b2ea-9df9-487e-9ffa-b17eb1bfd701", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "main.ts:780-args", + message: "Tool call arguments", + data: { + toolName: toolCall.name, + arguments: toolCall.arguments, + }, + timestamp: Date.now(), + sessionId: "debug-session", + runId: "run1", + hypothesisId: "G", + }), + }, + ).catch(() => {}); + // #endregion + + const toolResult = await executeToolCall( + toolCall.name, + toolCall.arguments, + ); + + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/8397b2ea-9df9-487e-9ffa-b17eb1bfd701", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "main.ts:780-result", + message: "Tool call result", + data: { + toolName: toolCall.name, + success: toolResult.success, + resultPreview: JSON.stringify( + toolResult.result ?? toolResult.error, + ).slice(0, 500), + }, + timestamp: Date.now(), + sessionId: "debug-session", + runId: "run1", + hypothesisId: "F", + }), + }, + ).catch(() => {}); + // #endregion + + executionMessages.push({ + role: "assistant", + content: `[Called ${toolCall.name}]`, + }); + executionMessages.push({ + role: "user", + content: `Tool result: ${JSON.stringify(toolResult.result ?? toolResult.error).slice(0, 4000)}`, + }); + } + + await publishProgress(source, chatId, "🧠 Processing..."); + } + + if (!response) { + response = "Desculpe, a operação excedeu o limite de iterações."; + } + } else { + response = "Desculpe, não entendi. Pode reformular?"; + } + + addMessage(thread.id, "assistant", response); + await publishProgress(source, chatId, "💬 Replying..."); + + console.error(`[pilot] ✅ Response: "${response.slice(0, 100)}..."`); + + const responseEventType = getResponseEventType(source); + await publishEvent(responseEventType, { + source, + chatId, + text: response, + isFinal: true, + }); + + return { response, action }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + console.error("[pilot] ❌ Error:", errorMsg); + + await publishProgress(source, chatId, "❌ Error: " + errorMsg); + + const responseEventType = getResponseEventType(source); + await publishEvent(responseEventType, { + source, + chatId, + text: "Erro: " + errorMsg, + isFinal: true, + }); + + return { response: "Erro: " + errorMsg, action: "reply" }; + } +} + +// ============================================================================ +// Main +// ============================================================================ + +async function main() { + const server = new McpServer({ + name: "pilot", + version: PILOT_VERSION, + }); + + // ========================================================================== + // Configuration Tools + // ========================================================================== + + server.registerTool( + "MCP_CONFIGURATION", + { + title: "MCP Configuration", + description: "Returns the configuration schema for this MCP server", + inputSchema: z.object({}), + annotations: { readOnlyHint: true }, + }, + async () => { + const rawStateSchema = zodToJsonSchema(StateSchema, { + $refStrategy: "none", + }) as Record; + + const result = { + stateSchema: rawStateSchema, + scopes: [ + "LLM::LLM_DO_GENERATE", + "LLM::COLLECTION_LLM_LIST", + "EVENT_BUS::*", + ], + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result as Record, + }; + }, + ); + + server.registerTool( + "ON_MCP_CONFIGURATION", + { + title: "Receive Configuration", + description: "Receive configuration from Mesh", + inputSchema: z.object({ + state: z.record(z.string(), z.any()).optional(), + authorization: z.string().optional(), + meshUrl: z.string().optional(), + }), + }, + async (args) => { + const { state, authorization, meshUrl } = args; + + if (authorization) config.meshToken = authorization; + if (meshUrl) config.meshUrl = meshUrl; + if (state?.LLM?.value) llmConnectionId = state.LLM.value; + if (state?.AGENT?.value) agentId = state.AGENT.value; + if (state?.EVENT_BUS?.value) eventBusConnectionId = state.EVENT_BUS.value; + if (state?.USER_API_KEY?.value) userApiKey = state.USER_API_KEY.value; + + console.error(`[pilot] Configuration received`); + console.error(`[pilot] LLM: ${llmConnectionId || "not set"}`); + console.error(`[pilot] AGENT: ${agentId || "not set"}`); + console.error( + `[pilot] EVENT_BUS: ${eventBusConnectionId || "not set"}`, + ); + console.error( + `[pilot] USER_API_KEY: ${userApiKey ? "set" : "not set"}`, + ); + + if (eventBusConnectionId) { + subscribeToEvents().catch((e) => + console.error("[pilot] Event subscription error:", e), + ); + } + + return { + content: [{ type: "text", text: JSON.stringify({ success: true }) }], + structuredContent: { success: true }, + }; + }, + ); + + // ========================================================================== + // Core Tools + // ========================================================================== + + server.registerTool( + "WORKFLOW_START", + { + title: "Start Workflow", + description: "Start a workflow execution synchronously", + inputSchema: z.object({ + workflowId: z.string().describe("Workflow ID to execute"), + input: z.record(z.string(), z.any()).describe("Workflow input"), + source: z.string().optional().describe("Source interface"), + chatId: z.string().optional().describe("Chat ID"), + }), + }, + async (args) => { + if (!agentId) throw new Error("AGENT not configured"); + + const { workflowId, input, source, chatId } = args; + + const workflowResult = (await callAgentTool("COLLECTION_WORKFLOW_GET", { + id: workflowId, + })) as { item?: { id: string; gateway_id?: string } | null }; + + if (!workflowResult.item) { + throw new Error(`Workflow not found: ${workflowId}`); + } + + const executionResult = (await callAgentTool( + "COLLECTION_WORKFLOW_EXECUTION_CREATE", + { + workflow_collection_id: workflowId, + input: { + ...input, + __meta: { + source: source || "api", + chatId, + workflowType: "workflow", + }, + }, + gateway_id: workflowResult.item.gateway_id || agentId, + start_at_epoch_ms: Date.now(), + }, + )) as { item?: { id: string } }; + + if (!executionResult.item) { + throw new Error("Failed to create workflow execution"); + } + + const executionId = executionResult.item.id; + const maxWait = 30000; + const pollInterval = 500; + const startTime = Date.now(); + + while (Date.now() - startTime < maxWait) { + const exec = (await callAgentTool("COLLECTION_WORKFLOW_EXECUTION_GET", { + id: executionId, + })) as { + item?: { status: string; output?: unknown; error?: unknown } | null; + }; + + if (!exec.item) throw new Error("Execution not found"); + + if (exec.item.status === "success") { + const output = exec.item.output as { response?: string } | undefined; + return { + content: [{ type: "text", text: output?.response || "Done" }], + structuredContent: { + response: output?.response || "Done", + taskId: executionId, + status: "success", + }, + }; + } + + if (exec.item.status === "error" || exec.item.status === "failed") { + throw new Error( + (exec.item.error as { message?: string })?.message || "Failed", + ); + } + + await new Promise((resolve) => setTimeout(resolve, pollInterval)); + } + + return { + content: [{ type: "text", text: `Running (ID: ${executionId})` }], + structuredContent: { taskId: executionId, status: "running" }, + }; + }, + ); + + server.registerTool( + "MESSAGE", + { + title: "Handle Message", + description: "Handle a message with thread continuation", + inputSchema: z.object({ + text: z.string().describe("The message"), + source: z.string().optional().describe("Source interface"), + chatId: z.string().optional().describe("Chat ID"), + forceNewThread: z.boolean().optional().describe("Force new thread"), + }), + }, + async (args) => { + const { text, source, chatId, forceNewThread } = args; + + const result = await handleMessage(text, source || "api", chatId, { + forceNewThread, + }); + + return { + content: [{ type: "text", text: result.response }], + structuredContent: { response: result.response }, + }; + }, + ); + + server.registerTool( + "NEW_THREAD", + { + title: "Start New Thread", + description: "Start a fresh conversation", + inputSchema: z.object({}), + }, + async () => { + return { + content: [{ type: "text", text: JSON.stringify({ success: true }) }], + structuredContent: { success: true }, + }; + }, + ); + + // ========================================================================== + // Event Handler + // ========================================================================== + + server.registerTool( + "ON_EVENTS", + { + title: "Receive Events", + description: "Receive CloudEvents from mesh", + inputSchema: z.object({ + events: z.array( + z.object({ + id: z.string(), + type: z.string(), + source: z.string(), + time: z.string().optional(), + data: z.any(), + }), + ), + }), + }, + async (args: { + events: Array<{ + id: string; + type: string; + source: string; + time?: string; + data: unknown; + }>; + }) => { + const { events } = args; + const results: Record = {}; + + for (const event of events) { + try { + if (event.type === EVENT_TYPES.USER_MESSAGE) { + const parsed = UserMessageEventSchema.safeParse(event.data); + if (!parsed.success) { + results[event.id] = { success: false, error: "Invalid data" }; + continue; + } + + const data = parsed.data; + + if (data.text.trim().toLowerCase() === "/new") { + closeAllThreadsForSource(data.source); + await publishEvent(getResponseEventType(data.source), { + source: data.source, + chatId: data.chatId, + text: "🆕 Started new thread.", + isFinal: true, + }); + results[event.id] = { success: true }; + continue; + } + + handleMessage(data.text, data.source, data.chatId).catch((e) => + console.error(`[pilot] Error: ${e}`), + ); + results[event.id] = { success: true }; + } else if ( + event.type === EVENT_TYPES.CONNECTION_CREATED || + event.type === EVENT_TYPES.CONNECTION_DELETED + ) { + invalidateToolCache(); + results[event.id] = { success: true }; + } else if (event.type === "bridge.agent.info.requested") { + if (!agentId) { + results[event.id] = { success: false, error: "No AGENT" }; + continue; + } + + try { + let gatewayInfo: { + item?: { id: string; title: string }; + } | null = null; + if (eventBusConnectionId) { + try { + gatewayInfo = await callMeshTool( + eventBusConnectionId, + "COLLECTION_GATEWAY_GET", + { id: agentId }, + ); + } catch { + // ignore + } + } + + // Use userApiKey for gateway calls (has user's permissions) + const agentAuthToken = userApiKey || config.meshToken; + const toolsResponse = await fetch( + `${config.meshUrl}/mcp/gateway/${agentId}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + Authorization: `Bearer ${agentAuthToken}`, + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: Date.now(), + method: "tools/list", + params: {}, + }), + }, + ); + + if (!toolsResponse.ok) { + throw new Error(`API error (${toolsResponse.status})`); + } + + // Handle SSE response if needed + const contentType = + toolsResponse.headers.get("Content-Type") || ""; + let toolsJson: { result?: { tools?: unknown[] } }; + + if (contentType.includes("text/event-stream")) { + const text = await toolsResponse.text(); + const lines = text.split("\n"); + const dataLines = lines.filter((line) => + line.startsWith("data: "), + ); + const lastData = dataLines[dataLines.length - 1]; + if (!lastData) { + throw new Error("Empty SSE response"); + } + toolsJson = JSON.parse(lastData.slice(6)); + } else { + toolsJson = await toolsResponse.json(); + } + + const toolsList = toolsJson.result?.tools || []; + + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/8397b2ea-9df9-487e-9ffa-b17eb1bfd701", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "main.ts:1040", + message: "Agent info: raw tools from gateway", + data: { + totalTools: toolsList.length, + toolNames: toolsList + .map((t: { name?: string }) => t.name) + .slice(0, 20), + }, + timestamp: Date.now(), + sessionId: "debug-session", + runId: "run1", + hypothesisId: "B", + }), + }, + ).catch(() => {}); + // #endregion + + const publicTools = toolsList.filter( + (t: { name?: string }) => + t.name && + !t.name.startsWith("COLLECTION_") && + !t.name.startsWith("EVENT_") && + t.name !== "ON_EVENTS" && + t.name !== "ON_MCP_CONFIGURATION" && + t.name !== "MCP_CONFIGURATION", + ); + + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/8397b2ea-9df9-487e-9ffa-b17eb1bfd701", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "main.ts:1055", + message: "Agent info: publishing response", + data: { + publicToolCount: publicTools.length, + publicToolNames: publicTools.map( + (t: { name: string }) => t.name, + ), + gatewayTitle: gatewayInfo?.item?.title, + }, + timestamp: Date.now(), + sessionId: "debug-session", + runId: "run1", + hypothesisId: "B", + }), + }, + ).catch(() => {}); + // #endregion + + await publishEvent("agent.info.response", { + id: agentId, + title: gatewayInfo?.item?.title || `Agent`, + tools: publicTools.map( + (t: { name: string; description?: string }) => ({ + name: t.name, + description: t.description, + }), + ), + }); + + results[event.id] = { success: true }; + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + console.error(`[pilot] ❌ Agent info failed: ${errMsg}`); + results[event.id] = { success: false, error: errMsg }; + } + } else { + results[event.id] = { success: true }; + } + } catch (error) { + results[event.id] = { + success: false, + error: error instanceof Error ? error.message : "Failed", + }; + } + } + + return { + content: [{ type: "text", text: JSON.stringify({ results }) }], + structuredContent: { results }, + }; + }, + ); + + // ========================================================================== + // Start Server + // ========================================================================== + + const transport = new StdioServerTransport(); + await server.connect(transport); + + console.error(`[pilot] Started v${PILOT_VERSION}`); + + if (eventBusConnectionId) { + setTimeout(() => { + subscribeToEvents().catch((e) => + console.error("[pilot] Event subscription error:", e), + ); + }, 100); + } +} + +main().catch((error) => { + console.error("[pilot] Fatal error:", error); + process.exit(1); +}); diff --git a/pilot/server/thread-manager.ts b/pilot/server/thread-manager.ts new file mode 100644 index 0000000..c9263a2 --- /dev/null +++ b/pilot/server/thread-manager.ts @@ -0,0 +1,283 @@ +/** + * Thread Manager + * + * File-based JSON thread management for conversation continuity. + * + * Threads are stored in ~/.deco/pilot/threads/ to avoid triggering HMR. + * Each thread has a TTL (default 5 min) - messages within TTL continue the same thread. + */ + +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + writeFileSync, +} from "node:fs"; +import { join } from "node:path"; +import { homedir } from "node:os"; + +// ============================================================================ +// Configuration +// ============================================================================ + +// Store threads in ~/.deco/pilot/threads to avoid triggering HMR during dev +const THREADS_DIR = join(homedir(), ".deco", "pilot", "threads"); +const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes + +// ============================================================================ +// Types +// ============================================================================ + +export interface ThreadMessage { + role: "user" | "assistant"; + content: string; + timestamp: string; +} + +export interface Thread { + id: string; + source: string; + chatId: string; + createdAt: string; + lastActivityAt: string; + status: "open" | "closed"; + messages: ThreadMessage[]; +} + +// ============================================================================ +// Helpers +// ============================================================================ + +function ensureThreadsDir(): void { + if (!existsSync(THREADS_DIR)) { + mkdirSync(THREADS_DIR, { recursive: true }); + } +} + +function getThreadPath(threadId: string): string { + return join(THREADS_DIR, `${threadId}.json`); +} + +function generateThreadId(): string { + const now = new Date(); + const date = now.toISOString().slice(0, 10).replace(/-/g, ""); + const time = now.toISOString().slice(11, 19).replace(/:/g, ""); + const rand = Math.random().toString(36).slice(2, 6); + return `thread-${date}-${time}-${rand}`; +} + +// ============================================================================ +// Thread Operations +// ============================================================================ + +/** + * Find an open thread for the given source/chatId within TTL. + */ +export function findActiveThread( + source: string, + chatId: string, + ttlMs: number = DEFAULT_TTL_MS, +): Thread | null { + ensureThreadsDir(); + + const now = Date.now(); + const files = readdirSync(THREADS_DIR).filter((f) => f.endsWith(".json")); + + // Sort by modification time (newest first) + const sortedFiles = files + .map((f) => { + const path = join(THREADS_DIR, f); + let mtime = 0; + try { + const content = readFileSync(path, "utf-8"); + const thread = JSON.parse(content) as Thread; + mtime = new Date(thread.lastActivityAt).getTime(); + } catch { + // Invalid JSON or read error, skip + } + return { name: f, path, mtime }; + }) + .sort((a, b) => b.mtime - a.mtime); + + for (const file of sortedFiles) { + try { + const content = readFileSync(file.path, "utf-8"); + const thread: Thread = JSON.parse(content); + + // Check if thread matches source/chatId and is open + if (thread.source !== source) continue; + if (thread.chatId !== chatId) continue; + if (thread.status !== "open") continue; + + // Check TTL + const lastActivity = new Date(thread.lastActivityAt).getTime(); + if (now - lastActivity > ttlMs) { + // Thread expired - mark as closed + thread.status = "closed"; + writeFileSync(file.path, JSON.stringify(thread, null, 2)); + continue; + } + + return thread; + } catch { + // Invalid JSON, skip + continue; + } + } + + return null; +} + +/** + * Create a new thread. + */ +export function createThread(source: string, chatId: string): Thread { + ensureThreadsDir(); + + const now = new Date().toISOString(); + const thread: Thread = { + id: generateThreadId(), + source, + chatId, + createdAt: now, + lastActivityAt: now, + status: "open", + messages: [], + }; + + writeFileSync(getThreadPath(thread.id), JSON.stringify(thread, null, 2)); + console.error(`[thread] Created new thread: ${thread.id}`); + + return thread; +} + +/** + * Add a message to a thread and update lastActivityAt. + */ +export function addMessage( + threadId: string, + role: "user" | "assistant", + content: string, +): void { + const path = getThreadPath(threadId); + if (!existsSync(path)) { + console.error(`[thread] Thread not found: ${threadId}`); + return; + } + + let thread: Thread; + try { + const content = readFileSync(path, "utf-8"); + thread = JSON.parse(content); + } catch (error) { + console.error(`[thread] Failed to parse thread ${threadId}:`, error); + return; + } + + thread.messages.push({ + role, + content, + timestamp: new Date().toISOString(), + }); + thread.lastActivityAt = new Date().toISOString(); + + writeFileSync(path, JSON.stringify(thread, null, 2)); +} + +/** + * Close a thread (mark as closed). + */ +export function closeThread(threadId: string): void { + const path = getThreadPath(threadId); + if (!existsSync(path)) { + console.error(`[thread] Thread not found: ${threadId}`); + return; + } + + const thread: Thread = JSON.parse(readFileSync(path, "utf-8")); + thread.status = "closed"; + thread.lastActivityAt = new Date().toISOString(); + + writeFileSync(path, JSON.stringify(thread, null, 2)); + console.error(`[thread] Closed thread: ${threadId}`); +} + +/** + * Get a thread by ID. + */ +export function getThread(threadId: string): Thread | null { + const path = getThreadPath(threadId); + if (!existsSync(path)) return null; + + try { + return JSON.parse(readFileSync(path, "utf-8")); + } catch { + return null; + } +} + +/** + * Close all open threads for a source (used when /new is called). + */ +export function closeAllThreadsForSource(source: string): number { + ensureThreadsDir(); + + let closed = 0; + const files = readdirSync(THREADS_DIR).filter((f) => f.endsWith(".json")); + + for (const file of files) { + try { + const path = join(THREADS_DIR, file); + const thread: Thread = JSON.parse(readFileSync(path, "utf-8")); + + if (thread.source === source && thread.status === "open") { + thread.status = "closed"; + thread.lastActivityAt = new Date().toISOString(); + writeFileSync(path, JSON.stringify(thread, null, 2)); + closed++; + } + } catch { + continue; + } + } + + console.error(`[thread] Closed ${closed} threads for source: ${source}`); + return closed; +} + +/** + * Get or create a thread for handling a message. + * Returns existing active thread if within TTL, otherwise creates new one. + */ +export function getOrCreateThread( + source: string, + chatId: string, + forceNew: boolean = false, +): Thread { + if (forceNew) { + closeAllThreadsForSource(source); + } + + const existing = findActiveThread(source, chatId); + if (existing) { + console.error( + `[thread] Continuing thread: ${existing.id} (${existing.messages.length} messages)`, + ); + return existing; + } + + return createThread(source, chatId); +} + +/** + * Build message history for LLM context from a thread. + */ +export function buildMessageHistory( + thread: Thread, +): Array<{ role: string; content: string }> { + return thread.messages.map((m) => ({ + role: m.role, + content: m.content, + })); +} diff --git a/pilot/server/tools/index.ts b/pilot/server/tools/index.ts new file mode 100644 index 0000000..1f0f21e --- /dev/null +++ b/pilot/server/tools/index.ts @@ -0,0 +1,18 @@ +/** + * Tools Index + * + * Exports all local tools available to the Pilot agent. + */ + +import { systemTools } from "./system.ts"; +import { speechTools } from "./speech.ts"; +import type { Tool } from "./system.ts"; + +export type { Tool, ToolResult } from "./system.ts"; + +/** + * Get all local tools + */ +export function getAllLocalTools(): Tool[] { + return [...systemTools, ...speechTools]; +} diff --git a/pilot/server/tools/speech.test.ts b/pilot/server/tools/speech.test.ts new file mode 100644 index 0000000..0cd4898 --- /dev/null +++ b/pilot/server/tools/speech.test.ts @@ -0,0 +1,45 @@ +/** + * Speech Tools Tests + */ + +import { describe, it, expect } from "bun:test"; +import { detectLanguage, getVoiceForLanguage } from "./speech.ts"; + +describe("Speech Tools", () => { + describe("detectLanguage", () => { + it("detects Portuguese from common words", () => { + expect(detectLanguage("Olá, como você está?")).toBe("pt"); + expect(detectLanguage("Isso é muito bom")).toBe("pt"); + expect(detectLanguage("Não sei o que fazer")).toBe("pt"); + expect(detectLanguage("Obrigado pela ajuda")).toBe("pt"); + }); + + it("detects Portuguese from accented characters", () => { + expect(detectLanguage("Está funcionando")).toBe("pt"); + expect(detectLanguage("Açúcar e café")).toBe("pt"); + expect(detectLanguage("Informação")).toBe("pt"); + }); + + it("defaults to English for English text", () => { + expect(detectLanguage("Hello, how are you?")).toBe("en"); + expect(detectLanguage("This is working great")).toBe("en"); + expect(detectLanguage("The quick brown fox")).toBe("en"); + }); + + it("defaults to English for mixed/unclear text", () => { + expect(detectLanguage("123456")).toBe("en"); + expect(detectLanguage("OK")).toBe("en"); + expect(detectLanguage("")).toBe("en"); + }); + }); + + describe("getVoiceForLanguage", () => { + it("returns Luciana for Portuguese", () => { + expect(getVoiceForLanguage("pt")).toBe("Luciana"); + }); + + it("returns Samantha for English", () => { + expect(getVoiceForLanguage("en")).toBe("Samantha"); + }); + }); +}); diff --git a/pilot/server/tools/speech.ts b/pilot/server/tools/speech.ts new file mode 100644 index 0000000..164aac0 --- /dev/null +++ b/pilot/server/tools/speech.ts @@ -0,0 +1,186 @@ +/** + * Speech Tools + * + * Tools for text-to-speech using macOS `say` command. + */ + +import { spawn, type Subprocess } from "bun"; +import type { Tool, ToolResult } from "./system.ts"; + +// Voice configuration +const DEFAULT_VOICE = "Samantha"; +const PT_VOICE = "Luciana"; +const EN_VOICE = "Samantha"; + +// Track active speech process +let activeSayProcess: Subprocess<"ignore", "pipe", "pipe"> | null = null; + +/** + * Detect language from text (simple heuristic) + */ +export function detectLanguage(text: string): "pt" | "en" { + const ptPatterns = [ + /\b(você|voce|não|nao|está|esta|isso|esse|ela|ele|como|para|por|que|uma|um|com|são|sao|também|tambem|ainda|aqui|agora|onde|quando|porque|muito|bem|obrigado|olá|ola|bom|boa|dia|noite|tarde)\b/i, + /[áàâãéêíóôõúç]/i, + ]; + + for (const pattern of ptPatterns) { + if (pattern.test(text)) { + return "pt"; + } + } + + return "en"; +} + +/** + * Get voice for a language + */ +export function getVoiceForLanguage(lang: "pt" | "en"): string { + return lang === "pt" ? PT_VOICE : EN_VOICE; +} + +/** + * Stop any active speech + */ +export function stopSpeaking(): boolean { + const processToKill = activeSayProcess; + if (processToKill) { + try { + processToKill.kill(); + // Only clear if it's still the active process (no race condition) + if (activeSayProcess === processToKill) { + activeSayProcess = null; + } + return true; + } catch { + // Only clear if it's still the active process (no race condition) + if (activeSayProcess === processToKill) { + activeSayProcess = null; + } + return false; + } + } + return false; +} + +/** + * Speak text aloud + */ +export async function speakText( + text: string, + voice?: string, +): Promise<{ success: boolean; voice: string }> { + // Stop any current speech + stopSpeaking(); + + const detectedLang = detectLanguage(text); + const selectedVoice = voice || getVoiceForLanguage(detectedLang); + + try { + activeSayProcess = spawn(["say", "-v", selectedVoice, text], { + stdout: "pipe", + stderr: "pipe", + }); + + await activeSayProcess.exited; + activeSayProcess = null; + + return { success: true, voice: selectedVoice }; + } catch (error) { + activeSayProcess = null; + throw error; + } +} + +// ============================================================================ +// Tool Definitions +// ============================================================================ + +/** + * SAY_TEXT - Speak text aloud + */ +export const SAY_TEXT: Tool = { + name: "SAY_TEXT", + description: + "Speak text aloud using text-to-speech. Auto-detects Portuguese vs English.", + inputSchema: { + type: "object", + properties: { + text: { + type: "string", + description: "Text to speak", + }, + voice: { + type: "string", + description: "Voice to use (optional - auto-detects based on language)", + }, + }, + required: ["text"], + }, + execute: async (args): Promise => { + const text = args.text as string; + const voice = args.voice as string | undefined; + + try { + const result = await speakText(text, voice); + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + success: true, + voice: result.voice, + textLength: text.length, + }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : "Speech failed", + }), + }, + ], + isError: true, + }; + } + }, +}; + +/** + * STOP_SPEAKING - Stop any active speech + */ +export const STOP_SPEAKING: Tool = { + name: "STOP_SPEAKING", + description: "Stop any currently playing text-to-speech", + inputSchema: { + type: "object", + properties: {}, + }, + execute: async (): Promise => { + const wasSpeaking = stopSpeaking(); + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + success: true, + wasSpeaking, + message: wasSpeaking ? "Stopped speaking" : "Nothing was playing", + }), + }, + ], + }; + }, +}; + +// Export all speech tools +export const speechTools: Tool[] = [SAY_TEXT, STOP_SPEAKING]; diff --git a/pilot/server/tools/system.ts b/pilot/server/tools/system.ts new file mode 100644 index 0000000..ce7b3f5 --- /dev/null +++ b/pilot/server/tools/system.ts @@ -0,0 +1,621 @@ +/** + * System Tools + * + * Tools for interacting with the local system: + * - File operations (list, read) + * - Shell commands + * - Clipboard + * - Notifications + * - Running applications + */ + +import { spawn } from "bun"; +import { readdir, readFile, stat } from "fs/promises"; +import { join, resolve } from "path"; + +// Safety config +const ALLOWED_PATHS = (process.env.ALLOWED_PATHS || "/Users/guilherme/Projects") + .split(",") + .filter(Boolean); +const BLOCKED_COMMANDS = ( + process.env.BLOCKED_COMMANDS || "rm -rf,sudo,chmod 777,mkfs,dd if=" +) + .split(",") + .filter(Boolean); +const SHELL_TIMEOUT = 30000; + +/** + * Check if a path is within allowed directories + * Ensures the path is either exactly the allowed path or starts with it followed by a path separator + */ +function isPathAllowed(path: string): boolean { + const resolved = resolve(path); + return ALLOWED_PATHS.some((allowed) => { + const normalizedAllowed = resolve(allowed); + // Exact match + if (resolved === normalizedAllowed) return true; + // Starts with allowed path followed by path separator + const prefix = + normalizedAllowed + (normalizedAllowed.endsWith("/") ? "" : "/"); + return resolved.startsWith(prefix); + }); +} + +/** + * Check if a command contains blocked patterns + */ +function isCommandBlocked(command: string): string | null { + for (const pattern of BLOCKED_COMMANDS) { + if (command.includes(pattern)) { + return pattern; + } + } + return null; +} + +// ============================================================================ +// Tool Definitions +// ============================================================================ + +export interface ToolResult { + content: Array<{ type: "text"; text: string }>; + isError?: boolean; +} + +export interface Tool { + name: string; + description: string; + inputSchema: Record; + execute: (args: Record) => Promise; +} + +/** + * LIST_FILES - List directory contents + */ +export const LIST_FILES: Tool = { + name: "LIST_FILES", + description: + "List files and directories in a path. Returns file names, sizes, and types.", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Directory path to list", + }, + }, + required: ["path"], + }, + execute: async (args) => { + // Handle various argument formats LLMs might use + let path: string; + if (typeof args.path === "string") { + path = args.path; + } else if (Array.isArray(args.paths) && args.paths.length > 0) { + path = String(args.paths[0]); + } else if (typeof args.directory === "string") { + path = args.directory; + } else { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: `Missing required 'path' argument. Provide a directory path to list.`, + }), + }, + ], + isError: true, + }; + } + + if (!isPathAllowed(path)) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: `Path not allowed. Allowed: ${ALLOWED_PATHS.join(", ")}`, + }), + }, + ], + isError: true, + }; + } + + try { + const entries = await readdir(path, { withFileTypes: true }); + const files = await Promise.all( + entries + .filter((e) => !e.name.startsWith(".")) + .slice(0, 50) + .map(async (entry) => { + const fullPath = join(path, entry.name); + try { + const stats = await stat(fullPath); + return { + name: entry.name, + type: entry.isDirectory() ? "directory" : "file", + size: stats.size, + modified: stats.mtime.toISOString(), + }; + } catch { + return { + name: entry.name, + type: entry.isDirectory() ? "directory" : "file", + }; + } + }), + ); + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + path, + files, + count: entries.length, + showing: files.length, + }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: error instanceof Error ? error.message : "Failed to list", + }), + }, + ], + isError: true, + }; + } + }, +}; + +/** + * READ_FILE - Read file contents + */ +export const READ_FILE: Tool = { + name: "READ_FILE", + description: "Read the contents of a file. Returns the file content as text.", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "File path to read", + }, + limit: { + type: "number", + description: "Maximum lines to read (default: 500)", + }, + }, + required: ["path"], + }, + execute: async (args) => { + // Handle various argument formats LLMs might use + let path: string; + if (typeof args.path === "string") { + path = args.path; + } else if (typeof args.file === "string") { + path = args.file; + } else if (typeof args.filePath === "string") { + path = args.filePath; + } else if (typeof args.file_path === "string") { + path = args.file_path; + } else { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: `Missing required 'path' argument. Provide a file path to read.`, + }), + }, + ], + isError: true, + }; + } + const limit = (args.limit as number) || 500; + + if (!isPathAllowed(path)) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: `Path not allowed. Allowed: ${ALLOWED_PATHS.join(", ")}`, + }), + }, + ], + isError: true, + }; + } + + try { + const content = await readFile(path, "utf-8"); + const lines = content.split("\n"); + const truncated = lines.length > limit; + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + path, + content: lines.slice(0, limit).join("\n"), + totalLines: lines.length, + truncated, + }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: error instanceof Error ? error.message : "Failed to read", + }), + }, + ], + isError: true, + }; + } + }, +}; + +/** + * RUN_SHELL - Execute a shell command + */ +export const RUN_SHELL: Tool = { + name: "RUN_SHELL", + description: + "Execute a shell command. Use with caution - dangerous commands are blocked.", + inputSchema: { + type: "object", + properties: { + command: { + type: "string", + description: "Shell command to execute", + }, + cwd: { + type: "string", + description: "Working directory (default: first allowed path)", + }, + }, + required: ["command"], + }, + execute: async (args) => { + const command = args.command as string; + const cwd = (args.cwd as string) || ALLOWED_PATHS[0]; + + const blocked = isCommandBlocked(command); + if (blocked) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: `Command blocked: contains "${blocked}"`, + }), + }, + ], + isError: true, + }; + } + + if (cwd && !isPathAllowed(cwd)) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ error: "Working directory not allowed" }), + }, + ], + isError: true, + }; + } + + try { + const proc = spawn(["bash", "-c", command], { + stdout: "pipe", + stderr: "pipe", + cwd, + }); + + // Race between process and timeout + const timeout = new Promise((resolve) => + setTimeout(() => resolve(null), SHELL_TIMEOUT), + ); + + const result = await Promise.race([proc.exited, timeout]); + + if (result === null) { + proc.kill(); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: `Command timed out after ${SHELL_TIMEOUT / 1000}s`, + }), + }, + ], + isError: true, + }; + } + + const [stdout, stderr] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + command, + exitCode: await proc.exited, + stdout: stdout.slice(0, 5000), + stderr: stderr.slice(0, 2000), + }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: + error instanceof Error + ? error.message + : "Command execution failed", + }), + }, + ], + isError: true, + }; + } + }, +}; + +/** + * LIST_APPS - List running applications (macOS) + */ +export const LIST_APPS: Tool = { + name: "LIST_APPS", + description: "List currently running applications on macOS", + inputSchema: { + type: "object", + properties: {}, + }, + execute: async () => { + try { + const proc = spawn( + [ + "osascript", + "-e", + 'tell application "System Events" to get name of every process whose background only is false', + ], + { stdout: "pipe", stderr: "pipe" }, + ); + + const output = await new Response(proc.stdout).text(); + await proc.exited; + + const apps = output.trim().split(", ").filter(Boolean); + + return { + content: [ + { + type: "text", + text: JSON.stringify({ apps, count: apps.length }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: + error instanceof Error ? error.message : "Failed to list apps", + }), + }, + ], + isError: true, + }; + } + }, +}; + +/** + * GET_CLIPBOARD - Get clipboard contents + */ +export const GET_CLIPBOARD: Tool = { + name: "GET_CLIPBOARD", + description: "Get the current clipboard contents", + inputSchema: { + type: "object", + properties: {}, + }, + execute: async () => { + try { + const proc = spawn(["pbpaste"], { stdout: "pipe" }); + const content = await new Response(proc.stdout).text(); + await proc.exited; + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + content: content.slice(0, 5000), + length: content.length, + }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: + error instanceof Error + ? error.message + : "Failed to get clipboard", + }), + }, + ], + isError: true, + }; + } + }, +}; + +/** + * SET_CLIPBOARD - Set clipboard contents + */ +export const SET_CLIPBOARD: Tool = { + name: "SET_CLIPBOARD", + description: "Set the clipboard contents", + inputSchema: { + type: "object", + properties: { + content: { + type: "string", + description: "Content to copy to clipboard", + }, + }, + required: ["content"], + }, + execute: async (args) => { + const content = args.content as string; + + try { + const proc = spawn(["pbcopy"], { stdin: "pipe" }); + proc.stdin.write(content); + proc.stdin.end(); + await proc.exited; + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + success: true, + length: content.length, + }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: + error instanceof Error + ? error.message + : "Failed to set clipboard", + }), + }, + ], + isError: true, + }; + } + }, +}; + +/** + * SEND_NOTIFICATION - Send a system notification (macOS) + */ +export const SEND_NOTIFICATION: Tool = { + name: "SEND_NOTIFICATION", + description: "Send a system notification (macOS)", + inputSchema: { + type: "object", + properties: { + message: { + type: "string", + description: "Notification message", + }, + title: { + type: "string", + description: "Notification title (default: Pilot)", + }, + }, + required: ["message"], + }, + execute: async (args) => { + const message = args.message as string; + const title = (args.title as string) || "Pilot"; + + try { + // Escape backslashes first, then quotes to prevent injection + const escapedMessage = message + .replace(/\\/g, "\\\\") + .replace(/"/g, '\\"'); + const escapedTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + + const proc = spawn( + [ + "osascript", + "-e", + `display notification "${escapedMessage}" with title "${escapedTitle}"`, + ], + { stdout: "pipe", stderr: "pipe" }, + ); + await proc.exited; + + return { + content: [ + { + type: "text", + text: JSON.stringify({ success: true, message, title }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: + error instanceof Error + ? error.message + : "Failed to send notification", + }), + }, + ], + isError: true, + }; + } + }, +}; + +// Export all system tools +export const systemTools: Tool[] = [ + LIST_FILES, + READ_FILE, + RUN_SHELL, + LIST_APPS, + GET_CLIPBOARD, + SET_CLIPBOARD, + SEND_NOTIFICATION, +]; diff --git a/pilot/server/types/workflow.ts b/pilot/server/types/workflow.ts new file mode 100644 index 0000000..174f7b8 --- /dev/null +++ b/pilot/server/types/workflow.ts @@ -0,0 +1,39 @@ +/** + * Workflow Types + * + * Minimal type definitions for workflow validation. + * Actual workflow execution is handled by MCP Studio's orchestrator. + */ + +// ============================================================================ +// Step +// ============================================================================ + +export interface Step { + name: string; + description?: string; + action: { + type: string; + [key: string]: unknown; + }; + input?: Record; + config?: { + maxAttempts?: number; + timeoutMs?: number; + continueOnError?: boolean; + }; +} + +// ============================================================================ +// Workflow +// ============================================================================ + +export interface Workflow { + id: string; + title: string; + description?: string; + steps: Step[]; + defaultInput?: Record; + createdAt?: string; + updatedAt?: string; +} diff --git a/pilot/tsconfig.json b/pilot/tsconfig.json new file mode 100644 index 0000000..94246f6 --- /dev/null +++ b/pilot/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": ".", + "declaration": false, + "allowImportingTsExtensions": true, + "noEmit": true, + "isolatedModules": true + }, + "include": ["server/**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/pilot/workflows/direct-execution.json b/pilot/workflows/direct-execution.json new file mode 100644 index 0000000..23c47f6 --- /dev/null +++ b/pilot/workflows/direct-execution.json @@ -0,0 +1,22 @@ +{ + "id": "direct-execution", + "title": "Direct Execution", + "description": "Skip routing, execute directly with all available tools", + "steps": [ + { + "name": "execute", + "description": "Direct execution with smart model", + "action": { + "type": "llm", + "prompt": "@input.message", + "model": "smart", + "tools": "all", + "maxIterations": 30 + }, + "input": { + "message": "@input.message", + "history": "@input.history" + } + } + ] +} \ No newline at end of file diff --git a/pilot/workflows/execute-multi-step.json b/pilot/workflows/execute-multi-step.json new file mode 100644 index 0000000..56a51d5 --- /dev/null +++ b/pilot/workflows/execute-multi-step.json @@ -0,0 +1,41 @@ +{ + "id": "execute-multi-step", + "title": "Execute Multi-Step Task", + "description": "Two-phase workflow for complex tasks: FAST plans the approach, SMART executes. Use when no specific workflow matches.", + "steps": [ + { + "name": "plan", + "description": "Analyze the task and plan the approach", + "action": { + "type": "llm", + "prompt": "@input.message", + "model": "fast", + "systemPrompt": "You are PILOT PLANNER. Analyze the task and create an execution plan.\n\n## YOUR JOB\n\n1. Understand what the user wants to accomplish\n2. Discover what tools are available\n3. Create a clear plan for SMART to execute\n\n## DISCOVERY TOOLS\n\n- `list_mesh_tools()` - List API tools from Mesh connections\n- `list_local_tools()` - List file/shell/local tools\n- `LIST_FILES` - Browse directories to find relevant files\n\n## OUTPUT FORMAT\n\nAfter discovering tools, output:\n```json\n{\n \"response\": \"I'll [brief description of plan]\",\n \"taskForSmartAgent\": \"Detailed step-by-step instructions for execution\",\n \"toolsForSmartAgent\": [\"TOOL1\", \"TOOL2\", \"TOOL3\"]\n}\n```\n\nBe specific in taskForSmartAgent - include:\n- Exact steps to perform\n- File paths discovered\n- Tool parameters needed\n- Expected outputs", + "tools": "discover", + "maxIterations": 10 + }, + "input": { + "message": "@input.message" + } + }, + { + "name": "execute", + "description": "Execute the plan (only runs if tools are available)", + "action": { + "type": "llm", + "prompt": "@plan.taskForSmartAgent", + "model": "smart", + "systemPrompt": "You are PILOT EXECUTOR. The planning step has prepared everything you need.\n\n## YOUR JOB\n\nExecute the task step-by-step using the provided tools.\n\n## RULES\n\n1. Follow the plan from the planning step\n2. Use function calling for ALL tool invocations\n3. Read files BEFORE using their content\n4. Write actual content, never placeholders\n5. Handle errors gracefully\n6. Complete the entire task before responding\n\n## OUTPUT\n\nProvide a clear summary of:\n- What you accomplished\n- Any results, links, or outputs\n- Any issues encountered", + "tools": "@plan.toolsForSmartAgent", + "maxIterations": 50 + }, + "input": { + "task": "@plan.taskForSmartAgent", + "tools": "@plan.toolsForSmartAgent" + }, + "config": { + "skipIf": "empty:@plan.toolsForSmartAgent" + } + } + ] +} diff --git a/pilot/workflows/fast-router.json b/pilot/workflows/fast-router.json new file mode 100644 index 0000000..69bbd20 --- /dev/null +++ b/pilot/workflows/fast-router.json @@ -0,0 +1,23 @@ +{ + "id": "fast-router", + "title": "Fast Router", + "description": "Routes messages to direct response, single tool call, or async workflow. Entry point for all requests.", + "steps": [ + { + "name": "route", + "description": "Analyze request and route to appropriate handler", + "action": { + "type": "llm", + "prompt": "@input.message", + "model": "fast", + "systemPrompt": "You are PILOT, a helpful AI assistant. You MUST ALWAYS respond with text.\n\n## CRITICAL: ALWAYS RESPOND\n\nNo matter what the user says, you MUST provide a text response. Even for simple greetings:\n- \"oi\", \"hi\", \"hello\" → Respond: \"Olá! Como posso ajudar?\" or \"Hi! How can I help?\"\n- \"obrigado\", \"thanks\" → Respond: \"De nada!\" or \"You're welcome!\"\n\n## TOOL NAMES (use exact names)\n\n- `list_tasks` - See recent tasks\n- `list_workflows` - See available workflows\n- `start_task` - Start a workflow as background task\n- `check_task` - Check task status\n\n## ROUTING DECISION\n\n### 1. GREETINGS & SIMPLE MESSAGES\nFor: \"oi\", \"hi\", \"hello\", \"thanks\", questions\n→ RESPOND DIRECTLY with friendly text. No tools needed.\n\n### 2. CONTEXT-DEPENDENT MESSAGES\nFor: \"draft this\", \"continue\", \"yes\", \"check on that\"\n→ First call `list_tasks({ limit: 3 })` to see recent context, then act accordingly.\n\n### 3. SINGLE TOOL REQUESTS\nFor: \"research X\", \"list files\", \"read file X\"\n→ Call the appropriate tool, then respond with the result.\n\n### 4. COMPLEX TASKS (articles, multi-step work)\nFor: \"write an article about...\", \"create...\", \"run workflow...\"\n→ Call `list_workflows({})` to find the right workflow, then `start_task({ workflowId: '...', input: {...} })`\n→ Respond: \"Started [workflow]. I'll notify you when done.\"\n\n## WORKFLOW CHAINING\n\nCommon chains:\n- `create-article-research` → `create-article-draft` → finalize\n\nWhen user says \"draft this\" after research → start draft workflow with the research task ID.\n\n## KEY RULES\n\n1. ALWAYS provide a text response - never return empty\n2. For greetings, just respond warmly - no tools needed\n3. Be pragmatic - infer intent from context\n4. Multi-step work → use start_task with workflowId", + "tools": ["list_tasks", "list_workflows", "start_task", "check_task", "delete_task", "NEW_THREAD", "LIST_FILES", "READ_FILE"], + "maxIterations": 6 + }, + "input": { + "message": "@input.message", + "history": "@input.history" + } + } + ] +} diff --git a/pilot/workflows/research-first.json b/pilot/workflows/research-first.json new file mode 100644 index 0000000..fd100c0 --- /dev/null +++ b/pilot/workflows/research-first.json @@ -0,0 +1,32 @@ +{ + "id": "research-first", + "title": "Research First", + "description": "Read context files before responding", + "steps": [ + { + "name": "gather_context", + "description": "Read relevant context files", + "action": { + "type": "tool", + "toolName": "READ_FILE" + }, + "input": { + "path": "@input.contextPath" + } + }, + { + "name": "respond", + "description": "Respond with gathered context", + "action": { + "type": "llm", + "prompt": "Context:\n@gather_context.content\n\nUser message: @input.message", + "model": "smart", + "tools": "all" + }, + "input": { + "message": "@input.message", + "context": "@gather_context.content" + } + } + ] +} \ No newline at end of file diff --git a/pilot/workflows/thread.json b/pilot/workflows/thread.json new file mode 100644 index 0000000..a8964c6 --- /dev/null +++ b/pilot/workflows/thread.json @@ -0,0 +1,24 @@ +{ + "id": "thread", + "title": "Conversation Thread", + "description": "Basic agentic loop for conversations. Uses meta-tools for discovery and execution.", + "type": "thread", + "steps": [ + { + "name": "respond", + "description": "Process message with LLM and meta-tools", + "action": { + "type": "llm", + "prompt": "@input.message", + "model": "fast", + "systemPrompt": "You are a helpful AI assistant with access to tools and workflows.\n\nAvailable meta-tools:\n- list_workflows: Discover available workflows\n- start_workflow: Execute a workflow by ID\n- list_tools: Discover available tools from connected MCPs\n- call_tool: Execute a specific tool by name\n\nStrategy:\n1. For simple questions, respond directly\n2. For tasks, first check list_workflows for relevant workflows\n3. Use start_workflow to run workflows\n4. Use list_tools to discover MCP capabilities\n5. Use call_tool to execute specific tools\n\nAlways provide a clear, helpful response.", + "tools": "meta", + "maxIterations": 10 + }, + "input": { + "message": "@input.message", + "history": "@input.history" + } + } + ] +}