diff --git a/bun.lock b/bun.lock index e136c79..962eca0 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,6 @@ "": { "name": "dashboard-app-template", "dependencies": { - "@ai-sdk/react": "^3.0.74", "@capacitor/android": "^8.0.2", "@capacitor/app": "^8.0.0", "@capacitor/camera": "^8.0.0", @@ -31,8 +30,8 @@ "@hookform/resolvers": "^5.2.2", "@json-render/core": "^0.4.0", "@json-render/react": "^0.4.0", + "@modelcontextprotocol/sdk": "^1.26.0", "@opennextjs/cloudflare": "^1.14.4", - "@openrouter/ai-sdk-provider": "^2.1.1", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-aspect-ratio": "^1.1.8", @@ -76,7 +75,7 @@ "@workos-inc/authkit-nextjs": "^2.13.0", "@workos-inc/node": "^8.1.0", "@xyflow/react": "^12.10.0", - "ai": "^6.0.73", + "agent-core": "file:packages/agent-core", "better-sqlite3": "^11.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -144,16 +143,10 @@ "packages": { "@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.36", "", { "dependencies": { "@ai-sdk/provider": "3.0.7", "@ai-sdk/provider-utils": "4.0.13", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2r1Q6azvqMYxQ1hqfWZmWg4+8MajoldD/ty65XdhCaCoBfvDu7trcvxXDfTSU+3/wZ1JIDky46SWYFOHnTbsBw=="], - - "@ai-sdk/provider": ["@ai-sdk/provider@3.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-VkPLrutM6VdA924/mG8OS+5frbVTcu6e046D2bgDo00tehBANR1QBJ/mPcZ9tXMFOsVcm6SQArOregxePzTFPw=="], - - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.13", "", { "dependencies": { "@ai-sdk/provider": "3.0.7", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HHG72BN4d+OWTcq2NwTxOm/2qvk1duYsnhCDtsbYwn/h/4zeqURu1S0+Cn0nY2Ysq9a9HGKvrYuMn9bgFhR2Og=="], - - "@ai-sdk/react": ["@ai-sdk/react@3.0.74", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.13", "ai": "6.0.72", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-L8N9HNM9Vt3rxORhX6+KCrsYRI6ZXGz1q8o/ysw6+Sx3MC0pqSZLiaKYifIYe2TSWgLP5mWcGlA5hHPuq5Jdfw=="], - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.74.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.2", "", { "dependencies": { "@csstools/css-calc": "^3.0.0", "@csstools/css-color-parser": "^4.0.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.5" } }, "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg=="], "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.7.8", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.5" } }, "sha512-stisC1nULNc9oH5lakAj8MH88ZxeGxzyWNDfbdCxvJSJIvDsHNZqYvscGTgy/ysgXWLJPt6K/4t0/GjvtKcFJQ=="], @@ -464,6 +457,8 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -564,6 +559,8 @@ "@json-render/react": ["@json-render/react@0.4.0", "", { "dependencies": { "@json-render/core": "0.4.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-OAXdWdOrAXHEFzpEF7xV+84D00JEmLMKlt5u0wc+C/P+q4q6TnpAWx2j28PBpDB2mpidkW9VnTUM+SCH9J8Lrw=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "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.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], "@next/env": ["@next/env@15.5.9", "", {}, "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg=="], @@ -610,8 +607,6 @@ "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.15.0", "", { "dependencies": { "@ast-grep/napi": "0.40.0", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.9.11", "cloudflare": "^4.4.1", "enquirer": "^2.4.1", "glob": "^12.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "next": "^14.2.35 || ~15.0.7 || ~15.1.11 || ~15.2.8 || ~15.3.8 || ~15.4.10 || ~15.5.9 || ^16.0.10", "wrangler": "^4.59.2" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-AZPaqk25XUBxtdkfjUZQBbY3ovifVLC4GgSRHuejqsIWfv8KjTRNFVdaCaaPmbLkrgymqxNhkbfJS5sD28AK/g=="], - "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.1.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-UypPbVnSExxmG/4Zg0usRiit3auvQVrjUXSyEhm0sZ9GQnW/d8p/bKgCk2neh1W5YyRSo7PNQvCrAEBHZnqQkQ=="], - "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.6.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg=="], @@ -1254,8 +1249,6 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], - "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], - "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], @@ -1290,12 +1283,14 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "agent-core": ["agent-core@file:packages/agent-core", { "dependencies": { "@anthropic-ai/sdk": "^0.74.0", "zod": "^3.24.1" }, "devDependencies": { "bun-types": "^1.3.9" } }], + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ai": ["ai@6.0.73", "", { "dependencies": { "@ai-sdk/gateway": "3.0.36", "@ai-sdk/provider": "3.0.7", "@ai-sdk/provider-utils": "4.0.13", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-p2/ICXIjAM4+bIFHEkAB+l58zq+aTmxAkotsb6doNt/CEms72zt6gxv2ky1fQDwU4ecMOcmMh78VJUSEKECzlg=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -1380,6 +1375,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], @@ -1440,10 +1437,12 @@ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -1650,6 +1649,8 @@ "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], @@ -1660,6 +1661,8 @@ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -1672,6 +1675,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-xml-parser": ["fast-xml-parser@4.2.5", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], @@ -1792,6 +1797,8 @@ "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], @@ -1834,6 +1841,8 @@ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "iron-session": ["iron-session@8.0.4", "", { "dependencies": { "cookie": "^0.7.2", "iron-webcrypto": "^1.2.1", "uncrypto": "^0.1.3" } }, "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA=="], @@ -1936,10 +1945,12 @@ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], @@ -2244,6 +2255,8 @@ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], @@ -2526,8 +2539,6 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "swr": ["swr@2.4.0", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-sUlC20T8EOt1pHmDiqueUWMmRRX03W7w5YxovWX7VR2KHEPCTMly85x05vpkP5i6Bu4h44ePSMD9Tc+G2MItFw=="], - "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], @@ -2544,8 +2555,6 @@ "terser": ["terser@5.16.9", "", { "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg=="], - "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], - "through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], @@ -2582,6 +2591,8 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], "ts-tqdm": ["ts-tqdm@0.8.6", "", {}, "sha512-3X3M1PZcHtgQbnwizL+xU8CAgbYbeLHrrDwL9xxcZZrV5J+e7loJm1XrXozHjSkl44J0Zg0SgA8rXbh83kCkcQ=="], @@ -2744,12 +2755,12 @@ "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + "zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@ai-sdk/react/ai": ["ai@6.0.72", "", { "dependencies": { "@ai-sdk/gateway": "3.0.35", "@ai-sdk/provider": "3.0.7", "@ai-sdk/provider-utils": "4.0.13", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-D3TzDX6LzYL8qwi1A0rLnmuUexqDcCu4LSg77hcDHsqNRkaGspGItkz1U3RnN3ojv31XQYI9VmoWpkj44uvIUA=="], - "@aws-crypto/crc32/@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-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug=="], @@ -3276,10 +3287,14 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "@node-minify/core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], "@opennextjs/aws/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "@opennextjs/aws/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -3404,6 +3419,10 @@ "@workos-inc/authkit-nextjs/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + "agent-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -3432,8 +3451,6 @@ "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], @@ -3442,8 +3459,6 @@ "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], - "iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "iron-session/iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], "jsdom/parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], @@ -3516,7 +3531,7 @@ "yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "@ai-sdk/react/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.7", "@ai-sdk/provider-utils": "4.0.13", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9aRTVM1P1u4yUIjBpco/WCF1WXr/DgWKuDYgLLHdENS8kiEuxDOPJuGbc/6+7EwQ6ZqSh0UOgeqvHfGJfU23Qg=="], + "youch/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="], @@ -3980,6 +3995,8 @@ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@node-minify/core/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], "@node-minify/core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], @@ -4018,6 +4035,8 @@ "@workos-inc/authkit-nextjs/@workos-inc/node/jose": ["jose@5.6.3", "", {}, "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g=="], + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "cliui/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], diff --git a/next.config.ts b/next.config.ts index ff2cbe4..8f3e6a3 100755 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { + transpilePackages: ["agent-core"], eslint: { ignoreDuringBuilds: true, }, diff --git a/package.json b/package.json index 26d227a..676b30d 100755 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@hookform/resolvers": "^5.2.2", "@json-render/core": "^0.4.0", "@json-render/react": "^0.4.0", + "@modelcontextprotocol/sdk": "^1.26.0", "@opennextjs/cloudflare": "^1.14.4", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", @@ -101,6 +102,7 @@ "@workos-inc/authkit-nextjs": "^2.13.0", "@workos-inc/node": "^8.1.0", "@xyflow/react": "^12.10.0", + "agent-core": "file:packages/agent-core", "better-sqlite3": "^11.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/packages/agent-core/bun.lock b/packages/agent-core/bun.lock new file mode 100644 index 0000000..4e2efa4 --- /dev/null +++ b/packages/agent-core/bun.lock @@ -0,0 +1,214 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "agent-core", + "dependencies": { + "@anthropic-ai/sdk": "^0.74.0", + "@modelcontextprotocol/sdk": "^1.26.0", + "zod": "^3.24.1", + }, + "devDependencies": { + "bun-types": "^1.3.9", + }, + }, + }, + "packages": { + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.74.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw=="], + + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "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.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="], + + "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + } +} diff --git a/packages/agent-core/package.json b/packages/agent-core/package.json new file mode 100644 index 0000000..b6db879 --- /dev/null +++ b/packages/agent-core/package.json @@ -0,0 +1,14 @@ +{ + "name": "agent-core", + "version": "0.1.0", + "type": "module", + "main": "src/index.ts", + "dependencies": { + "@anthropic-ai/sdk": "^0.74.0", + "@modelcontextprotocol/sdk": "^1.26.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "bun-types": "^1.3.9" + } +} diff --git a/packages/agent-core/src/client.ts b/packages/agent-core/src/client.ts new file mode 100644 index 0000000..e315945 --- /dev/null +++ b/packages/agent-core/src/client.ts @@ -0,0 +1,27 @@ +import Anthropic from "@anthropic-ai/sdk" +import type { ProviderConfig } from "./types" + +export function createClient(provider: ProviderConfig): Anthropic { + switch (provider.type) { + case "anthropic": + return new Anthropic({ + apiKey: provider.apiKey, + ...(provider.baseUrl ? { baseURL: provider.baseUrl } : {}), + }) + case "openrouter": + return new Anthropic({ + apiKey: provider.apiKey ?? "", + baseURL: "https://openrouter.ai/api", + }) + case "ollama": + return new Anthropic({ + apiKey: "ollama", + baseURL: provider.baseUrl ?? "http://localhost:11434", + }) + case "custom": + return new Anthropic({ + apiKey: provider.apiKey ?? "", + ...(provider.baseUrl ? { baseURL: provider.baseUrl } : {}), + }) + } +} diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts new file mode 100644 index 0000000..819a96e --- /dev/null +++ b/packages/agent-core/src/index.ts @@ -0,0 +1,21 @@ +export { createClient } from "./client" +export { runAgent } from "./loop" +export { createTools } from "./tools" +export type { ToolDef } from "./tools" +export { zodToJsonSchema } from "./tools" +export { buildSystemPrompt } from "./system-prompt" +export { sseEncode, createSSEStream } from "./stream-helpers" +export { + createCompassServer, + createClientManager, +} from "./mcp" +export type { + McpServerConfig, + McpClientManager, +} from "./mcp" +export type { + ProviderConfig, + AgentContext, + DataSource, + SSEData, +} from "./types" diff --git a/packages/agent-core/src/loop.ts b/packages/agent-core/src/loop.ts new file mode 100644 index 0000000..9cfb173 --- /dev/null +++ b/packages/agent-core/src/loop.ts @@ -0,0 +1,232 @@ +import type Anthropic from "@anthropic-ai/sdk" +import type { Tool } from "@anthropic-ai/sdk/resources/messages/messages" +import { createClient } from "./client" +import type { ProviderConfig, SSEData } from "./types" +import type { ToolDef } from "./tools" +import type { McpClientManager } from "./mcp/types" + +interface AgentOptions { + readonly provider: ProviderConfig + readonly model: string + readonly systemPrompt: string + readonly messages: ReadonlyArray<{ + role: "user" | "assistant" + content: string + }> + readonly tools?: readonly ToolDef[] + readonly mcpClientManager?: McpClientManager + readonly maxTurns?: number +} + +export async function* runAgent( + opts: AgentOptions +): AsyncGenerator { + const client = createClient(opts.provider) + const maxTurns = opts.maxTurns ?? 25 + + // Mutable messages array for the agentic loop + const messages: Anthropic.MessageParam[] = opts.messages.map( + (m) => ({ + role: m.role, + content: m.content, + }) + ) + + // Build tool map for execution and API tool definitions + const toolMap = new Map< + string, + (input: unknown) => Promise | string + >() + const apiTools: Tool[] = [] + + // Register direct tools first (they take priority) + if (opts.tools) { + for (const tool of opts.tools) { + toolMap.set(tool.name, tool.run) + apiTools.push({ + name: tool.name, + description: tool.description, + input_schema: + tool.input_schema as Tool.InputSchema, + }) + } + } + + // Register MCP tools (skip if already provided as direct) + const mcpManager = opts.mcpClientManager + if (mcpManager) { + for (const tool of mcpManager.listTools()) { + if (toolMap.has(tool.name)) continue + apiTools.push({ + name: tool.name, + description: tool.description, + input_schema: + tool.input_schema as Tool.InputSchema, + }) + } + } + + let turn = 0 + + while (turn < maxTurns) { + turn++ + + try { + const stream = client.messages.stream({ + model: opts.model, + max_tokens: 8192, + system: opts.systemPrompt, + messages, + tools: apiTools, + }) + + // Stream text deltas and tool_use starts to the caller + for await (const event of stream) { + if (event.type === "content_block_start") { + const block = event.content_block + if (block.type === "tool_use") { + yield { + type: "tool_use", + name: block.name, + toolCallId: block.id, + input: {}, + } + } + } + + if (event.type === "content_block_delta") { + const delta = event.delta + if ( + "text" in delta && + delta.type === "text_delta" + ) { + yield { type: "text", content: delta.text } + } + } + } + + const message = await stream.finalMessage() + + // No tool calls — we're done + if (message.stop_reason !== "tool_use") { + yield { + type: "result", + subtype: "success", + result: message.content + .filter( + (b): b is Anthropic.TextBlock => + b.type === "text" + ) + .map((b) => b.text) + .join(""), + usage: message.usage + ? { + inputTokens: message.usage.input_tokens, + outputTokens: message.usage.output_tokens, + totalCostUsd: 0, + } + : undefined, + } + return + } + + // Execute tool calls and continue the loop + messages.push({ + role: "assistant", + content: message.content, + }) + + const toolResults: Anthropic.ToolResultBlockParam[] = [] + + for (const block of message.content) { + if (block.type !== "tool_use") continue + + const runFn = toolMap.get(block.name) + + // Route: direct tool -> MCP manager -> unknown + if (!runFn && !mcpManager) { + const errorResult = JSON.stringify({ + error: `Unknown tool: ${block.name}`, + }) + yield { + type: "tool_result", + toolCallId: block.id, + output: errorResult, + } + toolResults.push({ + type: "tool_result", + tool_use_id: block.id, + content: errorResult, + is_error: true, + }) + continue + } + + try { + let result: string + if (runFn) { + result = await runFn(block.input) + } else if (mcpManager) { + result = await mcpManager.callTool( + block.name, + block.input + ) + } else { + result = JSON.stringify({ + error: `Unknown tool: ${block.name}`, + }) + } + let parsed: unknown + try { + parsed = JSON.parse(result) + } catch { + parsed = result + } + yield { + type: "tool_result", + toolCallId: block.id, + output: parsed, + } + toolResults.push({ + type: "tool_result", + tool_use_id: block.id, + content: result, + }) + } catch (err) { + const errorMsg = + err instanceof Error + ? err.message + : String(err) + yield { + type: "tool_result", + toolCallId: block.id, + output: { error: errorMsg }, + } + toolResults.push({ + type: "tool_result", + tool_use_id: block.id, + content: JSON.stringify({ error: errorMsg }), + is_error: true, + }) + } + } + + messages.push({ role: "user", content: toolResults }) + } catch (err) { + yield { + type: "error", + error: + err instanceof Error ? err.message : String(err), + } + return + } + } + + // Max turns reached + yield { + type: "result", + subtype: "success", + result: "Max turns reached", + usage: undefined, + } +} diff --git a/packages/agent-core/src/mcp/client-manager.ts b/packages/agent-core/src/mcp/client-manager.ts new file mode 100644 index 0000000..ba33381 --- /dev/null +++ b/packages/agent-core/src/mcp/client-manager.ts @@ -0,0 +1,228 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js" +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" +import { + StreamableHTTPClientTransport, +} from "@modelcontextprotocol/sdk/client/streamableHttp.js" +import type { Server } from "@modelcontextprotocol/sdk/server/index.js" +import type { McpServerConfig, McpClientManager } from "./types" + +interface ConnectedServer { + readonly client: Client + readonly toolNames: ReadonlySet +} + +interface ToolEntry { + readonly name: string + readonly description: string + readonly input_schema: Record + readonly serverName: string + readonly originalName: string +} + +const COMPASS_SERVER_NAME = "compass" + +export function createClientManager( + compassServer?: Server +): McpClientManager { + const servers = new Map() + const toolIndex = new Map() + let cachedTools: ToolEntry[] = [] + + async function connectServer( + config: McpServerConfig + ): Promise { + const client = new Client({ + name: `compass-client-${config.name}`, + version: "1.0.0", + }) + + const transport = config.transport + + if (transport.type === "in-memory") { + if (!compassServer) { + console.error( + "in-memory transport requires compassServer" + ) + return + } + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair() + await compassServer.connect(serverTransport) + await client.connect(clientTransport) + } else if (transport.type === "stdio") { + const stdioTransport = new StdioClientTransport({ + command: transport.command, + args: transport.args ? [...transport.args] : undefined, + env: transport.env + ? { ...transport.env } + : undefined, + }) + await client.connect(stdioTransport) + } else if (transport.type === "http") { + const httpTransport = new StreamableHTTPClientTransport( + new URL(transport.url), + transport.headers + ? { + requestInit: { + headers: { ...transport.headers }, + }, + } + : undefined + ) + await client.connect(httpTransport) + } + + const result = await client.listTools() + const toolNames = new Set() + + for (const tool of result.tools) { + toolNames.add(tool.name) + } + + servers.set(config.name, { client, toolNames }) + } + + function buildToolIndex(): void { + toolIndex.clear() + cachedTools = [] + + for (const [serverName, server] of servers) { + const isCompass = serverName === COMPASS_SERVER_NAME + + for (const toolName of server.toolNames) { + const publicName = isCompass + ? toolName + : `${serverName}__${toolName}` + + toolIndex.set(publicName, { + serverName, + originalName: toolName, + }) + } + } + } + + return { + async connect( + configs: readonly McpServerConfig[] + ): Promise { + for (const config of configs) { + if (!config.enabled) continue + + try { + await connectServer(config) + } catch (err) { + console.error( + `Failed to connect MCP server "${config.name}":`, + err + ) + } + } + + buildToolIndex() + + // Fetch full tool metadata after index is built + for (const [serverName, server] of servers) { + const isCompass = serverName === COMPASS_SERVER_NAME + + try { + const result = await server.client.listTools() + for (const tool of result.tools) { + const publicName = isCompass + ? tool.name + : `${serverName}__${tool.name}` + + cachedTools.push({ + name: publicName, + description: tool.description ?? "", + input_schema: + (tool.inputSchema as Record) ?? + {}, + serverName, + originalName: tool.name, + }) + } + } catch (err) { + console.error( + `Failed to list tools from "${serverName}":`, + err + ) + } + } + }, + + listTools(): Array<{ + name: string + description: string + input_schema: Record + serverName: string + }> { + return cachedTools.map((t) => ({ + name: t.name, + description: t.description, + input_schema: t.input_schema, + serverName: t.serverName, + })) + }, + + async callTool( + name: string, + input: unknown + ): Promise { + const entry = toolIndex.get(name) + if (!entry) { + return JSON.stringify({ + error: `Unknown MCP tool: ${name}`, + }) + } + + const server = servers.get(entry.serverName) + if (!server) { + return JSON.stringify({ + error: `MCP server "${entry.serverName}" not connected`, + }) + } + + const result = await server.client.callTool({ + name: entry.originalName, + arguments: input as Record, + }) + + // Extract text content from MCP result + const contents = result.content + if (!Array.isArray(contents) || contents.length === 0) { + return "" + } + + const textParts: string[] = [] + for (const part of contents) { + if ( + typeof part === "object" && + part !== null && + "type" in part && + part.type === "text" && + "text" in part && + typeof part.text === "string" + ) { + textParts.push(part.text) + } + } + + return textParts.join("") + }, + + async disconnect(): Promise { + for (const [, server] of servers) { + try { + await server.client.close() + } catch { + // ignore close errors + } + } + servers.clear() + toolIndex.clear() + cachedTools = [] + }, + } +} diff --git a/packages/agent-core/src/mcp/compass-server.ts b/packages/agent-core/src/mcp/compass-server.ts new file mode 100644 index 0000000..c29f404 --- /dev/null +++ b/packages/agent-core/src/mcp/compass-server.ts @@ -0,0 +1,73 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { + ListToolsRequestSchema, + CallToolRequestSchema, + type ListToolsResult, + type CallToolResult, +} from "@modelcontextprotocol/sdk/types.js" +import { createTools } from "../tools" +import type { DataSource } from "../types" + +// McpServer (high-level API) requires Zod schemas for tool registration. +// Our tools use JSON Schema (Record), so we use the +// low-level Server with manual request handlers instead. + +// Type predicates to convert our JSON Schema shape into the +// MCP-expected inputSchema type without using `as` assertions. +function isObjectRecord(val: unknown): val is Record { + return typeof val === "object" && val !== null && !Array.isArray(val) +} + +function isStringArray(val: unknown): val is string[] { + return Array.isArray(val) && val.every((v) => typeof v === "string") +} + +function toInputSchema( + raw: Record, +): { type: "object"; properties?: Record; required?: string[] } { + const props = raw["properties"] + const reqs = raw["required"] + return { + type: "object", + ...(isObjectRecord(props) && { properties: props }), + ...(isStringArray(reqs) && { required: reqs }), + } +} + +export function createCompassServer(dataSource: DataSource): Server { + const server = new Server( + { name: "compass", version: "1.0.0" }, + { capabilities: { tools: {} } }, + ) + + const tools = createTools(dataSource) + const toolMap = new Map(tools.map((t) => [t.name, t])) + + server.setRequestHandler( + ListToolsRequestSchema, + (): ListToolsResult => ({ + tools: tools.map((t) => ({ + name: t.name, + description: t.description, + inputSchema: toInputSchema(t.input_schema), + })), + }), + ) + + server.setRequestHandler( + CallToolRequestSchema, + async (req): Promise => { + const tool = toolMap.get(req.params.name) + if (tool === undefined) { + return { + content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }], + isError: true, + } + } + const result = await tool.run(req.params.arguments ?? {}) + return { content: [{ type: "text", text: result }] } + }, + ) + + return server +} diff --git a/packages/agent-core/src/mcp/index.ts b/packages/agent-core/src/mcp/index.ts new file mode 100644 index 0000000..1c7c8aa --- /dev/null +++ b/packages/agent-core/src/mcp/index.ts @@ -0,0 +1,6 @@ +export { createCompassServer } from "./compass-server" +export { createClientManager } from "./client-manager" +export type { + McpServerConfig, + McpClientManager, +} from "./types" diff --git a/packages/agent-core/src/mcp/types.ts b/packages/agent-core/src/mcp/types.ts new file mode 100644 index 0000000..574287e --- /dev/null +++ b/packages/agent-core/src/mcp/types.ts @@ -0,0 +1,32 @@ +export interface McpServerConfig { + readonly name: string + readonly transport: + | { readonly type: "in-memory" } + | { + readonly type: "stdio" + readonly command: string + readonly args?: readonly string[] + readonly env?: Readonly> + } + | { + readonly type: "http" + readonly url: string + readonly headers?: Readonly> + } + readonly enabled: boolean +} + +export interface McpClientManager { + connect(configs: readonly McpServerConfig[]): Promise + + listTools(): Array<{ + name: string + description: string + input_schema: Record + serverName: string + }> + + callTool(name: string, input: unknown): Promise + + disconnect(): Promise +} diff --git a/packages/agent-core/src/stream-helpers.ts b/packages/agent-core/src/stream-helpers.ts new file mode 100644 index 0000000..6fc9d2f --- /dev/null +++ b/packages/agent-core/src/stream-helpers.ts @@ -0,0 +1,34 @@ +import type { SSEData } from "./types" + +export function sseEncode(data: SSEData): string { + return `data: ${JSON.stringify(data)}\n\n` +} + +export function createSSEStream( + generator: AsyncGenerator +): ReadableStream { + const encoder = new TextEncoder() + + return new ReadableStream({ + async start(controller) { + try { + for await (const event of generator) { + controller.enqueue(encoder.encode(sseEncode(event))) + } + controller.enqueue(encoder.encode("data: [DONE]\n\n")) + controller.close() + } catch (err) { + const errorEvent: SSEData = { + type: "error", + error: + err instanceof Error ? err.message : String(err), + } + controller.enqueue( + encoder.encode(sseEncode(errorEvent)) + ) + controller.enqueue(encoder.encode("data: [DONE]\n\n")) + controller.close() + } + }, + }) +} diff --git a/packages/agent-core/src/system-prompt.ts b/packages/agent-core/src/system-prompt.ts new file mode 100644 index 0000000..7dba874 --- /dev/null +++ b/packages/agent-core/src/system-prompt.ts @@ -0,0 +1,90 @@ +import type { AgentContext } from "./types" + +interface Message { + readonly role: "user" | "assistant" + readonly content: string +} + +interface McpToolInfo { + readonly serverName: string + readonly name: string +} + +interface SystemPromptOptions { + readonly context: AgentContext + readonly messages: readonly Message[] + readonly externalMcpTools?: readonly McpToolInfo[] +} + +export function buildSystemPrompt( + contextOrOpts: AgentContext | SystemPromptOptions, + messages?: readonly Message[] +): string { + // Support both old and new call signatures + const opts: SystemPromptOptions = + "context" in contextOrOpts + ? contextOrOpts + : { context: contextOrOpts, messages: messages ?? [] } + + const ctx = opts.context + const history = buildConversationHistory(opts.messages) + const externalTools = buildExternalToolsSection( + opts.externalMcpTools + ) + + return `You are Compass AI, an intelligent assistant for project \ +management and collaboration. + +Current context: +- User ID: ${ctx.userId} +- Organization: ${ctx.orgId} +- Role: ${ctx.role} +- Current page: ${ctx.currentPage} +- Timezone: ${ctx.timezone} + +You have tools for querying data, navigating the UI, managing \ +schedules, themes, memories, skills, dashboards, and GitHub \ +integration. + +When a tool returns an "action" field in its result, that action \ +will be forwarded to the client for execution (navigation, toasts, \ +UI generation, theme changes, etc.). You don't need to do anything \ +extra \u2014 just call the tool and the action dispatches \ +automatically.${externalTools}${history}` +} + +function buildExternalToolsSection( + tools?: readonly McpToolInfo[] +): string { + if (!tools || tools.length === 0) return "" + + const byServer = new Map() + for (const tool of tools) { + const list = byServer.get(tool.serverName) ?? [] + list.push(tool.name) + byServer.set(tool.serverName, list) + } + + const sections: string[] = [] + for (const [server, names] of byServer) { + sections.push( + `- ${server}: ${names.join(", ")}` + ) + } + + return `\n\nExternal MCP servers connected:\n${sections.join("\n")}` +} + +function buildConversationHistory( + messages: readonly Message[] +): string { + const history = messages.slice(0, -1) + if (history.length === 0) return "" + + const lines = history.map((m) => { + const role = m.role === "user" ? "User" : "Assistant" + return `${role}: ${m.content}` + }) + + return `\n\nConversation so far:\n${lines.join("\n")}\n\nContinue the conversation naturally.` +} diff --git a/packages/agent-core/src/tools/dashboards.ts b/packages/agent-core/src/tools/dashboards.ts new file mode 100644 index 0000000..483facb --- /dev/null +++ b/packages/agent-core/src/tools/dashboards.ts @@ -0,0 +1,110 @@ +import { z } from "zod" +import type { DataSource } from "../types" +import type { ToolDef } from "./data" +import { zodToJsonSchema } from "./data" + +const saveSchema = z.object({ + name: z.string().describe("Dashboard display name"), + description: z + .string() + .optional() + .describe("Brief description of the dashboard"), + dashboardId: z + .string() + .optional() + .describe("Existing dashboard ID to update (for edits)"), +}) + +const listSchema = z.object({}) + +const editSchema = z.object({ + dashboardId: z + .string() + .describe("ID of the dashboard to edit"), + editPrompt: z + .string() + .optional() + .describe("Description of changes to make"), +}) + +const deleteSchema = z.object({ + dashboardId: z + .string() + .describe("ID of the dashboard to delete"), +}) + +export function dashboardTools( + dataSource: DataSource +): ToolDef[] { + return [ + { + name: "saveDashboard", + description: + "Save the currently rendered UI as a named dashboard. " + + "The client captures the current spec and data context " + + "automatically. Returns an action for the client to " + + "handle the save.", + input_schema: zodToJsonSchema(saveSchema), + run: async (input: unknown): Promise => { + const args = saveSchema.parse(input) + return JSON.stringify({ + action: "save_dashboard", + name: args.name, + description: args.description ?? "", + dashboardId: args.dashboardId, + }) + }, + }, + + { + name: "listDashboards", + description: "List the user's saved custom dashboards.", + input_schema: zodToJsonSchema(listSchema), + run: async (): Promise => { + const result = await dataSource.fetch( + "/api/compass/dashboards/list" + ) + return JSON.stringify(result) + }, + }, + + { + name: "editDashboard", + description: + "Load a saved dashboard for editing. The client " + + "injects the spec into the render context and " + + "navigates to /dashboard. Optionally pass an " + + "editPrompt to trigger immediate re-generation.", + input_schema: zodToJsonSchema(editSchema), + run: async (input: unknown): Promise => { + const args = editSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/dashboards/get", + { dashboardId: args.dashboardId } + ) + return JSON.stringify({ + action: "load_dashboard", + dashboardId: args.dashboardId, + spec: result, + editPrompt: args.editPrompt, + }) + }, + }, + + { + name: "deleteDashboard", + description: + "Delete a saved dashboard. Always confirm with the " + + "user before deleting.", + input_schema: zodToJsonSchema(deleteSchema), + run: async (input: unknown): Promise => { + const args = deleteSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/dashboards/delete", + args + ) + return JSON.stringify(result) + }, + }, + ] +} diff --git a/packages/agent-core/src/tools/data.ts b/packages/agent-core/src/tools/data.ts new file mode 100644 index 0000000..26e8934 --- /dev/null +++ b/packages/agent-core/src/tools/data.ts @@ -0,0 +1,164 @@ +import { z } from "zod" +import type { DataSource } from "../types" + +export interface ToolDef { + readonly name: string + readonly description: string + readonly input_schema: Record + readonly run: (input: unknown) => Promise +} + +const queryDataSchema = z.object({ + queryType: z.enum([ + "customers", + "vendors", + "projects", + "invoices", + "vendor_bills", + "schedule_tasks", + "project_detail", + "customer_detail", + "vendor_detail", + ]), + id: z.string().optional().describe("Record ID for detail queries"), + search: z + .string() + .optional() + .describe("Search term to filter results"), + limit: z + .number() + .optional() + .describe("Max results to return (default 20)"), +}) + +type QueryDataInput = z.infer + +export function dataTools(dataSource: DataSource): ToolDef[] { + return [ + { + name: "queryData", + description: + "Query the application database. Describe what data " + + "you need in natural language and provide a query type.", + input_schema: zodToJsonSchema(queryDataSchema), + run: async (input: unknown): Promise => { + const args = queryDataSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/query", + args + ) + return JSON.stringify(result) + }, + }, + ] +} + +/** Minimal zod-to-JSON-schema converter for our tool schemas */ +export function zodToJsonSchema( + schema: z.ZodObject +): Record { + const jsonSchema = zodObjectToProperties(schema) + return { + type: "object", + properties: jsonSchema.properties, + required: jsonSchema.required, + } +} + +function zodObjectToProperties(schema: z.ZodObject): { + properties: Record + required: string[] +} { + const shape = schema.shape + const properties: Record = {} + const required: string[] = [] + + for (const [key, value] of Object.entries(shape)) { + const { schema: propSchema, isOptional } = unwrapOptional( + value as z.ZodType + ) + properties[key] = zodTypeToJsonSchema(propSchema) + if (!isOptional) { + required.push(key) + } + } + + return { properties, required } +} + +function unwrapOptional( + schema: z.ZodType +): { schema: z.ZodType; isOptional: boolean } { + if (schema instanceof z.ZodOptional) { + return { schema: schema.unwrap(), isOptional: true } + } + if (schema instanceof z.ZodNullable) { + return { schema: schema.unwrap(), isOptional: true } + } + return { schema, isOptional: false } +} + +function zodTypeToJsonSchema( + schema: z.ZodType +): Record { + const desc = schema.description + + if (schema instanceof z.ZodString) { + return { type: "string", ...(desc ? { description: desc } : {}) } + } + if (schema instanceof z.ZodNumber) { + const result: Record = { + type: "number", + ...(desc ? { description: desc } : {}), + } + const checks = (schema as z.ZodNumber)._def.checks + for (const check of checks) { + if (check.kind === "min") result.minimum = check.value + if (check.kind === "max") result.maximum = check.value + } + return result + } + if (schema instanceof z.ZodBoolean) { + return { type: "boolean", ...(desc ? { description: desc } : {}) } + } + if (schema instanceof z.ZodEnum) { + return { + type: "string", + enum: schema.options, + ...(desc ? { description: desc } : {}), + } + } + if (schema instanceof z.ZodArray) { + return { + type: "array", + items: zodTypeToJsonSchema(schema.element), + ...(desc ? { description: desc } : {}), + } + } + if (schema instanceof z.ZodObject) { + const inner = zodObjectToProperties(schema) + return { + type: "object", + properties: inner.properties, + required: inner.required, + ...(desc ? { description: desc } : {}), + } + } + if (schema instanceof z.ZodRecord) { + return { + type: "object", + additionalProperties: zodTypeToJsonSchema(schema.element), + ...(desc ? { description: desc } : {}), + } + } + if (schema instanceof z.ZodNullable) { + const inner = zodTypeToJsonSchema(schema.unwrap()) + return { ...inner, nullable: true } + } + if (schema instanceof z.ZodOptional) { + return zodTypeToJsonSchema(schema.unwrap()) + } + + // Fallback + return { ...(desc ? { description: desc } : {}) } +} diff --git a/packages/agent-core/src/tools/github.ts b/packages/agent-core/src/tools/github.ts new file mode 100644 index 0000000..30b8718 --- /dev/null +++ b/packages/agent-core/src/tools/github.ts @@ -0,0 +1,132 @@ +import { z } from "zod" +import type { DataSource } from "../types" +import type { ToolDef } from "./data" +import { zodToJsonSchema } from "./data" + +const querySchema = z.object({ + queryType: z + .enum([ + "commits", + "commit_diff", + "pull_requests", + "issues", + "contributors", + "milestones", + "repo_stats", + ]) + .describe("Type of GitHub data to query"), + sha: z + .string() + .optional() + .describe("Commit SHA for commit_diff queries"), + state: z + .enum(["open", "closed", "all"]) + .optional() + .describe("State filter for PRs, issues, milestones"), + labels: z + .string() + .optional() + .describe("Comma-separated labels to filter issues"), + limit: z + .number() + .optional() + .describe("Max results to return (default 10)"), +}) + +const createIssueSchema = z.object({ + title: z.string().describe("Issue title"), + body: z.string().describe("Issue body in markdown"), + labels: z + .array(z.string()) + .optional() + .describe("Labels to apply"), + assignee: z + .string() + .optional() + .describe("GitHub username to assign"), + milestone: z + .number() + .optional() + .describe("Milestone number to attach to"), +}) + +const interviewSchema = z.object({ + responses: z + .array( + z.object({ + question: z.string(), + answer: z.string(), + }) + ) + .describe( + "Array of question/answer pairs from the interview" + ), + summary: z + .string() + .describe("Brief summary of the interview findings"), + painPoints: z + .array(z.string()) + .optional() + .describe("Key pain points identified"), + featureRequests: z + .array(z.string()) + .optional() + .describe("Feature requests from the user"), + overallSentiment: z + .enum(["positive", "neutral", "negative", "mixed"]) + .describe("Overall sentiment of the feedback"), +}) + +export function githubTools(dataSource: DataSource): ToolDef[] { + return [ + { + name: "queryGitHub", + description: + "Query GitHub repository data: commits, pull requests, " + + "issues, contributors, milestones, or repo stats.", + input_schema: zodToJsonSchema(querySchema), + run: async (input: unknown): Promise => { + const args = querySchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/github/query", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "createGitHubIssue", + description: + "Create a new GitHub issue in the Compass repository. " + + "Always confirm with the user before creating.", + input_schema: zodToJsonSchema(createIssueSchema), + run: async (input: unknown): Promise => { + const args = createIssueSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/github/create-issue", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "saveInterviewFeedback", + description: + "Save the results of a UX interview. Call this after " + + "completing an interview with the user. Saves to the " + + "database and creates a GitHub issue tagged " + + "user-feedback.", + input_schema: zodToJsonSchema(interviewSchema), + run: async (input: unknown): Promise => { + const args = interviewSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/github/save-interview", + args + ) + return JSON.stringify(result) + }, + }, + ] +} diff --git a/packages/agent-core/src/tools/index.ts b/packages/agent-core/src/tools/index.ts new file mode 100644 index 0000000..0c30781 --- /dev/null +++ b/packages/agent-core/src/tools/index.ts @@ -0,0 +1,26 @@ +import type { DataSource } from "../types" +import type { ToolDef } from "./data" +import { dataTools } from "./data" +import { uiTools } from "./ui" +import { scheduleTools } from "./schedule" +import { themeTools } from "./theme" +import { memoryTools } from "./memory" +import { skillTools } from "./skills" +import { githubTools } from "./github" +import { dashboardTools } from "./dashboards" + +export type { ToolDef } from "./data" +export { zodToJsonSchema } from "./data" + +export function createTools(dataSource: DataSource): ToolDef[] { + return [ + ...dataTools(dataSource), + ...uiTools(), + ...scheduleTools(dataSource), + ...themeTools(dataSource), + ...memoryTools(dataSource), + ...skillTools(dataSource), + ...githubTools(dataSource), + ...dashboardTools(dataSource), + ] +} diff --git a/packages/agent-core/src/tools/memory.ts b/packages/agent-core/src/tools/memory.ts new file mode 100644 index 0000000..db47df1 --- /dev/null +++ b/packages/agent-core/src/tools/memory.ts @@ -0,0 +1,74 @@ +import { z } from "zod" +import type { DataSource } from "../types" +import type { ToolDef } from "./data" +import { zodToJsonSchema } from "./data" + +const rememberSchema = z.object({ + content: z + .string() + .describe( + "What to remember (a preference, decision, fact, " + + "or workflow)" + ), + memoryType: z + .enum(["preference", "workflow", "fact", "decision"]) + .describe("Category of memory"), + tags: z + .string() + .optional() + .describe("Comma-separated tags for categorization"), + importance: z + .number() + .min(0.3) + .max(1.0) + .optional() + .describe("Importance weight 0.3-1.0 (default 0.7)"), +}) + +const recallSchema = z.object({ + query: z + .string() + .describe("What to search for in memories"), + limit: z + .number() + .optional() + .describe("Max results (default 5)"), +}) + +export function memoryTools(dataSource: DataSource): ToolDef[] { + return [ + { + name: "rememberContext", + description: + "Save something to persistent memory. Use when the " + + "user shares a preference, makes a decision, or " + + "mentions a fact worth remembering across sessions.", + input_schema: zodToJsonSchema(rememberSchema), + run: async (input: unknown): Promise => { + const args = rememberSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/memory/save", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "recallMemory", + description: + "Search persistent memories for this user. Use when " + + "the user asks if you remember something or when you " + + "need to look up a past preference or decision.", + input_schema: zodToJsonSchema(recallSchema), + run: async (input: unknown): Promise => { + const args = recallSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/memory/recall", + args + ) + return JSON.stringify(result) + }, + }, + ] +} diff --git a/packages/agent-core/src/tools/schedule.ts b/packages/agent-core/src/tools/schedule.ts new file mode 100644 index 0000000..755f380 --- /dev/null +++ b/packages/agent-core/src/tools/schedule.ts @@ -0,0 +1,270 @@ +import { z } from "zod" +import type { DataSource } from "../types" +import type { ToolDef } from "./data" +import { zodToJsonSchema } from "./data" + +const getScheduleSchema = z.object({ + projectId: z.string().describe("The project UUID"), +}) + +const createTaskSchema = z.object({ + projectId: z.string().describe("The project UUID"), + title: z.string().describe("Task title"), + startDate: z + .string() + .describe("Start date in YYYY-MM-DD format"), + workdays: z.number().describe("Duration in working days"), + phase: z + .string() + .describe( + "Construction phase (preconstruction, sitework, " + + "foundation, framing, roofing, electrical, plumbing, " + + "hvac, insulation, drywall, finish, landscaping, " + + "closeout)" + ), + isMilestone: z + .boolean() + .optional() + .describe("Whether this is a milestone (0 workdays)"), + percentComplete: z + .number() + .min(0) + .max(100) + .optional() + .describe("Initial percent complete (0-100)"), + assignedTo: z + .string() + .optional() + .describe("Name of the person assigned"), +}) + +const updateTaskSchema = z.object({ + taskId: z.string().describe("The task UUID"), + title: z.string().optional().describe("New title"), + startDate: z + .string() + .optional() + .describe("New start date (YYYY-MM-DD)"), + workdays: z + .number() + .optional() + .describe("New duration in working days"), + phase: z.string().optional().describe("New phase"), + status: z + .enum(["PENDING", "IN_PROGRESS", "COMPLETE", "BLOCKED"]) + .optional() + .describe("New status"), + isMilestone: z + .boolean() + .optional() + .describe("Set milestone flag"), + percentComplete: z + .number() + .min(0) + .max(100) + .optional() + .describe("New percent complete (0-100)"), + assignedTo: z + .string() + .nullable() + .optional() + .describe("New assignee (null to unassign)"), +}) + +const deleteTaskSchema = z.object({ + taskId: z.string().describe("The task UUID to delete"), +}) + +const createDepSchema = z.object({ + projectId: z.string().describe("The project UUID"), + predecessorId: z + .string() + .describe("UUID of the predecessor task"), + successorId: z + .string() + .describe("UUID of the successor task"), + type: z + .enum(["FS", "SS", "FF", "SF"]) + .describe( + "Dependency type: FS (finish-to-start), " + + "SS (start-to-start), FF (finish-to-finish), " + + "SF (start-to-finish)" + ), + lagDays: z + .number() + .optional() + .describe("Lag in working days (default 0)"), +}) + +const deleteDepSchema = z.object({ + dependencyId: z + .string() + .describe("The dependency UUID to delete"), + projectId: z + .string() + .describe("The project UUID (for revalidation)"), +}) + +const addExceptionSchema = z.object({ + projectId: z.string().describe("The project UUID"), + date: z + .string() + .describe("Exception date in YYYY-MM-DD format"), + category: z + .enum(["holiday", "non_working", "extra_working"]) + .describe("Exception category"), + recurrence: z + .enum(["none", "annual", "weekly"]) + .optional() + .describe("Recurrence pattern (default none)"), + description: z + .string() + .optional() + .describe("Description of the exception"), +}) + +const removeExceptionSchema = z.object({ + exceptionId: z + .string() + .describe("The exception UUID to remove"), + projectId: z.string().describe("The project UUID"), +}) + +export function scheduleTools( + dataSource: DataSource +): ToolDef[] { + return [ + { + name: "getProjectSchedule", + description: + "Get the full schedule for a project including tasks, " + + "dependencies, workday exceptions, and a computed " + + "summary (counts, overall %, critical path). Always " + + "call this before making schedule mutations to resolve " + + "task names to IDs.", + input_schema: zodToJsonSchema(getScheduleSchema), + run: async (input: unknown): Promise => { + const args = getScheduleSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/schedule/get", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "createScheduleTask", + description: + "Create a new task on a project schedule. Returns a " + + "toast confirmation. Dates are ISO format (YYYY-MM-DD).", + input_schema: zodToJsonSchema(createTaskSchema), + run: async (input: unknown): Promise => { + const args = createTaskSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/schedule/create", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "updateScheduleTask", + description: + "Update an existing schedule task. Provide only the " + + "fields to change. Use getProjectSchedule first to " + + "resolve task names to IDs.", + input_schema: zodToJsonSchema(updateTaskSchema), + run: async (input: unknown): Promise => { + const args = updateTaskSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/schedule/update", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "deleteScheduleTask", + description: + "Delete a schedule task. Always confirm with the user " + + "before deleting. This also removes any dependencies " + + "involving the task.", + input_schema: zodToJsonSchema(deleteTaskSchema), + run: async (input: unknown): Promise => { + const args = deleteTaskSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/schedule/delete", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "createScheduleDependency", + description: + "Create a dependency between two tasks. Has built-in " + + "cycle detection. Use getProjectSchedule first to " + + "resolve task names to IDs.", + input_schema: zodToJsonSchema(createDepSchema), + run: async (input: unknown): Promise => { + const args = createDepSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/schedule/create-dependency", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "deleteScheduleDependency", + description: + "Delete a dependency between tasks. Use " + + "getProjectSchedule first to find the dependency ID.", + input_schema: zodToJsonSchema(deleteDepSchema), + run: async (input: unknown): Promise => { + const args = deleteDepSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/schedule/delete-dependency", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "addWorkdayException", + description: + "Add a workday exception to a project (holiday, " + + "non-working day, or extra working day).", + input_schema: zodToJsonSchema(addExceptionSchema), + run: async (input: unknown): Promise => { + const args = addExceptionSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/schedule/add-exception", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "removeWorkdayException", + description: + "Remove a workday exception from a project.", + input_schema: zodToJsonSchema(removeExceptionSchema), + run: async (input: unknown): Promise => { + const args = removeExceptionSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/schedule/remove-exception", + args + ) + return JSON.stringify(result) + }, + }, + ] +} diff --git a/packages/agent-core/src/tools/skills.ts b/packages/agent-core/src/tools/skills.ts new file mode 100644 index 0000000..947a30a --- /dev/null +++ b/packages/agent-core/src/tools/skills.ts @@ -0,0 +1,94 @@ +import { z } from "zod" +import type { DataSource } from "../types" +import type { ToolDef } from "./data" +import { zodToJsonSchema } from "./data" + +const installSchema = z.object({ + source: z + .string() + .describe( + "GitHub source path, e.g. 'cloudflare/skills/wrangler'" + ), +}) + +const listSchema = z.object({}) + +const toggleSchema = z.object({ + pluginId: z + .string() + .describe("The plugin ID of the skill"), + enabled: z + .boolean() + .describe("true to enable, false to disable"), +}) + +const uninstallSchema = z.object({ + pluginId: z + .string() + .describe("The plugin ID of the skill"), +}) + +export function skillTools(dataSource: DataSource): ToolDef[] { + return [ + { + name: "installSkill", + description: + "Install a skill from GitHub (skills.sh format). " + + "Source format: owner/repo or owner/repo/skill-name. " + + "Requires admin role. Always confirm with the user " + + "what skill they want before installing.", + input_schema: zodToJsonSchema(installSchema), + run: async (input: unknown): Promise => { + const args = installSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/skills/install", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "listInstalledSkills", + description: + "List all installed agent skills with their status.", + input_schema: zodToJsonSchema(listSchema), + run: async (): Promise => { + const result = await dataSource.fetch( + "/api/compass/skills/list" + ) + return JSON.stringify(result) + }, + }, + + { + name: "toggleInstalledSkill", + description: "Enable or disable an installed skill.", + input_schema: zodToJsonSchema(toggleSchema), + run: async (input: unknown): Promise => { + const args = toggleSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/skills/toggle", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "uninstallSkill", + description: + "Remove an installed skill permanently. Requires " + + "admin role. Always confirm before uninstalling.", + input_schema: zodToJsonSchema(uninstallSchema), + run: async (input: unknown): Promise => { + const args = uninstallSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/skills/uninstall", + args + ) + return JSON.stringify(result) + }, + }, + ] +} diff --git a/packages/agent-core/src/tools/theme.ts b/packages/agent-core/src/tools/theme.ts new file mode 100644 index 0000000..e31904b --- /dev/null +++ b/packages/agent-core/src/tools/theme.ts @@ -0,0 +1,155 @@ +import { z } from "zod" +import type { DataSource } from "../types" +import type { ToolDef } from "./data" +import { zodToJsonSchema } from "./data" + +const listThemesSchema = z.object({}) + +const setThemeSchema = z.object({ + themeId: z.string().describe("The theme ID to activate"), +}) + +const generateThemeSchema = z.object({ + name: z.string().describe("Theme display name"), + description: z.string().describe("Brief theme description"), + light: z + .record(z.string(), z.string()) + .describe( + "Light mode color map with all 32 ThemeColorKey entries" + ), + dark: z + .record(z.string(), z.string()) + .describe( + "Dark mode color map with all 32 ThemeColorKey entries" + ), + fonts: z + .object({ + sans: z.string(), + serif: z.string(), + mono: z.string(), + }) + .describe("CSS font-family strings"), + googleFonts: z + .array(z.string()) + .optional() + .describe("Google Font names to load (case-sensitive)"), + radius: z + .string() + .optional() + .describe("Border radius (e.g. '0.5rem')"), + spacing: z + .string() + .optional() + .describe("Base spacing (e.g. '0.25rem')"), +}) + +const editThemeSchema = z.object({ + themeId: z + .string() + .describe("ID of existing custom theme to edit"), + name: z.string().optional().describe("New display name"), + description: z + .string() + .optional() + .describe("New description"), + light: z + .record(z.string(), z.string()) + .optional() + .describe("Partial light color overrides (only changed keys)"), + dark: z + .record(z.string(), z.string()) + .optional() + .describe("Partial dark color overrides (only changed keys)"), + fonts: z + .object({ + sans: z.string().optional(), + serif: z.string().optional(), + mono: z.string().optional(), + }) + .optional() + .describe("Partial font overrides"), + googleFonts: z + .array(z.string()) + .optional() + .describe("Replace Google Font list"), + radius: z + .string() + .optional() + .describe("New border radius"), + spacing: z + .string() + .optional() + .describe("New base spacing"), +}) + +export function themeTools(dataSource: DataSource): ToolDef[] { + return [ + { + name: "listThemes", + description: + "List available visual themes (presets + user custom " + + "themes).", + input_schema: zodToJsonSchema(listThemesSchema), + run: async (): Promise => { + const result = await dataSource.fetch( + "/api/compass/themes/list" + ) + return JSON.stringify(result) + }, + }, + + { + name: "setTheme", + description: + "Switch the user's visual theme. Use a preset ID " + + "(native-compass, corpo, notebook, doom-64, " + + "bubblegum, developers-choice, anslopics-clood, " + + "violet-bloom, soy, mocha) or a custom theme UUID.", + input_schema: zodToJsonSchema(setThemeSchema), + run: async (input: unknown): Promise => { + const args = setThemeSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/themes/set", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "generateTheme", + description: + "Generate and save a custom visual theme. Provide " + + "complete light and dark color maps (all 32 keys), " + + "fonts, optional Google Font names, and design tokens. " + + "All colors must be in oklch() format.", + input_schema: zodToJsonSchema(generateThemeSchema), + run: async (input: unknown): Promise => { + const args = generateThemeSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/themes/generate", + args + ) + return JSON.stringify(result) + }, + }, + + { + name: "editTheme", + description: + "Edit an existing custom theme. Provide the theme ID " + + "and only the properties you want to change. " + + "Unspecified properties are preserved from the existing " + + "theme. Only works on custom themes (not presets).", + input_schema: zodToJsonSchema(editThemeSchema), + run: async (input: unknown): Promise => { + const args = editThemeSchema.parse(input) + const result = await dataSource.fetch( + "/api/compass/themes/edit", + args + ) + return JSON.stringify(result) + }, + }, + ] +} diff --git a/packages/agent-core/src/tools/ui.ts b/packages/agent-core/src/tools/ui.ts new file mode 100644 index 0000000..b7cdfed --- /dev/null +++ b/packages/agent-core/src/tools/ui.ts @@ -0,0 +1,144 @@ +import { z } from "zod" +import type { ToolDef } from "./data" +import { zodToJsonSchema } from "./data" + +const VALID_ROUTES: ReadonlyArray = [ + /^\/dashboard$/, + /^\/dashboard\/contacts$/, + /^\/dashboard\/customers$/, + /^\/dashboard\/vendors$/, + /^\/dashboard\/projects$/, + /^\/dashboard\/projects\/[^/]+$/, + /^\/dashboard\/projects\/[^/]+\/schedule$/, + /^\/dashboard\/financials$/, + /^\/dashboard\/people$/, + /^\/dashboard\/files$/, + /^\/dashboard\/files\/.+$/, + /^\/dashboard\/boards\/[^/]+$/, + /^\/dashboard\/conversations$/, + /^\/dashboard\/settings$/, +] + +function isValidRoute(path: string): boolean { + return VALID_ROUTES.some((r) => r.test(path)) +} + +function normalizePath(path: string): string { + let p = path.trim() + if (!p.startsWith("/dashboard")) { + p = p.startsWith("/") ? `/dashboard${p}` : `/dashboard/${p}` + } + return p +} + +const navigateSchema = z.object({ + path: z + .string() + .describe( + "Page path, e.g. 'projects' or '/dashboard/projects'" + ), + reason: z + .string() + .optional() + .describe("Brief explanation of why navigating"), +}) + +const notificationSchema = z.object({ + message: z.string().describe("The notification message"), + type: z + .enum(["default", "success", "error"]) + .optional() + .describe("Notification style"), +}) + +const generateUISchema = z.object({ + description: z + .string() + .describe( + "Layout and content description for the dashboard to " + + "generate. Be specific about what components and data " + + "to display." + ), + dataContext: z + .record(z.string(), z.unknown()) + .optional() + .describe( + "Data to include in the rendered UI. Pass query results here." + ), +}) + +type NavigateInput = z.infer +type NotificationInput = z.infer +type GenerateUIInput = z.infer + +export function uiTools(): ToolDef[] { + return [ + { + name: "navigateTo", + description: + "Navigate the user to a page. Paths are relative to " + + "/dashboard \u2014 e.g. pass 'projects' or " + + "'/dashboard/projects'. Valid pages: projects, " + + "projects/{id}, projects/{id}/schedule, contacts, " + + "customers, vendors, financials, people, files, " + + "files/{path}, boards/{id}, conversations, settings.", + input_schema: zodToJsonSchema(navigateSchema), + run: async (input: unknown): Promise => { + const args = navigateSchema.parse(input) as NavigateInput + const resolved = normalizePath(args.path) + if (!isValidRoute(resolved)) { + return JSON.stringify({ + error: + `"${args.path}" (resolved to "${resolved}") ` + + "is not a valid page. Valid: projects, " + + "projects/{id}, contacts, financials, people, " + + "files, boards/{id}", + }) + } + return JSON.stringify({ + action: "navigate", + path: resolved, + reason: args.reason ?? null, + }) + }, + }, + + { + name: "showNotification", + description: + "Show a toast notification to the user. Use for " + + "confirmations or important alerts.", + input_schema: zodToJsonSchema(notificationSchema), + run: async (input: unknown): Promise => { + const args = notificationSchema.parse( + input + ) as NotificationInput + return JSON.stringify({ + action: "toast", + message: args.message, + type: args.type ?? "default", + }) + }, + }, + + { + name: "generateUI", + description: + "Generate a rich interactive UI dashboard. Use when " + + "the user wants to see structured data (tables, charts, " + + "stats, forms). Always fetch data with queryData first, " + + "then pass it here as dataContext.", + input_schema: zodToJsonSchema(generateUISchema), + run: async (input: unknown): Promise => { + const args = generateUISchema.parse( + input + ) as GenerateUIInput + return JSON.stringify({ + action: "generateUI", + renderPrompt: args.description, + dataContext: args.dataContext ?? {}, + }) + }, + }, + ] +} diff --git a/packages/agent-core/src/types.ts b/packages/agent-core/src/types.ts new file mode 100644 index 0000000..79d7c37 --- /dev/null +++ b/packages/agent-core/src/types.ts @@ -0,0 +1,46 @@ +export interface ProviderConfig { + readonly type: "anthropic" | "openrouter" | "ollama" | "custom" + readonly apiKey?: string + readonly baseUrl?: string + readonly modelOverrides?: Readonly> +} + +export interface AgentContext { + readonly userId: string + readonly orgId: string + readonly role: string + readonly isDemoUser: boolean + readonly currentPage: string + readonly timezone: string +} + +/** Abstracts HTTP calls vs direct DB access for tools */ +export interface DataSource { + fetch(path: string, body?: unknown): Promise +} + +/** SSE events emitted by the agentic loop */ +export type SSEData = + | { readonly type: "text"; readonly content: string } + | { + readonly type: "tool_use" + readonly name: string + readonly toolCallId: string + readonly input: unknown + } + | { + readonly type: "tool_result" + readonly toolCallId: string + readonly output: unknown + } + | { + readonly type: "result" + readonly subtype: string + readonly result: string + readonly usage?: { + readonly inputTokens: number + readonly outputTokens: number + readonly totalCostUsd: number + } + } + | { readonly type: "error"; readonly error: string } diff --git a/packages/agent-core/tsconfig.json b/packages/agent-core/tsconfig.json new file mode 100644 index 0000000..c05d14d --- /dev/null +++ b/packages/agent-core/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "declaration": true, + "types": ["bun-types"] + }, + "include": ["src"] +} diff --git a/packages/agent-server/bun.lock b/packages/agent-server/bun.lock index a53b9f6..2943527 100644 --- a/packages/agent-server/bun.lock +++ b/packages/agent-server/bun.lock @@ -5,8 +5,7 @@ "": { "name": "agent-server", "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.42", - "@anthropic-ai/sdk": "^0.74.0", + "agent-core": "file:../agent-core", "jose": "^5.9.6", "zod": "^3.24.1", }, @@ -16,46 +15,16 @@ }, }, "packages": { - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.42", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-/CugP7AjP57Dqtl2sbsDtxdbpQoPKIhjyF5WrTViGu4NHQdM+UikrRs4MhZ2jeotiC5R7iK9ZUN9SiBgcZ8oLw=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.74.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw=="], "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], - "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + "agent-core": ["agent-core@file:../agent-core", { "dependencies": { "@anthropic-ai/sdk": "^0.74.0", "zod": "^3.24.1" }, "devDependencies": { "bun-types": "^1.3.9" } }], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], diff --git a/packages/agent-server/package.json b/packages/agent-server/package.json index 1e9fbe4..9b5cedb 100644 --- a/packages/agent-server/package.json +++ b/packages/agent-server/package.json @@ -1,7 +1,7 @@ { "name": "agent-server", "version": "0.1.0", - "description": "Standalone Node.js agent server wrapping Anthropic Agent SDK", + "description": "Standalone agent server using agent-core", "type": "module", "bin": { "agent-server": "./src/index.ts" @@ -12,8 +12,7 @@ "build": "bun build src/index.ts --target=bun --outdir=dist --minify" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.42", - "@anthropic-ai/sdk": "^0.74.0", + "agent-core": "file:../agent-core", "jose": "^5.9.6", "zod": "^3.24.1" }, diff --git a/packages/agent-server/src/mcp/api-client.ts b/packages/agent-server/src/api-client.ts similarity index 100% rename from packages/agent-server/src/mcp/api-client.ts rename to packages/agent-server/src/api-client.ts diff --git a/packages/agent-server/src/index.ts b/packages/agent-server/src/index.ts index 02bf82a..109f2a5 100644 --- a/packages/agent-server/src/index.ts +++ b/packages/agent-server/src/index.ts @@ -1,12 +1,11 @@ #!/usr/bin/env bun /** * Compass Agent Server - * Standalone Node.js server wrapping Anthropic Agent SDK + * Standalone server using agent-core for the agentic loop */ import { config } from "./config" import { validateAuth } from "./auth" -import { getOrCreateSession } from "./sessions" import { createAgentStream } from "./stream" interface ChatRequest { @@ -39,9 +38,6 @@ async function handleChat(request: Request): Promise { const timezone = request.headers.get("x-timezone") || "UTC" const model = request.headers.get("x-model") || "sonnet" - // Get or create session - getOrCreateSession(sessionId) - // Parse body let body: ChatRequest try { diff --git a/packages/agent-server/src/mcp/compass-server.ts b/packages/agent-server/src/mcp/compass-server.ts deleted file mode 100644 index bb3edbe..0000000 --- a/packages/agent-server/src/mcp/compass-server.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk" -import { dataTools } from "./data-tools" -import { uiTools } from "./ui-tools" -import { scheduleTools } from "./schedule-tools" -import { themeTools } from "./theme-tools" -import { memoryTools } from "./memory-tools" -import { skillTools } from "./skill-tools" -import { githubTools } from "./github-tools" -import { dashboardTools } from "./dashboard-tools" - -/** - * Create the Compass MCP server with all domain tools. - * - * @param apiBaseUrl - Base URL for the Compass Workers API (e.g., "https://compass.example.com") - * @param authToken - JWT auth token for API requests - * @returns MCP server instance - */ -export function createCompassMcpServer(apiBaseUrl: string, authToken: string) { - return createSdkMcpServer({ - name: "compass", - version: "1.0.0", - tools: [ - ...dataTools(apiBaseUrl, authToken), - ...uiTools(), - ...scheduleTools(apiBaseUrl, authToken), - ...themeTools(apiBaseUrl, authToken), - ...memoryTools(apiBaseUrl, authToken), - ...skillTools(apiBaseUrl, authToken), - ...githubTools(apiBaseUrl, authToken), - ...dashboardTools(apiBaseUrl, authToken), - ] - }) -} diff --git a/packages/agent-server/src/mcp/dashboard-tools.ts b/packages/agent-server/src/mcp/dashboard-tools.ts deleted file mode 100644 index 07fd06d..0000000 --- a/packages/agent-server/src/mcp/dashboard-tools.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { tool } from "@anthropic-ai/claude-agent-sdk" -import { z } from "zod" -import { compassApi } from "./api-client" - -export function dashboardTools(apiBaseUrl: string, authToken: string) { - return [ - tool( - "saveDashboard", - "Save the currently rendered UI as a named dashboard. The client captures the current spec and data context automatically. Returns an action for the client to handle the save.", - { - name: z.string().describe("Dashboard display name"), - description: z.string().optional().describe( - "Brief description of the dashboard" - ), - dashboardId: z.string().optional().describe( - "Existing dashboard ID to update (for edits)" - ), - }, - async (args) => { - return { - content: [{ - type: "text", - text: JSON.stringify({ - action: "save_dashboard", - name: args.name, - description: args.description ?? "", - dashboardId: args.dashboardId, - }) - }] - } - } - ), - - tool( - "listDashboards", - "List the user's saved custom dashboards.", - {}, - async () => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/dashboards/list", - authToken - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "editDashboard", - "Load a saved dashboard for editing. The client injects the spec into the render context and navigates to /dashboard. Optionally pass an editPrompt to trigger immediate re-generation.", - { - dashboardId: z.string().describe("ID of the dashboard to edit"), - editPrompt: z.string().optional().describe("Description of changes to make"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/dashboards/get", - authToken, - { dashboardId: args.dashboardId } - ) - return { - content: [{ - type: "text", - text: JSON.stringify({ - action: "load_dashboard", - dashboardId: args.dashboardId, - spec: result, - editPrompt: args.editPrompt, - }) - }] - } - } - ), - - tool( - "deleteDashboard", - "Delete a saved dashboard. Always confirm with the user before deleting.", - { - dashboardId: z.string().describe("ID of the dashboard to delete"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/dashboards/delete", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ) - ] -} diff --git a/packages/agent-server/src/mcp/data-tools.ts b/packages/agent-server/src/mcp/data-tools.ts deleted file mode 100644 index fa5e0f1..0000000 --- a/packages/agent-server/src/mcp/data-tools.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { tool } from "@anthropic-ai/claude-agent-sdk" -import { z } from "zod" -import { compassApi } from "./api-client" - -export function dataTools(apiBaseUrl: string, authToken: string) { - return [ - tool( - "queryData", - "Query the application database. Describe what data you need in natural language and provide a query type.", - { - queryType: z.enum([ - "customers", - "vendors", - "projects", - "invoices", - "vendor_bills", - "schedule_tasks", - "project_detail", - "customer_detail", - "vendor_detail", - ]), - id: z.string().optional().describe("Record ID for detail queries"), - search: z.string().optional().describe("Search term to filter results"), - limit: z.number().optional().describe("Max results to return (default 20)"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/query", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ) - ] -} diff --git a/packages/agent-server/src/mcp/github-tools.ts b/packages/agent-server/src/mcp/github-tools.ts deleted file mode 100644 index 1b4ef9e..0000000 --- a/packages/agent-server/src/mcp/github-tools.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { tool } from "@anthropic-ai/claude-agent-sdk" -import { z } from "zod" -import { compassApi } from "./api-client" - -export function githubTools(apiBaseUrl: string, authToken: string) { - return [ - tool( - "queryGitHub", - "Query GitHub repository data: commits, pull requests, issues, contributors, milestones, or repo stats.", - { - queryType: z.enum([ - "commits", - "commit_diff", - "pull_requests", - "issues", - "contributors", - "milestones", - "repo_stats", - ]).describe("Type of GitHub data to query"), - sha: z.string().optional().describe("Commit SHA for commit_diff queries"), - state: z.enum(["open", "closed", "all"]).optional().describe( - "State filter for PRs, issues, milestones" - ), - labels: z.string().optional().describe("Comma-separated labels to filter issues"), - limit: z.number().optional().describe("Max results to return (default 10)"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/github/query", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "createGitHubIssue", - "Create a new GitHub issue in the Compass repository. Always confirm with the user before creating.", - { - title: z.string().describe("Issue title"), - body: z.string().describe("Issue body in markdown"), - labels: z.array(z.string()).optional().describe("Labels to apply"), - assignee: z.string().optional().describe("GitHub username to assign"), - milestone: z.number().optional().describe("Milestone number to attach to"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/github/create-issue", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "saveInterviewFeedback", - "Save the results of a UX interview. Call this after completing an interview with the user. Saves to the database and creates a GitHub issue tagged user-feedback.", - { - responses: z.array( - z.object({ - question: z.string(), - answer: z.string(), - }) - ).describe("Array of question/answer pairs from the interview"), - summary: z.string().describe("Brief summary of the interview findings"), - painPoints: z.array(z.string()).optional().describe("Key pain points identified"), - featureRequests: z.array(z.string()).optional().describe( - "Feature requests from the user" - ), - overallSentiment: z.enum([ - "positive", - "neutral", - "negative", - "mixed" - ]).describe("Overall sentiment of the feedback"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/github/save-interview", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ) - ] -} diff --git a/packages/agent-server/src/mcp/index.ts b/packages/agent-server/src/mcp/index.ts deleted file mode 100644 index 53cf6b9..0000000 --- a/packages/agent-server/src/mcp/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * MCP server exports - */ - -export { createCompassMcpServer } from "./compass-server" -export { compassApi } from "./api-client" -export { dataTools } from "./data-tools" -export { uiTools } from "./ui-tools" -export { scheduleTools } from "./schedule-tools" -export { themeTools } from "./theme-tools" -export { memoryTools } from "./memory-tools" -export { skillTools } from "./skill-tools" -export { githubTools } from "./github-tools" -export { dashboardTools } from "./dashboard-tools" diff --git a/packages/agent-server/src/mcp/memory-tools.ts b/packages/agent-server/src/mcp/memory-tools.ts deleted file mode 100644 index f45f9b0..0000000 --- a/packages/agent-server/src/mcp/memory-tools.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { tool } from "@anthropic-ai/claude-agent-sdk" -import { z } from "zod" -import { compassApi } from "./api-client" - -export function memoryTools(apiBaseUrl: string, authToken: string) { - return [ - tool( - "rememberContext", - "Save something to persistent memory. Use when the user shares a preference, makes a decision, or mentions a fact worth remembering across sessions.", - { - content: z.string().describe( - "What to remember (a preference, decision, fact, or workflow)" - ), - memoryType: z.enum([ - "preference", - "workflow", - "fact", - "decision", - ]).describe("Category of memory"), - tags: z.string().optional().describe("Comma-separated tags for categorization"), - importance: z.number().min(0.3).max(1.0).optional().describe( - "Importance weight 0.3-1.0 (default 0.7)" - ), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/memory/save", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "recallMemory", - "Search persistent memories for this user. Use when the user asks if you remember something or when you need to look up a past preference or decision.", - { - query: z.string().describe("What to search for in memories"), - limit: z.number().optional().describe("Max results (default 5)"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/memory/recall", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ) - ] -} diff --git a/packages/agent-server/src/mcp/schedule-tools.ts b/packages/agent-server/src/mcp/schedule-tools.ts deleted file mode 100644 index bc4a44f..0000000 --- a/packages/agent-server/src/mcp/schedule-tools.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { tool } from "@anthropic-ai/claude-agent-sdk" -import { z } from "zod" -import { compassApi } from "./api-client" - -export function scheduleTools(apiBaseUrl: string, authToken: string) { - return [ - tool( - "getProjectSchedule", - "Get the full schedule for a project including tasks, dependencies, workday exceptions, and a computed summary (counts, overall %, critical path). Always call this before making schedule mutations to resolve task names to IDs.", - { - projectId: z.string().describe("The project UUID"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/schedule/get", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "createScheduleTask", - "Create a new task on a project schedule. Returns a toast confirmation. Dates are ISO format (YYYY-MM-DD).", - { - projectId: z.string().describe("The project UUID"), - title: z.string().describe("Task title"), - startDate: z.string().describe("Start date in YYYY-MM-DD format"), - workdays: z.number().describe("Duration in working days"), - phase: z.string().describe( - "Construction phase (preconstruction, sitework, foundation, framing, roofing, electrical, plumbing, hvac, insulation, drywall, finish, landscaping, closeout)" - ), - isMilestone: z.boolean().optional().describe("Whether this is a milestone (0 workdays)"), - percentComplete: z.number().min(0).max(100).optional().describe("Initial percent complete (0-100)"), - assignedTo: z.string().optional().describe("Name of the person assigned"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/schedule/create", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "updateScheduleTask", - "Update an existing schedule task. Provide only the fields to change. Use getProjectSchedule first to resolve task names to IDs.", - { - taskId: z.string().describe("The task UUID"), - title: z.string().optional().describe("New title"), - startDate: z.string().optional().describe("New start date (YYYY-MM-DD)"), - workdays: z.number().optional().describe("New duration in working days"), - phase: z.string().optional().describe("New phase"), - status: z.enum(["PENDING", "IN_PROGRESS", "COMPLETE", "BLOCKED"]).optional().describe("New status"), - isMilestone: z.boolean().optional().describe("Set milestone flag"), - percentComplete: z.number().min(0).max(100).optional().describe("New percent complete (0-100)"), - assignedTo: z.string().nullable().optional().describe("New assignee (null to unassign)"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/schedule/update", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "deleteScheduleTask", - "Delete a schedule task. Always confirm with the user before deleting. This also removes any dependencies involving the task.", - { - taskId: z.string().describe("The task UUID to delete"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/schedule/delete", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "createScheduleDependency", - "Create a dependency between two tasks. Has built-in cycle detection. Use getProjectSchedule first to resolve task names to IDs.", - { - projectId: z.string().describe("The project UUID"), - predecessorId: z.string().describe("UUID of the predecessor task"), - successorId: z.string().describe("UUID of the successor task"), - type: z.enum(["FS", "SS", "FF", "SF"]).describe( - "Dependency type: FS (finish-to-start), SS (start-to-start), FF (finish-to-finish), SF (start-to-finish)" - ), - lagDays: z.number().optional().describe("Lag in working days (default 0)"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/schedule/create-dependency", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "deleteScheduleDependency", - "Delete a dependency between tasks. Use getProjectSchedule first to find the dependency ID.", - { - dependencyId: z.string().describe("The dependency UUID to delete"), - projectId: z.string().describe("The project UUID (for revalidation)"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/schedule/delete-dependency", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "addWorkdayException", - "Add a workday exception to a project (holiday, non-working day, or extra working day).", - { - projectId: z.string().describe("The project UUID"), - date: z.string().describe("Exception date in YYYY-MM-DD format"), - category: z.enum(["holiday", "non_working", "extra_working"]).describe("Exception category"), - recurrence: z.enum(["none", "annual", "weekly"]).optional().describe("Recurrence pattern (default none)"), - description: z.string().optional().describe("Description of the exception"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/schedule/add-exception", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "removeWorkdayException", - "Remove a workday exception from a project.", - { - exceptionId: z.string().describe("The exception UUID to remove"), - projectId: z.string().describe("The project UUID"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/schedule/remove-exception", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ) - ] -} diff --git a/packages/agent-server/src/mcp/skill-tools.ts b/packages/agent-server/src/mcp/skill-tools.ts deleted file mode 100644 index 8b4415c..0000000 --- a/packages/agent-server/src/mcp/skill-tools.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { tool } from "@anthropic-ai/claude-agent-sdk" -import { z } from "zod" -import { compassApi } from "./api-client" - -export function skillTools(apiBaseUrl: string, authToken: string) { - return [ - tool( - "installSkill", - "Install a skill from GitHub (skills.sh format). Source format: owner/repo or owner/repo/skill-name. Requires admin role. Always confirm with the user what skill they want before installing.", - { - source: z.string().describe( - "GitHub source path, e.g. 'cloudflare/skills/wrangler'" - ), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/skills/install", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "listInstalledSkills", - "List all installed agent skills with their status.", - {}, - async () => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/skills/list", - authToken - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "toggleInstalledSkill", - "Enable or disable an installed skill.", - { - pluginId: z.string().describe("The plugin ID of the skill"), - enabled: z.boolean().describe("true to enable, false to disable"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/skills/toggle", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "uninstallSkill", - "Remove an installed skill permanently. Requires admin role. Always confirm before uninstalling.", - { - pluginId: z.string().describe("The plugin ID of the skill"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/skills/uninstall", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ) - ] -} diff --git a/packages/agent-server/src/mcp/theme-tools.ts b/packages/agent-server/src/mcp/theme-tools.ts deleted file mode 100644 index 37614fd..0000000 --- a/packages/agent-server/src/mcp/theme-tools.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { tool } from "@anthropic-ai/claude-agent-sdk" -import { z } from "zod" -import { compassApi } from "./api-client" - -export function themeTools(apiBaseUrl: string, authToken: string) { - return [ - tool( - "listThemes", - "List available visual themes (presets + user custom themes).", - {}, - async () => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/themes/list", - authToken - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "setTheme", - "Switch the user's visual theme. Use a preset ID (native-compass, corpo, notebook, doom-64, bubblegum, developers-choice, anslopics-clood, violet-bloom, soy, mocha) or a custom theme UUID.", - { - themeId: z.string().describe("The theme ID to activate"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/themes/set", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "generateTheme", - "Generate and save a custom visual theme. Provide complete light and dark color maps (all 32 keys), fonts, optional Google Font names, and design tokens. All colors must be in oklch() format.", - { - name: z.string().describe("Theme display name"), - description: z.string().describe("Brief theme description"), - light: z.record(z.string(), z.string()).describe( - "Light mode color map with all 32 ThemeColorKey entries" - ), - dark: z.record(z.string(), z.string()).describe( - "Dark mode color map with all 32 ThemeColorKey entries" - ), - fonts: z.object({ - sans: z.string(), - serif: z.string(), - mono: z.string(), - }).describe("CSS font-family strings"), - googleFonts: z.array(z.string()).optional().describe( - "Google Font names to load (case-sensitive)" - ), - radius: z.string().optional().describe("Border radius (e.g. '0.5rem')"), - spacing: z.string().optional().describe("Base spacing (e.g. '0.25rem')"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/themes/generate", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ), - - tool( - "editTheme", - "Edit an existing custom theme. Provide the theme ID and only the properties you want to change. Unspecified properties are preserved from the existing theme. Only works on custom themes (not presets).", - { - themeId: z.string().describe("ID of existing custom theme to edit"), - name: z.string().optional().describe("New display name"), - description: z.string().optional().describe("New description"), - light: z.record(z.string(), z.string()).optional().describe( - "Partial light color overrides (only changed keys)" - ), - dark: z.record(z.string(), z.string()).optional().describe( - "Partial dark color overrides (only changed keys)" - ), - fonts: z.object({ - sans: z.string().optional(), - serif: z.string().optional(), - mono: z.string().optional(), - }).optional().describe("Partial font overrides"), - googleFonts: z.array(z.string()).optional().describe("Replace Google Font list"), - radius: z.string().optional().describe("New border radius"), - spacing: z.string().optional().describe("New base spacing"), - }, - async (args) => { - const result = await compassApi( - apiBaseUrl, - "/api/compass/themes/edit", - authToken, - args - ) - return { - content: [{ type: "text", text: JSON.stringify(result) }] - } - } - ) - ] -} diff --git a/packages/agent-server/src/mcp/ui-tools.ts b/packages/agent-server/src/mcp/ui-tools.ts deleted file mode 100644 index eeab718..0000000 --- a/packages/agent-server/src/mcp/ui-tools.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { tool } from "@anthropic-ai/claude-agent-sdk" -import { z } from "zod" - -const VALID_ROUTES: ReadonlyArray = [ - /^\/dashboard$/, - /^\/dashboard\/contacts$/, - /^\/dashboard\/customers$/, - /^\/dashboard\/vendors$/, - /^\/dashboard\/projects$/, - /^\/dashboard\/projects\/[^/]+$/, - /^\/dashboard\/projects\/[^/]+\/schedule$/, - /^\/dashboard\/financials$/, - /^\/dashboard\/people$/, - /^\/dashboard\/files$/, - /^\/dashboard\/files\/.+$/, - /^\/dashboard\/boards\/[^/]+$/, - /^\/dashboard\/conversations$/, - /^\/dashboard\/settings$/, -] - -function isValidRoute(path: string): boolean { - return VALID_ROUTES.some((r) => r.test(path)) -} - -/** Try to normalize a path to a valid dashboard route */ -function normalizePath(path: string): string { - let p = path.trim() - // Strip leading/trailing slashes for normalization - if (!p.startsWith("/dashboard")) { - // Try prepending /dashboard - p = p.startsWith("/") ? `/dashboard${p}` : `/dashboard/${p}` - } - return p -} - -export function uiTools() { - return [ - tool( - "navigateTo", - "Navigate the user to a page. Paths are relative to /dashboard — e.g. pass 'projects' or '/dashboard/projects'. Valid pages: projects, projects/{id}, projects/{id}/schedule, contacts, customers, vendors, financials, people, files, files/{path}, boards/{id}, conversations, settings.", - { - path: z.string().describe("Page path, e.g. 'projects' or '/dashboard/projects'"), - reason: z.string().optional().describe("Brief explanation of why navigating"), - }, - async (args) => { - const resolved = normalizePath(args.path) - if (!isValidRoute(resolved)) { - return { - content: [{ - type: "text", - text: JSON.stringify({ - error: - `"${args.path}" (resolved to "${resolved}") is not a valid page. ` + - "Valid: projects, projects/{id}, contacts, " + - "financials, people, files, boards/{id}", - }) - }] - } - } - return { - content: [{ - type: "text", - text: JSON.stringify({ - action: "navigate", - path: resolved, - reason: args.reason ?? null, - }) - }] - } - } - ), - - tool( - "showNotification", - "Show a toast notification to the user. Use for confirmations or important alerts.", - { - message: z.string().describe("The notification message"), - type: z.enum(["default", "success", "error"]).optional().describe("Notification style"), - }, - async (args) => { - return { - content: [{ - type: "text", - text: JSON.stringify({ - action: "toast", - message: args.message, - type: args.type ?? "default", - }) - }] - } - } - ), - - tool( - "generateUI", - "Generate a rich interactive UI dashboard. Use when the user wants to see structured data (tables, charts, stats, forms). Always fetch data with queryData first, then pass it here as dataContext.", - { - description: z.string().describe( - "Layout and content description for the dashboard to generate. Be specific about what components and data to display." - ), - dataContext: z.record(z.string(), z.unknown()).optional().describe( - "Data to include in the rendered UI. Pass query results here." - ), - }, - async (args) => { - return { - content: [{ - type: "text", - text: JSON.stringify({ - action: "generateUI", - renderPrompt: args.description, - dataContext: args.dataContext ?? {}, - }) - }] - } - } - ) - ] -} diff --git a/packages/agent-server/src/sessions.ts b/packages/agent-server/src/sessions.ts deleted file mode 100644 index 87d8377..0000000 --- a/packages/agent-server/src/sessions.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * In-memory session store for SDK session state - */ - -export interface SessionState { - sessionId: string - createdAt: Date - lastAccessedAt: Date - metadata?: Record -} - -const sessions = new Map() - -export function getOrCreateSession(sessionId: string): SessionState { - let session = sessions.get(sessionId) - - if (!session) { - session = { - sessionId, - createdAt: new Date(), - lastAccessedAt: new Date(), - } - sessions.set(sessionId, session) - } else { - session.lastAccessedAt = new Date() - } - - return session -} - -export function deleteSession(sessionId: string): boolean { - return sessions.delete(sessionId) -} - -export function getAllSessions(): SessionState[] { - return Array.from(sessions.values()) -} - -// Cleanup sessions older than 1 hour -export function cleanupStaleSessions(maxAgeMs: number = 3600000): number { - const now = Date.now() - let deleted = 0 - - for (const [sessionId, session] of sessions.entries()) { - if (now - session.lastAccessedAt.getTime() > maxAgeMs) { - sessions.delete(sessionId) - deleted++ - } - } - - return deleted -} - -// Run cleanup every 10 minutes -setInterval(() => { - const deleted = cleanupStaleSessions() - if (deleted > 0) { - console.log(`Cleaned up ${deleted} stale sessions`) - } -}, 600000) diff --git a/packages/agent-server/src/stream.ts b/packages/agent-server/src/stream.ts index f4bbb45..56a1169 100644 --- a/packages/agent-server/src/stream.ts +++ b/packages/agent-server/src/stream.ts @@ -1,34 +1,26 @@ /** - * SSE streaming wrapper for Anthropic Agent SDK - * - * The SDK's query() yields SDKMessage union types: - * - SDKAssistantMessage (type: "assistant") — completed message with content blocks - * - SDKPartialAssistantMessage (type: "stream_event") — streaming deltas - * - SDKResultMessage (type: "result") — final result with usage - * - SDKToolProgressMessage (type: "tool_progress") — tool execution status - * - SDKSystemMessage, SDKStatusMessage, etc. — internal, not forwarded - * - * We convert these into a flat SSE protocol the browser can consume: - * data: {"type":"text_delta","content":"Hello"} - * data: {"type":"tool_use","name":"queryData","toolCallId":"...","input":{}} - * data: {"type":"tool_result","toolCallId":"...","output":{}} - * data: {"type":"result","subtype":"success","result":"...","usage":{}} - * data: {"type":"error","error":"..."} - * data: [DONE] + * SSE streaming using agent-core's agentic loop with MCP-based + * tool routing. Supports both in-process compass tools and + * external MCP servers (stdio + HTTP). */ import { - query, - type SDKMessage, - type SDKUserMessage, -} from "@anthropic-ai/claude-agent-sdk" -import type { MessageParam } from "@anthropic-ai/sdk/resources" + runAgent, + buildSystemPrompt, + createSSEStream, + createCompassServer, + createClientManager, +} from "agent-core" import type { - BetaRawContentBlockStartEvent, - BetaRawContentBlockDeltaEvent, -} from "@anthropic-ai/sdk/resources/beta/messages/messages" -import type { AuthContext, ProviderConfig } from "./auth" -import { createCompassMcpServer } from "./mcp/compass-server" + DataSource, + ProviderConfig, + McpServerConfig, +} from "agent-core" +import type { + AuthContext, + ProviderConfig as JWTProvider, +} from "./auth" +import { compassApi } from "./api-client" import { config } from "./config" interface Message { @@ -36,383 +28,188 @@ interface Message { content: string } +interface ExternalServerConfig { + readonly name: string + readonly slug: string + readonly transport: "stdio" | "http" + readonly command?: string + readonly args?: string + readonly env?: string + readonly url?: string + readonly headers?: string +} + interface StreamContext { auth: AuthContext authToken: string sessionId: string currentPage: string timezone: string - provider?: ProviderConfig + provider?: JWTProvider model: string + externalServers?: readonly ExternalServerConfig[] } -/** - * Convert messages array to async generator (required by SDK when using MCP). - * Only yields the LAST user message — earlier conversation history is - * injected into the system prompt so the model has context without - * the SDK re-processing old turns. - */ -async function* createPromptGenerator( - messages: readonly Message[], - sessionId: string, -): AsyncGenerator { - // find the last user message - const lastUser = [...messages].reverse().find((m) => m.role === "user") - if (!lastUser) return - - const messageParam: MessageParam = { - role: "user", - content: lastUser.content, +function mapProvider(context: StreamContext): ProviderConfig { + if (!context.provider) { + return { + type: "anthropic", + apiKey: config.anthropicApiKey, + baseUrl: config.anthropicBaseUrl, + } } - yield { - type: "user" as const, - message: messageParam, - parent_tool_use_id: null, - session_id: sessionId, - } -} - -/** - * Build conversation history block for the system prompt. - * Includes all messages EXCEPT the last user message (which is - * yielded separately via the prompt generator). - */ -function buildConversationHistory(messages: readonly Message[]): string { - // everything except the final user message - const history = messages.slice(0, -1) - if (history.length === 0) return "" - - const lines = history.map((m) => { - const role = m.role === "user" ? "User" : "Assistant" - return `${role}: ${m.content}` - }) - - return `\n\nConversation so far:\n${lines.join("\n")}\n\nContinue the conversation naturally.` -} - -function buildSystemPrompt( - context: StreamContext, - messages: readonly Message[], -): string { - const history = buildConversationHistory(messages) - - return `You are Compass AI, an intelligent assistant for project management and collaboration. - -Current context: -- User ID: ${context.auth.userId} -- Organization: ${context.auth.orgId} -- Role: ${context.auth.role} -- Current page: ${context.currentPage} -- Timezone: ${context.timezone} - -You have access to tools via the compass MCP server for querying data, navigating the UI, managing schedules, themes, memories, skills, dashboards, and GitHub integration. - -When a tool returns an "action" field in its result, that action will be forwarded to the client for execution (navigation, toasts, UI generation, theme changes, etc.). You don't need to do anything extra — just call the tool and the action dispatches automatically.${history}` -} - -/** - * SSE helpers — each function returns a JSON string to send as a data: line - */ -function sseTextDelta(content: string): string { - return JSON.stringify({ type: "text", content }) -} - -function sseToolUse(name: string, toolCallId: string, input: unknown): string { - return JSON.stringify({ type: "tool_use", name, toolCallId, input }) -} - -function sseToolResult(toolCallId: string, output: unknown): string { - return JSON.stringify({ type: "tool_result", toolCallId, output }) -} - -function sseResult( - subtype: string, - result: string, - usage?: { inputTokens: number; outputTokens: number; totalCostUsd: number }, -): string { - return JSON.stringify({ type: "result", subtype, result, usage }) -} - -function sseError(error: string): string { - return JSON.stringify({ type: "error", error }) -} - -/** - * Extract SSE events from an SDKMessage. - * Returns an array because one SDK message can produce multiple SSE events - * (e.g. an assistant message with text + tool_use blocks). - */ -function extractSSEEvents(message: SDKMessage, emittedToolIds?: Set): string[] { - const events: string[] = [] - - switch (message.type) { - // Completed assistant message — SKIP all content blocks. - // Text is already streamed via stream_event text_delta. - // Tool use starts are already streamed via content_block_start. - // Tool results come through user messages (see case "user" below). - case "assistant": - break - - // Streaming delta — text chunks and tool use starts - case "stream_event": { - const event = message.event - if (!event) break - - if (event.type === "content_block_start") { - const startEvent = event as BetaRawContentBlockStartEvent - const block = startEvent.content_block - if (block && "type" in block) { - if ((block.type === "tool_use" || block.type === "mcp_tool_use") && "id" in block) { - const tb = block as { id: string; name: string; input: unknown } - if (!emittedToolIds?.has(tb.id)) { - emittedToolIds?.add(tb.id) - events.push(sseToolUse(tb.name, tb.id, tb.input ?? {})) - } - } - } + switch (context.provider.type) { + case "anthropic-key": + case "anthropic-oauth": + return { + type: "anthropic", + apiKey: context.provider.apiKey, + baseUrl: context.provider.baseUrl, } - - if (event.type === "content_block_delta") { - const deltaEvent = event as BetaRawContentBlockDeltaEvent - const delta = deltaEvent.delta - if (delta && "type" in delta) { - if (delta.type === "text_delta" && "text" in delta) { - events.push(sseTextDelta(delta.text as string)) - } - // input_json_delta — partial tool input, skip for now - // (the full input comes in content_block_start or assistant message) - } + case "openrouter": + return { + type: "openrouter", + apiKey: context.provider.apiKey, } - break - } - - // Final result - case "result": { - if (message.subtype === "success") { - events.push(sseResult( - "success", - (message as { result?: string }).result ?? "", - { - inputTokens: message.usage?.input_tokens ?? 0, - outputTokens: message.usage?.output_tokens ?? 0, - totalCostUsd: (message as { total_cost_usd?: number }).total_cost_usd ?? 0, - }, - )) - } else { - const errMsg = message as { errors?: string[] } - events.push(sseError( - errMsg.errors?.join("; ") ?? `Agent error: ${message.subtype}`, - )) + case "ollama": + return { + type: "ollama", + baseUrl: context.provider.baseUrl, } - break - } - - // Tool progress — optional, forward as status - case "tool_progress": { - const tp = message as { - tool_name: string - tool_use_id: string - elapsed_time_seconds: number - } - events.push(JSON.stringify({ - type: "tool_progress", - toolName: tp.tool_name, - toolCallId: tp.tool_use_id, - elapsedSeconds: tp.elapsed_time_seconds, - })) - break - } - - // User messages contain tool results from MCP tool execution. - // The SDK sends tool_result blocks inside user messages after - // executing MCP tools. We need to forward these to the client - // so action payloads (navigate, toast, etc.) get dispatched. - case "user": { - const userMsg = (message as { message?: { content?: unknown[] } }).message - if (!userMsg?.content || !Array.isArray(userMsg.content)) break - - for (const block of userMsg.content) { - const b = block as Record - if ( - (b.type === "tool_result" || b.type === "mcp_tool_result") && - typeof b.tool_use_id === "string" - ) { - // MCP tool results wrap content as [{type:"text", text:"..."}] - // Unwrap and parse the JSON text to get the actual output - const content = b.content as Array<{ type: string; text?: string }> | undefined - let output: unknown = content - if (Array.isArray(content) && content.length === 1 && content[0].type === "text" && content[0].text) { - try { - output = JSON.parse(content[0].text) - } catch { - output = content[0].text - } - } - events.push(sseToolResult(b.tool_use_id as string, output)) - } - } - break - } - - // Internal SDK messages — skip - case "system": default: - break + return { + type: "custom", + apiKey: context.provider.apiKey, + baseUrl: context.provider.baseUrl, + } } - - return events } -/** - * Build a clean env for the Claude Code subprocess. - * The SDK spawns the claude CLI as a child process, which reads - * ANTHROPIC_API_KEY from its env. We also need to unset CLAUDECODE - * to avoid the nested-session guard. - */ -function buildSubprocessEnv(provider?: ProviderConfig): Record { - const env: Record = { ...process.env } +function buildExternalConfigs( + servers?: readonly ExternalServerConfig[] +): McpServerConfig[] { + if (!servers) return [] - // Unset nested-session guard - delete env.CLAUDECODE - - // Handle provider-specific configuration - if (provider) { - switch (provider.type) { - case "anthropic-oauth": - // Use OAuth credentials from ~/.claude/.credentials.json - delete env.ANTHROPIC_API_KEY - delete env.ANTHROPIC_BASE_URL - break - - case "anthropic-key": - // Direct Anthropic API with user's key - if (provider.apiKey) { - env.ANTHROPIC_API_KEY = provider.apiKey - } - delete env.ANTHROPIC_BASE_URL - break - - case "openrouter": - // OpenRouter proxy - env.ANTHROPIC_BASE_URL = "https://openrouter.ai/api" - env.ANTHROPIC_AUTH_TOKEN = provider.apiKey || "" - env.ANTHROPIC_API_KEY = "" - break - - case "ollama": - // Local Ollama instance - if (provider.baseUrl) { - env.ANTHROPIC_BASE_URL = provider.baseUrl - } - env.ANTHROPIC_API_KEY = "ollama" - break - - case "custom": - // Custom endpoint (e.g., LiteLLM, vLLM) - if (provider.baseUrl) { - env.ANTHROPIC_BASE_URL = provider.baseUrl - } - if (provider.apiKey) { - env.ANTHROPIC_API_KEY = provider.apiKey - } - break + const configs: McpServerConfig[] = [] + for (const s of servers) { + if (s.transport === "stdio" && s.command) { + const args = s.args + ? (JSON.parse(s.args) as string[]) + : undefined + const env = s.env + ? (JSON.parse(s.env) as Record) + : undefined + configs.push({ + name: s.slug, + transport: { + type: "stdio", + command: s.command, + args, + env, + }, + enabled: true, + }) + } else if (s.transport === "http" && s.url) { + const headers = s.headers + ? (JSON.parse(s.headers) as Record) + : undefined + configs.push({ + name: s.slug, + transport: { + type: "http", + url: s.url, + headers, + }, + enabled: true, + }) } - - // Apply model overrides if provided - if (provider.modelOverrides) { - if (provider.modelOverrides.sonnet) { - env.ANTHROPIC_DEFAULT_SONNET_MODEL = provider.modelOverrides.sonnet - } - if (provider.modelOverrides.opus) { - env.ANTHROPIC_DEFAULT_OPUS_MODEL = provider.modelOverrides.opus - } - if (provider.modelOverrides.haiku) { - env.ANTHROPIC_DEFAULT_HAIKU_MODEL = provider.modelOverrides.haiku - } - } - } else if (config.anthropicApiKey) { - // Fallback to server-configured API key - env.ANTHROPIC_API_KEY = config.anthropicApiKey - if (config.anthropicBaseUrl) { - env.ANTHROPIC_BASE_URL = config.anthropicBaseUrl - } - } else { - // No provider or config — use OAuth - delete env.ANTHROPIC_API_KEY - delete env.ANTHROPIC_BASE_URL } - - return env + return configs } -/** - * Create SSE stream from SDK query() - */ export async function createAgentStream( messages: Message[], - context: StreamContext, + context: StreamContext ): Promise> { - const encoder = new TextEncoder() + const provider = mapProvider(context) - return new ReadableStream({ + const dataSource: DataSource = { + async fetch( + path: string, + body?: unknown + ): Promise { + return compassApi( + config.compassApiBaseUrl, + path, + context.authToken, + body + ) + }, + } + + // Set up MCP-based tool routing + const compassServer = createCompassServer(dataSource) + const manager = createClientManager(compassServer) + + const mcpConfigs: McpServerConfig[] = [ + { + name: "compass", + transport: { type: "in-memory" }, + enabled: true, + }, + ...buildExternalConfigs(context.externalServers), + ] + + await manager.connect(mcpConfigs) + + // Identify external tools for system prompt + const allTools = manager.listTools() + const externalMcpTools = allTools + .filter((t) => t.serverName !== "compass") + .map((t) => ({ + serverName: t.serverName, + name: t.name, + })) + + const systemPrompt = buildSystemPrompt({ + context: { + userId: context.auth.userId, + orgId: context.auth.orgId, + role: context.auth.role, + isDemoUser: context.auth.isDemoUser, + currentPage: context.currentPage, + timezone: context.timezone, + }, + messages, + externalMcpTools: + externalMcpTools.length > 0 + ? externalMcpTools + : undefined, + }) + + const agentStream = runAgent({ + provider, + model: context.model, + systemPrompt, + messages, + mcpClientManager: manager, + }) + + // Wrap to disconnect MCP after stream ends + const sseStream = createSSEStream(agentStream) + return new ReadableStream({ async start(controller) { + const reader = sseStream.getReader() try { - const promptGen = createPromptGenerator(messages, context.sessionId) - - const compassMcpServer = createCompassMcpServer( - config.compassApiBaseUrl, - context.authToken, - ) - - const subprocessEnv = buildSubprocessEnv(context.provider) - - // Use a unique session ID per query() call. The SDK passes this - // to the Claude Code CLI subprocess, which uses it for session - // state/locks. Reusing the same ID across calls causes the - // subprocess to crash with exit code 1 on subsequent requests. - // Conversation continuity is handled via system prompt history. - const querySessionId = crypto.randomUUID() - - const stream = query({ - prompt: promptGen, - options: { - systemPrompt: buildSystemPrompt(context, messages), - model: context.model, - env: subprocessEnv, - settingSources: [], - mcpServers: { - compass: compassMcpServer, - }, - allowedTools: ["mcp__compass__*"], - maxTurns: 25, - permissionMode: "bypassPermissions", - allowDangerouslySkipPermissions: true, - tools: [], - includePartialMessages: true, - sessionId: querySessionId, - }, - }) - - // Track emitted tool IDs to prevent duplicates. - // The SDK can emit the same tool call as both tool_use and - // mcp_tool_use in content_block_start events. - const emittedToolIds = new Set() - - for await (const message of stream) { - const events = extractSSEEvents(message, emittedToolIds) - for (const event of events) { - controller.enqueue(encoder.encode(`data: ${event}\n\n`)) - } + while (true) { + const { done, value } = await reader.read() + if (done) break + controller.enqueue(value) } - - controller.enqueue(encoder.encode("data: [DONE]\n\n")) - controller.close() - } catch (err) { - console.error("Agent stream error:", err) - controller.enqueue(encoder.encode(`data: ${sseError(String(err))}\n\n`)) - controller.enqueue(encoder.encode("data: [DONE]\n\n")) + } finally { controller.close() + await manager.disconnect() } }, }) diff --git a/src/app/actions/mcp-servers.ts b/src/app/actions/mcp-servers.ts new file mode 100644 index 0000000..28dffd9 --- /dev/null +++ b/src/app/actions/mcp-servers.ts @@ -0,0 +1,192 @@ +"use server" + +import { getCloudflareContext } from "@opennextjs/cloudflare" +import { getDb } from "@/db" +import { mcpServers } from "@/db/schema-mcp" +import type { McpServer } from "@/db/schema-mcp" +import { eq, and } from "drizzle-orm" +import { getCurrentUser } from "@/lib/auth" +import { revalidatePath } from "next/cache" + +type CreateMcpServerData = { + readonly name: string + readonly slug: string + readonly transport: string + readonly command?: string + readonly args?: string + readonly env?: string + readonly url?: string + readonly headers?: string +} + +type UpdateMcpServerData = Partial + +export async function getMcpServers(): Promise< + | { success: true; data: ReadonlyArray } + | { success: false; error: string } +> { + try { + const user = await getCurrentUser() + if (!user) return { success: false, error: "Unauthorized" } + if (!user.organizationId) return { success: false, error: "No organization" } + + const { env } = await getCloudflareContext() + const db = getDb(env.DB) + + const rows = await db + .select() + .from(mcpServers) + .where(eq(mcpServers.orgId, user.organizationId)) + .all() + + return { success: true, data: rows } + } catch (error) { + console.error("getMcpServers failed:", error) + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } +} + +export async function createMcpServer( + data: CreateMcpServerData +): Promise<{ success: true; data: McpServer } | { success: false; error: string }> { + try { + const user = await getCurrentUser() + if (!user) return { success: false, error: "Unauthorized" } + if (!user.organizationId) return { success: false, error: "No organization" } + + const { env } = await getCloudflareContext() + const db = getDb(env.DB) + + const row: McpServer = { + id: crypto.randomUUID(), + orgId: user.organizationId, + name: data.name, + slug: data.slug, + transport: data.transport, + command: data.command ?? null, + args: data.args ?? null, + env: data.env ?? null, + url: data.url ?? null, + headers: data.headers ?? null, + isEnabled: true, + createdAt: new Date().toISOString(), + createdBy: user.id, + } + + await db.insert(mcpServers).values(row).run() + revalidatePath("/dashboard/settings") + + return { success: true, data: row } + } catch (error) { + console.error("createMcpServer failed:", error) + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } +} + +export async function updateMcpServer( + id: string, + data: UpdateMcpServerData +): Promise<{ success: true } | { success: false; error: string }> { + try { + const user = await getCurrentUser() + if (!user) return { success: false, error: "Unauthorized" } + if (!user.organizationId) return { success: false, error: "No organization" } + + const { env } = await getCloudflareContext() + const db = getDb(env.DB) + + const existing = await db + .select({ id: mcpServers.id }) + .from(mcpServers) + .where(and(eq(mcpServers.id, id), eq(mcpServers.orgId, user.organizationId))) + .get() + + if (!existing) return { success: false, error: "Server not found" } + + await db.update(mcpServers).set(data).where(eq(mcpServers.id, id)).run() + revalidatePath("/dashboard/settings") + + return { success: true } + } catch (error) { + console.error("updateMcpServer failed:", error) + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } +} + +export async function deleteMcpServer( + id: string +): Promise<{ success: true } | { success: false; error: string }> { + try { + const user = await getCurrentUser() + if (!user) return { success: false, error: "Unauthorized" } + if (!user.organizationId) return { success: false, error: "No organization" } + + const { env } = await getCloudflareContext() + const db = getDb(env.DB) + + const existing = await db + .select({ id: mcpServers.id }) + .from(mcpServers) + .where(and(eq(mcpServers.id, id), eq(mcpServers.orgId, user.organizationId))) + .get() + + if (!existing) return { success: false, error: "Server not found" } + + await db.delete(mcpServers).where(eq(mcpServers.id, id)).run() + revalidatePath("/dashboard/settings") + + return { success: true } + } catch (error) { + console.error("deleteMcpServer failed:", error) + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } +} + +export async function toggleMcpServer( + id: string, + enabled: boolean +): Promise<{ success: true } | { success: false; error: string }> { + try { + const user = await getCurrentUser() + if (!user) return { success: false, error: "Unauthorized" } + if (!user.organizationId) return { success: false, error: "No organization" } + + const { env } = await getCloudflareContext() + const db = getDb(env.DB) + + const existing = await db + .select({ id: mcpServers.id }) + .from(mcpServers) + .where(and(eq(mcpServers.id, id), eq(mcpServers.orgId, user.organizationId))) + .get() + + if (!existing) return { success: false, error: "Server not found" } + + await db + .update(mcpServers) + .set({ isEnabled: enabled }) + .where(eq(mcpServers.id, id)) + .run() + revalidatePath("/dashboard/settings") + + return { success: true } + } catch (error) { + console.error("toggleMcpServer failed:", error) + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } +} diff --git a/src/app/api/agent/route.ts b/src/app/api/agent/route.ts new file mode 100644 index 0000000..e41ef73 --- /dev/null +++ b/src/app/api/agent/route.ts @@ -0,0 +1,297 @@ +/** + * Cloud-mode agent API route. + * Runs on Cloudflare Workers via OpenNext. Uses agent-core + * for the agentic loop with MCP-based tool routing. + */ + +import { getCurrentUser } from "@/lib/auth" +import { getProviderConfigForJwt } from "@/app/actions/provider-config" +import { generateAgentToken } from "@/lib/agent/api-auth" +import { getCloudflareContext } from "@opennextjs/cloudflare" +import { getDb } from "@/db" +import { mcpServers } from "@/db/schema-mcp" +import { eq } from "drizzle-orm" +import { + runAgent, + buildSystemPrompt, + createSSEStream, + createCompassServer, + createClientManager, +} from "agent-core" +import type { + DataSource, + ProviderConfig, + McpServerConfig, +} from "agent-core" + +interface ChatRequest { + readonly messages: ReadonlyArray<{ + readonly role: "user" | "assistant" + readonly content: string + }> +} + +function mapProviderType( + type: string +): ProviderConfig["type"] { + switch (type) { + case "anthropic-key": + case "anthropic-oauth": + return "anthropic" + case "openrouter": + return "openrouter" + case "ollama": + return "ollama" + default: + return "custom" + } +} + +export async function POST( + request: Request +): Promise { + const user = await getCurrentUser() + if (!user) { + return new Response( + JSON.stringify({ error: "Unauthorized" }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + } + ) + } + + let body: ChatRequest + try { + body = await request.json() + } catch { + return new Response( + JSON.stringify({ error: "Invalid JSON body" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ) + } + + if ( + !Array.isArray(body.messages) || + body.messages.length === 0 + ) { + return new Response( + JSON.stringify({ + error: + "messages array is required and cannot be empty", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ) + } + + const model = + request.headers.get("x-model") ?? "sonnet" + const currentPage = + request.headers.get("x-current-page") ?? "/dashboard" + const timezone = + request.headers.get("x-timezone") ?? "UTC" + + // Resolve provider config from DB + let providerConfig = await getProviderConfigForJwt( + user.id + ) + if (!providerConfig) { + providerConfig = await getProviderConfigForJwt( + "org_default" + ) + } + + const provider: ProviderConfig = providerConfig + ? { + type: mapProviderType(providerConfig.type), + apiKey: providerConfig.apiKey ?? undefined, + baseUrl: providerConfig.baseUrl ?? undefined, + modelOverrides: + providerConfig.modelOverrides ?? undefined, + } + : { type: "anthropic" } + + // Generate JWT for bridge route auth + const { env } = await getCloudflareContext() + const envRecord = env as unknown as Record< + string, + string + > + const agentSecret = envRecord.AGENT_AUTH_SECRET + if (!agentSecret) { + return new Response( + JSON.stringify({ + error: "AGENT_AUTH_SECRET not configured", + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ) + } + + const token = await generateAgentToken( + agentSecret, + user.id, + user.organizationId ?? "", + user.role, + false + ) + + const baseUrl = + envRecord.COMPASS_API_BASE_URL ?? + request.headers.get("origin") ?? + "" + + const dataSource: DataSource = { + async fetch( + path: string, + fetchBody?: unknown + ): Promise { + const res = await fetch(`${baseUrl}${path}`, { + method: fetchBody ? "POST" : "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: fetchBody + ? JSON.stringify(fetchBody) + : undefined, + }) + if (!res.ok) { + const err = await res + .json() + .catch(() => ({ error: res.statusText })) + const errObj = err as { error?: string } + throw new Error( + errObj.error ?? `API error ${res.status}` + ) + } + return res.json() + }, + } + + // Set up MCP-based tool routing + const compassServer = createCompassServer(dataSource) + const manager = createClientManager(compassServer) + + // Load external MCP servers from DB (HTTP only on Workers) + const mcpConfigs: McpServerConfig[] = [ + { + name: "compass", + transport: { type: "in-memory" }, + enabled: true, + }, + ] + + if (user.organizationId) { + try { + const db = getDb(env.DB) + const rows = await db + .select() + .from(mcpServers) + .where(eq(mcpServers.orgId, user.organizationId)) + .all() + + for (const row of rows) { + if (!row.isEnabled) continue + // Workers can't spawn processes — skip stdio + if (row.transport === "stdio") continue + if (row.transport === "http" && row.url) { + const headers = row.headers + ? (JSON.parse(row.headers) as Record< + string, + string + >) + : undefined + mcpConfigs.push({ + name: row.slug, + transport: { + type: "http", + url: row.url, + headers, + }, + enabled: true, + }) + } + } + } catch (err) { + console.error( + "Failed to load external MCP servers:", + err + ) + } + } + + await manager.connect(mcpConfigs) + + // Identify external tools for system prompt + const allTools = manager.listTools() + const externalMcpTools = allTools + .filter((t) => t.serverName !== "compass") + .map((t) => ({ + serverName: t.serverName, + name: t.name, + })) + + const msgs = body.messages as Array<{ + role: "user" | "assistant" + content: string + }> + + const systemPrompt = buildSystemPrompt({ + context: { + userId: user.id, + orgId: user.organizationId ?? "", + role: user.role, + isDemoUser: false, + currentPage, + timezone, + }, + messages: msgs, + externalMcpTools: + externalMcpTools.length > 0 + ? externalMcpTools + : undefined, + }) + + const stream = runAgent({ + provider, + model, + systemPrompt, + messages: msgs, + mcpClientManager: manager, + }) + + // Wrap stream to disconnect MCP after completion + const sseStream = createSSEStream(stream) + const wrappedStream = new ReadableStream({ + async start(controller) { + const reader = sseStream.getReader() + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + controller.enqueue(value) + } + } finally { + controller.close() + await manager.disconnect() + } + }, + }) + + return new Response(wrappedStream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }) +} diff --git a/src/db/schema-mcp.ts b/src/db/schema-mcp.ts index b67292b..74e1be6 100644 --- a/src/db/schema-mcp.ts +++ b/src/db/schema-mcp.ts @@ -3,7 +3,7 @@ import { text, integer, } from "drizzle-orm/sqlite-core" -import { users } from "./schema" +import { users, organizations } from "./schema" export const mcpApiKeys = sqliteTable("mcp_api_keys", { id: text("id").primaryKey(), @@ -41,3 +41,28 @@ export type McpApiKey = typeof mcpApiKeys.$inferSelect export type NewMcpApiKey = typeof mcpApiKeys.$inferInsert export type McpUsage = typeof mcpUsage.$inferSelect export type NewMcpUsage = typeof mcpUsage.$inferInsert + +export const mcpServers = sqliteTable("mcp_servers", { + id: text("id").primaryKey(), + orgId: text("org_id") + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + name: text("name").notNull(), + slug: text("slug").notNull(), // used as tool name prefix + transport: text("transport").notNull(), // "stdio" | "http" + command: text("command"), // stdio only + args: text("args"), // stdio: JSON array + env: text("env"), // stdio: JSON env vars + url: text("url"), // http only + headers: text("headers"), // http: JSON headers + isEnabled: integer("is_enabled", { mode: "boolean" }) + .notNull() + .default(true), + createdAt: text("created_at").notNull(), + createdBy: text("created_by") + .notNull() + .references(() => users.id), +}) + +export type McpServer = typeof mcpServers.$inferSelect +export type NewMcpServer = typeof mcpServers.$inferInsert diff --git a/src/hooks/use-agent.ts b/src/hooks/use-agent.ts index b926a09..3cd2fb2 100644 --- a/src/hooks/use-agent.ts +++ b/src/hooks/use-agent.ts @@ -31,7 +31,7 @@ export interface UseAgentReturn { */ export function useAgent(options: UseAgentOptions = {}): UseAgentReturn { const { - agentServerUrl = "http://localhost:3001", + agentServerUrl = "", sessionId = crypto.randomUUID(), currentPage = "/dashboard", timezone = Intl.DateTimeFormat().resolvedOptions().timeZone, @@ -78,27 +78,36 @@ export function useAgent(options: UseAgentOptions = {}): UseAgentReturn { abortControllerRef.current = controller try { - // get auth token from server action - const { getAgentToken } = await import("@/app/actions/agent-auth") - const tokenResult = await getAgentToken() + // Determine endpoint based on mode + const isStandalone = agentServerUrl !== "" + const endpoint = isStandalone + ? `${agentServerUrl}/agent/chat` + : `/api/agent` - if ("error" in tokenResult) { - throw new Error(tokenResult.error) + const headers: Record = { + "Content-Type": "application/json", + "x-session-id": sessionId, + "x-current-page": currentPage, + "x-timezone": timezone, + "x-model": getAgentModelId(), + } + + // Standalone mode: JWT auth via server action + // Cloud mode: WorkOS session cookie (same-origin, automatic) + if (isStandalone) { + const { getAgentToken } = await import("@/app/actions/agent-auth") + const tokenResult = await getAgentToken() + if ("error" in tokenResult) { + throw new Error(tokenResult.error) + } + headers["Authorization"] = `Bearer ${tokenResult.token}` } const allMessages = [...messages, userMessage] - // POST to agent server - const response = await fetch(`${agentServerUrl}/agent/chat`, { + const response = await fetch(endpoint, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${tokenResult.token}`, - "x-session-id": sessionId, - "x-current-page": currentPage, - "x-timezone": timezone, - "x-model": getAgentModelId(), - }, + headers, body: JSON.stringify({ messages: allMessages.map((m) => ({ role: m.role, diff --git a/src/hooks/use-compass-chat.ts b/src/hooks/use-compass-chat.ts index af50500..82c8aac 100755 --- a/src/hooks/use-compass-chat.ts +++ b/src/hooks/use-compass-chat.ts @@ -34,7 +34,11 @@ export function useCompassChat(options?: UseCompassChatOptions) { // use the new agent hook const agent = useAgent({ - agentServerUrl: "http://localhost:3001", + agentServerUrl: + typeof window !== "undefined" && + "__TAURI__" in window + ? "http://localhost:3001" + : "", sessionId: options?.conversationId ?? undefined, currentPage: pathname, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,