feat(agent): MCP-based tool architecture

Extract agent-core as shared package with agentic loop,
tool definitions, and MCP integration. Compass tools
wrapped as MCP server using low-level Server API. Client
manager connects multiple MCP servers (in-memory, stdio,
HTTP) with unified tool routing. External MCP server
configs stored in DB with CRUD actions. Both Workers and
Bun runtimes use the new MCP client manager.
This commit is contained in:
Nicholai Vogel 2026-02-16 20:14:57 -07:00
parent 7ee5304176
commit 3f8d273986
46 changed files with 2972 additions and 1364 deletions

View File

@ -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=="],

View File

@ -1,6 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["agent-core"],
eslint: {
ignoreDuringBuilds: true,
},

View File

@ -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",

View File

@ -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=="],
}
}

View File

@ -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"
}
}

View File

@ -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 } : {}),
})
}
}

View File

@ -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"

View File

@ -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<SSEData> {
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> | 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,
}
}

View File

@ -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<string>
}
interface ToolEntry {
readonly name: string
readonly description: string
readonly input_schema: Record<string, unknown>
readonly serverName: string
readonly originalName: string
}
const COMPASS_SERVER_NAME = "compass"
export function createClientManager(
compassServer?: Server
): McpClientManager {
const servers = new Map<string, ConnectedServer>()
const toolIndex = new Map<string, { serverName: string; originalName: string }>()
let cachedTools: ToolEntry[] = []
async function connectServer(
config: McpServerConfig
): Promise<void> {
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<string>()
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<void> {
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<string, unknown>) ??
{},
serverName,
originalName: tool.name,
})
}
} catch (err) {
console.error(
`Failed to list tools from "${serverName}":`,
err
)
}
}
},
listTools(): Array<{
name: string
description: string
input_schema: Record<string, unknown>
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<string> {
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<string, unknown>,
})
// 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<void> {
for (const [, server] of servers) {
try {
await server.client.close()
} catch {
// ignore close errors
}
}
servers.clear()
toolIndex.clear()
cachedTools = []
},
}
}

View File

@ -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<string, unknown>), 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<string, object> {
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<string, unknown>,
): { type: "object"; properties?: Record<string, object>; 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<CallToolResult> => {
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
}

View File

@ -0,0 +1,6 @@
export { createCompassServer } from "./compass-server"
export { createClientManager } from "./client-manager"
export type {
McpServerConfig,
McpClientManager,
} from "./types"

View File

@ -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<Record<string, string>>
}
| {
readonly type: "http"
readonly url: string
readonly headers?: Readonly<Record<string, string>>
}
readonly enabled: boolean
}
export interface McpClientManager {
connect(configs: readonly McpServerConfig[]): Promise<void>
listTools(): Array<{
name: string
description: string
input_schema: Record<string, unknown>
serverName: string
}>
callTool(name: string, input: unknown): Promise<string>
disconnect(): Promise<void>
}

View File

@ -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<SSEData>
): ReadableStream<Uint8Array> {
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()
}
},
})
}

View File

@ -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<string, string[]>()
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.`
}

View File

@ -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<string> => {
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<string> => {
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<string> => {
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<string> => {
const args = deleteSchema.parse(input)
const result = await dataSource.fetch(
"/api/compass/dashboards/delete",
args
)
return JSON.stringify(result)
},
},
]
}

View File

@ -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<string, unknown>
readonly run: (input: unknown) => Promise<string>
}
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<typeof queryDataSchema>
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<string> => {
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<z.ZodRawShape>
): Record<string, unknown> {
const jsonSchema = zodObjectToProperties(schema)
return {
type: "object",
properties: jsonSchema.properties,
required: jsonSchema.required,
}
}
function zodObjectToProperties(schema: z.ZodObject<z.ZodRawShape>): {
properties: Record<string, unknown>
required: string[]
} {
const shape = schema.shape
const properties: Record<string, unknown> = {}
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<string, unknown> {
const desc = schema.description
if (schema instanceof z.ZodString) {
return { type: "string", ...(desc ? { description: desc } : {}) }
}
if (schema instanceof z.ZodNumber) {
const result: Record<string, unknown> = {
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 } : {}) }
}

View File

@ -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<string> => {
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<string> => {
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<string> => {
const args = interviewSchema.parse(input)
const result = await dataSource.fetch(
"/api/compass/github/save-interview",
args
)
return JSON.stringify(result)
},
},
]
}

View File

@ -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),
]
}

View File

@ -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<string> => {
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<string> => {
const args = recallSchema.parse(input)
const result = await dataSource.fetch(
"/api/compass/memory/recall",
args
)
return JSON.stringify(result)
},
},
]
}

View File

@ -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<string> => {
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<string> => {
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<string> => {
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<string> => {
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<string> => {
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<string> => {
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<string> => {
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<string> => {
const args = removeExceptionSchema.parse(input)
const result = await dataSource.fetch(
"/api/compass/schedule/remove-exception",
args
)
return JSON.stringify(result)
},
},
]
}

View File

@ -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<string> => {
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<string> => {
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<string> => {
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<string> => {
const args = uninstallSchema.parse(input)
const result = await dataSource.fetch(
"/api/compass/skills/uninstall",
args
)
return JSON.stringify(result)
},
},
]
}

View File

@ -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<string> => {
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<string> => {
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<string> => {
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<string> => {
const args = editThemeSchema.parse(input)
const result = await dataSource.fetch(
"/api/compass/themes/edit",
args
)
return JSON.stringify(result)
},
},
]
}

View File

@ -0,0 +1,144 @@
import { z } from "zod"
import type { ToolDef } from "./data"
import { zodToJsonSchema } from "./data"
const VALID_ROUTES: ReadonlyArray<RegExp> = [
/^\/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<typeof navigateSchema>
type NotificationInput = z.infer<typeof notificationSchema>
type GenerateUIInput = z.infer<typeof generateUISchema>
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<string> => {
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<string> => {
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<string> => {
const args = generateUISchema.parse(
input
) as GenerateUIInput
return JSON.stringify({
action: "generateUI",
renderPrompt: args.description,
dataContext: args.dataContext ?? {},
})
},
},
]
}

View File

@ -0,0 +1,46 @@
export interface ProviderConfig {
readonly type: "anthropic" | "openrouter" | "ollama" | "custom"
readonly apiKey?: string
readonly baseUrl?: string
readonly modelOverrides?: Readonly<Record<string, string>>
}
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<unknown>
}
/** 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 }

View File

@ -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"]
}

View File

@ -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=="],

View File

@ -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"
},

View File

@ -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<Response> {
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 {

View File

@ -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),
]
})
}

View File

@ -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) }]
}
}
)
]
}

View File

@ -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) }]
}
}
)
]
}

View File

@ -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) }]
}
}
)
]
}

View File

@ -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"

View File

@ -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) }]
}
}
)
]
}

View File

@ -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) }]
}
}
)
]
}

View File

@ -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) }]
}
}
)
]
}

View File

@ -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) }]
}
}
)
]
}

View File

@ -1,119 +0,0 @@
import { tool } from "@anthropic-ai/claude-agent-sdk"
import { z } from "zod"
const VALID_ROUTES: ReadonlyArray<RegExp> = [
/^\/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 ?? {},
})
}]
}
}
)
]
}

View File

@ -1,60 +0,0 @@
/**
* In-memory session store for SDK session state
*/
export interface SessionState {
sessionId: string
createdAt: Date
lastAccessedAt: Date
metadata?: Record<string, unknown>
}
const sessions = new Map<string, SessionState>()
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)

View File

@ -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<SDKUserMessage> {
// 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,
}
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>): 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 ?? {}))
}
}
function mapProvider(context: StreamContext): ProviderConfig {
if (!context.provider) {
return {
type: "anthropic",
apiKey: config.anthropicApiKey,
baseUrl: config.anthropicBaseUrl,
}
}
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)
}
}
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}`,
))
}
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<string, unknown>
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 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<string, string | undefined> {
const env: Record<string, string | undefined> = { ...process.env }
// 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
switch (context.provider.type) {
case "anthropic-key":
// Direct Anthropic API with user's key
if (provider.apiKey) {
env.ANTHROPIC_API_KEY = provider.apiKey
case "anthropic-oauth":
return {
type: "anthropic",
apiKey: context.provider.apiKey,
baseUrl: context.provider.baseUrl,
}
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
return {
type: "openrouter",
apiKey: context.provider.apiKey,
}
case "ollama":
// Local Ollama instance
if (provider.baseUrl) {
env.ANTHROPIC_BASE_URL = provider.baseUrl
return {
type: "ollama",
baseUrl: context.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
}
// 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
default:
return {
type: "custom",
apiKey: context.provider.apiKey,
baseUrl: context.provider.baseUrl,
}
}
} 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
}
/**
* Create SSE stream from SDK query()
*/
function buildExternalConfigs(
servers?: readonly ExternalServerConfig[]
): McpServerConfig[] {
if (!servers) return []
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<string, string>)
: 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<string, string>)
: undefined
configs.push({
name: s.slug,
transport: {
type: "http",
url: s.url,
headers,
},
enabled: true,
})
}
}
return configs
}
export async function createAgentStream(
messages: Message[],
context: StreamContext,
context: StreamContext
): Promise<ReadableStream<Uint8Array>> {
const encoder = new TextEncoder()
const provider = mapProvider(context)
return new ReadableStream({
async start(controller) {
try {
const promptGen = createPromptGenerator(messages, context.sessionId)
const compassMcpServer = createCompassMcpServer(
const dataSource: DataSource = {
async fetch(
path: string,
body?: unknown
): Promise<unknown> {
return compassApi(
config.compassApiBaseUrl,
path,
context.authToken,
body
)
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,
}
// 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,
})
// 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<string>()
const agentStream = runAgent({
provider,
model: context.model,
systemPrompt,
messages,
mcpClientManager: manager,
})
for await (const message of stream) {
const events = extractSSEEvents(message, emittedToolIds)
for (const event of events) {
controller.enqueue(encoder.encode(`data: ${event}\n\n`))
// Wrap to disconnect MCP after stream ends
const sseStream = createSSEStream(agentStream)
return new ReadableStream<Uint8Array>({
async start(controller) {
const reader = sseStream.getReader()
try {
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()
}
},
})

View File

@ -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<CreateMcpServerData>
export async function getMcpServers(): Promise<
| { success: true; data: ReadonlyArray<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 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",
}
}
}

297
src/app/api/agent/route.ts Normal file
View File

@ -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<Response> {
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<unknown> {
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<Uint8Array>({
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",
},
})
}

View File

@ -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

View File

@ -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 allMessages = [...messages, userMessage]
// POST to agent server
const response = await fetch(`${agentServerUrl}/agent/chat`, {
method: "POST",
headers: {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenResult.token}`,
"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]
const response = await fetch(endpoint, {
method: "POST",
headers,
body: JSON.stringify({
messages: allMessages.map((m) => ({
role: m.role,

View File

@ -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,