From c24d4d0ac470d42e32ea063f011d252cb6975cb9 Mon Sep 17 00:00:00 2001 From: Yash Dewasthale Date: Thu, 13 Aug 2026 18:46:28 +0530 Subject: [PATCH] feat: introduce Cortex SDK with CLI for AI agent development: - Added Cortex SDK for unified access to AI models and tools, including multiple providers. - Implemented a CLI for module management, allowing users to initialize, add, remove, and update modules easily. - Created a comprehensive README and configuration management for user-friendly setup. - Included TypeScript support with appropriate configurations and build scripts. --- bun.lock | 315 +++++++---- packages/cortex-sdk/.gitignore | 3 + packages/cortex-sdk/README.md | 27 + packages/cortex-sdk/cli/src/catalog.ts | 220 ++++++++ packages/cortex-sdk/cli/src/commands/add.ts | 28 + packages/cortex-sdk/cli/src/commands/find.ts | 84 +++ packages/cortex-sdk/cli/src/commands/flow.ts | 129 +++++ packages/cortex-sdk/cli/src/commands/init.ts | 44 ++ packages/cortex-sdk/cli/src/commands/list.ts | 29 + .../cortex-sdk/cli/src/commands/remove.ts | 24 + .../cortex-sdk/cli/src/commands/update.ts | 40 ++ packages/cortex-sdk/cli/src/config.ts | 125 +++++ packages/cortex-sdk/cli/src/index.ts | 86 +++ packages/cortex-sdk/cli/src/tui/banner.ts | 12 + packages/cortex-sdk/cli/src/tui/input.ts | 68 +++ .../cortex-sdk/cli/src/tui/multiselect.ts | 108 ++++ packages/cortex-sdk/cli/src/tui/renderer.ts | 67 +++ packages/cortex-sdk/cli/src/tui/select.ts | 67 +++ packages/cortex-sdk/package.json | 76 +++ .../src/agent-handler/client.test.ts | 219 ++++++++ .../cortex-sdk/src/agent-handler/client.ts | 137 +++++ .../cortex-sdk/src/agent-handler/index.ts | 13 + .../src/agent-handler/tool-packs.test.ts | 110 ++++ .../src/agent-handler/tool-packs.ts | 107 ++++ .../cortex-sdk/src/agent-handler/types.ts | 37 ++ packages/cortex-sdk/src/composio/apps.ts | 88 +++ .../cortex-sdk/src/composio/client.test.ts | 317 +++++++++++ packages/cortex-sdk/src/composio/index.ts | 178 ++++++ packages/cortex-sdk/src/composio/oauth.ts | 41 ++ packages/cortex-sdk/src/composio/session.ts | 274 ++++++++++ packages/cortex-sdk/src/composio/types.ts | 76 +++ packages/cortex-sdk/src/core/errors.ts | 40 ++ packages/cortex-sdk/src/core/types.ts | 71 +++ packages/cortex-sdk/src/gateway/base.test.ts | 131 +++++ packages/cortex-sdk/src/gateway/base.ts | 273 ++++++++++ .../cortex-sdk/src/gateway/concentrateai.ts | 44 ++ packages/cortex-sdk/src/gateway/gemini.ts | 60 ++ packages/cortex-sdk/src/gateway/index.test.ts | 121 +++++ packages/cortex-sdk/src/gateway/index.ts | 95 ++++ packages/cortex-sdk/src/gateway/mergedev.ts | 44 ++ packages/cortex-sdk/src/gateway/minimax.ts | 42 ++ packages/cortex-sdk/src/gateway/nim.ts | 44 ++ .../cortex-sdk/src/gateway/openrouter.test.ts | 146 +++++ packages/cortex-sdk/src/gateway/openrouter.ts | 512 ++++++++++++++++++ packages/cortex-sdk/src/gateway/orcarouter.ts | 44 ++ .../cortex-sdk/src/gateway/supercode-cloud.ts | 282 ++++++++++ packages/cortex-sdk/src/index.ts | 40 ++ packages/cortex-sdk/src/mcp/index.ts | 23 + packages/cortex-sdk/src/voice/elevenlabs.ts | 125 +++++ packages/cortex-sdk/src/voice/groq.ts | 68 +++ packages/cortex-sdk/src/voice/index.ts | 47 ++ packages/cortex-sdk/src/voice/smallest.ts | 120 ++++ packages/cortex-sdk/src/voice/stt.ts | 73 +++ packages/cortex-sdk/src/voice/tts.ts | 64 +++ packages/cortex-sdk/src/voice/types.ts | 20 + packages/cortex-sdk/src/voice/voice.test.ts | 216 ++++++++ .../cortex-sdk/src/web-search/client.test.ts | 235 ++++++++ .../src/web-search/contextdev.test.ts | 203 +++++++ .../cortex-sdk/src/web-search/contextdev.ts | 176 ++++++ packages/cortex-sdk/src/web-search/exa.ts | 144 +++++ .../cortex-sdk/src/web-search/firecrawl.ts | 140 +++++ packages/cortex-sdk/src/web-search/index.ts | 35 ++ packages/cortex-sdk/src/web-search/types.ts | 58 ++ packages/cortex-sdk/tsconfig.json | 20 + packages/cortex-sdk/tsup.cli.config.ts | 29 + packages/cortex-sdk/tsup.config.ts | 31 ++ 66 files changed, 6847 insertions(+), 118 deletions(-) create mode 100644 packages/cortex-sdk/.gitignore create mode 100644 packages/cortex-sdk/README.md create mode 100644 packages/cortex-sdk/cli/src/catalog.ts create mode 100644 packages/cortex-sdk/cli/src/commands/add.ts create mode 100644 packages/cortex-sdk/cli/src/commands/find.ts create mode 100644 packages/cortex-sdk/cli/src/commands/flow.ts create mode 100644 packages/cortex-sdk/cli/src/commands/init.ts create mode 100644 packages/cortex-sdk/cli/src/commands/list.ts create mode 100644 packages/cortex-sdk/cli/src/commands/remove.ts create mode 100644 packages/cortex-sdk/cli/src/commands/update.ts create mode 100644 packages/cortex-sdk/cli/src/config.ts create mode 100644 packages/cortex-sdk/cli/src/index.ts create mode 100644 packages/cortex-sdk/cli/src/tui/banner.ts create mode 100644 packages/cortex-sdk/cli/src/tui/input.ts create mode 100644 packages/cortex-sdk/cli/src/tui/multiselect.ts create mode 100644 packages/cortex-sdk/cli/src/tui/renderer.ts create mode 100644 packages/cortex-sdk/cli/src/tui/select.ts create mode 100644 packages/cortex-sdk/package.json create mode 100644 packages/cortex-sdk/src/agent-handler/client.test.ts create mode 100644 packages/cortex-sdk/src/agent-handler/client.ts create mode 100644 packages/cortex-sdk/src/agent-handler/index.ts create mode 100644 packages/cortex-sdk/src/agent-handler/tool-packs.test.ts create mode 100644 packages/cortex-sdk/src/agent-handler/tool-packs.ts create mode 100644 packages/cortex-sdk/src/agent-handler/types.ts create mode 100644 packages/cortex-sdk/src/composio/apps.ts create mode 100644 packages/cortex-sdk/src/composio/client.test.ts create mode 100644 packages/cortex-sdk/src/composio/index.ts create mode 100644 packages/cortex-sdk/src/composio/oauth.ts create mode 100644 packages/cortex-sdk/src/composio/session.ts create mode 100644 packages/cortex-sdk/src/composio/types.ts create mode 100644 packages/cortex-sdk/src/core/errors.ts create mode 100644 packages/cortex-sdk/src/core/types.ts create mode 100644 packages/cortex-sdk/src/gateway/base.test.ts create mode 100644 packages/cortex-sdk/src/gateway/base.ts create mode 100644 packages/cortex-sdk/src/gateway/concentrateai.ts create mode 100644 packages/cortex-sdk/src/gateway/gemini.ts create mode 100644 packages/cortex-sdk/src/gateway/index.test.ts create mode 100644 packages/cortex-sdk/src/gateway/index.ts create mode 100644 packages/cortex-sdk/src/gateway/mergedev.ts create mode 100644 packages/cortex-sdk/src/gateway/minimax.ts create mode 100644 packages/cortex-sdk/src/gateway/nim.ts create mode 100644 packages/cortex-sdk/src/gateway/openrouter.test.ts create mode 100644 packages/cortex-sdk/src/gateway/openrouter.ts create mode 100644 packages/cortex-sdk/src/gateway/orcarouter.ts create mode 100644 packages/cortex-sdk/src/gateway/supercode-cloud.ts create mode 100644 packages/cortex-sdk/src/index.ts create mode 100644 packages/cortex-sdk/src/mcp/index.ts create mode 100644 packages/cortex-sdk/src/voice/elevenlabs.ts create mode 100644 packages/cortex-sdk/src/voice/groq.ts create mode 100644 packages/cortex-sdk/src/voice/index.ts create mode 100644 packages/cortex-sdk/src/voice/smallest.ts create mode 100644 packages/cortex-sdk/src/voice/stt.ts create mode 100644 packages/cortex-sdk/src/voice/tts.ts create mode 100644 packages/cortex-sdk/src/voice/types.ts create mode 100644 packages/cortex-sdk/src/voice/voice.test.ts create mode 100644 packages/cortex-sdk/src/web-search/client.test.ts create mode 100644 packages/cortex-sdk/src/web-search/contextdev.test.ts create mode 100644 packages/cortex-sdk/src/web-search/contextdev.ts create mode 100644 packages/cortex-sdk/src/web-search/exa.ts create mode 100644 packages/cortex-sdk/src/web-search/firecrawl.ts create mode 100644 packages/cortex-sdk/src/web-search/index.ts create mode 100644 packages/cortex-sdk/src/web-search/types.ts create mode 100644 packages/cortex-sdk/tsconfig.json create mode 100644 packages/cortex-sdk/tsup.cli.config.ts create mode 100644 packages/cortex-sdk/tsup.config.ts diff --git a/bun.lock b/bun.lock index 45952f7..443a190 100644 --- a/bun.lock +++ b/bun.lock @@ -61,23 +61,6 @@ "typescript": "^5", }, }, - "apps/jarvis": { - "name": "jarvis-ui", - "version": "0.1.0", - "dependencies": { - "@tauri-apps/api": "^2.0.0", - "@tauri-apps/plugin-global-shortcut": "^2.0.0", - "@tauri-apps/plugin-shell": "^2.0.0", - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^4.0.0", - "@tauri-apps/cli": "^2.0.0", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "typescript": "^5.0.0", - "vite": "^6.0.0", - }, - }, "apps/supercode-cli": { "name": "supercode-cli-root", "version": "0.1.0", @@ -347,6 +330,32 @@ "name": "@super/config", "version": "0.1.0", }, + "packages/cortex-sdk": { + "name": "cortex-sdk", + "version": "0.1.0", + "dependencies": { + "@ai-sdk/google": "^3.0.80", + "@ai-sdk/mcp": "^2.0.30", + "@ai-sdk/openai-compatible": "^2.0.48", + "@ai-sdk/provider": "^3.0.10", + "@opentui/core": "^0.5.2", + "context.dev": "^2.10.0", + "vercel-minimax-ai-provider": "^0.0.2", + "zod": "^3.25.0", + }, + "devDependencies": { + "@types/bun": "^1.3.14", + "tsup": "^8.5.1", + "typescript": "^5.7.0", + }, + "optionalDependencies": { + "@composio/core": "^0.13.1", + "@modelcontextprotocol/sdk": "^1.9.0", + }, + "peerDependencies": { + "ai": "^4.0.0 || ^5.0.0 || ^6.0.0", + }, + }, "packages/dashboard": { "name": "@super/dashboard", "version": "0.1.0", @@ -446,11 +455,11 @@ "@ai-sdk/google": ["@ai-sdk/google@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VKguP0x/PUYpdQyuA/uy5pDGJy6reL0X/yDKxHfL207aCUXpFIBmyMhVs4US39dkEVhtmIFSwXauY0Pt170JRw=="], - "@ai-sdk/mcp": ["@ai-sdk/mcp@2.0.10", "", { "dependencies": { "@ai-sdk/provider": "4.0.3", "@ai-sdk/provider-utils": "5.0.7", "pkce-challenge": "^5.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LP47CEk3e6BwCqXamvv4nKbgOWQ5z5T/qS/J2fola5h98KTy7JT7I1yw5LKtB89MZBLviYmEh98Ep4ovHOz/vA=="], + "@ai-sdk/mcp": ["@ai-sdk/mcp@2.0.30", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.26", "pkce-challenge": "^5.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-blXjM1TVI6sYER6PuMd8ZM+nTq57LUHq4Cc5gmFw1ub9204rALpew0iiusYKjgyaEtCuUcFr/ybBr6RavqV/2A=="], "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z9MC6M4Oh/yUY/F/eszOtO8wc2nMz99XmZQKd2gWTtyIfe716xTfrKe3aYZKg20NZDtyjqPPKPSR+wqz7q1T7Q=="], - "@ai-sdk/provider": ["@ai-sdk/provider@0.0.26", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg=="], + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "0.0.26", "eventsource-parser": "^1.1.2", "nanoid": "^3.3.7", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ=="], @@ -1134,6 +1143,24 @@ "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="], + "@opentui/core": ["@opentui/core@0.5.2", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.2", "@opentui/core-darwin-x64": "0.5.2", "@opentui/core-linux-arm64": "0.5.2", "@opentui/core-linux-arm64-musl": "0.5.2", "@opentui/core-linux-x64": "0.5.2", "@opentui/core-linux-x64-musl": "0.5.2", "@opentui/core-win32-arm64": "0.5.2", "@opentui/core-win32-x64": "0.5.2" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-GOuD0Mq1ylybq9eEX6L0TdYlfJ2zJVLGecuh6yEM8RpT9oMtR0f+gEWqGJsBN/ay+Y5TOCH9b3vePEcwOW01fg=="], + + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fGi/RubZIhxcU8+M3GXNaBmEHqf127ZOnKmUTSp4ty2QdQ0kT4RTd+t91a/oyxrLZjjCmry0jXyquT0CZR1Byw=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ezFpB+5mQTd9BS4VvHOYEgQK6YxiYwVUmFb2sfTbJBnTqbOFP3Z/cdE5qNUHoeLQ01/nOs2dmUPcGyWxtMZIzw=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-9S53zkIkbwRb0iIv2thibK+IofpTCEz/c48rJNaBx3qDylaDcTA11k5oqt1gkFcOLernlqIcow8aMCNDnRYMAA=="], + + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-s2DB6he7jgbBqWOHBAIV/p4X12mj+Hlk1pAEcwK36j2wg+L4nVVdWecUJm1xeJn6LJSRcaJYE29sA45w5b7B2g=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-HO0TWovqxo95hxFoTBx47MTP6pzzTW/QMoOIiD8jw0hkrn1DF8rSVPva0vLQMRer/drO9RpjyroFRNra7710og=="], + + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-zyuJaRGBRbUOABgf3nJ2Jk/NmD+gS2dQpJzC2ikr2OMqclPVk7gFbPTHW2s/A3SJH7D8d0PJ6wL2wzXWMUdrWQ=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-J1QxYJqxyAO49RyocqZSoGWs5uszlYot8sInLasi6nUbjCFg4h7Kvh+64+gxE2i4LASeyvMRpAIYwblI6eMbtg=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-9/F1Q19GdtkZ1jypa0dRP6v9SJHHc91XHs4ZSroamDeqSDyTaGu1bbWnKiC503gpNpQMY0mjOVg4g+WVJLdDig=="], + "@oxc-project/types": ["@oxc-project/types@0.110.0", "", {}, "sha512-6Ct21OIlrEnFEJk5LT4e63pk3btsI6/TusD/GStLi7wYlGJNOl1GI9qvXAnRAxQU9zqA2Oz+UwhfTOU2rPZVow=="], "@oxc-transform/binding-android-arm-eabi": ["@oxc-transform/binding-android-arm-eabi@0.111.0", "", { "os": "android", "cpu": "arm" }, "sha512-NdFLicvorfHYu0g2ftjVJaH7+Dz27AQUNJOq8t/ofRUoWmczOodgUCHx8C1M1htCN4ZmhS/FzfSy6yd/UngJGg=="], @@ -1642,12 +1669,6 @@ "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.12", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-J1jNYG23QWd67UfrQSFHtjhV37r9mVi0gdc12A3MWPldOjRK35Xk+um+qACPVjgw3AleiqoyEAhok8Wm3q46NA=="], - "@sveltejs/load-config": ["@sveltejs/load-config@0.2.2", "", {}, "sha512-K7dsJDQxBOF+f+epuhMactcjK2VP4MRkLKtwSykNtEI+cKVEyzrmmhQ1pmoxI800m4JKcyJ05L2M4yPwAGiBNw=="], - - "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@4.0.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", "debug": "^4.3.7", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.12", "vitefu": "^1.0.3" }, "peerDependencies": { "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA=="], - - "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@3.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ=="], - "@svgr/babel-plugin-add-jsx-attribute": ["@svgr/babel-plugin-add-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g=="], "@svgr/babel-plugin-remove-jsx-attribute": ["@svgr/babel-plugin-remove-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA=="], @@ -1712,36 +1733,6 @@ "@tanstack/react-query": ["@tanstack/react-query@5.90.17", "", { "dependencies": { "@tanstack/query-core": "5.90.17" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ=="], - "@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="], - - "@tauri-apps/cli": ["@tauri-apps/cli@2.11.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.4", "@tauri-apps/cli-darwin-x64": "2.11.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", "@tauri-apps/cli-linux-arm64-musl": "2.11.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", "@tauri-apps/cli-linux-x64-gnu": "2.11.4", "@tauri-apps/cli-linux-x64-musl": "2.11.4", "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", "@tauri-apps/cli-win32-x64-msvc": "2.11.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ=="], - - "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ=="], - - "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.11.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A=="], - - "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.11.4", "", { "os": "linux", "cpu": "arm" }, "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ=="], - - "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.11.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA=="], - - "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.11.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw=="], - - "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.11.4", "", { "os": "linux", "cpu": "none" }, "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ=="], - - "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.11.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ=="], - - "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.11.4", "", { "os": "linux", "cpu": "x64" }, "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A=="], - - "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.11.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ=="], - - "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.11.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA=="], - - "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw=="], - - "@tauri-apps/plugin-global-shortcut": ["@tauri-apps/plugin-global-shortcut@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-UReHNXrLvpEjylE4jb4oCYiy96uRykPUthoCQCmRXYrd5hs5X9DrW+qOn7GLW57EJN4tdK8bgK5twBTz2NOxzA=="], - - "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.5", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg=="], - "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], "@traceloop/ai-semantic-conventions": ["@traceloop/ai-semantic-conventions@0.20.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-bvivhZU6U8TW4TKktYnjdTi+7GE4WxI8epaGjawalSKDunmxaA+4UVFQ+4tSCBvp2Scby+gnYNaTZSrtABfOlQ=="], @@ -2038,7 +2029,7 @@ "ai": ["ai@5.0.157", "", { "dependencies": { "@ai-sdk/gateway": "2.0.61", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Gb8r7gRzsFmRafbGYiy3QQjlcs6nHpIa+Qui0pssLlJv2veLRgHDN3TQPflLvQvAwdvKt3DuS6oe+WxTqUr6Fw=="], - "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": ["ajv@8.17.1", "", { "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-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], @@ -2152,14 +2143,20 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "c12": ["c12@3.3.4", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.4", "defu": "^6.1.6", "dotenv": "^17.3.1", "exsolve": "^1.0.8", "giget": "^3.2.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", "rc9": "^3.0.1" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + "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=="], "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=="], @@ -2256,6 +2253,8 @@ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "context.dev": ["context.dev@2.10.0", "", {}, "sha512-hTymKw2SrPXWt+1JSPceQcbIgnX32PdSzR9qvAMXWK5qwDTB/kJE1YK5IIXkBUUzZG2rT5QQqwYAGFooYQ8Yow=="], + "convert-hrtime": ["convert-hrtime@3.0.0", "", {}, "sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -2268,6 +2267,8 @@ "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "cortex-sdk": ["cortex-sdk@workspace:packages/cortex-sdk"], + "cosmiconfig": ["cosmiconfig@9.0.1", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="], "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], @@ -2384,7 +2385,7 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], "docs": ["docs@workspace:apps/docs"], @@ -2418,7 +2419,7 @@ "embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="], - "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "emojilib": ["emojilib@2.4.0", "", {}, "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw=="], @@ -2622,6 +2623,8 @@ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], @@ -2910,14 +2913,14 @@ "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], - "jarvis-ui": ["jarvis-ui@workspace:apps/jarvis"], - "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], @@ -3004,8 +3007,12 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], + "loader-runner": ["loader-runner@4.3.2", "", {}, "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w=="], "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], @@ -3194,6 +3201,8 @@ "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], "motion-dom": ["motion-dom@12.24.11", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-DlWOmsXMJrV8lzZyd+LKjG2CXULUs++bkq8GZ2Sr0R0RRhs30K2wtY+LKiTjhmJU3W61HK+rB0GLz6XmPvTA1A=="], @@ -3394,10 +3403,12 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "pip-requirements-js": ["pip-requirements-js@1.0.2", "", { "dependencies": { "ohm-js": "^17.1.0" } }, "sha512-awqoNOSOl4Blu4E4Hzp7jL0g8WKEhCwO+s7C2ibtIW3CAJMwspgoTXd4vnHd21UmhdrsI44Pn8FFSuA8QKrzvg=="], + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], @@ -3410,6 +3421,8 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + "postcss-modules-extract-imports": ["postcss-modules-extract-imports@3.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q=="], "postcss-modules-local-by-default": ["postcss-modules-local-by-default@4.2.0", "", { "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw=="], @@ -3590,8 +3603,6 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], - "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -3610,7 +3621,7 @@ "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "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=="], @@ -3750,6 +3761,8 @@ "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + "supercode-cli": ["supercode-cli@workspace:apps/supercode-cli/server"], "supercode-cli-client": ["supercode-cli-client@workspace:apps/supercode-cli/client"], @@ -3768,8 +3781,6 @@ "svelte": ["svelte@5.56.8", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg=="], - "svelte-check": ["svelte-check@4.7.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.2", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-NnkHGCTPH6k4ka1E9IpTuNv40uLArHnX52kLEuaHSGqRlPYTnkbFs529jSWG+y+wDy+v+jA2PQ0soN1umVK+OA=="], - "svg-parser": ["svg-parser@2.0.4", "", {}, "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ=="], "swagger2openapi": ["swagger2openapi@7.0.8", "", { "dependencies": { "call-me-maybe": "^1.0.1", "node-fetch": "^2.6.1", "node-fetch-h2": "^2.3.0", "node-readfiles": "^0.2.0", "oas-kit-common": "^1.0.8", "oas-resolver": "^2.5.6", "oas-schema-walker": "^1.1.5", "oas-validator": "^5.0.8", "reftools": "^1.1.9", "yaml": "^1.10.0", "yargs": "^17.0.1" }, "bin": { "swagger2openapi": "swagger2openapi.js", "oas-validate": "oas-validate.js", "boast": "boast.js" } }, "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g=="], @@ -3840,6 +3851,8 @@ "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "ts-morph": ["ts-morph@17.0.1", "", { "dependencies": { "@ts-morph/common": "~0.18.0", "code-block-writer": "^11.0.3" } }, "sha512-10PkHyXmrtsTvZSL+cqtJLTgFXkU43Gd0JCc0Rw6GchWbqKe0Rwgt1v3ouobTZwQzF1mGhDeAlWYBMGRV7y+3g=="], "ts-toolbelt": ["ts-toolbelt@6.15.5", "", {}, "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A=="], @@ -3848,6 +3861,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], "turbo": ["turbo@2.8.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.3", "turbo-darwin-arm64": "2.8.3", "turbo-linux-64": "2.8.3", "turbo-linux-arm64": "2.8.3", "turbo-windows-64": "2.8.3", "turbo-windows-arm64": "2.8.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-8Osxz5Tu/Dw2kb31EAY+nhq/YZ3wzmQSmYa1nIArqxgCAldxv9TPlrAiaBUDVnKA4aiPn0OFBD1ACcpc5VFOAQ=="], @@ -3888,6 +3903,8 @@ "typescript-eslint": ["typescript-eslint@8.51.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.51.0", "@typescript-eslint/parser": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/utils": "8.51.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA=="], + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + "uid-promise": ["uid-promise@1.0.0", "", {}, "sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig=="], "ulid": ["ulid@2.4.0", "", { "bin": { "ulid": "bin/cli.js" } }, "sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg=="], @@ -3982,8 +3999,6 @@ "vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], - "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], - "watchpack": ["watchpack@2.5.2", "", { "dependencies": { "graceful-fs": "^4.1.2" } }, "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg=="], "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], @@ -3992,6 +4007,8 @@ "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], + "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], + "web-vitals": ["web-vitals@0.2.4", "", {}, "sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -4066,6 +4083,8 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@0.0.26", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg=="], + "@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], @@ -4076,14 +4095,14 @@ "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], - "@ai-sdk/mcp/@ai-sdk/provider": ["@ai-sdk/provider@4.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw=="], + "@ai-sdk/mcp/@ai-sdk/provider": ["@ai-sdk/provider@4.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q=="], - "@ai-sdk/mcp/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.7", "", { "dependencies": { "@ai-sdk/provider": "4.0.3", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OSm5/5kdrHa11WIOo5LYgDKnxYWp5aB/wx5EXRHi0jpUGduMDeB6oht9U6p+UNNWIP3F/EqPpV8d7vdP/iRnqg=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/mcp/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.26", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0mqhqx+Dcv+msI84+zkbLXmnCHb/mpkYJ9DU3HBvB1px+UjFQss4lLZqRCSzJ0w3NxwQk4Ishk7cP0r8wEQD5w=="], "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@0.0.26", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg=="], + "@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@1.1.2", "", {}, "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA=="], "@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -4140,16 +4159,18 @@ "@better-auth/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@composio/core/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + "@dotenvx/dotenvx/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], "@ecies/ciphers/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@eslint/eslintrc/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=="], + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@eslint/eslintrc/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -4166,12 +4187,12 @@ "@mapbox/node-pre-gyp/node-fetch": ["node-fetch@2.6.9", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg=="], + "@mapbox/node-pre-gyp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@mdx-js/mdx/@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], "@mdx-js/react/@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], - "@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "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-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], "@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], @@ -4412,6 +4433,10 @@ "@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ=="], + "@opentui/core/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + + "@opentui/core/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "@prisma/adapter-pg/pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="], "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="], @@ -4420,8 +4445,6 @@ "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], - "@prisma/streams-local/ajv": ["ajv@8.17.1", "", { "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-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "@prisma/streams-local/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], "@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], @@ -4524,8 +4547,6 @@ "@readme/oas-to-har/oas": ["oas@20.11.0", "", { "dependencies": { "@readme/json-schema-ref-parser": "^1.2.0", "@types/json-schema": "^7.0.11", "json-schema-merge-allof": "^0.8.1", "jsonpath": "^1.1.1", "jsonpointer": "^5.0.0", "memoizee": "^0.4.14", "oas-normalize": "^8.3.0", "openapi-types": "^12.1.0", "path-to-regexp": "^6.2.0" } }, "sha512-Eio2qil8z86PD8KJTdyGPN7hbcF3dV0gc5B6mkSQgL3MLMVFE/XyVAmq/9BRh51zkIXOPmZpSIF3nStADbnkHA=="], - "@readme/openapi-parser/ajv": ["ajv@8.17.1", "", { "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-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "@readme/postman-to-openapi/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "@readme/postman-to-openapi/marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="], @@ -4554,8 +4575,6 @@ "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "@rollup/pluginutils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "@rspack/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], "@svgr/core/camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], @@ -4594,6 +4613,8 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@unrs/resolver-binding-wasm32-wasi/@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=="], "@vercel/backends/@vercel/nft": ["@vercel/nft@1.3.0", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-i4EYGkCsIjzu4vorDUbqglZc5eFtQI2syHb++9ZUDm6TU4edVywGpVnYDein35x9sevONOn9/UabfQXuNXtuzQ=="], @@ -4644,8 +4665,6 @@ "@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "@vercel/nft/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "@vercel/node/@types/node": ["@types/node@20.11.0", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ=="], "@vercel/node/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -4684,12 +4703,6 @@ "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], - "ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "ajv-formats/ajv": ["ajv@8.17.1", "", { "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-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "ajv-keywords/ajv": ["ajv@8.20.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-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "api/@ai-sdk/google": ["@ai-sdk/google@3.0.80", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5ORbm/yFUPO0MEvZsxBMN0cdKw2+lwU/wVn5KN3KF8Dmk1LughuDuUohMh/7iU/XFTiyB0OvmTW/tdV/J7O9zg=="], @@ -4720,12 +4733,12 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "cortex-sdk/@ai-sdk/google": ["@ai-sdk/google@3.0.80", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5ORbm/yFUPO0MEvZsxBMN0cdKw2+lwU/wVn5KN3KF8Dmk1LughuDuUohMh/7iU/XFTiyB0OvmTW/tdV/J7O9zg=="], + "cosmiconfig/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "css-loader/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "d3-geo/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], "d3-transition/d3-color": ["d3-color@2.0.0", "", {}, "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ=="], @@ -4762,6 +4775,8 @@ "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "eslint/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=="], + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -4776,6 +4791,8 @@ "eslint-plugin-jsx-a11y/aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + "eslint-plugin-jsx-a11y/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -4804,6 +4821,8 @@ "inngest/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "isomorphic-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], @@ -4824,6 +4843,10 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "mlly/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + + "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "msw/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "msw/path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], @@ -4912,6 +4935,8 @@ "shadcn/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "shadcn/diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + "shadcn/execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], "shadcn/ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], @@ -4922,14 +4947,16 @@ "shadcn/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "supercode-cli/@ai-sdk/google": ["@ai-sdk/google@3.0.80", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5ORbm/yFUPO0MEvZsxBMN0cdKw2+lwU/wVn5KN3KF8Dmk1LughuDuUohMh/7iU/XFTiyB0OvmTW/tdV/J7O9zg=="], "supercode-cli/ai": ["ai@6.0.195", "", { "dependencies": { "@ai-sdk/gateway": "3.0.123", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-IYZpuVz0boWbpIQYyinfWFrvQ1N0dG+EVB63it45B2YAU/MxxCnwz4zBswjYnPtHnJBedpMPNrwVbeczbl2GKg=="], @@ -4964,8 +4991,6 @@ "superdesign/tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], - "svelte-check/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "swagger2openapi/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "swagger2openapi/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], @@ -4974,12 +4999,14 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "tsconfig-paths/minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "tsup/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "tsup/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "tsx/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], @@ -4992,6 +5019,8 @@ "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "web/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], "web/better-auth": ["better-auth@1.6.23", "", { "dependencies": { "@better-auth/core": "1.6.23", "@better-auth/drizzle-adapter": "1.6.23", "@better-auth/kysely-adapter": "1.6.23", "@better-auth/memory-adapter": "1.6.23", "@better-auth/mongo-adapter": "1.6.23", "@better-auth/prisma-adapter": "1.6.23", "@better-auth/telemetry": "1.6.23", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.7", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-4vOaRd9UiKGKm9R+ej0jjU1es3MiJIiNc9Qq3VCnYqOZ4/nb5272QqTxWYoDxyUXl5x6A2x2we5KZKQO9teTQQ=="], @@ -5016,6 +5045,8 @@ "@ai-sdk/mcp/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + "@ai-sdk/mcp/@ai-sdk/provider-utils/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + "@ai-sdk/openai-compatible/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], "@apidevtools/json-schema-ref-parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -5090,6 +5121,8 @@ "@dotenvx/dotenvx/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "@floating-ui/react/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], @@ -5106,8 +5139,6 @@ "@openrouter/ai-sdk-provider/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.123", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rL+3Sp9crOlfE7MwguFPS30qVp6HFcr9na0KYMb4CcQdxAIjBJec3EEdCjc94UyRVZWLmgQ6Yr605FmPlFIi0w=="], - "@openrouter/ai-sdk-provider/ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "@openrouter/ai-sdk-provider/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], "@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg=="], @@ -5426,8 +5457,6 @@ "@vercel/backends/@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "@vercel/backends/@vercel/nft/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "@vercel/backends/execa/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], "@vercel/backends/execa/human-signals": ["human-signals@1.1.1", "", {}, "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="], @@ -5476,8 +5505,6 @@ "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "api/@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "api/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], "api/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.28", "", { "dependencies": { "@ai-sdk/provider": "4.0.3", "@ai-sdk/provider-utils": "5.0.12", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ee9TsNO3mkgHDWmTmJ8Fvltr6PFh52zhrV2+FaKJY3F3iu7kWZi5tCRJCR1HVhhlpj6lHniUb28nLQdPHkA/1Q=="], @@ -5522,6 +5549,8 @@ "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "cortex-sdk/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "cosmiconfig/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "d3-geo/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], @@ -5536,9 +5565,9 @@ "eslint-plugin-import/tsconfig-paths/minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "eve/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.123", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rL+3Sp9crOlfE7MwguFPS30qVp6HFcr9na0KYMb4CcQdxAIjBJec3EEdCjc94UyRVZWLmgQ6Yr605FmPlFIi0w=="], + "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "eve/ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "eve/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.123", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rL+3Sp9crOlfE7MwguFPS30qVp6HFcr9na0KYMb4CcQdxAIjBJec3EEdCjc94UyRVZWLmgQ6Yr605FmPlFIi0w=="], "eve/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], @@ -5550,6 +5579,8 @@ "micro/raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "msw/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "msw/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -5674,14 +5705,10 @@ "supercode-cli-client/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - "supercode-cli/@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "supercode-cli/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], "supercode-cli/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.123", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rL+3Sp9crOlfE7MwguFPS30qVp6HFcr9na0KYMb4CcQdxAIjBJec3EEdCjc94UyRVZWLmgQ6Yr605FmPlFIi0w=="], - "supercode-cli/ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "supercode-cli/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], "supercode-cli/api/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -5692,6 +5719,8 @@ "supercode-cli/api/oas": ["oas@20.11.0", "", { "dependencies": { "@readme/json-schema-ref-parser": "^1.2.0", "@types/json-schema": "^7.0.11", "json-schema-merge-allof": "^0.8.1", "jsonpath": "^1.1.1", "jsonpointer": "^5.0.0", "memoizee": "^0.4.14", "oas-normalize": "^8.3.0", "openapi-types": "^12.1.0", "path-to-regexp": "^6.2.0" } }, "sha512-Eio2qil8z86PD8KJTdyGPN7hbcF3dV0gc5B6mkSQgL3MLMVFE/XyVAmq/9BRh51zkIXOPmZpSIF3nStADbnkHA=="], + "supercode-cli/api/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "supercode-cli/better-auth/@better-auth/core": ["@better-auth/core@1.6.23", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.7", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-beEhOs0uVeOxYOZKUfIEBd/nQV2Bd4/6wyLxZ0OFkn6CMTK2Vi+hXuZLnyPBeB6RdHpebEoJWiHqwHxBIxgPDQ=="], "supercode-cli/better-auth/@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-2+/PTVfIP9E7iz6af8TB3lhnowHUj9ljC66kECmHaFEdUqPgzHoWux9epotKwO7XDg2ui4ttWQ8CMeNFLvQeKQ=="], @@ -5726,12 +5755,12 @@ "supercode-video/eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.8", "", { "dependencies": { "@eslint/core": "^0.13.0", "levn": "^0.4.1" } }, "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA=="], + "supercode-video/eslint/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=="], + "supercode-video/eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "superdesign/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.123", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rL+3Sp9crOlfE7MwguFPS30qVp6HFcr9na0KYMb4CcQdxAIjBJec3EEdCjc94UyRVZWLmgQ6Yr605FmPlFIi0w=="], - "superdesign/ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "superdesign/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], "swagger2openapi/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], @@ -5740,6 +5769,58 @@ "swagger2openapi/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "tsup/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "tsup/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "tsup/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "tsup/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "tsup/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "tsup/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "tsup/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "tsup/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "tsup/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "tsup/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "tsup/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "tsup/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "tsup/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "tsup/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "tsup/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "tsup/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "tsup/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "tsup/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "tsup/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "tsup/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "tsup/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "tsup/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "tsup/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "tsup/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "tsup/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "tsup/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], @@ -6022,6 +6103,8 @@ "cli-table3/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "cortex-sdk/@ai-sdk/google/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + "eve/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "eve/ai/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], @@ -6084,6 +6167,8 @@ "supercode-video/eslint/@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="], + "supercode-video/eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "superdesign/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "superdesign/ai/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], @@ -6118,16 +6203,12 @@ "@remotion/eslint-config-flat/typescript-eslint/@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@remotion/eslint-config-flat/typescript-eslint/@typescript-eslint/parser/@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "@remotion/eslint-config-flat/typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.21.0", "", { "dependencies": { "@typescript-eslint/types": "8.21.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w=="], "@remotion/eslint-config-flat/typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.21.0", "", { "dependencies": { "@typescript-eslint/types": "8.21.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w=="], "@remotion/eslint-config-flat/typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@remotion/eslint-config-flat/typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "@vercel/express/ts-morph/@ts-morph/common/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@vercel/hono/ts-morph/@ts-morph/common/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -6172,8 +6253,6 @@ "@remotion/eslint-config-flat/typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@remotion/eslint-config-flat/typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "@remotion/eslint-config-flat/typescript-eslint/@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@remotion/eslint-config-flat/typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], diff --git a/packages/cortex-sdk/.gitignore b/packages/cortex-sdk/.gitignore new file mode 100644 index 0000000..578fcf9 --- /dev/null +++ b/packages/cortex-sdk/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.turbo diff --git a/packages/cortex-sdk/README.md b/packages/cortex-sdk/README.md new file mode 100644 index 0000000..76032a7 --- /dev/null +++ b/packages/cortex-sdk/README.md @@ -0,0 +1,27 @@ +# cortex-sdk + +Unified SDK for building AI agents — 8 gateway providers, 3 web search providers, 6 MCP platforms, and 3 voice providers. + +## Install + +```bash +bun add cortex-sdk +``` + +Requires `ai` as a peer dependency (`^4.0.0 || ^5.0.0 || ^6.0.0`). + +## Modules + +| Import path | Purpose | +| -------------------------- | ----------------------------------------------- | +| `cortex-sdk` | Root exports + `SupercodeAgent` orchestration | +| `cortex-sdk/gateway` | `createGateway` — model access via 8 providers | +| `cortex-sdk/agent-handler` | `createAgentHandler` — MergeDev tool packs | +| `cortex-sdk/composio` | `createComposio` — 150+ app integrations | +| `cortex-sdk/web-search` | `createWebSearch` — Exa, Firecrawl, Context.dev | +| `cortex-sdk/mcp` | `createMcpManager` — direct MCP connections | +| `cortex-sdk/voice` | `createVoice` — STT + TTS | + +## Status + +Package scaffolded (Phase 1). Module factories are stubs that throw `SdkError("NOT_IMPLEMENTED")` until their implementation phases land. diff --git a/packages/cortex-sdk/cli/src/catalog.ts b/packages/cortex-sdk/cli/src/catalog.ts new file mode 100644 index 0000000..f65c872 --- /dev/null +++ b/packages/cortex-sdk/cli/src/catalog.ts @@ -0,0 +1,220 @@ +export type ModuleId = + | "gateway" + | "web-search" + | "voice" + | "composio" + | "agent-handler" + | "mcp" + +export interface Provider { + id: string + name: string + description: string + envKey?: string + defaultModel?: string + defaultBaseURL?: string + needsKey: boolean +} + +export interface Module { + id: ModuleId + name: string + description: string + providers: Provider[] +} + +export const MODULES: Module[] = [ + { + id: "gateway", + name: "Gateway", + description: "LLM gateway — chat, streaming, and tool calls over one OpenAI-compatible API", + providers: [ + { + id: "concentrateai", + name: "Concentrate AI", + description: "Aggregated model router", + envKey: "CONCENTRATEAI_API_KEY", + defaultModel: "deepseek/deepseek-v4-flash", + defaultBaseURL: "https://api.concentrate.ai/v1", + needsKey: true, + }, + { + id: "mergedev", + name: "Merge Dev", + description: "Multi-provider API gateway", + envKey: "MERGEDEV_API_KEY", + defaultModel: "anthropic/claude-opus-4-8", + defaultBaseURL: "https://api-gateway.merge.dev/v1/openai", + needsKey: true, + }, + { + id: "openrouter", + name: "OpenRouter", + description: "Unified access to 300+ models", + envKey: "OPENROUTER_API_KEY", + defaultModel: "moonshotai/kimi-k2.6", + defaultBaseURL: "https://openrouter.ai/api/v1", + needsKey: true, + }, + { + id: "gemini", + name: "Google Gemini", + description: "Gemini models via Generative Language API", + envKey: "GOOGLE_GENERATIVE_AI_API_KEY", + defaultModel: "gemini-2.5-flash", + needsKey: true, + }, + { + id: "minimax", + name: "MiniMax", + description: "MiniMax models", + envKey: "MINIMAX_API_KEY", + defaultModel: "MiniMax-M1", + needsKey: true, + }, + { + id: "nim", + name: "NVIDIA NIM", + description: "NVIDIA-hosted open models", + envKey: "NVIDIA_API_KEY", + defaultModel: "minimaxai/minimax-m3", + defaultBaseURL: "https://integrate.api.nvidia.com/v1", + needsKey: true, + }, + { + id: "orcarouter", + name: "Orca Router", + description: "Budget LLM router", + envKey: "ORCAROUTER_API_KEY", + defaultModel: "openai/gpt-4o-mini", + defaultBaseURL: "https://api.orcarouter.ai/v1", + needsKey: true, + }, + { + id: "supercode-cloud", + name: "SuperCode Cloud", + description: "Hosted SuperCode gateway (no key required)", + defaultModel: "deepseek-v4-flash", + defaultBaseURL: "https://supercode-8w7e.onrender.com", + needsKey: false, + }, + ], + }, + { + id: "web-search", + name: "Web Search", + description: "Search tool providers — exa, firecrawl, context.dev", + providers: [ + { + id: "exa", + name: "Exa", + description: "Neural web search API", + envKey: "EXA_API_KEY", + needsKey: true, + }, + { + id: "firecrawl", + name: "Firecrawl", + description: "Crawl, scrape, and search the web", + envKey: "FIRECRAWL_API_KEY", + needsKey: true, + }, + { + id: "contextdev", + name: "context.dev", + description: "Context search engine", + envKey: "CONTEXTDEV_API_KEY", + needsKey: true, + }, + ], + }, + { + id: "voice", + name: "Voice", + description: "Speech-to-text and text-to-speech providers", + providers: [ + { + id: "smallest", + name: "Smallest.ai", + description: "Pulse STT + TTS (primary STT provider)", + envKey: "SMALLEST_API_KEY", + needsKey: true, + }, + { + id: "elevenlabs", + name: "ElevenLabs", + description: "High-quality TTS + Scribe STT", + envKey: "ELEVENLABS_API_KEY", + needsKey: true, + }, + { + id: "groq", + name: "Groq", + description: "Whisper STT via Groq", + envKey: "GROQ_API_KEY", + needsKey: true, + }, + ], + }, + { + id: "composio", + name: "Composio", + description: "Tool integrations for agents (GitHub, Gmail, Slack, ...)", + providers: [ + { + id: "composio", + name: "Composio", + description: "Tool integration platform", + envKey: "COMPOSIO_API_KEY", + needsKey: true, + }, + ], + }, + { + id: "agent-handler", + name: "Agent Handler", + description: "Backend agent handler client (SuperCode or custom)", + providers: [ + { + id: "agent-handler", + name: "Agent Handler", + description: "HTTP/SSE agent client", + envKey: "AGENT_HANDLER_API_KEY", + needsKey: true, + }, + ], + }, + { + id: "mcp", + name: "MCP", + description: "Model Context Protocol servers (coming soon)", + providers: [], + }, +] + +export function getModule(id: ModuleId): Module | undefined { + return MODULES.find((m) => m.id === id) +} + +export function configKey(moduleId: ModuleId): string { + return moduleId.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) +} + +export function moduleFromKey(key: string): Module | undefined { + return MODULES.find((m) => configKey(m.id) === key || m.id === key) +} + +export function getProvider(moduleId: ModuleId, providerId: string): Provider | undefined { + return getModule(moduleId)?.providers.find((p) => p.id === providerId) +} + +export function findUnit(term: string): { module: Module; provider: Provider } | undefined { + for (const module of MODULES) { + for (const provider of module.providers) { + if (provider.id === term || `${module.id}.${provider.id}` === term) { + return { module, provider } + } + } + } + return undefined +} diff --git a/packages/cortex-sdk/cli/src/commands/add.ts b/packages/cortex-sdk/cli/src/commands/add.ts new file mode 100644 index 0000000..aeca8e0 --- /dev/null +++ b/packages/cortex-sdk/cli/src/commands/add.ts @@ -0,0 +1,28 @@ +import type { TuiContext } from "../tui/renderer" +import { destroyTui } from "../tui/renderer" +import { findUnit, type ModuleId } from "../catalog" +import { collectConfig } from "./flow" +import { writeConfig, readConfig } from "../config" + +export async function runAdd(tui: TuiContext, term: string): Promise { + const unit = findUnit(term) + if (!unit) { + await destroyTui(tui) + console.log(`Unknown unit: ${term}`) + console.log("Run `cortex-sdk list` to see available units.") + return 1 + } + + const { module, provider } = unit + const entry = await collectConfig(tui, module.id, provider.id, provider.needsKey, provider.defaultModel, provider.envKey) + await destroyTui(tui) + + if (!entry) { + console.log("Cancelled.") + return 0 + } + + const result = await writeConfig(process.cwd(), [entry], { append: true }) + console.log(`\nAdded ${module.id}.${provider.id} → ${result.path}`) + return 0 +} diff --git a/packages/cortex-sdk/cli/src/commands/find.ts b/packages/cortex-sdk/cli/src/commands/find.ts new file mode 100644 index 0000000..3f3fc3b --- /dev/null +++ b/packages/cortex-sdk/cli/src/commands/find.ts @@ -0,0 +1,84 @@ +import type { TuiContext } from "../tui/renderer" +import { destroyTui } from "../tui/renderer" +import { MODULES, type ModuleId } from "../catalog" +import { promptSelect, type SelectOption } from "../tui/select" +import { promptMultiSelect } from "../tui/multiselect" +import { resolveModule, selectModules, collectConfig } from "./flow" +import { writeConfig, readConfig } from "../config" + +export async function runFind(tui: TuiContext): Promise { + // 1. pick a unit (provider or module) + const unitOptions: SelectOption[] = MODULES.flatMap((module) => + module.providers.map((p) => ({ + name: p.name, + description: `${module.name} — ${p.description}`, + value: `${module.id}.${p.id}`, + })), + ) + + const pick = await promptSelect(tui, { + title: "Find a provider", + subtitle: "Search or scroll to choose what to install", + options: unitOptions, + }) + if (!pick) { + await destroyTui(tui) + console.log("Cancelled.") + return 0 + } + + const [moduleId, providerId] = pick.split(".") as [ModuleId, string] + + // 2. ask which SDK modules to enable + const modules = await selectModules(tui) + if (!modules || modules.length === 0) { + await destroyTui(tui) + console.log("No modules selected — no changes made.") + return 0 + } + + // 3. resolve the picked provider first, then the rest + const entries = [] + const pickedEntry = await resolveProviderDirect(tui, moduleId, providerId) + if (pickedEntry) entries.push(pickedEntry) + + for (const m of modules) { + if (m === moduleId) continue + const entry = await resolveModule(tui, m) + if (entry) entries.push(entry) + } + + await destroyTui(tui) + + if (entries.length === 0) { + console.log("No modules configured — no changes made.") + return 0 + } + + const result = await writeConfig(process.cwd(), entries, { append: true }) + const existing = await readConfig(process.cwd()) + console.log(`\nWrote ${result.path}`) + console.log("") + for (const [name, config] of Object.entries(existing.modules)) { + console.log(` export const ${name} = ${JSON.stringify(config)}`) + } + return 0 +} + +async function resolveProviderDirect( + tui: TuiContext, + moduleId: ModuleId, + providerId: string, +): Promise<{ name: string; config: Record } | undefined> { + const module = MODULES.find((m) => m.id === moduleId) + if (!module) return undefined + const provider = module.providers.find((p) => p.id === providerId) + if (!provider) return undefined + + if (moduleId === "voice") { + // keep the voice flow (stt + tts) but pre-pick the chosen provider as stt + return resolveModule(tui, moduleId) + } + + return collectConfig(tui, moduleId, providerId, provider.needsKey, provider.defaultModel, provider.envKey) +} diff --git a/packages/cortex-sdk/cli/src/commands/flow.ts b/packages/cortex-sdk/cli/src/commands/flow.ts new file mode 100644 index 0000000..b23124b --- /dev/null +++ b/packages/cortex-sdk/cli/src/commands/flow.ts @@ -0,0 +1,129 @@ +import type { ModuleId } from "../catalog" +import { getModule, MODULES, configKey } from "../catalog" +import type { ConfigModule } from "../config" +import type { TuiContext } from "../tui/renderer" +import { promptSelect, type SelectOption } from "../tui/select" +import { promptMultiSelect } from "../tui/multiselect" +import { promptInput } from "../tui/input" + +export interface InstallSelection { + modules: ModuleId[] + entries: ConfigModule[] +} + +export async function selectModules(tui: TuiContext): Promise { + const options: SelectOption[] = MODULES.map((m) => ({ + name: m.name, + description: m.description, + value: m.id, + })) + const result = await promptMultiSelect(tui, { + title: "Which SDK modules to enable", + subtitle: "Pick the modules you want to install", + options, + }) + if (result.cancelled) return undefined + return result.selected as ModuleId[] +} + +export async function resolveModule(tui: TuiContext, moduleId: ModuleId): Promise { + const module = getModule(moduleId) + if (!module) return undefined + + // voice needs two providers (stt + tts); others need one + if (moduleId === "voice") { + return resolveVoice(tui) + } + + if (module.providers.length === 0) { + // no providers to configure (e.g. mcp stub) + return { name: configKey(module.id), config: {} } + } + + const provider = await promptSelect(tui, { + title: `Select a ${module.name} provider`, + subtitle: module.description, + options: module.providers.map((p) => ({ + name: p.name, + description: p.description, + value: p.id, + })), + }) + if (!provider) return undefined + + const providerDef = module.providers.find((p) => p.id === provider)! + return collectConfig(tui, module.id, provider, providerDef.needsKey, providerDef.defaultModel, providerDef.envKey) +} + +async function resolveVoice(tui: TuiContext): Promise { + const stt = await promptSelect(tui, { + title: "Select a speech-to-text provider", + subtitle: "STT converts your voice into text", + options: [ + { name: "Smallest.ai", description: "Pulse STT — primary provider", value: "smallest" }, + { name: "ElevenLabs", description: "Scribe STT", value: "elevenlabs" }, + { name: "Groq", description: "Whisper via Groq", value: "groq" }, + ], + }) + if (!stt) return undefined + + const tts = await promptSelect(tui, { + title: "Select a text-to-speech provider", + subtitle: "TTS speaks the assistant's reply", + options: [ + { name: "Smallest.ai", description: "TTS via Smallest", value: "smallest" }, + { name: "ElevenLabs", description: "High-quality TTS", value: "elevenlabs" }, + ], + }) + if (!tts) return undefined + + const config: Record = { + stt, + tts, + } + + const sttKey = await promptKey(tui, `Enter your ${stt} STT API key`, stt === "smallest" ? "SMALLEST_API_KEY" : stt === "elevenlabs" ? "ELEVENLABS_API_KEY" : "GROQ_API_KEY") + if (sttKey === undefined) return undefined + if (sttKey) config.sttApiKey = sttKey + + if (tts !== stt) { + const ttsKey = await promptKey(tui, `Enter your ${tts} TTS API key`, tts === "smallest" ? "SMALLEST_API_KEY" : "ELEVENLABS_API_KEY") + if (ttsKey === undefined) return undefined + if (ttsKey) config.ttsApiKey = ttsKey + } else if (config.sttApiKey) { + config.ttsApiKey = config.sttApiKey + } + + return { name: "voice", config } +} + +export async function collectConfig( + tui: TuiContext, + moduleId: ModuleId, + providerId: string, + needsKey: boolean, + defaultModel?: string, + envKey?: string, +): Promise { + const config: Record = { provider: providerId } + if (defaultModel) config.model = defaultModel + + if (needsKey) { + const key = await promptKey(tui, `Enter your API key for ${providerId}`, envKey) + if (key === undefined) return undefined + if (key) config.apiKey = key + } + + return { name: configKey(moduleId), config } +} + +export async function promptKey(tui: TuiContext, title: string, envKey?: string): Promise { + const result = await promptInput(tui, { + title, + subtitle: envKey ? `Leave empty to use the ${envKey} environment variable` : "Leave empty to skip", + placeholder: envKey ? `e.g. ${envKey.toLowerCase()}_...` : undefined, + secure: true, + }) + if (result.cancelled) return undefined + return result.value +} diff --git a/packages/cortex-sdk/cli/src/commands/init.ts b/packages/cortex-sdk/cli/src/commands/init.ts new file mode 100644 index 0000000..0c83fb0 --- /dev/null +++ b/packages/cortex-sdk/cli/src/commands/init.ts @@ -0,0 +1,44 @@ +import type { TuiContext } from "../tui/renderer" +import { destroyTui } from "../tui/renderer" +import { selectModules, resolveModule } from "./flow" +import { writeConfig, readConfig, configPath } from "../config" + +export async function runInit(tui: TuiContext): Promise { + const modules = await selectModules(tui) + if (!modules || modules.length === 0) { + await destroyTui(tui) + console.log("Nothing selected — no changes made.") + return 0 + } + + const entries = [] + for (const moduleId of modules) { + const entry = await resolveModule(tui, moduleId) + if (entry) entries.push(entry) + } + + await destroyTui(tui) + + if (entries.length === 0) { + console.log("No modules configured — no changes made.") + return 0 + } + + const result = await writeConfig(process.cwd(), entries, { append: true }) + const existing = await readConfig(process.cwd()) + console.log(`\nWrote ${result.path}`) + console.log("") + for (const [name, config] of Object.entries(existing.modules)) { + console.log(` export const ${name} = ${JSON.stringify(config)}`) + } + console.log("") + console.log("Next: import the config in your app and pass each export to the matching SDK factory:") + for (const entry of entries) { + if (entry.name === "gateway") console.log(` import { createGateway } from "cortex-sdk/gateway"`) + if (entry.name === "web-search") console.log(` import { createWebSearch } from "cortex-sdk/web-search"`) + if (entry.name === "voice") console.log(` import { createVoice } from "cortex-sdk/voice"`) + if (entry.name === "composio") console.log(` import { createComposio } from "cortex-sdk/composio"`) + if (entry.name === "agent-handler") console.log(` import { createAgentHandler } from "cortex-sdk/agent-handler"`) + } + return 0 +} diff --git a/packages/cortex-sdk/cli/src/commands/list.ts b/packages/cortex-sdk/cli/src/commands/list.ts new file mode 100644 index 0000000..92cb946 --- /dev/null +++ b/packages/cortex-sdk/cli/src/commands/list.ts @@ -0,0 +1,29 @@ +import { MODULES, configKey } from "../catalog" +import { readConfig } from "../config" + +export async function runList(): Promise { + const config = await readConfig(process.cwd()) + + console.log("Installed SDK modules") + console.log("") + const installed = Object.entries(config.modules) + if (installed.length === 0) { + console.log(" (none — run `cortex-sdk init` or `cortex-sdk find` to install)") + } else { + for (const [name, cfg] of installed) { + const summary = JSON.stringify(cfg) + console.log(` ${name}: ${summary}`) + } + } + + console.log("") + console.log("Available units") + console.log("") + for (const module of MODULES) { + for (const provider of module.providers) { + const installedMark = config.modules[configKey(module.id)]?.provider === provider.id ? " • installed" : "" + console.log(` ${module.id}.${provider.id} ${provider.name} (${provider.description})${installedMark}`) + } + } + return 0 +} diff --git a/packages/cortex-sdk/cli/src/commands/remove.ts b/packages/cortex-sdk/cli/src/commands/remove.ts new file mode 100644 index 0000000..48df0cb --- /dev/null +++ b/packages/cortex-sdk/cli/src/commands/remove.ts @@ -0,0 +1,24 @@ +import { findUnit, configKey } from "../catalog" +import { readConfig, writeConfig } from "../config" + +export async function runRemove(term: string): Promise { + const unit = findUnit(term) + if (!unit) { + console.log(`Unknown unit: ${term}`) + console.log("Run `cortex-sdk list` to see available units.") + return 1 + } + + const key = configKey(unit.module.id) + const config = await readConfig(process.cwd()) + if (!config.modules[key]) { + console.log(`${key} is not installed — nothing to remove.`) + return 0 + } + + delete config.modules[key] + const entries = Object.entries(config.modules).map(([name, cfg]) => ({ name, config: cfg })) + await writeConfig(process.cwd(), entries, { append: false }) + console.log(`Removed ${key} from cortex.config.ts`) + return 0 +} diff --git a/packages/cortex-sdk/cli/src/commands/update.ts b/packages/cortex-sdk/cli/src/commands/update.ts new file mode 100644 index 0000000..a0433b2 --- /dev/null +++ b/packages/cortex-sdk/cli/src/commands/update.ts @@ -0,0 +1,40 @@ +import type { TuiContext } from "../tui/renderer" +import { destroyTui } from "../tui/renderer" +import { findUnit, configKey } from "../catalog" +import { collectConfig } from "./flow" +import { readConfig, writeConfig } from "../config" + +export async function runUpdate(tui: TuiContext, term: string): Promise { + const unit = findUnit(term) + if (!unit) { + await destroyTui(tui) + console.log(`Unknown unit: ${term}`) + console.log("Run `cortex-sdk list` to see available units.") + return 1 + } + + const { module, provider } = unit + const key = configKey(module.id) + const existing = await readConfig(process.cwd()) + const current = existing.modules[key] ?? {} + + const entry = await collectConfig( + tui, + module.id, + provider.id, + provider.needsKey, + (current.model as string) || provider.defaultModel, + provider.envKey, + ) + await destroyTui(tui) + + if (!entry) { + console.log("Cancelled.") + return 0 + } + + const merged = { ...current, ...entry.config } + await writeConfig(process.cwd(), [{ name: key, config: merged }], { append: true }) + console.log(`\nUpdated ${key} in cortex.config.ts`) + return 0 +} diff --git a/packages/cortex-sdk/cli/src/config.ts b/packages/cortex-sdk/cli/src/config.ts new file mode 100644 index 0000000..ab65ee0 --- /dev/null +++ b/packages/cortex-sdk/cli/src/config.ts @@ -0,0 +1,125 @@ +import { readFile, writeFile, access } from "node:fs/promises" +import { join } from "node:path" + +export interface ConfigModule { + name: string + config: Record +} + +export interface CortexConfig { + path: string + modules: Record> +} + +const HEADER = `// cortex.config.ts — generated by the cortex-sdk CLI +// Pass each export to the matching SDK factory: +// import { createGateway } from "cortex-sdk/gateway" +// import { gateway, webSearch } from "./cortex.config" +// const gatewayClient = createGateway(gateway) +` + +const EXPORT_RE = /export const\s+(\w+)\s*=\s*(\{)/g + +export async function configExists(dir: string): Promise { + const target = join(dir, "cortex.config.ts") + try { + await access(target) + return true + } catch { + return false + } +} + +export async function configPath(dir: string): Promise { + return join(dir, "cortex.config.ts") +} + +export async function readConfig(dir: string): Promise { + const path = await configPath(dir) + const modules: Record> = {} + let content = "" + try { + content = await readFile(path, "utf8") + } catch { + return { path, modules } + } + EXPORT_RE.lastIndex = 0 + let match: RegExpExecArray | null + while ((match = EXPORT_RE.exec(content)) !== null) { + const name = match[1] + if (!name) continue + const block = extractObjectLiteral(content, match.index + match[0].length - 1) + if (!block) continue + try { + modules[name] = JSON.parse(block) as Record + } catch { + // skip malformed blocks rather than fail the whole read + } + } + return { path, modules } +} + +export async function writeConfig( + dir: string, + entries: ConfigModule[], + opts?: { append?: boolean; force?: boolean }, +): Promise { + const target = await configPath(dir) + const existing = opts?.append ? await readConfig(dir) : { path: target, modules: {} } + for (const entry of entries) { + existing.modules[entry.name] = entry.config + } + const lines = [HEADER, ""] + for (const [name, config] of Object.entries(existing.modules)) { + lines.push(`export const ${name} = ${jsonLiteral(config)}`) + lines.push("") + } + lines.push("") + await writeFile(target, lines.join("\n")) + return { path: target, modules: existing.modules } +} + +function extractObjectLiteral(content: string, openBraceIndex: number): string | undefined { + let depth = 0 + let inString = false + let quote = "" + let escaped = false + for (let i = openBraceIndex; i < content.length; i++) { + const ch = content[i] + if (inString) { + if (escaped) { + escaped = false + continue + } + if (ch === "\\") { + escaped = true + continue + } + if (ch === quote) inString = false + continue + } + if (ch === '"' || ch === "'" || ch === "`") { + inString = true + quote = ch + continue + } + if (ch === "{") depth++ + else if (ch === "}") { + depth-- + if (depth === 0) return content.slice(openBraceIndex, i + 1) + } + } + return undefined +} + +function jsonLiteral(config: Record): string { + return JSON.stringify(config, null, 2).replace(/^\{/, "{").replace(/\n\s*$/m, "") +} + +export function defaultConfigDir(): string { + return process.cwd() +} + +export function resolveTargetDir(cwd?: string): string { + return cwd ?? process.cwd() +} diff --git a/packages/cortex-sdk/cli/src/index.ts b/packages/cortex-sdk/cli/src/index.ts new file mode 100644 index 0000000..3bf4dc8 --- /dev/null +++ b/packages/cortex-sdk/cli/src/index.ts @@ -0,0 +1,86 @@ +import { createTui, destroyTui } from "./tui/renderer" +import { runInit } from "./commands/init" +import { runFind } from "./commands/find" +import { runList } from "./commands/list" +import { runAdd } from "./commands/add" +import { runRemove } from "./commands/remove" +import { runUpdate } from "./commands/update" + +const VERSION = "0.1.0" + +const HELP = `cortex-sdk — install and configure the cortex SDK + +Usage: + cortex-sdk init Interactive setup: pick modules and providers + cortex-sdk find Browse providers and install them + cortex-sdk list Show installed modules and available units + cortex-sdk add Add a unit (e.g. gateway.openrouter) + cortex-sdk remove Remove an installed unit + cortex-sdk update Reconfigure an installed unit + cortex-sdk --help Show this help + cortex-sdk --version Show version + +Units are named ., e.g.: + gateway.openrouter, gateway.mergedev, web-search.exa, voice.smallest +` + +async function main(): Promise { + const args = process.argv.slice(2) + + if (args.length === 0 || args.includes("--help") || args.includes("-h") || args[0] === "help") { + console.log(HELP) + return + } + if (args.includes("--version") || args.includes("-v")) { + console.log(VERSION) + return + } + + const [command, term] = args + + switch (command) { + case "init": + await runInit(await createTui()) + return + case "find": + await runFind(await createTui()) + return + case "list": + await runList() + return + case "add": + if (!term) { + console.log("Usage: cortex-sdk add ") + console.log("Run `cortex-sdk list` to see available units.") + process.exitCode = 1 + return + } + await runAdd(await createTui(), term) + return + case "remove": + if (!term) { + console.log("Usage: cortex-sdk remove ") + process.exitCode = 1 + return + } + await runRemove(term) + return + case "update": + if (!term) { + console.log("Usage: cortex-sdk update ") + process.exitCode = 1 + return + } + await runUpdate(await createTui(), term) + return + default: + console.log(`Unknown command: ${command}`) + console.log("Run `cortex-sdk --help` for usage.") + process.exitCode = 1 + } +} + +main().catch(async (err) => { + console.error("Error:", err?.message ?? err) + process.exitCode = 1 +}) diff --git a/packages/cortex-sdk/cli/src/tui/banner.ts b/packages/cortex-sdk/cli/src/tui/banner.ts new file mode 100644 index 0000000..8003b6c --- /dev/null +++ b/packages/cortex-sdk/cli/src/tui/banner.ts @@ -0,0 +1,12 @@ +import { ASCIIFont, Text, Box } from "@opentui/core" + +export function renderBanner(title: string, subtitle?: string) { + return Box( + { flexDirection: "column", width: "100%" }, + ASCIIFont({ text: title, font: "tiny" }), + subtitle + ? Text({ content: subtitle, paddingLeft: 1, paddingTop: 1 }) + : Text({ content: "", paddingTop: 1 }), + Text({ content: "", paddingTop: 1 }), + ) +} diff --git a/packages/cortex-sdk/cli/src/tui/input.ts b/packages/cortex-sdk/cli/src/tui/input.ts new file mode 100644 index 0000000..6c0e3a9 --- /dev/null +++ b/packages/cortex-sdk/cli/src/tui/input.ts @@ -0,0 +1,68 @@ +import { Input, InputRenderableEvents, Box, Text, type ProxiedVNode } from "@opentui/core" +import type { TuiContext } from "./renderer" +import { renderBanner } from "./banner" + +export interface InputResult { + value: string + cancelled: boolean +} + +export async function promptInput( + tui: TuiContext, + opts: { + title: string + subtitle?: string + placeholder?: string + secure?: boolean + initialValue?: string + }, +): Promise { + return new Promise(async (resolve) => { + const input = Input({ + placeholder: opts.placeholder, + value: opts.initialValue ?? "", + maxLength: 512, + }) + + let settled = false + + const onEnter = (value: string) => { + if (settled) return + settled = true + cleanup() + resolve({ value, cancelled: false }) + } + const onKey = (key: { name?: string; ctrl?: boolean }) => { + if (settled) return + if (key.name === "escape") { + settled = true + cleanup() + resolve({ value: "", cancelled: true }) + } + if (key.ctrl && key.name === "c") { + settled = true + cleanup() + process.exit(130) + } + } + const cleanup = () => { + input.off(InputRenderableEvents.ENTER, onEnter) + tui.renderer.keyInput.off("keypress", onKey) + } + + input.on(InputRenderableEvents.ENTER, onEnter) + tui.renderer.keyInput.on("keypress", onKey) + + const secureNote = opts.secure + ? Text({ content: "(input hidden)", paddingLeft: 1, paddingBottom: 1 }) + : Text({ content: "", paddingBottom: 1 }) + const node = Box( + { flexDirection: "column", width: "100%" }, + renderBanner(opts.title, opts.subtitle), + secureNote, + input as unknown as ProxiedVNode, + ) + await tui.screen(node) + tui.focus() + }) +} diff --git a/packages/cortex-sdk/cli/src/tui/multiselect.ts b/packages/cortex-sdk/cli/src/tui/multiselect.ts new file mode 100644 index 0000000..87f3d7c --- /dev/null +++ b/packages/cortex-sdk/cli/src/tui/multiselect.ts @@ -0,0 +1,108 @@ +import { Select, SelectRenderableEvents, Box, Text, type ProxiedVNode } from "@opentui/core" +import type { TuiContext } from "./renderer" +import { renderBanner } from "./banner" + +export interface MultiSelectOption { + name: string + description: string + value?: string +} + +export interface MultiSelectResult { + selected: string[] + cancelled: boolean +} + +export async function promptMultiSelect( + tui: TuiContext, + opts: { + title: string + subtitle?: string + options: MultiSelectOption[] + defaults?: string[] + }, +): Promise { + const state = new Map() + for (const opt of opts.options) { + state.set(opt.value ?? opt.name, opts.defaults?.includes(opt.value ?? opt.name) ?? false) + } + + return new Promise(async (resolve) => { + const select = Select({ + options: [], + selectedIndex: 0, + showDescription: true, + showScrollIndicator: true, + height: Math.min(opts.options.length, 8) * 2 + 1, + }) + + let currentValue: string | undefined = opts.options[0] ? opts.options[0].value ?? opts.options[0].name : undefined + let resolved = false + + const labelFor = (opt: MultiSelectOption): string => { + const v = opt.value ?? opt.name + const checked = state.get(v) ? "[x]" : "[ ]" + return `${checked} ${opt.name}` + } + + const applyOptions = () => { + select.options = opts.options.map((o) => ({ + name: labelFor(o), + description: o.description, + value: o.value ?? o.name, + })) + } + + const onSelectionChanged = (_index: number, option: { name: string; value?: string }) => { + currentValue = option.value ?? option.name + } + const onKey = (key: { name?: string; ctrl?: boolean }) => { + if (key.ctrl && key.name === "c") { + cleanup() + process.exit(130) + } + } + const onToggleKey = (key: { name?: string; ctrl?: boolean }) => { + if (key.name === "space") { + if (!currentValue) return + state.set(currentValue, !(state.get(currentValue) ?? false)) + applyOptions() + } + if (key.name === "return") { + if (resolved) return + resolved = true + cleanup() + const selected = opts.options + .filter((o) => state.get(o.value ?? o.name)) + .map((o) => o.value ?? o.name) + resolve({ selected, cancelled: false }) + } + if (key.name === "escape") { + if (resolved) return + resolved = true + cleanup() + resolve({ selected: [], cancelled: true }) + } + } + + const cleanup = () => { + select.off(SelectRenderableEvents.SELECTION_CHANGED, onSelectionChanged) + tui.renderer.keyInput.off("keypress", onKey) + tui.renderer.keyInput.off("keypress", onToggleKey) + } + + select.on(SelectRenderableEvents.SELECTION_CHANGED, onSelectionChanged) + tui.renderer.keyInput.on("keypress", onKey) + tui.renderer.keyInput.on("keypress", onToggleKey) + + const node = Box( + { flexDirection: "column", width: "100%" }, + renderBanner(opts.title, opts.subtitle), + Text({ content: "↑/↓ move • space toggle • enter done • esc cancel", paddingLeft: 1, paddingBottom: 1 }), + select as unknown as ProxiedVNode, + ) + applyOptions() + await tui.screen(node) + tui.focus() + }) +} diff --git a/packages/cortex-sdk/cli/src/tui/renderer.ts b/packages/cortex-sdk/cli/src/tui/renderer.ts new file mode 100644 index 0000000..229bfef --- /dev/null +++ b/packages/cortex-sdk/cli/src/tui/renderer.ts @@ -0,0 +1,67 @@ +import { + createCliRenderer, + instantiate, + type CliRenderer, + type Renderable, + type VNode, + type ProxiedVNode, +} from "@opentui/core" + +export interface TuiContext { + renderer: CliRenderer + mounted: Renderable[] + screen(node: ProxiedVNode): Promise + focus(): void + keypress(onKey: (key: { name?: string; sequence?: string; ctrl?: boolean }) => void): void +} + +export async function createTui(): Promise { + const renderer = await createCliRenderer({ + exitOnCtrlC: false, + screenMode: "alternate-screen", + }) + const mounted: Renderable[] = [] + + async function screen(node: ProxiedVNode): Promise { + for (const child of mounted) { + renderer.root.remove(child) + } + mounted.length = 0 + const renderable = instantiate(renderer, node as unknown as VNode) + renderer.root.add(renderable) + mounted.push(renderable) + await new Promise((r) => setTimeout(r, 100)) + return renderable + } + + return { + renderer, + mounted, + screen, + focus() { + const findFocusable = (node: Renderable): Renderable | null => { + if (node.focusable) return node + const children = (node as unknown as { _childrenInLayoutOrder: Renderable[] })._childrenInLayoutOrder ?? [] + for (const child of children) { + const found = findFocusable(child) + if (found) return found + } + return null + } + for (const root of mounted) { + const target = findFocusable(root) + if (target) { + target.focus() + return + } + } + }, + keypress(onKey) { + renderer.keyInput.on("keypress", (key) => onKey(key)) + }, + } +} + +export async function destroyTui(tui: TuiContext): Promise { + await tui.renderer.destroy() +} diff --git a/packages/cortex-sdk/cli/src/tui/select.ts b/packages/cortex-sdk/cli/src/tui/select.ts new file mode 100644 index 0000000..4dccd0f --- /dev/null +++ b/packages/cortex-sdk/cli/src/tui/select.ts @@ -0,0 +1,67 @@ +import { Select, SelectRenderableEvents, Box, type ProxiedVNode } from "@opentui/core" +import type { TuiContext } from "./renderer" +import { renderBanner } from "./banner" + +export interface SelectOption { + name: string + description: string + value?: string +} + +export async function promptSelect( + tui: TuiContext, + opts: { + title: string + subtitle?: string + options: SelectOption[] + defaultIndex?: number + }, +): Promise { + return new Promise(async (resolve) => { + const select = Select({ + options: opts.options.map((o) => ({ name: o.name, description: o.description, value: o.value })), + selectedIndex: opts.defaultIndex ?? 0, + showDescription: true, + showScrollIndicator: true, + height: Math.min(opts.options.length, 8) * 2 + 1, + }) + + let settled = false + + const onSelect = (_index: number, option: { name: string; value?: string }) => { + if (settled) return + settled = true + cleanup() + resolve(option.value ?? option.name) + } + const onKey = (key: { name?: string; ctrl?: boolean }) => { + if (settled) return + if (key.name === "escape") { + settled = true + cleanup() + resolve(undefined) + } + if (key.ctrl && key.name === "c") { + settled = true + cleanup() + process.exit(130) + } + } + + const cleanup = () => { + select.off(SelectRenderableEvents.ITEM_SELECTED, onSelect) + tui.renderer.keyInput.off("keypress", onKey) + } + + select.on(SelectRenderableEvents.ITEM_SELECTED, onSelect) + tui.renderer.keyInput.on("keypress", onKey) + + const node = Box( + { flexDirection: "column", width: "100%" }, + renderBanner(opts.title, opts.subtitle), + select as unknown as ProxiedVNode, + ) + await tui.screen(node) + tui.focus() + }) +} diff --git a/packages/cortex-sdk/package.json b/packages/cortex-sdk/package.json new file mode 100644 index 0000000..90372ed --- /dev/null +++ b/packages/cortex-sdk/package.json @@ -0,0 +1,76 @@ +{ + "name": "cortex-sdk", + "version": "0.1.0", + "description": "Unified SDK for building AI agents with 8 gateway providers, 3 web search providers, 6 MCP platforms, and 3 voice providers", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "cortex-sdk": "./dist/cli/index.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./cli": { + "types": "./dist/cli/index.d.ts", + "import": "./dist/cli/index.js" + }, + "./gateway": { + "types": "./dist/gateway/index.d.ts", + "import": "./dist/gateway/index.js" + }, + "./agent-handler": { + "types": "./dist/agent-handler/index.d.ts", + "import": "./dist/agent-handler/index.js" + }, + "./composio": { + "types": "./dist/composio/index.d.ts", + "import": "./dist/composio/index.js" + }, + "./web-search": { + "types": "./dist/web-search/index.d.ts", + "import": "./dist/web-search/index.js" + }, + "./mcp": { + "types": "./dist/mcp/index.d.ts", + "import": "./dist/mcp/index.js" + }, + "./voice": { + "types": "./dist/voice/index.d.ts", + "import": "./dist/voice/index.js" + } + }, + "files": ["dist"], + "scripts": { + "build": "tsup", + "build:cli": "tsup --config tsup.cli.config.ts", + "lint": "echo 'cortex-sdk lint pending'", + "typecheck": "tsc --noEmit", + "test": "bun test" + }, + "peerDependencies": { + "ai": "^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "dependencies": { + "@ai-sdk/google": "^3.0.80", + "@ai-sdk/mcp": "^2.0.30", + "@ai-sdk/openai-compatible": "^2.0.48", + "@ai-sdk/provider": "^3.0.10", + "@opentui/core": "^0.5.2", + "context.dev": "^2.10.0", + "vercel-minimax-ai-provider": "^0.0.2", + "zod": "^3.25.0" + }, + "optionalDependencies": { + "@composio/core": "^0.13.1", + "@modelcontextprotocol/sdk": "^1.9.0" + }, + "devDependencies": { + "@types/bun": "^1.3.14", + "tsup": "^8.5.1", + "typescript": "^5.7.0" + } +} diff --git a/packages/cortex-sdk/src/agent-handler/client.test.ts b/packages/cortex-sdk/src/agent-handler/client.test.ts new file mode 100644 index 0000000..1b36c26 --- /dev/null +++ b/packages/cortex-sdk/src/agent-handler/client.test.ts @@ -0,0 +1,219 @@ +import { describe, expect, test } from "bun:test" +import { ConnectionError, SdkError, ToolPackError } from "../core/errors" +import type { AgentHandlerClientOptions, McpClientLike } from "./types" +import { AgentHandlerClientImpl } from "./client" +import { listToolPacks, TOOL_PACK_PRESETS } from "./tool-packs" + +function fakeMcpClient(tools: Record): McpClientLike { + return { + tools: async () => tools as never, + close: async () => {}, + } +} + +function makeOptions(overrides: Partial = {}): AgentHandlerClientOptions { + const created: Array<{ config: { transport: { type: string; url: string } } }> = [] + const options: AgentHandlerClientOptions = { + apiKey: "sk-test", + registeredUserId: "user-42", + createMcpClient: (config) => { + created.push(config as never) + return Promise.resolve(fakeMcpClient({})) + }, + fetch: async () => new Response(JSON.stringify(TOOL_PACK_PRESETS), { status: 200 }), + ...overrides, + } + return options +} + +describe("AgentHandlerClientImpl", () => { + test("connect() requires at least one selected pack", async () => { + const handler = new AgentHandlerClientImpl(makeOptions({ toolPacks: [] })) + await expect(handler.connect()).rejects.toBeInstanceOf(SdkError) + expect(handler.isConnected).toBe(false) + }) + + test("connect() throws MISSING_REGISTERED_USER_ID without registeredUserId", async () => { + const handler = new AgentHandlerClientImpl( + makeOptions({ toolPacks: ["web-search"], registeredUserId: undefined }), + ) + await expect(handler.connect()).rejects.toThrow(/registeredUserId/) + }) + + test("connect() and getTools() merge tools from selected packs", async () => { + const handler = new AgentHandlerClientImpl( + makeOptions({ + toolPacks: ["web-search"], + createMcpClient: () => + Promise.resolve(fakeMcpClient({ firecrawl_search: { type: "function" } })), + }), + ) + await handler.connect() + expect(handler.isConnected).toBe(true) + const tools = await handler.getTools() + expect(Object.keys(tools)).toEqual(["firecrawl_search"]) + }) + + test("disconnect() closes clients and clears connection", async () => { + let closed = 0 + const trackingClient: McpClientLike = { + tools: async () => ({}), + close: async () => { + closed++ + }, + } + const handler = new AgentHandlerClientImpl( + makeOptions({ + toolPacks: ["web-search"], + createMcpClient: () => Promise.resolve(trackingClient), + }), + ) + await handler.connect() + expect(handler.isConnected).toBe(true) + await handler.disconnect() + expect(closed).toBe(1) + expect(handler.isConnected).toBe(false) + }) + + test("getTools() auto-connects when disconnected", async () => { + const handler = new AgentHandlerClientImpl( + makeOptions({ + toolPacks: ["web-search"], + createMcpClient: () => + Promise.resolve(fakeMcpClient({ exa_search: { type: "function" } })), + }), + ) + const tools = await handler.getTools() + expect(Object.keys(tools)).toEqual(["exa_search"]) + }) + + test("getTools() wraps pack tool failure in ToolPackError", async () => { + const handler = new AgentHandlerClientImpl( + makeOptions({ + toolPacks: ["web-search"], + createMcpClient: () => + Promise.resolve({ + tools: async () => { + throw new Error("mcp down") + }, + close: async () => {}, + }), + }), + ) + await expect(handler.getTools()).rejects.toBeInstanceOf(ToolPackError) + }) + + test("selectPacks() validates ids against known packs", async () => { + const handler = new AgentHandlerClientImpl(makeOptions()) + await expect(handler.selectPacks(["nope"])).rejects.toBeInstanceOf(ToolPackError) + }) + + test("selectPacks() returns matching pack info", async () => { + const handler = new AgentHandlerClientImpl(makeOptions()) + await handler.selectPacks(["web-search"]) + expect(handler.selectedPacks).toEqual(["web-search"]) + const selected = await handler.selectPacks(["web-search", "exa-search"]) + expect(selected.map((p) => p.id)).toEqual(["web-search", "exa-search"]) + expect(selected[0]?.name).toBeDefined() + }) + + test("selectPacks() reconnects when already connected", async () => { + const handler = new AgentHandlerClientImpl( + makeOptions({ + toolPacks: ["web-search"], + createMcpClient: () => Promise.resolve(fakeMcpClient({})), + }), + ) + await handler.connect() + expect(handler.isConnected).toBe(true) + await handler.selectPacks(["web-search", "exa-search"]) + expect(handler.isConnected).toBe(true) + expect(handler.selectedPacks).toEqual(["web-search", "exa-search"]) + }) + + test("selectPacks() falls back to presets when discovery API fails", async () => { + const handler = new AgentHandlerClientImpl( + makeOptions({ + createMcpClient: () => Promise.resolve(fakeMcpClient({})), + fetch: async () => { + throw new TypeError("network down") + }, + }), + ) + const selected = await handler.selectPacks(["web-search"]) + expect(selected.map((p) => p.id)).toEqual(["web-search"]) + expect(handler.selectedPacks).toEqual(["web-search"]) + }) + + test("listToolPacks() caches discovery results", async () => { + let fetchCount = 0 + const handler = new AgentHandlerClientImpl( + makeOptions({ + fetch: async () => { + fetchCount++ + return new Response(JSON.stringify(TOOL_PACK_PRESETS), { status: 200 }) + }, + }), + ) + const first = await handler.listToolPacks() + const second = await handler.listToolPacks() + expect(first.length).toBeGreaterThan(0) + expect(second).toEqual(first) + expect(fetchCount).toBe(1) + }) + + test("clearToolPackCache() forces re-discovery", async () => { + let fetchCount = 0 + const handler = new AgentHandlerClientImpl( + makeOptions({ + fetch: async () => { + fetchCount++ + return new Response(JSON.stringify(TOOL_PACK_PRESETS), { status: 200 }) + }, + }), + ) + await handler.listToolPacks() + handler.clearToolPackCache() + await handler.listToolPacks() + expect(fetchCount).toBe(2) + }) + + test("selectPacks() throws for unknown id even when API fails", async () => { + const handler = new AgentHandlerClientImpl( + makeOptions({ + fetch: async () => { + throw new TypeError("network down") + }, + }), + ) + await expect(handler.selectPacks(["nope"])).rejects.toBeInstanceOf(ToolPackError) + }) + + test("listToolPacks() returns discovered packs", async () => { + const handler = new AgentHandlerClientImpl(makeOptions()) + const packs = await handler.listToolPacks() + expect(packs.length).toBeGreaterThan(0) + }) +}) + +describe("createClient auth failure", () => { + test("maps 401 message to ConnectionError with AUTH_ERROR code", async () => { + const handler = new AgentHandlerClientImpl( + makeOptions({ + toolPacks: ["web-search"], + createMcpClient: () => Promise.reject(new Error("401 Unauthorized")), + }), + ) + await expect(handler.connect()).rejects.toMatchObject({ code: "AUTH_ERROR" }) + await expect(handler.connect()).rejects.toBeInstanceOf(ConnectionError) + }) +}) + +describe("listToolPacks integration", () => { + test("uses provided fetch and returns presets", async () => { + const packs = await listToolPacks({ + fetch: async () => new Response(JSON.stringify(TOOL_PACK_PRESETS), { status: 200 }), + }) + expect(packs.length).toBeGreaterThan(0) + }) +}) diff --git a/packages/cortex-sdk/src/agent-handler/client.ts b/packages/cortex-sdk/src/agent-handler/client.ts new file mode 100644 index 0000000..62e6fb7 --- /dev/null +++ b/packages/cortex-sdk/src/agent-handler/client.ts @@ -0,0 +1,137 @@ +import { createMCPClient, type MCPClientConfig } from "@ai-sdk/mcp" +import type { Tool } from "ai" +import { ConnectionError, SdkError, ToolPackError } from "../core/errors" +import { authHeaders, listToolPacks, mcpUrl, TOOL_PACK_PRESETS } from "./tool-packs" +import type { AgentHandlerClient, AgentHandlerClientOptions, McpClientLike, ToolPackInfo } from "./types" + +export class AgentHandlerClientImpl implements AgentHandlerClient { + private readonly options: AgentHandlerClientOptions + private readonly clients = new Map() + private selected: string[] + private discovered: ToolPackInfo[] | null = null + + constructor(options: AgentHandlerClientOptions) { + this.options = options + this.selected = options.toolPacks ?? [] + } + + get isConnected(): boolean { + return this.clients.size > 0 + } + + get selectedPacks(): string[] { + return [...this.selected] + } + + async connect(): Promise { + if (this.selected.length === 0) { + throw new SdkError("connect: no tool packs selected. Call selectPacks() first", { + code: "NO_TOOL_PACKS", + }) + } + for (const packId of this.selected) { + if (this.clients.has(packId)) continue + const client = await this.createClient(packId) + this.clients.set(packId, client) + } + } + + async disconnect(): Promise { + const clients = [...this.clients.values()] + this.clients.clear() + await Promise.allSettled(clients.map((client) => client.close())) + } + + async reconnect(): Promise { + await this.disconnect() + await this.connect() + } + + async getTools(): Promise> { + if (!this.isConnected) { + await this.connect() + } + const tools: Record = {} + for (const [packId, client] of this.clients) { + try { + Object.assign(tools, await client.tools()) + } catch (error) { + throw new ToolPackError(`getTools: pack "${packId}" failed to return tools`, { cause: error }) + } + } + return tools + } + + async listToolPacks(): Promise { + if (this.discovered) { + return this.discovered + } + const packs = await listToolPacks({ + apiUrl: this.options.apiUrl, + apiKey: this.options.apiKey, + fetch: this.options.fetch, + }) + this.discovered = packs + return packs + } + + clearToolPackCache(): void { + this.discovered = null + } + + async selectPacks(ids: string[]): Promise { + let known: ToolPackInfo[] + try { + known = await this.listToolPacks() + } catch { + known = this.discovered ?? TOOL_PACK_PRESETS + } + const knownIds = new Set(known.map((pack) => pack.id)) + for (const id of ids) { + if (!knownIds.has(id)) { + throw new ToolPackError(`selectPacks: unknown tool pack "${id}"`, { code: "UNKNOWN_TOOL_PACK" }) + } + } + if (this.isConnected) { + await this.disconnect() + this.selected = ids + await this.connect() + } else { + this.selected = ids + } + return known.filter((pack) => ids.includes(pack.id)) + } + + private async createClient(packId: string): Promise { + const transport = this.options.transport ?? "http" + const apiUrl = this.options.apiUrl ?? "https://ah-api.merge.dev" + const registeredUserId = this.options.registeredUserId + if (!registeredUserId) { + throw new SdkError("createAgentHandler: registeredUserId is required", { + code: "MISSING_REGISTERED_USER_ID", + }) + } + const config = { + transport: { + type: transport, + url: mcpUrl({ apiUrl, toolPackId: packId, registeredUserId }), + headers: authHeaders(this.options.apiKey), + }, + } + try { + const createClient = + this.options.createMcpClient ?? + ((c) => createMCPClient(c as MCPClientConfig) as unknown as Promise) + return await createClient(config) + } catch (error) { + if (error instanceof SdkError) throw error + if (error instanceof Error && error.message.includes("401")) { + throw new ConnectionError(`connect: authentication failed for pack "${packId}"`, { + code: "AUTH_ERROR", + cause: error, + }) + } + throw new ConnectionError(`connect: failed to connect to pack "${packId}"`, { cause: error }) + } + } +} \ No newline at end of file diff --git a/packages/cortex-sdk/src/agent-handler/index.ts b/packages/cortex-sdk/src/agent-handler/index.ts new file mode 100644 index 0000000..094fb71 --- /dev/null +++ b/packages/cortex-sdk/src/agent-handler/index.ts @@ -0,0 +1,13 @@ +import type { AgentHandlerConfig } from "../core/types" +import { AgentHandlerClientImpl } from "./client" +import type { AgentHandlerClient, AgentHandlerClientOptions, ToolPackInfo } from "./types" + +export type { AgentHandlerConfig } from "../core/types" +export type { AgentHandlerClient, AgentHandlerClientOptions, ToolPackInfo } from "./types" + +export { listToolPacks, TOOL_PACK_PRESETS } from "./tool-packs" +export type { McpClientLike, AgentHandlerTransportConfig } from "./types" + +export function createAgentHandler(config: AgentHandlerConfig): AgentHandlerClient { + return new AgentHandlerClientImpl(config as AgentHandlerClientOptions) +} \ No newline at end of file diff --git a/packages/cortex-sdk/src/agent-handler/tool-packs.test.ts b/packages/cortex-sdk/src/agent-handler/tool-packs.test.ts new file mode 100644 index 0000000..bf52b75 --- /dev/null +++ b/packages/cortex-sdk/src/agent-handler/tool-packs.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, test } from "bun:test" +import { ConnectionError, SdkError, ToolPackError } from "../core/errors" +import type { FetchLike } from "../core/types" +import { authHeaders, listToolPacks, mergePresets, mcpUrl, TOOL_PACK_PRESETS } from "./tool-packs" + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }) +} + +function jsonFetch(body: unknown, status = 200): FetchLike { + return async () => jsonResponse(body, status) +} + +describe("mcpUrl", () => { + test("builds the MCP endpoint URL", () => { + const url = mcpUrl({ + apiUrl: "https://ah-api.merge.dev", + toolPackId: "web-search", + registeredUserId: "user-42", + }) + expect(url).toBe( + "https://ah-api.merge.dev/api/v1/tool-packs/web-search/registered-users/user-42/mcp", + ) + }) +}) + +describe("authHeaders", () => { + test("adds bearer token when apiKey provided", () => { + expect(authHeaders("sk-test")).toEqual({ Authorization: "Bearer sk-test" }) + }) + + test("returns empty headers without apiKey", () => { + expect(authHeaders()).toEqual({}) + }) +}) + +describe("mergePresets", () => { + test("deduplicates discovered packs against presets by id", () => { + const merged = mergePresets([ + { id: "web-search", name: "Web Search", tools: ["firecrawl_search"] }, + ]) + expect(merged.map((p) => p.id)).toEqual(["web-search", "exa-search"]) + expect(merged[0]?.tools).toEqual(["firecrawl_search"]) + }) + + test("preserves discovered pack order", () => { + const merged = mergePresets([ + { id: "custom", name: "Custom", tools: ["x"] }, + { id: "web-search", name: "Web Search", tools: [] }, + ]) + expect(merged.map((p) => p.id)).toEqual(["custom", "web-search", "exa-search"]) + }) +}) + +describe("listToolPacks", () => { + test("parses an array body and merges presets", async () => { + const packs = await listToolPacks({ + apiKey: "sk-test", + fetch: jsonFetch([{ id: "exa-search", name: "Exa Search", tools: ["exa_search"] }]), + }) + expect(packs.map((p) => p.id)).toEqual(["exa-search", "web-search"]) + expect(packs[0]?.tools).toEqual(["exa_search"]) + }) + + test("parses data/toolPacks/results/packs shapes", async () => { + const packs = await listToolPacks({ + fetch: jsonFetch({ toolPacks: [{ id: "web-search", name: "Web Search", tools: [] }] }), + }) + expect(packs.map((p) => p.id)).toContain("web-search") + }) + + test("sends bearer auth and Accept header", async () => { + let captured: RequestInit | undefined + const fetchImpl: FetchLike = async (_input, init) => { + captured = init + return jsonResponse([{ id: "p1", name: "P1", tools: ["t"] }]) + } + await listToolPacks({ apiKey: "sk-test", fetch: fetchImpl }) + expect(captured?.method).toBe("GET") + const headers = captured?.headers as Record + expect(headers.Authorization).toBe("Bearer sk-test") + expect(headers.Accept).toBe("application/json") + }) + + test("throws ToolPackError on non-OK response", async () => { + await expect( + listToolPacks({ fetch: jsonFetch({ error: "down" }, 500) }), + ).rejects.toBeInstanceOf(ToolPackError) + }) + + test("throws ToolPackError when no packs returned", async () => { + await expect(listToolPacks({ fetch: jsonFetch([]) })).rejects.toBeInstanceOf(ToolPackError) + }) + + test("throws ConnectionError on network failure", async () => { + const fetchImpl: FetchLike = async () => { + throw new TypeError("fetch failed") + } + await expect(listToolPacks({ fetch: fetchImpl })).rejects.toBeInstanceOf(ConnectionError) + }) + + test("SdkError and ToolPackError are instanceof SdkError", async () => { + expect(TOOL_PACK_PRESETS.length).toBeGreaterThan(0) + const err = new SdkError("boom", { code: "SDK_ERROR" }) + expect(err).toBeInstanceOf(SdkError) + }) +}) diff --git a/packages/cortex-sdk/src/agent-handler/tool-packs.ts b/packages/cortex-sdk/src/agent-handler/tool-packs.ts new file mode 100644 index 0000000..0f4cabf --- /dev/null +++ b/packages/cortex-sdk/src/agent-handler/tool-packs.ts @@ -0,0 +1,107 @@ +import { ConnectionError, SdkError, ToolPackError } from "../core/errors" +import type { FetchLike } from "../core/types" +import type { ToolPackInfo } from "./types" + +export const AH_API_BASE = "https://ah-api.merge.dev" + +const AH_API_PATH = "/api/v1/tool-packs" + +export const TOOL_PACK_PRESETS: ToolPackInfo[] = [ + { + id: "web-search", + name: "Web Search", + tools: ["firecrawl_search", "firecrawl_scrape", "firecrawl_crawl", "firecrawl_map"], + }, + { + id: "exa-search", + name: "Exa Search", + tools: ["exa_search", "exa_web_fetch_exa"], + }, +] + +export function mcpUrl(options: { + apiUrl: string + toolPackId: string + registeredUserId: string +}): string { + return `${options.apiUrl}${AH_API_PATH}/${options.toolPackId}/registered-users/${options.registeredUserId}/mcp` +} + +export function authHeaders(apiKey?: string): Record { + return apiKey ? { Authorization: `Bearer ${apiKey}` } : {} +} + +export function mergePresets(discovered: ToolPackInfo[]): ToolPackInfo[] { + const seen = new Set() + const merged: ToolPackInfo[] = [] + for (const pack of [...discovered, ...TOOL_PACK_PRESETS]) { + if (!pack.id || seen.has(pack.id)) continue + seen.add(pack.id) + merged.push(pack) + } + return merged +} + +export function toToolPackInfo(raw: unknown): ToolPackInfo | null { + if (!raw || typeof raw !== "object") return null + const record = raw as Record + const id = typeof record.id === "string" ? record.id : typeof record.name === "string" ? record.name : "" + if (!id) return null + const name = typeof record.name === "string" ? record.name : id + const tools = Array.isArray(record.tools) + ? record.tools.filter((t): t is string => typeof t === "string") + : [] + return { id, name, tools } +} + +function extractPacks(body: unknown): ToolPackInfo[] { + if (Array.isArray(body)) { + return body.map(toToolPackInfo).filter((p): p is ToolPackInfo => p !== null) + } + if (body && typeof body === "object") { + const record = body as Record + for (const key of ["data", "toolPacks", "results", "packs"]) { + if (Array.isArray(record[key])) { + return extractPacks(record[key]) + } + } + } + return [] +} + +export async function listToolPacks(options: { + apiUrl?: string + apiKey?: string + fetch?: FetchLike +}): Promise { + const apiUrl = options.apiUrl ?? AH_API_BASE + const fetchImpl = options.fetch ?? fetch + try { + const response = await fetchImpl(`${apiUrl}${AH_API_PATH}`, { + method: "GET", + headers: { + ...authHeaders(options.apiKey), + Accept: "application/json", + }, + }) + if (!response.ok) { + throw new SdkError(`listToolPacks: HTTP ${response.status} ${response.statusText}`) + } + const body = (await response.json()) as unknown + const discovered = extractPacks(body) + if (discovered.length === 0) { + throw new SdkError("listToolPacks: no tool packs returned") + } + return mergePresets(discovered) + } catch (error) { + if (error instanceof ToolPackError) throw error + if (error instanceof SdkError && error.code !== "SDK_ERROR") throw error + if (error instanceof TypeError) { + throw new ConnectionError("listToolPacks: network error", { cause: error }) + } + if (error instanceof SdkError && error.code === "SDK_ERROR") { + throw new ToolPackError(error.message, { cause: error }) + } + throw new ToolPackError("listToolPacks: failed to discover tool packs", { cause: error }) + } +} \ No newline at end of file diff --git a/packages/cortex-sdk/src/agent-handler/types.ts b/packages/cortex-sdk/src/agent-handler/types.ts new file mode 100644 index 0000000..1bdaf07 --- /dev/null +++ b/packages/cortex-sdk/src/agent-handler/types.ts @@ -0,0 +1,37 @@ +import type { Tool } from "ai" +import type { AgentHandlerConfig } from "../core/types" + +export interface ToolPackInfo { + id: string + name: string + tools: string[] +} + +export interface AgentHandlerTransportConfig { + transport: { + type: "http" | "sse" + url: string + headers?: Record + } +} + +export interface McpClientLike { + tools(): Promise> + close(): Promise +} + +export interface AgentHandlerClientOptions extends AgentHandlerConfig { + createMcpClient?: (config: AgentHandlerTransportConfig) => Promise +} + +export interface AgentHandlerClient { + connect(): Promise + disconnect(): Promise + reconnect(): Promise + getTools(): Promise> + listToolPacks(): Promise + selectPacks(ids: string[]): Promise + clearToolPackCache(): void + readonly isConnected: boolean + readonly selectedPacks: string[] +} \ No newline at end of file diff --git a/packages/cortex-sdk/src/composio/apps.ts b/packages/cortex-sdk/src/composio/apps.ts new file mode 100644 index 0000000..7d5ccfc --- /dev/null +++ b/packages/cortex-sdk/src/composio/apps.ts @@ -0,0 +1,88 @@ +import { openBrowser } from "./oauth" +import type { ComposioSessionManager } from "./session" +import type { AppInfo, ComposioLike, ConnectAppResult, OpenBrowser } from "./types" + +export interface ComposioAppManagerOptions { + session: ComposioSessionManager + openBrowser?: OpenBrowser +} + +export class ComposioAppManager { + private readonly session: ComposioSessionManager + private readonly openBrowserFn: OpenBrowser | undefined + + constructor(options: ComposioAppManagerOptions) { + this.session = options.session + this.openBrowserFn = options.openBrowser + } + + async listApps(): Promise { + if (this.session.mode === "proxied") { + return this.session.listAppsFromServer() + } + const client = await this.session.getClient() + const [authConfigs, toolkits, connectedRes] = await Promise.all([ + client.authConfigs.list({}), + client.toolkits.get(), + client.connectedAccounts.list({}), + ]) + + const configuredSlugs = new Set( + authConfigs.items?.map((ac) => ac.toolkit?.slug).filter(Boolean) as string[], + ) + + const connectedMap = new Map() + for (const acct of connectedRes.items ?? []) { + const slug = acct.toolkit?.slug + if (slug && acct.status === "ACTIVE") { + connectedMap.set(slug, { id: acct.id, status: acct.status }) + } + } + + const toolkitMap = new Map() + for (const tk of toolkits) { + toolkitMap.set(tk.slug, tk) + } + + const apps: AppInfo[] = [] + for (const slug of configuredSlugs) { + const tk = toolkitMap.get(slug) + if (!tk) continue + const conn = connectedMap.get(slug) + apps.push({ + slug: tk.slug, + name: tk.name ?? slug, + description: tk.meta?.description, + logo: tk.meta?.logo, + connected: conn?.status === "ACTIVE", + connectedAccountId: conn?.id, + }) + } + + apps.sort((a, b) => { + if (a.connected !== b.connected) return a.connected ? -1 : 1 + return a.name.localeCompare(b.name) + }) + + return apps + } + + async connectApp(slug: string): Promise { + const client: ComposioLike = await this.session.getClient() + const req = await client.toolkits.authorize(this.session.userId, slug) + + const result: ConnectAppResult = { + connectedAccountId: req.id, + redirectUrl: req.redirectUrl, + waitForActive: async () => { + await req.waitForConnection() + }, + } + + if (req.redirectUrl) { + await openBrowser(req.redirectUrl, this.openBrowserFn) + } + + return result + } +} \ No newline at end of file diff --git a/packages/cortex-sdk/src/composio/client.test.ts b/packages/cortex-sdk/src/composio/client.test.ts new file mode 100644 index 0000000..ff2dcdc --- /dev/null +++ b/packages/cortex-sdk/src/composio/client.test.ts @@ -0,0 +1,317 @@ +import { describe, expect, test } from "bun:test" +import { AuthError, ConnectionError, SdkError } from "../core/errors" +import type { ComposioClientOptions, ComposioLike, McpClientLike } from "./types" +import { ComposioClientImpl } from "./index" + +function fakeMcpClient(tools: Record): McpClientLike { + return { + tools: async () => tools as never, + close: async () => {}, + } +} + +function makeComposio(overrides: Partial = {}): ComposioLike { + const base: ComposioLike = { + sessions: { + create: async () => ({ + mcp: { url: "https://mcp.example.com/sse", headers: { Authorization: "Bearer x" } }, + session_id: "sess-1", + }), + }, + connectedAccounts: { + list: async () => ({ items: [] }), + }, + toolkits: { + get: async () => [ + { slug: "github", name: "GitHub", meta: { description: "GitHub tools" } }, + { slug: "linear", name: "Linear" }, + ], + authorize: async () => ({ + id: "acct-1", + redirectUrl: "https://auth.example.com/start", + waitForConnection: async () => {}, + }), + }, + authConfigs: { + list: async () => ({ items: [{ toolkit: { slug: "github" } }] }), + }, + } + return { ...base, ...overrides } +} + +function fakeComposioLoader(composio: ComposioLike) { + return { + loadComposio: async () => ({ + Composio: function FakeComposio() { + return composio + } as never, + }), + } +} + +function jsonResponse(data: unknown, status = 200): Response { + return new Response(JSON.stringify(data), { status }) +} + +function makeOptions(overrides: Partial = {}): ComposioClientOptions { + const created: Array<{ transport: { type: string; url: string } }> = [] + const options: ComposioClientOptions = { + apiKey: "comp-key", + loadComposio: async () => ({ + Composio: function FakeComposio() { + return makeComposio() + } as never, + }), + createMcpClient: (config) => { + created.push(config as never) + return Promise.resolve(fakeMcpClient({ comp_tool: { type: "function" } })) + }, + openBrowser: async () => {}, + ...overrides, + } + return options +} + +describe("ComposioClientImpl", () => { + test("connect() opens an MCP session and reports connected", async () => { + const client = new ComposioClientImpl(makeOptions()) + await client.connect() + expect(client.isConnected).toBe(true) + }) + + test("connect() is idempotent", async () => { + const options = makeOptions() + const client = new ComposioClientImpl(options) + await client.connect() + await client.connect() + expect(client.isConnected).toBe(true) + }) + + test("getTools() auto-connects and returns composio tools", async () => { + const client = new ComposioClientImpl(makeOptions()) + const tools = await client.getTools() + expect(Object.keys(tools)).toEqual(["comp_tool"]) + }) + + test("disconnect() closes the MCP client and clears connection", async () => { + let closed = 0 + const client = new ComposioClientImpl( + makeOptions({ + createMcpClient: () => + Promise.resolve({ + tools: async () => ({}), + close: async () => { + closed++ + }, + }), + }), + ) + await client.connect() + expect(client.isConnected).toBe(true) + await client.disconnect() + expect(closed).toBe(1) + expect(client.isConnected).toBe(false) + }) + + test("listApps() returns merged apps with connection status", async () => { + const composio = makeComposio({ + connectedAccounts: { + list: async () => ({ items: [{ id: "acct-github", status: "ACTIVE", toolkit: { slug: "github" } }] }), + }, + }) + const client = new ComposioClientImpl( + makeOptions({ ...fakeComposioLoader(composio) }), + ) + const apps = await client.listApps() + expect(apps).toHaveLength(1) + expect(apps[0]?.slug).toBe("github") + expect(apps[0]?.connected).toBe(true) + expect(apps[0]?.connectedAccountId).toBe("acct-github") + }) + + test("listApps() sorts connected apps first", async () => { + const composio = makeComposio({ + authConfigs: { + list: async () => ({ + items: [{ toolkit: { slug: "github" } }, { toolkit: { slug: "linear" } }], + }), + }, + connectedAccounts: { + list: async () => ({ + items: [{ id: "acct-linear", status: "ACTIVE", toolkit: { slug: "linear" } }], + }), + }, + }) + const client = new ComposioClientImpl( + makeOptions({ ...fakeComposioLoader(composio) }), + ) + const apps = await client.listApps() + expect(apps.map((a) => a.slug)).toEqual(["linear", "github"]) + }) + + test("connectApp() starts OAuth, opens browser, waits for active, refreshes session", async () => { + const sessionCreates: string[] = [] + const composio = makeComposio({ + sessions: { + create: async (userId) => { + sessionCreates.push(userId) + return { + mcp: { url: "https://mcp.example.com/sse", headers: { Authorization: "Bearer x" } }, + session_id: `sess-${sessionCreates.length}`, + } + }, + }, + }) + let browserOpened = "" + const client = new ComposioClientImpl( + makeOptions({ + ...fakeComposioLoader(composio), + openBrowser: async (url) => { + browserOpened = url + }, + }), + ) + await client.connect() + const before = sessionCreates.length + const result = await client.connectApp("github") + expect(result.connectedAccountId).toBe("acct-1") + expect(browserOpened).toBe("https://auth.example.com/start") + expect(sessionCreates.length).toBeGreaterThan(before) + }) + + test("connectApp() falls back to proxied mode when no apiKey is set", async () => { + const client = new ComposioClientImpl(makeOptions({ apiKey: undefined })) + await expect(client.connectApp("github")).rejects.toMatchObject({ + code: "PROXIED_CONNECT_UNSUPPORTED", + }) + }) + + test("connectApp() throws PROXIED_CONNECT_UNSUPPORTED in proxied mode", async () => { + const client = new ComposioClientImpl( + makeOptions({ apiKey: undefined, serverUrl: "https://server.example.com", accessToken: "tok" }), + ) + await expect(client.connectApp("github")).rejects.toMatchObject({ + code: "PROXIED_CONNECT_UNSUPPORTED", + }) + }) + + test("selectApps() connects each app in order", async () => { + const connected: string[] = [] + const composio = makeComposio({ + toolkits: { + get: async () => [], + authorize: async (_userId: string, slug: string) => { + connected.push(slug) + return { + id: `acct-${slug}`, + redirectUrl: null, + waitForConnection: async () => {}, + } + }, + }, + }) + const client = new ComposioClientImpl( + makeOptions({ + createMcpClient: () => + Promise.resolve({ + tools: async () => ({}), + close: async () => {}, + }), + ...fakeComposioLoader(composio), + }), + ) + await client.selectApps(["github", "linear"]) + expect(connected).toEqual(["github", "linear"]) + }) +}) + +describe("proxied mode", () => { + test("connect() uses the server session endpoint", async () => { + const calls: Array<{ url: string; headers: Record }> = [] + const client = new ComposioClientImpl( + makeOptions({ + apiKey: undefined, + serverUrl: "https://server.example.com", + accessToken: "user-tok", + fetch: (async (url, init) => { + calls.push({ + url: String(url), + headers: (init?.headers ?? {}) as Record, + }) + return jsonResponse({ + url: "https://mcp.example.com/sse", + headers: { Authorization: "Bearer sess" }, + sessionId: "sess-proxy", + }) + }) as ComposioClientOptions["fetch"], + }), + ) + await client.connect() + expect(calls[0]?.url).toBe("https://server.example.com/api/composio/session") + expect(calls[0]?.headers.Authorization).toBe("Bearer user-tok") + expect(client.isConnected).toBe(true) + }) + + test("listApps() fetches from the server apps endpoint", async () => { + const client = new ComposioClientImpl( + makeOptions({ + apiKey: undefined, + serverUrl: "https://server.example.com", + accessToken: "user-tok", + fetch: (async () => + jsonResponse({ + apps: [{ slug: "github", name: "GitHub", connected: true, connectedAccountId: "a1" }], + })) as ComposioClientOptions["fetch"], + }), + ) + const apps = await client.listApps() + expect(apps).toHaveLength(1) + expect(apps[0]?.slug).toBe("github") + }) + + test("listApps() returns [] when server omits apps", async () => { + const client = new ComposioClientImpl( + makeOptions({ + apiKey: undefined, + serverUrl: "https://server.example.com", + accessToken: "user-tok", + fetch: (async () => jsonResponse({})) as ComposioClientOptions["fetch"], + }), + ) + const apps = await client.listApps() + expect(apps).toEqual([]) + }) + + test("non-ok server responses throw SdkError", async () => { + const client = new ComposioClientImpl( + makeOptions({ + apiKey: undefined, + serverUrl: "https://server.example.com", + accessToken: "user-tok", + fetch: (async () => new Response("boom", { status: 500 })) as ComposioClientOptions["fetch"], + }), + ) + await expect(client.listApps()).rejects.toBeInstanceOf(SdkError) + }) +}) + +describe("session edge cases", () => { + test("session create failure maps to ConnectionError", async () => { + const composio = makeComposio({ + sessions: { + create: async () => { + throw new Error("composio down") + }, + }, + }) + const client = new ComposioClientImpl( + makeOptions({ ...fakeComposioLoader(composio) }), + ) + await expect(client.connect()).rejects.toBeInstanceOf(ConnectionError) + }) + + test("missing apiKey in local mode throws AuthError on connect", async () => { + const client = new ComposioClientImpl(makeOptions({ apiKey: undefined })) + await expect(client.connect()).rejects.toBeInstanceOf(AuthError) + }) +}) diff --git a/packages/cortex-sdk/src/composio/index.ts b/packages/cortex-sdk/src/composio/index.ts new file mode 100644 index 0000000..b9a7df8 --- /dev/null +++ b/packages/cortex-sdk/src/composio/index.ts @@ -0,0 +1,178 @@ +import { createMCPClient } from "@ai-sdk/mcp" +import type { Tool } from "ai" +import { AuthError, ConnectionError, SdkError } from "../core/errors" +import type { ComposioConfig } from "../core/types" +import type { AgentHandlerTransportConfig } from "../agent-handler/types" +import { ComposioAppManager } from "./apps" +import { closeClient, recreateSession } from "./oauth" +import { + ComposioSessionManager, + COMPOSIO_DEFAULT_SERVER_URL, +} from "./session" +import type { + AppInfo, + ComposioClient, + ComposioClientOptions, + ConnectAppResult, + McpClientLike, +} from "./types" + +export type { ComposioConfig } from "../core/types" +export type { + AppInfo, + ComposioClient, + ComposioClientOptions, + ComposioSessionInfo, + ConnectAppResult, + McpClientLike, +} from "./types" + +export class ComposioClientImpl implements ComposioClient { + private readonly options: ComposioClientOptions + private readonly sessionManager: ComposioSessionManager + private readonly apps: ComposioAppManager + private mcpClient: McpClientLike | null = null + + constructor(options: ComposioConfig = {}) { + this.options = options as ComposioClientOptions + + const fetch = this.options.fetch + const loadComposio = this.options.loadComposio + const serverUrl = this.options.serverUrl ?? COMPOSIO_DEFAULT_SERVER_URL + const accessToken = this.options.accessToken + const apiKey = this.options.apiKey + + this.sessionManager = new ComposioSessionManager({ + apiKey, + serverUrl, + accessToken, + userId: this.options.userId, + fetch, + loadComposio, + }) + + this.apps = new ComposioAppManager({ + session: this.sessionManager, + openBrowser: this.options.openBrowser, + }) + } + + get isConnected(): boolean { + return this.mcpClient !== null + } + + async connect(): Promise { + if (this.mcpClient) return + const info = await this.getSessionInfo() + this.mcpClient = await this.createMcpClient(info.url, info.headers) + } + + async disconnect(): Promise { + const client = this.mcpClient + this.mcpClient = null + await closeClient(client) + this.sessionManager.resetSession() + } + + async getTools(): Promise> { + if (!this.mcpClient) { + await this.connect() + } + return await this.mcpClient!.tools() + } + + async listApps(): Promise { + return this.apps.listApps() + } + + async connectApp(slug: string): Promise { + if (this.sessionManager.mode === "proxied") { + throw new AuthError( + "createComposio.connectApp: proxied mode does not support toolkits.authorize; connect via the dashboard or use apiKey mode", + { code: "PROXIED_CONNECT_UNSUPPORTED" }, + ) + } + if (this.sessionManager.mode === "unconfigured") { + throw new AuthError( + `createComposio.connectApp("${slug}"): COMPOSIO_API_KEY not configured`, + { code: "NOT_CONFIGURED" }, + ) + } + + const result = await this.apps.connectApp(slug) + + try { + await result.waitForActive() + } catch (error) { + throw new ConnectionError( + `createComposio: connection for "${slug}" did not become active`, + { code: "CONNECTION_NOT_ACTIVE", cause: error }, + ) + } + + await this.refreshAfterConnection() + return result + } + + async selectApps(slugs: string[]): Promise { + for (const slug of slugs) { + await this.connectApp(slug) + } + } + + private async getSessionInfo() { + const existing = this.sessionManager.connectionInfo + if (existing) return existing + + if (this.sessionManager.mode === "proxied") { + return this.sessionManager.createSessionFromServer() + } + + try { + return await this.sessionManager.createSession() + } catch (error) { + throw new ConnectionError("createComposio: failed to create composio session", { + code: "SESSION_CREATE_FAILED", + cause: error, + }) + } + } + + private async createMcpClient(url: string, headers: Record): Promise { + const config: AgentHandlerTransportConfig = { + transport: { + type: url.endsWith("/sse") ? "sse" : "http", + url, + headers, + }, + } + try { + const createClient = + this.options.createMcpClient ?? + ((c: AgentHandlerTransportConfig) => + createMCPClient(c as never) as unknown as Promise) + return await createClient(config) + } catch (error) { + throw new ConnectionError("createComposio: failed to open MCP session", { + code: "MCP_SESSION_FAILED", + cause: error, + }) + } + } + + private async refreshAfterConnection(): Promise { + const client = this.mcpClient + this.mcpClient = null + await closeClient(client) + await recreateSession(this.sessionManager) + if (!this.sessionManager.connectionInfo) { + throw new SdkError("createComposio: session did not refresh after connection", { + code: "SESSION_REFRESH_FAILED", + }) + } + } +} + +export function createComposio(options: ComposioConfig = {}): ComposioClient { + return new ComposioClientImpl(options) +} \ No newline at end of file diff --git a/packages/cortex-sdk/src/composio/oauth.ts b/packages/cortex-sdk/src/composio/oauth.ts new file mode 100644 index 0000000..15da39e --- /dev/null +++ b/packages/cortex-sdk/src/composio/oauth.ts @@ -0,0 +1,41 @@ +import { ConnectionError } from "../core/errors" +import type { ComposioSessionManager } from "./session" +import type { McpClientLike, OpenBrowser } from "./types" + +export type { OpenBrowser } from "./types" + +export async function openBrowser(url: string, opener?: OpenBrowser): Promise { + if (opener) { + await opener(url) + return + } + try { + const open = (await import("open")).default + await open(url) + } catch (error) { + throw new ConnectionError(`opening browser for OAuth failed`, { + code: "OPEN_BROWSER_FAILED", + cause: error, + }) + } +} + +export async function recreateSession( + sessionManager: ComposioSessionManager, + userId?: string, +): Promise { + sessionManager.resetSession() + await sessionManager.createSession(userId ?? sessionManager.userId) +} + +export function closeClient(client: McpClientLike | null): Promise { + if (!client) return Promise.resolve() + try { + return client.close() + } catch (error) { + throw new ConnectionError("closing MCP session failed", { + code: "SESSION_CLOSE_FAILED", + cause: error, + }) + } +} \ No newline at end of file diff --git a/packages/cortex-sdk/src/composio/session.ts b/packages/cortex-sdk/src/composio/session.ts new file mode 100644 index 0000000..01986a2 --- /dev/null +++ b/packages/cortex-sdk/src/composio/session.ts @@ -0,0 +1,274 @@ +import { AuthError, ConnectionError, SdkError } from "../core/errors" +import type { FetchLike } from "../core/types" +import type { + AppInfo, + ComposioLike, + ComposioSessionInfo, + McpClientLike, +} from "./types" + +export type ComposioSessionMode = "local" | "proxied" | "unconfigured" + +export interface ComposioSessionManagerOptions { + apiKey?: string + serverUrl?: string + accessToken?: string + userId?: string + fetch?: FetchLike + loadComposio?: () => Promise<{ Composio: new (options: { apiKey: string }) => unknown }> +} + +export const COMPOSIO_DEFAULT_SERVER_URL = "https://supercode-8w7e.onrender.com" + +async function defaultLoadComposio(): Promise<{ + Composio: new (options: { apiKey: string }) => unknown +}> { + return (await import("@composio/core")) as unknown as { + Composio: new (options: { apiKey: string }) => unknown + } +} + +export class ComposioSessionManager { + private readonly options: ComposioSessionManagerOptions + private composio: ComposioLike | null = null + private session: ComposioSessionInfo | null = null + private serverApiKey: string | null = null + + constructor(options: ComposioSessionManagerOptions = {}) { + this.options = options + } + + get mode(): ComposioSessionMode { + if (this.apiKey) return "local" + if (this.options.serverUrl) return "proxied" + return "unconfigured" + } + + get isConfigured(): boolean { + return this.mode !== "unconfigured" + } + + get isConnected(): boolean { + return this.session !== null + } + + get connectionInfo(): ComposioSessionInfo | null { + return this.session + } + + get userId(): string { + return this.options.userId ?? "supercode-sdk" + } + + private get apiKey(): string | undefined { + return this.options.apiKey ?? this.serverApiKey ?? undefined + } + + async getClient(): Promise { + if (this.composio) return this.composio + if (!this.apiKey) { + throw new AuthError("createComposio: no API key or server URL configured", { + code: "NOT_CONFIGURED", + }) + } + const loadComposio = this.options.loadComposio ?? defaultLoadComposio + let mod: { Composio: new (options: { apiKey: string }) => unknown } + try { + mod = await loadComposio() + } catch (error) { + throw new ConnectionError("createComposio: @composio/core could not be loaded", { + code: "COMPOSIO_LOAD_FAILED", + cause: error, + }) + } + const composio = new mod.Composio({ apiKey: this.apiKey }) + this.composio = composio as unknown as ComposioLike + return this.composio + } + + async createSession(userId = this.userId): Promise { + if (this.session) return this.session + const composio = await this.getClient() + const connectedAccounts = await this.listConnectedAccountIds() + let s: any + try { + s = await composio.sessions.create(userId, { + mcp: true, + connectedAccounts, + }) + } catch (error) { + throw new ConnectionError("createComposio: composio session creation failed", { + code: "SESSION_CREATE_FAILED", + cause: error, + }) + } + const info: ComposioSessionInfo = { + url: (s as any).mcp?.url as string, + headers: (s as any).mcp?.headers as Record, + sessionId: (s as any).session_id as string, + } + if (!info.url) { + throw new ConnectionError("createComposio: composio session returned no MCP URL", { + code: "SESSION_INVALID", + }) + } + this.session = info + return info + } + + async createSessionFromServer( + serverUrl = this.options.serverUrl ?? COMPOSIO_DEFAULT_SERVER_URL, + ): Promise { + if (this.session) return this.session + const accessToken = this.options.accessToken + if (!serverUrl) { + throw new AuthError("createComposio: serverUrl is required in proxied mode", { + code: "NOT_CONFIGURED", + }) + } + if (!accessToken) { + throw new AuthError("createComposio: accessToken is required in proxied mode", { + code: "NOT_CONFIGURED", + }) + } + const res = await this.request(`${serverUrl}/api/composio/session`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + }) + const info = (await res.json()) as ComposioSessionInfo + this.session = info + if (info.apiKey) { + this.serverApiKey = info.apiKey + } + return info + } + + async listAppsFromServer( + serverUrl = this.options.serverUrl ?? COMPOSIO_DEFAULT_SERVER_URL, + ): Promise { + const accessToken = this.options.accessToken + if (!serverUrl) { + throw new AuthError("createComposio: serverUrl is required in proxied mode", { + code: "NOT_CONFIGURED", + }) + } + if (!accessToken) { + throw new AuthError("createComposio: accessToken is required in proxied mode", { + code: "NOT_CONFIGURED", + }) + } + const res = await this.request(`${serverUrl}/api/composio/apps`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + }) + const data = (await res.json()) as { apps?: AppInfo[] } + return data.apps ?? [] + } + + async listConnectedAccountIds(): Promise> { + const composio = await this.getClient() + let res: { items?: Array<{ id: string; status: string; toolkit?: { slug?: string } }> } + try { + res = (await (composio as any).connectedAccounts.list({})) as any + } catch (error) { + throw new ConnectionError("createComposio: connected accounts listing failed", { + code: "CONNECTED_ACCOUNTS_FAILED", + cause: error, + }) + } + const result: Record = {} + for (const acct of res.items ?? []) { + if (acct.status === "ACTIVE" && acct.toolkit?.slug) { + result[acct.toolkit.slug] = acct.id + } + } + return result + } + + async listApps(): Promise { + const composio = await this.getClient() + let authConfigs: { items?: Array<{ toolkit?: { slug?: string } }> } + let toolkits: Array<{ slug: string; name?: string; meta?: { description?: string; logo?: string } }> + let connectedRes: { items?: Array<{ id: string; status: string; toolkit?: { slug?: string } }> } + try { + ;[authConfigs, toolkits, connectedRes] = (await Promise.all([ + (composio as any).authConfigs.list({}), + (composio as any).toolkits.get(), + (composio as any).connectedAccounts.list({}), + ])) as any + } catch (error) { + throw new ConnectionError("createComposio: app listing failed", { + code: "LIST_APPS_FAILED", + cause: error, + }) + } + + const configuredSlugs = new Set( + (authConfigs.items ?? []).map((ac) => ac.toolkit?.slug).filter(Boolean) as string[], + ) + + const connectedMap = new Map() + for (const acct of connectedRes.items ?? []) { + const slug = acct.toolkit?.slug + if (slug && acct.status === "ACTIVE") { + connectedMap.set(slug, { id: acct.id, status: acct.status }) + } + } + + const toolkitMap = new Map() + for (const tk of toolkits) { + toolkitMap.set(tk.slug, tk) + } + + const apps: AppInfo[] = [] + for (const slug of configuredSlugs) { + const tk = toolkitMap.get(slug) + if (!tk) continue + const conn = connectedMap.get(slug) + apps.push({ + slug: tk.slug, + name: tk.name ?? slug, + description: tk.meta?.description, + logo: tk.meta?.logo, + connected: conn?.status === "ACTIVE", + connectedAccountId: conn?.id, + }) + } + + apps.sort((a, b) => { + if (a.connected !== b.connected) return a.connected ? -1 : 1 + return a.name.localeCompare(b.name) + }) + + return apps + } + + resetSession(): void { + this.session = null + } + + private async request(url: string, init: RequestInit): Promise { + const fetchImpl = this.options.fetch ?? ((...args: Parameters) => fetch(...args)) + let res: Response + try { + res = await fetchImpl(url, init as RequestInit) + } catch (error) { + throw new ConnectionError(`request to ${url} failed`, { + code: "REQUEST_FAILED", + cause: error, + }) + } + if (!res.ok) { + throw new SdkError(`request to ${url} failed with status ${res.status}`, { + code: "HTTP_ERROR", + }) + } + return res + } +} \ No newline at end of file diff --git a/packages/cortex-sdk/src/composio/types.ts b/packages/cortex-sdk/src/composio/types.ts new file mode 100644 index 0000000..f566ea7 --- /dev/null +++ b/packages/cortex-sdk/src/composio/types.ts @@ -0,0 +1,76 @@ +import type { Tool } from "ai" +import type { ComposioConfig } from "../core/types" +import type { AgentHandlerTransportConfig } from "../agent-handler/types" + +export type OpenBrowser = (url: string) => Promise | void + +export interface AppInfo { + slug: string + name: string + description?: string + logo?: string + connected: boolean + connectedAccountId?: string +} + +export interface ComposioSessionInfo { + url: string + headers: Record + sessionId: string + apiKey?: string +} + +export interface ConnectAppResult { + connectedAccountId: string + redirectUrl: string | null + waitForActive(): Promise +} + +export interface McpClientLike { + tools(): Promise> + close(): Promise +} + +export interface ComposioLike { + sessions: { + create( + userId: string, + options: { mcp: boolean; connectedAccounts: Record }, + ): Promise<{ + mcp: { url: string; headers: Record } + session_id: string + }> + } + connectedAccounts: { + list(options: Record): Promise<{ + items: Array<{ id: string; status: string; toolkit?: { slug: string } }> + }> + } + toolkits: { + get(): Promise< + Array<{ slug: string; name: string; meta?: { description?: string; logo?: string } }> + > + authorize( + userId: string, + slug: string, + ): Promise<{ id: string; redirectUrl: string | null; waitForConnection(): Promise }> + } + authConfigs: { + list(options: Record): Promise<{ items: Array<{ toolkit?: { slug: string } }> }> + } +} + +export interface ComposioClientOptions extends ComposioConfig { + loadComposio?: () => Promise<{ Composio: new (options: { apiKey: string }) => unknown }> + createMcpClient?: (config: AgentHandlerTransportConfig) => Promise +} + +export interface ComposioClient { + connect(): Promise + disconnect(): Promise + getTools(): Promise> + listApps(): Promise + connectApp(slug: string): Promise + selectApps(slugs: string[]): Promise + readonly isConnected: boolean +} \ No newline at end of file diff --git a/packages/cortex-sdk/src/core/errors.ts b/packages/cortex-sdk/src/core/errors.ts new file mode 100644 index 0000000..ac5355b --- /dev/null +++ b/packages/cortex-sdk/src/core/errors.ts @@ -0,0 +1,40 @@ +export interface SdkErrorOptions { + code?: string + cause?: unknown +} + +export class SdkError extends Error { + readonly code: string + readonly cause?: unknown + + constructor(message: string, options: SdkErrorOptions = {}) { + super(message) + this.name = new.target.name + this.code = options.code ?? "SDK_ERROR" + this.cause = options.cause + } +} + +export class ConnectionError extends SdkError { + constructor(message: string, options: SdkErrorOptions = {}) { + super(message, { code: options.code ?? "CONNECTION_ERROR", cause: options.cause }) + } +} + +export class AuthError extends SdkError { + constructor(message: string, options: SdkErrorOptions = {}) { + super(message, { code: options.code ?? "AUTH_ERROR", cause: options.cause }) + } +} + +export class ModelUnavailableError extends SdkError { + constructor(message: string, options: SdkErrorOptions = {}) { + super(message, { code: options.code ?? "MODEL_UNAVAILABLE", cause: options.cause }) + } +} + +export class ToolPackError extends SdkError { + constructor(message: string, options: SdkErrorOptions = {}) { + super(message, { code: options.code ?? "TOOL_PACK_ERROR", cause: options.cause }) + } +} diff --git a/packages/cortex-sdk/src/core/types.ts b/packages/cortex-sdk/src/core/types.ts new file mode 100644 index 0000000..9d1b635 --- /dev/null +++ b/packages/cortex-sdk/src/core/types.ts @@ -0,0 +1,71 @@ +export type GatewayProvider = + | "concentrateai" + | "mergedev" + | "openrouter" + | "gemini" + | "minimax" + | "nim" + | "orcarouter" + | "supercode-cloud" + +export type FetchLike = ( + input: string | URL | Request, + init?: RequestInit, +) => Promise + +export interface GatewayOptions { + provider: GatewayProvider + apiKey?: string + baseURL?: string + model?: string + headers?: Record + fetch?: FetchLike +} + +export interface AgentHandlerConfig { + apiUrl?: string + apiKey?: string + registeredUserId?: string + toolPacks?: string[] + transport?: "http" | "sse" + fetch?: FetchLike +} + +export interface ComposioConfig { + apiKey?: string + serverUrl?: string + accessToken?: string + userId?: string + apps?: string[] + fetch?: FetchLike + openBrowser?: (url: string) => Promise | void +} + +export type McpTransport = "stdio" | "http" | "sse" + +export interface McpServerConfig { + id: string + name: string + transport: McpTransport + command?: string + args?: string[] + url?: string + headers?: Record + env?: Record +} + +export type SttProvider = "smallest" | "elevenlabs" | "groq" + +export type TtsProvider = "smallest" | "elevenlabs" + +export interface VoiceConfig { + stt?: SttProvider + tts?: TtsProvider + sttApiKey?: string + ttsApiKey?: string + sttModel?: string + sttLanguage?: string + ttsModel?: string + voice?: string + fetch?: FetchLike +} diff --git a/packages/cortex-sdk/src/gateway/base.test.ts b/packages/cortex-sdk/src/gateway/base.test.ts new file mode 100644 index 0000000..5322620 --- /dev/null +++ b/packages/cortex-sdk/src/gateway/base.test.ts @@ -0,0 +1,131 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { + AuthError, + ConnectionError, + ModelUnavailableError, + SdkError, +} from "../core/errors" +import { BaseGatewayProvider, type GatewayProviderOptions } from "./base" + +class TestProvider extends BaseGatewayProvider { + constructor(options: Partial = {}) { + super({ provider: "concentrateai", ...options } as GatewayProviderOptions) + } + + async listModels() { + return [] + } + + protected buildModel(): import("ai").LanguageModel { + throw new Error("unused") + } +} + +const realSetTimeout = globalThis.setTimeout + +afterEach(() => { + Object.defineProperty(globalThis, "setTimeout", { + value: realSetTimeout, + configurable: true, + }) +}) + +function instantTimers() { + Object.defineProperty(globalThis, "setTimeout", { + value: (fn: () => void) => { + queueMicrotask(fn) + return 0 + }, + configurable: true, + }) +} + +describe("BaseGatewayProvider.fetchWithRetry", () => { + test("succeeds on first attempt", async () => { + let calls = 0 + const fetchImpl = async () => { + calls++ + return new Response("ok", { status: 200 }) + } + const provider = new TestProvider({ fetch: fetchImpl }) + const response = await provider.fetchWithRetry("https://example.com") + expect(response.status).toBe(200) + expect(calls).toBe(1) + }) + + test("retries on transient 5xx and recovers", async () => { + instantTimers() + let calls = 0 + const fetchImpl = async () => { + calls++ + if (calls < 3) return new Response("server error", { status: 500 }) + return new Response("ok", { status: 200 }) + } + const provider = new TestProvider({ fetch: fetchImpl, maxRetries: 2 }) + const response = await provider.fetchWithRetry("https://example.com") + expect(response.status).toBe(200) + expect(calls).toBe(3) + }) + + test("retries on 429 and 408", async () => { + instantTimers() + const codes = [429, 408, 200] + let i = 0 + const fetchImpl = async () => { + const status = codes[i] + i++ + return new Response("try again", { status }) + } + const provider = new TestProvider({ fetch: fetchImpl, maxRetries: 2 }) + const response = await provider.fetchWithRetry("https://example.com") + expect(response.status).toBe(200) + expect(i).toBe(3) + }) + + test("exhausts retries on persistent 5xx and throws ModelUnavailableError", async () => { + instantTimers() + let calls = 0 + const fetchImpl = async () => { + calls++ + return new Response("down", { status: 503 }) + } + const provider = new TestProvider({ fetch: fetchImpl, maxRetries: 2 }) + expect(provider.fetchWithRetry("https://example.com")).rejects.toBeInstanceOf(ModelUnavailableError) + expect(calls).toBe(3) + }) + + test("does not retry on 4xx", async () => { + let calls = 0 + const fetchImpl = async () => { + calls++ + return new Response("bad request", { status: 400 }) + } + const provider = new TestProvider({ fetch: fetchImpl, maxRetries: 3 }) + await expect(provider.fetchWithRetry("https://example.com")).rejects.toBeInstanceOf(SdkError) + expect(calls).toBe(1) + }) +}) + +describe("BaseGatewayProvider.normalizeError", () => { + test("429 maps to ConnectionError", async () => { + instantTimers() + const provider = new TestProvider({ + fetch: () => Promise.resolve(new Response("rate limited", { status: 429 })), + }) + await expect(provider.fetchWithRetry("https://example.com")).rejects.toBeInstanceOf(ConnectionError) + }) + + test("401 maps to AuthError", async () => { + const provider = new TestProvider({ + fetch: () => Promise.resolve(new Response("invalid key", { status: 401 })), + }) + await expect(provider.fetchWithRetry("https://example.com")).rejects.toBeInstanceOf(AuthError) + }) + + test("404 maps to ModelUnavailableError", async () => { + const provider = new TestProvider({ + fetch: () => Promise.resolve(new Response("not found", { status: 404 })), + }) + await expect(provider.fetchWithRetry("https://example.com")).rejects.toBeInstanceOf(ModelUnavailableError) + }) +}) \ No newline at end of file diff --git a/packages/cortex-sdk/src/gateway/base.ts b/packages/cortex-sdk/src/gateway/base.ts new file mode 100644 index 0000000..6d13740 --- /dev/null +++ b/packages/cortex-sdk/src/gateway/base.ts @@ -0,0 +1,273 @@ +import { generateText, streamText, type FinishReason, type LanguageModel, type LanguageModelUsage, type ModelMessage, type ToolSet } from "ai" +import { AuthError, ConnectionError, ModelUnavailableError, SdkError } from "../core/errors" +import type { FetchLike, GatewayProvider } from "../core/types" + +export interface ModelInfo { + id: string + name?: string + provider?: GatewayProvider +} + +export interface GatewayUsage { + provider: GatewayProvider + model: string + inputTokens: number + outputTokens: number + cachedInputTokens: number + totalTokens: number + durationMs: number | null +} + +export interface GatewayCost { + provider: GatewayProvider + model: string + inputTokens: number + outputTokens: number + inputCostUsd: number + outputCostUsd: number + totalCostUsd: number +} + +export interface GatewayProviderOptions { + provider: GatewayProvider + apiKey?: string + baseURL?: string + model?: string + headers?: Record + fetch?: FetchLike + timeoutMs?: number + maxRetries?: number + onUsage?: (usage: GatewayUsage) => void + onCost?: (cost: GatewayCost) => void +} + +const DEFAULT_TIMEOUT_MS = 120_000 +const DEFAULT_MAX_RETRIES = 3 + +interface HttpError extends Error { + status: number + statusText: string + body: string +} + +export abstract class BaseGatewayProvider { + readonly provider: GatewayProvider + readonly defaultModel: string + readonly apiKey: string + readonly baseURL: string + readonly headers: Record + readonly timeoutMs: number + readonly maxRetries: number + + protected readonly fetchImpl: typeof fetch + protected readonly onUsage?: (usage: GatewayUsage) => void + protected readonly onCost?: (cost: GatewayCost) => void + + private readonly modelCache = new Map() + + constructor(options: GatewayProviderOptions) { + this.provider = options.provider + this.apiKey = options.apiKey ?? "" + this.baseURL = options.baseURL ?? "" + this.defaultModel = options.model ?? "" + this.headers = options.headers ?? {} + this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS + this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES + this.fetchImpl = (options.fetch ?? fetch) as unknown as typeof fetch + this.onUsage = options.onUsage + this.onCost = options.onCost + } + + model(id?: string): LanguageModel { + const modelId = id ?? this.defaultModel + let cached = this.modelCache.get(modelId) + if (!cached) { + cached = this.buildModel(modelId) + this.modelCache.set(modelId, cached) + } + return cached + } + + abstract listModels(): Promise + + protected abstract buildModel(modelName: string): LanguageModel + + async fetchWithRetry(url: string | URL, init?: RequestInit): Promise { + let lastError: unknown + for (let attempt = 0; attempt <= this.maxRetries; attempt++) { + try { + const response = await this.fetchImpl(url, init) + const retryable = response.status === 408 || response.status === 429 || response.status >= 500 + if (response.ok || !retryable || attempt === this.maxRetries) { + if (!response.ok) { + const body = await response.text().catch(() => "") + const error: HttpError = new Error(`HTTP ${response.status} ${response.statusText}`) as HttpError + error.status = response.status + error.statusText = response.statusText + error.body = body + throw error + } + return response + } + lastError = new Error(`HTTP ${response.status} ${response.statusText}`) + } catch (error) { + lastError = error + if (error instanceof Error && "status" in error && !((error as HttpError).status >= 500)) { + throw this.normalizeError(error) + } + } + if (attempt < this.maxRetries) { + await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** attempt)) + } + } + throw this.normalizeError(lastError) + } + + createAbortController(external?: AbortSignal): { signal: AbortSignal; cleanup: () => void } { + const controller = new AbortController() + const timer = setTimeout(() => { + controller.abort(new Error("Request timed out")) + }, this.timeoutMs) + const onAbort = () => controller.abort() + if (external) { + if (external.aborted) { + controller.abort() + } else { + external.addEventListener("abort", onAbort, { once: true }) + } + } + return { + signal: controller.signal, + cleanup: () => { + clearTimeout(timer) + external?.removeEventListener("abort", onAbort) + }, + } + } + + normalizeError(error: unknown): Error { + if (error instanceof SdkError) return error + if (error instanceof Error && error.name === "AbortError") { + return new ConnectionError("Request aborted or timed out", { cause: error }) + } + if (error instanceof Error && "status" in error) { + const httpError = error as HttpError + if (httpError.status === 401 || httpError.status === 403) { + return new AuthError(`${this.provider} authentication failed: HTTP ${httpError.status}`, { cause: error }) + } + if (httpError.status === 404) { + return new ModelUnavailableError(`${this.provider} model not found: HTTP 404`, { cause: error }) + } + if (httpError.status === 429) { + return new ConnectionError(`${this.provider} rate limit exceeded: HTTP 429`, { cause: error }) + } + if (httpError.status >= 500) { + return new ModelUnavailableError(`${this.provider} provider error: HTTP ${httpError.status}`, { cause: error }) + } + } + if (error instanceof TypeError) { + return new ConnectionError(`${this.provider} network error`, { cause: error }) + } + return new SdkError(`${this.provider} gateway error`, { cause: error }) + } + + protected trackUsage(model: string, usage: LanguageModelUsage, durationMs: number | null): void { + if (!this.onUsage) return + this.onUsage({ + provider: this.provider, + model, + inputTokens: usage.inputTokens ?? 0, + outputTokens: usage.outputTokens ?? 0, + cachedInputTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0, + totalTokens: usage.totalTokens ?? 0, + durationMs, + }) + } + + protected async streamToContent(params: { + model: LanguageModel + modelId: string + system?: string + messages: ModelMessage[] + tools?: ToolSet + temperature?: number + maxOutputTokens?: number + signal?: AbortSignal + onChunk?: (text: string) => void + onReasoning?: (text: string) => void + onToolCall?: (toolCall: { toolName: string; args: unknown }) => void + }): Promise<{ + text: string + reasoning: string + toolCalls: Array<{ toolName: string; args: unknown }> + usage: LanguageModelUsage + finishReason: FinishReason + }> { + const startedAt = Date.now() + const result = streamText({ + model: params.model, + system: params.system, + messages: params.messages, + tools: params.tools, + temperature: params.temperature, + maxOutputTokens: params.maxOutputTokens, + abortSignal: params.signal, + }) + + let text = "" + let reasoning = "" + const toolCalls: Array<{ toolName: string; args: unknown }> = [] + + for await (const part of result.fullStream) { + switch (part.type) { + case "text-delta": + text += part.text + params.onChunk?.(part.text) + break + case "reasoning-delta": + reasoning += part.text + params.onReasoning?.(part.text) + break + case "tool-call": { + const args = typeof part.input === "string" ? safeParse(part.input) : part.input + toolCalls.push({ toolName: part.toolName, args }) + params.onToolCall?.({ toolName: part.toolName, args }) + break + } + } + } + + let usage = await result.usage + let finishReason = await result.finishReason + + if (!text.trim() && toolCalls.length === 0) { + const fallback = await generateText({ + model: params.model, + system: params.system, + messages: params.messages, + tools: params.tools, + temperature: params.temperature, + maxOutputTokens: params.maxOutputTokens, + abortSignal: params.signal, + }) + text = fallback.text + reasoning = fallback.reasoningText ?? "" + for (const call of fallback.toolCalls) { + toolCalls.push({ toolName: call.toolName, args: call.input }) + } + usage = fallback.usage + finishReason = fallback.finishReason + } + + this.trackUsage(params.modelId, usage, Date.now() - startedAt) + return { text, reasoning, toolCalls, usage, finishReason } + } +} + +function safeParse(input: string): unknown { + try { + return JSON.parse(input) + } catch { + return input + } +} diff --git a/packages/cortex-sdk/src/gateway/concentrateai.ts b/packages/cortex-sdk/src/gateway/concentrateai.ts new file mode 100644 index 0000000..d208519 --- /dev/null +++ b/packages/cortex-sdk/src/gateway/concentrateai.ts @@ -0,0 +1,44 @@ +import { createOpenAICompatible } from "@ai-sdk/openai-compatible" +import type { LanguageModel } from "ai" +import { BaseGatewayProvider, type GatewayProviderOptions, type ModelInfo } from "./base" + +const DEFAULT_BASE_URL = "https://api.concentrate.ai/v1" +const DEFAULT_MODEL = "deepseek/deepseek-v4-flash" + +export interface ConcentrateAIProviderOptions extends Omit {} + +export class ConcentrateAIProvider extends BaseGatewayProvider { + private readonly sdk: ReturnType + + constructor(options: ConcentrateAIProviderOptions = {}) { + super({ + ...options, + provider: "concentrateai", + apiKey: options.apiKey ?? "", + baseURL: options.baseURL ?? DEFAULT_BASE_URL, + model: options.model ?? DEFAULT_MODEL, + }) + this.sdk = createOpenAICompatible({ + name: "concentrateai", + baseURL: this.baseURL, + apiKey: this.apiKey, + headers: this.headers, + fetch: this.fetchImpl, + }) + } + + protected buildModel(modelName: string): LanguageModel { + return this.sdk.chatModel(modelName) + } + + async listModels(): Promise { + return [ + { id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash", provider: "concentrateai" }, + { id: "gpt-4o-mini", name: "GPT-4o Mini", provider: "concentrateai" }, + ] + } +} + +export function createConcentrateAIProvider(options?: ConcentrateAIProviderOptions): ConcentrateAIProvider { + return new ConcentrateAIProvider(options) +} diff --git a/packages/cortex-sdk/src/gateway/gemini.ts b/packages/cortex-sdk/src/gateway/gemini.ts new file mode 100644 index 0000000..a12ea24 --- /dev/null +++ b/packages/cortex-sdk/src/gateway/gemini.ts @@ -0,0 +1,60 @@ +import { createGoogleGenerativeAI } from "@ai-sdk/google" +import type { LanguageModel } from "ai" +import { BaseGatewayProvider, type GatewayProviderOptions, type ModelInfo } from "./base" + +const DEFAULT_MODEL = "gemini-2.5-flash" + +export interface GeminiProviderOptions extends Omit {} + +export class GeminiProvider extends BaseGatewayProvider { + private readonly sdk: ReturnType + + constructor(options: GeminiProviderOptions = {}) { + super({ + ...options, + provider: "gemini", + apiKey: options.apiKey ?? "", + baseURL: options.baseURL ?? "", + model: options.model ?? DEFAULT_MODEL, + }) + this.sdk = createGoogleGenerativeAI({ + apiKey: this.apiKey, + baseURL: this.baseURL || undefined, + fetch: this.fetchImpl, + }) + } + + protected buildModel(modelName: string): LanguageModel { + return this.sdk.chat(modelName) + } + + async listModels(): Promise { + const fallback: ModelInfo[] = [ + { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", provider: "gemini" }, + { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", provider: "gemini" }, + { id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite", provider: "gemini" }, + ] + if (!this.apiKey) return fallback + try { + const response = await this.fetchImpl( + `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(this.apiKey)}`, + ) + if (!response.ok) return fallback + const data = (await response.json()) as { models?: Array<{ name?: string; displayName?: string }> } + const models = (data.models ?? []) + .map((m) => { + const id = m.name?.replace(/^models\//, "") + if (!id) return null + return { id, name: m.displayName ?? id, provider: "gemini" as const } + }) + .filter((m): m is { id: string; name: string; provider: "gemini" } => m !== null) + return models.length > 0 ? models : fallback + } catch { + return fallback + } + } +} + +export function createGeminiProvider(options?: GeminiProviderOptions): GeminiProvider { + return new GeminiProvider(options) +} diff --git a/packages/cortex-sdk/src/gateway/index.test.ts b/packages/cortex-sdk/src/gateway/index.test.ts new file mode 100644 index 0000000..e6b7eac --- /dev/null +++ b/packages/cortex-sdk/src/gateway/index.test.ts @@ -0,0 +1,121 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { clearGatewayCache, createGateway } from "./index" +import { ConcentrateAIProvider } from "./concentrateai" +import { OpenRouterProvider } from "./openrouter" +import { SdkError } from "../core/errors" +import type { GatewayProvider } from "../core/types" + +afterEach(() => { + clearGatewayCache() +}) + +describe("createGateway routing", () => { + test("routes concentrateai and exposes provider + default model", () => { + const gateway = createGateway({ provider: "concentrateai", apiKey: "sk" }) + expect(gateway.provider).toBe("concentrateai") + expect(gateway.defaultModel).toBe("deepseek/deepseek-v4-flash") + }) + + test("model() returns an object usable with streamText (LanguageModel shape)", () => { + const gateway = createGateway({ provider: "concentrateai", apiKey: "sk" }) + const model = gateway.model() + expect(typeof model).toBe("object") + expect(model).not.toBeNull() + }) + + test("model(id) uses the requested id", () => { + const gateway = createGateway({ provider: "openrouter", apiKey: "sk" }) + const model = gateway.model("anthropic/claude-sonnet-4") + expect(model).toBeTruthy() + }) + + test("listModels returns provider-defined models", async () => { + const gateway = createGateway({ provider: "concentrateai", apiKey: "sk" }) + const models = await gateway.listModels() + expect(models.length).toBeGreaterThan(0) + expect(models.every((m) => m.provider === "concentrateai")).toBe(true) + }) + + test("throws SdkError for unsupported provider", () => { + expect(() => + createGateway({ provider: "nope" as unknown as GatewayProvider, apiKey: "sk" }), + ).toThrow(SdkError) + }) +}) + +describe("createGateway caching", () => { + test("reuses cached provider for identical options", async () => { + const a = createGateway({ provider: "concentrateai", apiKey: "sk" }) + const b = createGateway({ provider: "concentrateai", apiKey: "sk" }) + expect(a.defaultModel).toBe(b.defaultModel) + expect((await a.listModels()).length).toBe((await b.listModels()).length) + }) + + test("clearGatewayCache drops entries", async () => { + const a = createGateway({ provider: "concentrateai", apiKey: "sk" }) + clearGatewayCache() + const b = createGateway({ provider: "concentrateai", apiKey: "sk" }) + expect(a.defaultModel).toBe(b.defaultModel) + expect(a).not.toBe(b) + }) +}) + +describe("Provider env-var independence (BYOK hygiene)", () => { + const envKeys = [ + "CONCENTRATE_BYOK_PROD_KEY", + "CONCENTRATE_BYOK_DEV_KEY", + "CONCENTRATEAI_API_KEY", + "OPENROUTER_BYOK_PROD_KEY", + "OPENROUTER_BYOK_DEV_KEY", + "OPENROUTER_API_KEY", + ] + + const saved: Record = {} + + function lockEnv(values: Record) { + for (const k of envKeys) { + saved[k] = process.env[k] + if (values[k] === undefined) delete process.env[k] + else process.env[k] = values[k] + } + } + + function restoreEnv() { + for (const k of envKeys) { + if (saved[k] === undefined) delete process.env[k] + else process.env[k] = saved[k] + } + } + + test("ConcentrateAIProvider ignores leaked BYOK env vars", () => { + lockEnv({ + CONCENTRATE_BYOK_PROD_KEY: "should-not-be-used", + CONCENTRATE_BYOK_DEV_KEY: "should-not-be-used", + CONCENTRATEAI_API_KEY: "should-not-be-used", + }) + try { + const provider = new ConcentrateAIProvider({}) + expect(provider.apiKey).toBe("") + const explicit = new ConcentrateAIProvider({ apiKey: "user-key" }) + expect(explicit.apiKey).toBe("user-key") + } finally { + restoreEnv() + } + }) + + test("OpenRouterProvider ignores leaked BYOK env vars", () => { + lockEnv({ + OPENROUTER_BYOK_PROD_KEY: "should-not-be-used", + OPENROUTER_BYOK_DEV_KEY: "should-not-be-used", + OPENROUTER_API_KEY: "should-not-be-used", + }) + try { + const provider = new OpenRouterProvider({}) + expect(provider.apiKey).toBe("") + const explicit = new OpenRouterProvider({ apiKey: "user-key" }) + expect(explicit.apiKey).toBe("user-key") + } finally { + restoreEnv() + } + }) +}) diff --git a/packages/cortex-sdk/src/gateway/index.ts b/packages/cortex-sdk/src/gateway/index.ts new file mode 100644 index 0000000..0f37789 --- /dev/null +++ b/packages/cortex-sdk/src/gateway/index.ts @@ -0,0 +1,95 @@ +import type { LanguageModel } from "ai" +import { SdkError } from "../core/errors" +import type { GatewayOptions, GatewayProvider } from "../core/types" +import type { BaseGatewayProvider, GatewayCost, GatewayUsage, ModelInfo } from "./base" +import { ConcentrateAIProvider } from "./concentrateai" +import { GeminiProvider } from "./gemini" +import { MergeDevProvider } from "./mergedev" +import { MiniMaxProvider } from "./minimax" +import { NimProvider } from "./nim" +import { OpenRouterProvider } from "./openrouter" +import { OrcaRouterProvider } from "./orcarouter" +import { SupercodeCloudProvider } from "./supercode-cloud" + +export type { GatewayOptions, GatewayProvider } from "../core/types" +export type { GatewayCost, GatewayUsage, ModelInfo } from "./base" + +export type GatewayOptionsWithCallbacks = GatewayOptions & { + timeoutMs?: number + maxRetries?: number + onUsage?: (usage: GatewayUsage) => void + onCost?: (cost: GatewayCost) => void +} + +export interface GatewayClient { + model(id?: string): LanguageModel + listModels(): Promise + readonly provider: GatewayProvider + readonly defaultModel: string +} + +const providerCache = new Map() + +function cacheKey(options: GatewayOptions): string { + return JSON.stringify({ + provider: options.provider, + apiKey: options.apiKey, + baseURL: options.baseURL, + model: options.model, + headers: options.headers, + }) +} + +export function createGateway(options: GatewayOptionsWithCallbacks): GatewayClient { + const key = cacheKey(options) + let provider = providerCache.get(key) + if (!provider) { + provider = buildProvider(options) + providerCache.set(key, provider) + } + return { + get provider() { + return provider!.provider + }, + get defaultModel() { + return provider!.defaultModel + }, + model: (id?: string) => provider!.model(id), + listModels: () => provider!.listModels(), + } +} + +function buildProvider(options: GatewayOptionsWithCallbacks): BaseGatewayProvider { + const callbacks = { + timeoutMs: options.timeoutMs, + maxRetries: options.maxRetries, + onUsage: options.onUsage, + onCost: options.onCost, + } + switch (options.provider) { + case "concentrateai": + return new ConcentrateAIProvider({ ...options, ...callbacks }) + case "mergedev": + return new MergeDevProvider({ ...options, ...callbacks }) + case "openrouter": + return new OpenRouterProvider({ ...options, ...callbacks }) + case "gemini": + return new GeminiProvider({ ...options, ...callbacks }) + case "minimax": + return new MiniMaxProvider({ ...options, ...callbacks }) + case "nim": + return new NimProvider({ ...options, ...callbacks }) + case "orcarouter": + return new OrcaRouterProvider({ ...options, ...callbacks }) + case "supercode-cloud": + return new SupercodeCloudProvider({ ...options, ...callbacks }) + default: { + const provider = options.provider as string + throw new SdkError(`createGateway: unsupported provider "${provider}"`, { code: "INVALID_PROVIDER" }) + } + } +} + +export function clearGatewayCache(): void { + providerCache.clear() +} diff --git a/packages/cortex-sdk/src/gateway/mergedev.ts b/packages/cortex-sdk/src/gateway/mergedev.ts new file mode 100644 index 0000000..e69ad01 --- /dev/null +++ b/packages/cortex-sdk/src/gateway/mergedev.ts @@ -0,0 +1,44 @@ +import { createOpenAICompatible } from "@ai-sdk/openai-compatible" +import type { LanguageModel } from "ai" +import { BaseGatewayProvider, type GatewayProviderOptions, type ModelInfo } from "./base" + +const DEFAULT_BASE_URL = "https://api-gateway.merge.dev/v1/openai" +const DEFAULT_MODEL = "anthropic/claude-opus-4-8" + +export interface MergeDevProviderOptions extends Omit {} + +export class MergeDevProvider extends BaseGatewayProvider { + private readonly sdk: ReturnType + + constructor(options: MergeDevProviderOptions = {}) { + super({ + ...options, + provider: "mergedev", + apiKey: options.apiKey ?? "", + baseURL: options.baseURL ?? DEFAULT_BASE_URL, + model: options.model ?? DEFAULT_MODEL, + }) + this.sdk = createOpenAICompatible({ + name: "mergedev", + baseURL: this.baseURL, + apiKey: this.apiKey, + headers: this.headers, + fetch: this.fetchImpl, + }) + } + + protected buildModel(modelName: string): LanguageModel { + return this.sdk.chatModel(modelName) + } + + async listModels(): Promise { + return [ + { id: "anthropic/claude-opus-4-8", name: "Claude Opus 4.8", provider: "mergedev" }, + { id: "anthropic/claude-sonnet-4", name: "Claude Sonnet 4", provider: "mergedev" }, + ] + } +} + +export function createMergeDevProvider(options?: MergeDevProviderOptions): MergeDevProvider { + return new MergeDevProvider(options) +} diff --git a/packages/cortex-sdk/src/gateway/minimax.ts b/packages/cortex-sdk/src/gateway/minimax.ts new file mode 100644 index 0000000..f2905e2 --- /dev/null +++ b/packages/cortex-sdk/src/gateway/minimax.ts @@ -0,0 +1,42 @@ +import type { LanguageModel } from "ai" +import { createMinimax } from "vercel-minimax-ai-provider" +import { BaseGatewayProvider, type GatewayProviderOptions, type ModelInfo } from "./base" + +const DEFAULT_MODEL = "MiniMax-M1" + +export interface MiniMaxProviderOptions extends Omit {} + +export class MiniMaxProvider extends BaseGatewayProvider { + private readonly sdk: ReturnType + + constructor(options: MiniMaxProviderOptions = {}) { + super({ + ...options, + provider: "minimax", + apiKey: options.apiKey ?? "", + baseURL: options.baseURL ?? "", + model: options.model ?? DEFAULT_MODEL, + }) + this.sdk = createMinimax({ + apiKey: this.apiKey, + baseURL: this.baseURL || undefined, + headers: this.headers, + fetch: this.fetchImpl, + }) + } + + protected buildModel(modelName: string): LanguageModel { + return this.sdk.languageModel(modelName) + } + + async listModels(): Promise { + return [ + { id: "MiniMax-M1", name: "MiniMax M1", provider: "minimax" }, + { id: "MiniMax-M2", name: "MiniMax M2", provider: "minimax" }, + ] + } +} + +export function createMiniMaxProvider(options?: MiniMaxProviderOptions): MiniMaxProvider { + return new MiniMaxProvider(options) +} diff --git a/packages/cortex-sdk/src/gateway/nim.ts b/packages/cortex-sdk/src/gateway/nim.ts new file mode 100644 index 0000000..828fa22 --- /dev/null +++ b/packages/cortex-sdk/src/gateway/nim.ts @@ -0,0 +1,44 @@ +import { createOpenAICompatible } from "@ai-sdk/openai-compatible" +import type { LanguageModel } from "ai" +import { BaseGatewayProvider, type GatewayProviderOptions, type ModelInfo } from "./base" + +const DEFAULT_BASE_URL = "https://integrate.api.nvidia.com/v1" +const DEFAULT_MODEL = "minimaxai/minimax-m3" + +export interface NimProviderOptions extends Omit {} + +export class NimProvider extends BaseGatewayProvider { + private readonly sdk: ReturnType + + constructor(options: NimProviderOptions = {}) { + super({ + ...options, + provider: "nim", + apiKey: options.apiKey ?? "", + baseURL: options.baseURL ?? DEFAULT_BASE_URL, + model: options.model ?? DEFAULT_MODEL, + }) + this.sdk = createOpenAICompatible({ + name: "nim", + baseURL: this.baseURL, + apiKey: this.apiKey, + headers: this.headers, + fetch: this.fetchImpl, + }) + } + + protected buildModel(modelName: string): LanguageModel { + return this.sdk.chatModel(modelName) + } + + async listModels(): Promise { + return [ + { id: "minimaxai/minimax-m3", name: "MiniMax M3", provider: "nim" }, + { id: "nvidia/llama-3.1-nemotron-ultra-253b-v1", name: "Llama Nemotron Ultra", provider: "nim" }, + ] + } +} + +export function createNimProvider(options?: NimProviderOptions): NimProvider { + return new NimProvider(options) +} diff --git a/packages/cortex-sdk/src/gateway/openrouter.test.ts b/packages/cortex-sdk/src/gateway/openrouter.test.ts new file mode 100644 index 0000000..cfc81b1 --- /dev/null +++ b/packages/cortex-sdk/src/gateway/openrouter.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, test } from "bun:test" +import { createGateway } from "./index" +import { OpenRouterProvider } from "./openrouter" +import type { LanguageModelV2 } from "@ai-sdk/provider" +import { AuthError } from "../core/errors" + +interface CapturedRequest { + url: string + headers: Record + body: Record +} + +function makeFetch(behaviour: "ok-then-success" | "always-401" | "always-500-models" = "ok-then-success") { + const calls: CapturedRequest[] = [] + const record = (input: string | URL | Request, init?: RequestInit): CapturedRequest => { + const url = typeof input === "string" ? input : input.toString() + const headers: Record = {} + if (init?.headers) { + if (init.headers instanceof Headers) { + init.headers.forEach((v, k) => (headers[k] = v)) + } else if (Array.isArray(init.headers)) { + for (const [k, v] of init.headers) headers[k] = v + } else { + Object.assign(headers, init.headers) + } + } + const body = init && init.body ? (JSON.parse(init.body as string) as Record) : {} + const entry: CapturedRequest = { url, headers, body } + calls.push(entry) + return entry + } + + const fetchImpl = async (input: string | URL | Request, init?: RequestInit): Promise => { + const entry = record(input, init) + if (entry.url.endsWith("/models")) { + if (behaviour === "always-500-models") return new Response("down", { status: 500 }) + return new Response( + JSON.stringify({ data: [{ id: "anthropic/claude-sonnet-4", name: "Claude Sonnet 4" }] }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + } + if (entry.url.endsWith("/chat/completions")) { + if (behaviour === "always-401") return new Response("unauthorized", { status: 401 }) + return new Response( + JSON.stringify({ + id: "chatcmpl-test", + model: entry.body.model, + choices: [ + { + index: 0, + message: { role: "assistant", content: "Hello there", reasoning: null, tool_calls: [] }, + finish_reason: "stop", + }, + ], + usage: { + prompt_tokens: 4, + completion_tokens: 7, + total_tokens: 11, + prompt_tokens_details: { cached_tokens: 0 }, + }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + } + return new Response("not found", { status: 404 }) + } + + return { calls, fetchImpl } +} + +describe("OpenRouter provider", () => { + test("listModels fetches /models and falls back when non-OK", async () => { + const ok = makeFetch("ok-then-success") + const gateway = createGateway({ + provider: "openrouter", + apiKey: "sk-test", + fetch: ok.fetchImpl, + }) + const models = await gateway.listModels() + expect(models.some((m) => m.id === "anthropic/claude-sonnet-4")).toBe(true) + expect(ok.calls[0]?.url).toContain("/models") + + const bad = makeFetch("always-500-models") + const gateway2 = createGateway({ provider: "openrouter", apiKey: "sk", fetch: bad.fetchImpl }) + const fallback = await gateway2.listModels() + expect(fallback.length).toBeGreaterThan(0) + }) + + test("doGenerate sends Authorization Bearer and correct body shape", async () => { + const { calls, fetchImpl } = makeFetch("ok-then-success") + const gw = createGateway({ + provider: "openrouter", + apiKey: "sk-test", + model: "anthropic/claude-sonnet-4", + fetch: fetchImpl, + siteUrl: "https://example.com", + siteTitle: "Supercode", + forceProvider: "anthropic", + allowFallbacks: false, + } as Parameters[0] & { + siteUrl: string + siteTitle: string + forceProvider: string + allowFallbacks: boolean + }) + + const model = gw.model() as unknown as LanguageModelV2 + const result = await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "hi" }] }], + } as never) + + const chatCall = calls.find((c) => c.url.endsWith("/chat/completions")) + expect(chatCall).toBeDefined() + if (!chatCall) throw new Error("missing chat call") + expect(chatCall.headers["Authorization"]).toBe("Bearer sk-test") + expect(chatCall.headers["Content-Type"]).toBe("application/json") + expect(chatCall.headers["HTTP-Referer"]).toBe("https://example.com") + expect(chatCall.headers["X-Title"]).toBe("Supercode") + expect(chatCall.body.model).toBe("anthropic/claude-sonnet-4") + expect(chatCall.body.stream).toBe(false) + expect(chatCall.body.provider).toEqual({ force: true, order: ["anthropic"], allow_fallbacks: false }) + + const text = result.content.find((c) => c.type === "text") + expect(text?.type).toBe("text") + if (text && text.type === "text") expect(text.text).toBe("Hello there") + expect(result.finishReason).toBe("stop") + }) + + test("401 maps to AuthError via fetchWithRetry path", async () => { + const { fetchImpl } = makeFetch("always-401") + const provider = new OpenRouterProvider({ apiKey: "sk-bad", fetch: fetchImpl }) + await expect(provider.fetchWithRetry(`${provider.baseURL}/chat/completions`)).rejects.toBeInstanceOf(AuthError) + }) + + test("uses default baseURL when none provided", () => { + const provider = new OpenRouterProvider({ apiKey: "sk" }) + expect(provider.baseURL).toBe("https://openrouter.ai/api/v1") + }) + + test("allowFallbacks defaults to true and explicit false is honored", () => { + const def = new OpenRouterProvider({ apiKey: "sk" }) + const off = new OpenRouterProvider({ apiKey: "sk", allowFallbacks: false }) + expect(def.allowFallbacks).toBe(true) + expect(off.allowFallbacks).toBe(false) + }) +}) diff --git a/packages/cortex-sdk/src/gateway/openrouter.ts b/packages/cortex-sdk/src/gateway/openrouter.ts new file mode 100644 index 0000000..dd5012b --- /dev/null +++ b/packages/cortex-sdk/src/gateway/openrouter.ts @@ -0,0 +1,512 @@ +import type { JSONValue, LanguageModelV2, LanguageModelV2CallOptions, LanguageModelV2Content, LanguageModelV2FilePart, LanguageModelV2FinishReason, LanguageModelV2FunctionTool, LanguageModelV2Message, LanguageModelV2Prompt, LanguageModelV2ReasoningPart, LanguageModelV2StreamPart, LanguageModelV2TextPart, LanguageModelV2ToolCallPart, LanguageModelV2ToolChoice, LanguageModelV2ToolResultPart, LanguageModelV2Usage } from "@ai-sdk/provider" +import type { LanguageModel } from "ai" +import { ModelUnavailableError } from "../core/errors" +import { BaseGatewayProvider, type GatewayProviderOptions, type ModelInfo } from "./base" + +const DEFAULT_BASE_URL = "https://openrouter.ai/api/v1" +const DEFAULT_MODEL = "moonshotai/kimi-k2.6" + +export interface OpenRouterProviderOptions extends Omit { + siteUrl?: string + siteTitle?: string + forceProvider?: string + providerPreference?: string[] + autoSelect?: boolean + allowFallbacks?: boolean +} + +export class OpenRouterProvider extends BaseGatewayProvider { + readonly siteUrl?: string + readonly siteTitle?: string + readonly forceProvider?: string + readonly providerPreference?: string[] + readonly autoSelect?: boolean + readonly allowFallbacks: boolean + + constructor(options: OpenRouterProviderOptions = {}) { + super({ + ...options, + provider: "openrouter", + apiKey: options.apiKey ?? "", + baseURL: options.baseURL ?? DEFAULT_BASE_URL, + model: options.model ?? DEFAULT_MODEL, + }) + this.siteUrl = options.siteUrl + this.siteTitle = options.siteTitle + this.forceProvider = options.forceProvider + this.providerPreference = options.providerPreference + this.autoSelect = options.autoSelect + this.allowFallbacks = options.allowFallbacks ?? true + } + + protected buildModel(modelName: string): LanguageModel { + return new OpenRouterLanguageModel(this, modelName) + } + + async listModels(): Promise { + const fallback: ModelInfo[] = [ + { id: "moonshotai/kimi-k2.6", name: "Kimi K2.6", provider: "openrouter" }, + { id: "anthropic/claude-sonnet-4", name: "Claude Sonnet 4", provider: "openrouter" }, + { id: "deepseek/deepseek-chat", name: "DeepSeek Chat", provider: "openrouter" }, + ] + try { + const response = await this.fetchImpl(`${this.baseURL}/models`) + if (!response.ok) return fallback + const data = (await response.json()) as { data?: Array<{ id?: string; name?: string }> } + const models = data.data + ?.filter((m) => typeof m.id === "string") + .map((m) => ({ id: m.id!, name: m.name, provider: "openrouter" as const })) + return models && models.length > 0 ? models : fallback + } catch { + return fallback + } + } +} + +export function createOpenRouterProvider(options?: OpenRouterProviderOptions): OpenRouterProvider { + return new OpenRouterProvider(options) +} + +interface OpenRouterChunk { + id?: string + model?: string + choices?: Array<{ + index?: number + delta?: { + content?: string | null + reasoning?: string | null + tool_calls?: Array<{ + index?: number + id?: string + type?: string + function?: { name?: string; arguments?: string } + }> + } + message?: { + content?: string | null + reasoning?: string | null + tool_calls?: Array<{ id?: string; type?: string; function?: { name?: string; arguments?: string } }> + } + finish_reason?: string | null + }> + usage?: { + prompt_tokens?: number + completion_tokens?: number + total_tokens?: number + prompt_tokens_details?: { cached_tokens?: number } + } +} + +class OpenRouterLanguageModel implements LanguageModelV2 { + readonly specificationVersion = "v2" as const + readonly provider = "openrouter" + readonly modelId: string + readonly supportedUrls = {} + + constructor( + private readonly gateway: OpenRouterProvider, + modelId: string, + ) { + this.modelId = modelId + } + + async doGenerate(options: LanguageModelV2CallOptions): Promise<{ + content: LanguageModelV2Content[] + finishReason: LanguageModelV2FinishReason + usage: LanguageModelV2Usage + providerMetadata: Record> + request: { body: unknown } + warnings: Array<{ type: "other"; message: string }> + }> { + const { signal, cleanup } = this.gateway.createAbortController(options.abortSignal) + try { + const body = buildOpenRouterBody(options, this.gateway, this.modelId, false) + const response = await this.gateway.fetchWithRetry(`${this.gateway.baseURL}/chat/completions`, { + method: "POST", + headers: buildHeaders(this.gateway), + body: JSON.stringify(body), + signal, + }) + const data = (await response.json()) as OpenRouterChunk + const choice = data.choices?.[0] + const message = choice?.message + const content: LanguageModelV2Content[] = [] + if (message?.reasoning) { + content.push({ type: "reasoning", text: message.reasoning }) + } + if (message?.content) { + content.push({ type: "text", text: message.content }) + } + for (const call of message?.tool_calls ?? []) { + if (!call.function?.name) continue + content.push({ + type: "tool-call", + toolCallId: call.id ?? `tc_${content.length}`, + toolName: call.function.name, + input: call.function.arguments ?? "{}", + }) + } + return { + content, + finishReason: mapFinishReason(choice?.finish_reason), + usage: mapUsage(data.usage), + providerMetadata: { + openrouter: { id: data.id ?? null, model: data.model ?? null }, + }, + request: { body }, + warnings: [], + } + } catch (error) { + throw this.gateway.normalizeError(error) + } finally { + cleanup() + } + } + + async doStream(options: LanguageModelV2CallOptions): Promise<{ + stream: ReadableStream + request: { body: unknown } + }> { + const { signal, cleanup } = this.gateway.createAbortController(options.abortSignal) + const body = buildOpenRouterBody(options, this.gateway, this.modelId, true) + try { + const response = await this.gateway.fetchWithRetry(`${this.gateway.baseURL}/chat/completions`, { + method: "POST", + headers: buildHeaders(this.gateway), + body: JSON.stringify(body), + signal, + }) + if (!response.body) { + throw new ModelUnavailableError("openrouter: empty response body") + } + const reader = response.body.getReader() + const decoder = new TextDecoder() + const stream = new ReadableStream({ + async start(controller) { + let buffer = "" + let textId = randomId() + let reasoningId = randomId() + let textStarted = false + let reasoningStarted = false + const toolCalls = new Map() + let responseId: string | undefined + let responseModel: string | undefined + let usage: LanguageModelV2Usage | undefined + let finishReason: LanguageModelV2FinishReason | undefined + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split("\n") + buffer = lines.pop() ?? "" + for (const rawLine of lines) { + const line = rawLine.trim() + if (!line.startsWith("data:")) continue + const payload = line.slice(5).trim() + if (payload === "[DONE]") continue + let chunk: OpenRouterChunk + try { + chunk = JSON.parse(payload) as OpenRouterChunk + } catch { + continue + } + responseId = chunk.id ?? responseId + responseModel = chunk.model ?? responseModel + if (chunk.usage) usage = mapUsage(chunk.usage) + const choice = chunk.choices?.[0] + const delta = choice?.delta + if (delta?.reasoning) { + if (!reasoningStarted) { + reasoningId = randomId() + controller.enqueue({ type: "reasoning-start", id: reasoningId }) + reasoningStarted = true + } + controller.enqueue({ type: "reasoning-delta", id: reasoningId, delta: delta.reasoning }) + } + if (delta?.content) { + if (!textStarted) { + textId = randomId() + controller.enqueue({ type: "text-start", id: textId }) + textStarted = true + } + controller.enqueue({ type: "text-delta", id: textId, delta: delta.content }) + } + for (const call of delta?.tool_calls ?? []) { + const index = call.index ?? toolCalls.size + const existing = toolCalls.get(index) + if (existing) { + if (call.function?.name) existing.name = call.function.name + if (call.function?.arguments) existing.args += call.function.arguments + if (call.id) existing.id = call.id + } else { + toolCalls.set(index, { + id: call.id ?? randomId(), + name: call.function?.name ?? "", + args: call.function?.arguments ?? "", + }) + } + } + if (choice?.finish_reason) { + finishReason = mapFinishReason(choice.finish_reason) + } + } + } + if (buffer.trim()) { + const payload = buffer.trim().replace(/^data:/, "").trim() + if (payload && payload !== "[DONE]") { + try { + const chunk = JSON.parse(payload) as OpenRouterChunk + if (chunk.usage) usage = mapUsage(chunk.usage) + if (chunk.choices?.[0]?.finish_reason) { + finishReason = mapFinishReason(chunk.choices[0].finish_reason) + } + } catch { + // ignore trailing garbage + } + } + } + + if (reasoningStarted) controller.enqueue({ type: "reasoning-end", id: reasoningId }) + for (const call of toolCalls.values()) { + controller.enqueue({ type: "tool-input-start", id: call.id, toolName: call.name }) + controller.enqueue({ type: "tool-input-delta", id: call.id, delta: call.args }) + controller.enqueue({ type: "tool-input-end", id: call.id }) + controller.enqueue({ + type: "tool-call", + toolCallId: call.id, + toolName: call.name, + input: call.args, + }) + } + if (textStarted) controller.enqueue({ type: "text-end", id: textId }) + controller.enqueue({ + type: "response-metadata", + id: responseId, + modelId: responseModel, + }) + controller.enqueue({ + type: "finish", + usage: usage ?? { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined }, + finishReason: finishReason ?? "stop", + }) + controller.close() + } catch (error) { + controller.error(error) + } + }, + async cancel() { + await reader.cancel() + }, + }) + return { stream, request: { body } } + } catch (error) { + cleanup() + throw this.gateway.normalizeError(error) + } + } +} + +function buildHeaders(gateway: OpenRouterProvider): Record { + const headers: Record = { + "Content-Type": "application/json", + ...gateway.headers, + } + if (gateway.apiKey) { + headers.Authorization = `Bearer ${gateway.apiKey}` + } + if (gateway.siteUrl) headers["HTTP-Referer"] = gateway.siteUrl + if (gateway.siteTitle) headers["X-Title"] = gateway.siteTitle + return headers +} + +function buildOpenRouterBody( + options: LanguageModelV2CallOptions, + gateway: OpenRouterProvider, + modelId: string, + stream: boolean, +): Record { + const providerOptions: Record = {} + if (gateway.forceProvider) { + providerOptions.force = true + providerOptions.order = [gateway.forceProvider] + } else if (gateway.providerPreference && gateway.providerPreference.length > 0) { + providerOptions.order = gateway.providerPreference + } + if (gateway.autoSelect) { + providerOptions.sort = "throughput" + } + if (gateway.allowFallbacks === false) { + providerOptions.allow_fallbacks = false + } + + const body: Record = { + model: modelId, + messages: convertPrompt(options.prompt), + stream, + } + if (options.maxOutputTokens !== undefined) body.max_tokens = options.maxOutputTokens + if (options.temperature !== undefined) body.temperature = options.temperature + if (options.topP !== undefined) body.top_p = options.topP + if (options.presencePenalty !== undefined) body.presence_penalty = options.presencePenalty + if (options.frequencyPenalty !== undefined) body.frequency_penalty = options.frequencyPenalty + if (options.stopSequences && options.stopSequences.length > 0) body.stop = options.stopSequences + if (options.seed !== undefined) body.seed = options.seed + + const tools = options.tools?.filter((t): t is LanguageModelV2FunctionTool => t.type === "function") + if (tools && tools.length > 0) { + body.tools = tools.map((tool) => ({ + type: "function", + function: { + name: tool.name, + description: tool.description, + parameters: tool.inputSchema, + }, + })) + } + if (options.toolChoice) body.tool_choice = convertToolChoice(options.toolChoice) + + if (options.responseFormat) { + if (options.responseFormat.type === "json") { + body.response_format = { type: "json_object" } + } + } + + if (Object.keys(providerOptions).length > 0) { + body.provider = providerOptions + } + return body +} + +function convertToolChoice(toolChoice: LanguageModelV2ToolChoice): unknown { + switch (toolChoice.type) { + case "auto": + return "auto" + case "none": + return "none" + case "required": + return "required" + case "tool": + return { type: "function", function: { name: toolChoice.toolName } } + } +} + +function convertPrompt(prompt: LanguageModelV2Prompt): Array> { + const messages: Array> = [] + for (const message of prompt) { + messages.push(convertMessage(message)) + } + return messages +} + +function convertMessage(message: LanguageModelV2Message): Record { + switch (message.role) { + case "system": + return { role: "system", content: message.content } + case "user": { + const text = collectText(message.content) + const files = message.content.filter((part) => part.type === "file") + if (files.length === 0) { + return { role: "user", content: text } + } + const parts: Array> = [] + if (text) parts.push({ type: "text", text }) + for (const file of files as Array) { + parts.push(convertFilePart(file)) + } + return { role: "user", content: parts } + } + case "assistant": { + const text = collectText(message.content) + const toolCalls = message.content + .filter((part) => part.type === "tool-call") + .map((part) => { + const call = part as LanguageModelV2ToolCallPart + const input = typeof call.input === "string" ? call.input : JSON.stringify(call.input ?? {}) + return { + id: call.toolCallId, + type: "function", + function: { name: call.toolName, arguments: input }, + } + }) + const converted: Record = { role: "assistant", content: text } + if (toolCalls.length > 0) converted.tool_calls = toolCalls + return converted + } + case "tool": { + const results = message.content as Array + const first = results[0] + if (!first) return { role: "tool", tool_call_id: "", content: "" } + const output = typeof first.output === "object" && first.output !== null && "value" in first.output + ? String((first.output as { value: unknown }).value) + : JSON.stringify(first.output) + return { + role: "tool", + tool_call_id: first.toolCallId, + content: output, + } + } + } +} + +type AssistantParts = Array< + LanguageModelV2TextPart | LanguageModelV2FilePart | LanguageModelV2ReasoningPart | LanguageModelV2ToolCallPart | LanguageModelV2ToolResultPart +> + +function collectText(parts: AssistantParts): string { + return parts + .filter((part) => part.type === "text") + .map((part) => (part as LanguageModelV2TextPart).text) + .join("") +} + +function convertFilePart(file: LanguageModelV2FilePart): Record { + const data = file.data + const dataUrl = + data instanceof Uint8Array + ? `data:${file.mediaType};base64,${bufferToBase64(data)}` + : typeof data === "string" && data.startsWith("data:") + ? data + : String(data) + return { type: "image_url", image_url: { url: dataUrl } } +} + +function bufferToBase64(data: Uint8Array): string { + let binary = "" + const chunkSize = 0x8000 + for (let i = 0; i < data.length; i += chunkSize) { + binary += String.fromCharCode(...data.subarray(i, i + chunkSize)) + } + return btoa(binary) +} + +function mapFinishReason(reason: string | null | undefined): LanguageModelV2FinishReason { + switch (reason) { + case "stop": + return "stop" + case "length": + return "length" + case "content_filter": + return "content-filter" + case "tool_calls": + return "tool-calls" + case "error": + return "error" + default: + return "unknown" + } +} + +function mapUsage(usage: OpenRouterChunk["usage"]): LanguageModelV2Usage { + return { + inputTokens: usage?.prompt_tokens, + outputTokens: usage?.completion_tokens, + totalTokens: usage?.total_tokens, + cachedInputTokens: usage?.prompt_tokens_details?.cached_tokens, + } +} + +function randomId(): string { + return `id_${Math.random().toString(36).slice(2, 10)}` +} diff --git a/packages/cortex-sdk/src/gateway/orcarouter.ts b/packages/cortex-sdk/src/gateway/orcarouter.ts new file mode 100644 index 0000000..d8e9117 --- /dev/null +++ b/packages/cortex-sdk/src/gateway/orcarouter.ts @@ -0,0 +1,44 @@ +import { createOpenAICompatible } from "@ai-sdk/openai-compatible" +import type { LanguageModel } from "ai" +import { BaseGatewayProvider, type GatewayProviderOptions, type ModelInfo } from "./base" + +const DEFAULT_BASE_URL = "https://api.orcarouter.ai/v1" +const DEFAULT_MODEL = "openai/gpt-4o-mini" + +export interface OrcaRouterProviderOptions extends Omit {} + +export class OrcaRouterProvider extends BaseGatewayProvider { + private readonly sdk: ReturnType + + constructor(options: OrcaRouterProviderOptions = {}) { + super({ + ...options, + provider: "orcarouter", + apiKey: options.apiKey ?? "", + baseURL: options.baseURL ?? DEFAULT_BASE_URL, + model: options.model ?? DEFAULT_MODEL, + }) + this.sdk = createOpenAICompatible({ + name: "orcarouter", + baseURL: this.baseURL, + apiKey: this.apiKey, + headers: this.headers, + fetch: this.fetchImpl, + }) + } + + protected buildModel(modelName: string): LanguageModel { + return this.sdk.chatModel(modelName) + } + + async listModels(): Promise { + return [ + { id: "openai/gpt-4o-mini", name: "GPT-4o Mini", provider: "orcarouter" }, + { id: "openai/gpt-4o", name: "GPT-4o", provider: "orcarouter" }, + ] + } +} + +export function createOrcaRouterProvider(options?: OrcaRouterProviderOptions): OrcaRouterProvider { + return new OrcaRouterProvider(options) +} diff --git a/packages/cortex-sdk/src/gateway/supercode-cloud.ts b/packages/cortex-sdk/src/gateway/supercode-cloud.ts new file mode 100644 index 0000000..0b15b20 --- /dev/null +++ b/packages/cortex-sdk/src/gateway/supercode-cloud.ts @@ -0,0 +1,282 @@ +import type { JSONValue, LanguageModelV2, LanguageModelV2CallOptions, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2FunctionTool, LanguageModelV2Prompt, LanguageModelV2StreamPart, LanguageModelV2Usage } from "@ai-sdk/provider" +import type { LanguageModel } from "ai" +import { BaseGatewayProvider, type GatewayProviderOptions, type ModelInfo } from "./base" + +const DEFAULT_BASE_URL = "https://supercode-8w7e.onrender.com" +const DEFAULT_MODEL = "deepseek-v4-flash" + +export interface SupercodeCloudProviderOptions extends Omit {} + +export class SupercodeCloudProvider extends BaseGatewayProvider { + constructor(options: SupercodeCloudProviderOptions = {}) { + super({ + ...options, + provider: "supercode-cloud", + apiKey: options.apiKey ?? "", + baseURL: options.baseURL ?? DEFAULT_BASE_URL, + model: options.model ?? DEFAULT_MODEL, + }) + } + + protected buildModel(modelName: string): LanguageModel { + return new SupercodeCloudLanguageModel(this, modelName) + } + + async listModels(): Promise { + return [ + { id: "default", name: "Supercode default model", provider: "supercode-cloud" }, + { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", provider: "supercode-cloud" }, + ] + } +} + +export function createSupercodeCloudProvider(options?: SupercodeCloudProviderOptions): SupercodeCloudProvider { + return new SupercodeCloudProvider(options) +} + +interface SupercodeStreamEvent { + type?: string + content?: string + toolName?: string + args?: string | Record + toolCallId?: string + message?: string + reason?: string + usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number } +} + +class SupercodeCloudLanguageModel implements LanguageModelV2 { + readonly specificationVersion = "v2" as const + readonly provider = "supercode-cloud" + readonly modelId: string + readonly supportedUrls = {} + + constructor( + private readonly gateway: SupercodeCloudProvider, + modelId: string, + ) { + this.modelId = modelId + } + + private buildRequest(options: LanguageModelV2CallOptions, stream: boolean): Record { + const tools = options.tools + ?.filter((t): t is LanguageModelV2FunctionTool => t.type === "function") + .map((tool) => ({ + type: "function", + function: { + name: tool.name, + description: tool.description, + parameters: tool.inputSchema, + }, + })) + return { + messages: convertPrompt(options.prompt), + provider: "supercode-cloud", + model: this.modelId, + stream, + tools, + } + } + + async doGenerate(options: LanguageModelV2CallOptions): Promise<{ + content: LanguageModelV2Content[] + finishReason: LanguageModelV2FinishReason + usage: LanguageModelV2Usage + providerMetadata: Record> + request: { body: unknown } + warnings: Array<{ type: "other"; message: string }> + }> { + const { signal, cleanup } = this.gateway.createAbortController(options.abortSignal) + try { + const body = this.buildRequest(options, false) + const response = await this.gateway.fetchWithRetry(`${this.gateway.baseURL}/api/v1/gateway`, { + method: "POST", + headers: { "Content-Type": "application/json", ...this.gateway.headers }, + body: JSON.stringify(body), + signal, + }) + const data = (await response.json()) as { + content?: string + reasoning?: string + toolCalls?: Array<{ toolName: string; args: unknown; toolCallId: string }> + finishReason?: string + usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number } + } + const content: LanguageModelV2Content[] = [] + if (data.reasoning) content.push({ type: "reasoning", text: data.reasoning }) + if (data.content) content.push({ type: "text", text: data.content }) + for (const call of data.toolCalls ?? []) { + content.push({ + type: "tool-call", + toolCallId: call.toolCallId, + toolName: call.toolName, + input: JSON.stringify(call.args ?? {}), + }) + } + return { + content, + finishReason: (data.finishReason as LanguageModelV2FinishReason | undefined) ?? "stop", + usage: { + inputTokens: data.usage?.inputTokens, + outputTokens: data.usage?.outputTokens, + totalTokens: data.usage?.totalTokens, + }, + providerMetadata: {}, + request: { body }, + warnings: [], + } + } catch (error) { + throw this.gateway.normalizeError(error) + } finally { + cleanup() + } + } + + async doStream(options: LanguageModelV2CallOptions): Promise<{ + stream: ReadableStream + request: { body: unknown } + }> { + const { signal, cleanup } = this.gateway.createAbortController(options.abortSignal) + const body = this.buildRequest(options, true) + try { + const response = await this.gateway.fetchWithRetry(`${this.gateway.baseURL}/api/v1/gateway`, { + method: "POST", + headers: { "Content-Type": "application/json", ...this.gateway.headers }, + body: JSON.stringify(body), + signal, + }) + if (!response.body) { + throw new Error("supercode-cloud: empty response body") + } + const reader = response.body.getReader() + const decoder = new TextDecoder() + const stream = new ReadableStream({ + async start(controller) { + let buffer = "" + let textId = randomId() + let reasoningId = randomId() + let textStarted = false + let reasoningStarted = false + let usage: LanguageModelV2Usage | undefined + let finishReason: LanguageModelV2FinishReason | undefined + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split("\n") + buffer = lines.pop() ?? "" + for (const rawLine of lines) { + const line = rawLine.trim() + if (!line) continue + let event: SupercodeStreamEvent + try { + event = JSON.parse(line) as SupercodeStreamEvent + } catch { + continue + } + switch (event.type) { + case "text": + if (!textStarted) { + textId = randomId() + controller.enqueue({ type: "text-start", id: textId }) + textStarted = true + } + controller.enqueue({ type: "text-delta", id: textId, delta: event.content ?? "" }) + break + case "reasoning": + if (!reasoningStarted) { + reasoningId = randomId() + controller.enqueue({ type: "reasoning-start", id: reasoningId }) + reasoningStarted = true + } + controller.enqueue({ type: "reasoning-delta", id: reasoningId, delta: event.content ?? "" }) + break + case "tool-call": { + const callId = event.toolCallId ?? randomId() + const input = + typeof event.args === "string" ? event.args : JSON.stringify(event.args ?? {}) + controller.enqueue({ type: "tool-input-start", id: callId, toolName: event.toolName ?? "" }) + controller.enqueue({ type: "tool-input-delta", id: callId, delta: input }) + controller.enqueue({ type: "tool-input-end", id: callId }) + controller.enqueue({ + type: "tool-call", + toolCallId: callId, + toolName: event.toolName ?? "", + input, + }) + break + } + case "error": + controller.error(new Error(event.message ?? "supercode-cloud stream error")) + return + case "finish": + usage = { + inputTokens: event.usage?.inputTokens, + outputTokens: event.usage?.outputTokens, + totalTokens: event.usage?.totalTokens, + } + finishReason = (event.reason as LanguageModelV2FinishReason | undefined) ?? "stop" + break + } + } + } + if (reasoningStarted) controller.enqueue({ type: "reasoning-end", id: reasoningId }) + if (textStarted) controller.enqueue({ type: "text-end", id: textId }) + controller.enqueue({ + type: "finish", + usage: usage ?? { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined }, + finishReason: finishReason ?? "stop", + }) + controller.close() + } catch (error) { + controller.error(error) + } + }, + async cancel() { + await reader.cancel() + }, + }) + return { stream, request: { body } } + } catch (error) { + cleanup() + throw this.gateway.normalizeError(error) + } + } +} + +function convertPrompt(prompt: LanguageModelV2Prompt): Array> { + return prompt.map((message) => { + switch (message.role) { + case "system": + return { role: "system", content: message.content } + case "user": + return { role: "user", content: convertParts(message.content) } + case "assistant": + return { role: "assistant", content: convertParts(message.content) } + case "tool": { + const result = message.content[0] + const output = result && typeof result.output === "object" && result.output !== null && "value" in result.output + ? String((result.output as { value: unknown }).value) + : JSON.stringify(result?.output ?? "") + return { role: "tool", tool_call_id: result?.toolCallId ?? "", content: output } + } + } + }) +} + +function convertParts(parts: Array<{ type: string; text?: string; data?: unknown; mediaType?: string }>): unknown { + const converted = parts.map((part) => { + if (part.type === "text") return { type: "text", text: part.text ?? "" } + if (part.type === "reasoning") return { type: "reasoning", text: part.text ?? "" } + if (part.type === "file") return { type: "file", mediaType: part.mediaType, data: String(part.data) } + return part + }) + if (converted.length === 1) return converted[0] + return converted +} + +function randomId(): string { + return `id_${Math.random().toString(36).slice(2, 10)}` +} diff --git a/packages/cortex-sdk/src/index.ts b/packages/cortex-sdk/src/index.ts new file mode 100644 index 0000000..c7761ea --- /dev/null +++ b/packages/cortex-sdk/src/index.ts @@ -0,0 +1,40 @@ +export * from "./core/errors" +export * from "./core/types" + +export { createGateway } from "./gateway" +export type { + GatewayClient, + ModelInfo, +} from "./gateway" + +export { createAgentHandler, listToolPacks, TOOL_PACK_PRESETS } from "./agent-handler" +export type { + AgentHandlerClient, + AgentHandlerClientOptions, + AgentHandlerTransportConfig, + McpClientLike, + ToolPackInfo, +} from "./agent-handler" + +export { createComposio } from "./composio" +export type { + AppInfo, + ComposioClient, + ComposioClientOptions, + ComposioSessionInfo, + ConnectAppResult, +} from "./composio" + +export { createWebSearch } from "./web-search" +export type { + SearchResult, + WebSearchClient, + WebSearchConfig, + WebSearchProvider, +} from "./web-search" + +export { createMcpManager } from "./mcp" +export type { McpConnection, McpManager } from "./mcp" + +export { createVoice } from "./voice" +export type { SttResult, TtsResult, VoiceClient } from "./voice" diff --git a/packages/cortex-sdk/src/mcp/index.ts b/packages/cortex-sdk/src/mcp/index.ts new file mode 100644 index 0000000..abc7be5 --- /dev/null +++ b/packages/cortex-sdk/src/mcp/index.ts @@ -0,0 +1,23 @@ +import { SdkError } from "../core/errors" +import type { McpServerConfig } from "../core/types" + +export type { McpServerConfig, McpTransport } from "../core/types" + +export interface McpConnection { + serverId: string + connected: boolean +} + +export interface McpManager { + connectServer(id: string): Promise + listTools(serverId?: string): Promise + listServers(): McpServerConfig[] + disconnect(): Promise +} + +export function createMcpManager(options: { servers?: McpServerConfig[] }): McpManager { + void options + throw new SdkError("createMcpManager: not implemented yet (Phase 5)", { + code: "NOT_IMPLEMENTED", + }) +} diff --git a/packages/cortex-sdk/src/voice/elevenlabs.ts b/packages/cortex-sdk/src/voice/elevenlabs.ts new file mode 100644 index 0000000..f20b5c2 --- /dev/null +++ b/packages/cortex-sdk/src/voice/elevenlabs.ts @@ -0,0 +1,125 @@ +import { SdkError } from "../core/errors" +import type { FetchLike } from "../core/types" +import type { SttResult, TtsResult } from "./types" + +export interface ElevenLabsSttOptions { + apiKey: string + model?: string + language?: string + baseURL?: string + fetch?: FetchLike +} + +export interface ElevenLabsTtsOptions { + apiKey: string + model?: string + voiceId?: string + baseURL?: string + fetch?: FetchLike +} + +export const ELEVENLABS_STT_DEFAULT_MODEL = "scribe_v1" +export const ELEVENLABS_TTS_DEFAULT_MODEL = "eleven_turbo_v2_5" +export const ELEVENLABS_DEFAULT_VOICE_ID = "21m00Tcm4TlvDq8ikWAM" + +const ELEVENLABS_BASE_URL = "https://api.elevenlabs.io/v1" + +function resolveFetch(options: { fetch?: FetchLike }): FetchLike { + return options.fetch ?? ((input, init) => fetch(input, init)) +} + +export async function transcribeElevenLabs( + audio: Buffer, + options: ElevenLabsSttOptions, +): Promise { + const baseURL = (options.baseURL ?? ELEVENLABS_BASE_URL).replace(/\/$/, "") + const url = `${baseURL}/speech-to-text` + const doFetch = resolveFetch(options) + + const form = new FormData() + form.append("file", new Blob([new Uint8Array(audio)], { type: "audio/wav" }), "audio.wav") + form.append("model_id", options.model ?? ELEVENLABS_STT_DEFAULT_MODEL) + if (options.language) form.append("language", options.language) + + let res: Response + try { + res = await doFetch(url, { + method: "POST", + headers: { "xi-api-key": options.apiKey }, + body: form, + }) + } catch (err) { + throw new SdkError( + `ElevenLabs STT request failed: ${(err as Error).message}`, + { code: "VOICE_STT_FAILED" }, + ) + } + + const body = await res.text() + if (!res.ok) { + throw new SdkError(`ElevenLabs STT error ${res.status}: ${body}`, { + code: "VOICE_STT_FAILED", + }) + } + + let data: { text?: string } + try { + data = JSON.parse(body) as { text?: string } + } catch { + throw new SdkError(`ElevenLabs STT invalid JSON: ${body}`, { + code: "VOICE_STT_FAILED", + }) + } + + if (!data.text) { + throw new SdkError("ElevenLabs STT returned empty text", { + code: "VOICE_STT_FAILED", + }) + } + return { text: data.text } +} + +export async function synthesizeElevenLabs( + text: string, + options: ElevenLabsTtsOptions, +): Promise { + const baseURL = (options.baseURL ?? ELEVENLABS_BASE_URL).replace(/\/$/, "") + const voiceId = options.voiceId ?? ELEVENLABS_DEFAULT_VOICE_ID + const url = `${baseURL}/text-to-speech/${voiceId}/stream` + const doFetch = resolveFetch(options) + + let res: Response + try { + res = await doFetch(url, { + method: "POST", + headers: { + "xi-api-key": options.apiKey, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text, + model_id: options.model ?? ELEVENLABS_TTS_DEFAULT_MODEL, + }), + }) + } catch (err) { + throw new SdkError( + `ElevenLabs TTS request failed: ${(err as Error).message}`, + { code: "VOICE_TTS_FAILED" }, + ) + } + + if (!res.ok) { + const body = await res.text() + throw new SdkError(`ElevenLabs TTS error ${res.status}: ${body}`, { + code: "VOICE_TTS_FAILED", + }) + } + + const audio = Buffer.from(await res.arrayBuffer()) + if (audio.length === 0) { + throw new SdkError("ElevenLabs TTS returned empty audio", { + code: "VOICE_TTS_FAILED", + }) + } + return { audio, format: "mp3" } +} diff --git a/packages/cortex-sdk/src/voice/groq.ts b/packages/cortex-sdk/src/voice/groq.ts new file mode 100644 index 0000000..f40114d --- /dev/null +++ b/packages/cortex-sdk/src/voice/groq.ts @@ -0,0 +1,68 @@ +import { SdkError } from "../core/errors" +import type { FetchLike } from "../core/types" +import type { SttResult } from "./types" + +export interface GroqSttOptions { + apiKey: string + model?: string + baseURL?: string + fetch?: FetchLike +} + +export const GROQ_STT_DEFAULT_MODEL = "whisper-large-v3-turbo" + +const GROQ_BASE_URL = "https://api.groq.com/openai/v1" + +function resolveFetch(options: { fetch?: FetchLike }): FetchLike { + return options.fetch ?? ((input, init) => fetch(input, init)) +} + +export async function transcribeGroq( + audio: Buffer, + options: GroqSttOptions, +): Promise { + const baseURL = (options.baseURL ?? GROQ_BASE_URL).replace(/\/$/, "") + const url = `${baseURL}/audio/transcriptions` + const doFetch = resolveFetch(options) + + const form = new FormData() + form.append("file", new Blob([new Uint8Array(audio)], { type: "audio/wav" }), "audio.wav") + form.append("model", options.model ?? GROQ_STT_DEFAULT_MODEL) + + let res: Response + try { + res = await doFetch(url, { + method: "POST", + headers: { Authorization: `Bearer ${options.apiKey}` }, + body: form, + }) + } catch (err) { + throw new SdkError( + `Groq STT request failed: ${(err as Error).message}`, + { code: "VOICE_STT_FAILED" }, + ) + } + + const body = await res.text() + if (!res.ok) { + throw new SdkError(`Groq STT error ${res.status}: ${body}`, { + code: "VOICE_STT_FAILED", + }) + } + + let data: { text?: string } + try { + data = JSON.parse(body) as { text?: string } + } catch { + throw new SdkError(`Groq STT invalid JSON: ${body}`, { + code: "VOICE_STT_FAILED", + }) + } + + if (!data.text) { + throw new SdkError("Groq STT returned empty text", { + code: "VOICE_STT_FAILED", + }) + } + return { text: data.text } +} diff --git a/packages/cortex-sdk/src/voice/index.ts b/packages/cortex-sdk/src/voice/index.ts new file mode 100644 index 0000000..1147316 --- /dev/null +++ b/packages/cortex-sdk/src/voice/index.ts @@ -0,0 +1,47 @@ +import type { SttProvider, TtsProvider, VoiceConfig } from "../core/types" +import { STT_PROVIDERS, transcribe as sttTranscribe } from "./stt" +import { TTS_PROVIDERS, synthesize as ttsSynthesize } from "./tts" +import type { SttResult, TtsResult, VoiceClient } from "./types" + +export type { SttResult, TtsResult, VoiceClient } from "./types" +export type { SttProvider, TtsProvider, VoiceConfig } from "../core/types" + +export function createVoice(options: VoiceConfig = {}): VoiceClient { + return new VoiceClientImpl(options) +} + +class VoiceClientImpl implements VoiceClient { + constructor(private readonly options: VoiceConfig) {} + + async transcribe(audio: Buffer, provider?: SttProvider): Promise { + const stt = provider ?? this.options.stt ?? "smallest" + return sttTranscribe(audio, stt, { + apiKey: this.options.sttApiKey, + model: this.options.sttModel, + language: this.options.sttLanguage, + fetch: this.options.fetch, + }) + } + + async synthesize( + text: string, + voice?: string, + provider?: TtsProvider, + ): Promise { + const tts = provider ?? this.options.tts ?? "smallest" + return ttsSynthesize(text, tts, { + apiKey: this.options.ttsApiKey, + model: this.options.ttsModel, + voice: voice ?? this.options.voice, + fetch: this.options.fetch, + }) + } + + listSttProviders(): SttProvider[] { + return [...STT_PROVIDERS] + } + + listTtsProviders(): TtsProvider[] { + return [...TTS_PROVIDERS] + } +} diff --git a/packages/cortex-sdk/src/voice/smallest.ts b/packages/cortex-sdk/src/voice/smallest.ts new file mode 100644 index 0000000..dc82cea --- /dev/null +++ b/packages/cortex-sdk/src/voice/smallest.ts @@ -0,0 +1,120 @@ +import { SdkError } from "../core/errors" +import type { FetchLike } from "../core/types" +import type { SttResult, TtsResult } from "./types" + +export interface SmallestSttOptions { + apiKey: string + model?: string + language?: string + baseURL?: string + fetch?: FetchLike +} + +export interface SmallestTtsOptions { + apiKey: string + model?: string + baseURL?: string + fetch?: FetchLike +} + +export const SMALLEST_STT_DEFAULT_MODEL = "pulse-pro" +export const SMALLEST_TTS_DEFAULT_MODEL = "lightning_v3.1_pro" + +const SMALLEST_BASE_URL = "https://api.smallest.ai/v1" + +function resolveFetch(options: { fetch?: FetchLike }): FetchLike { + return options.fetch ?? ((input, init) => fetch(input, init)) +} + +export async function transcribeSmallest( + audio: Buffer, + options: SmallestSttOptions, +): Promise { + const baseURL = (options.baseURL ?? SMALLEST_BASE_URL).replace(/\/$/, "") + const url = `${baseURL}/speech-to-text/transcribe` + const doFetch = resolveFetch(options) + + const form = new FormData() + form.append("file", new Blob([new Uint8Array(audio)], { type: "audio/wav" }), "audio.wav") + form.append("model", options.model ?? SMALLEST_STT_DEFAULT_MODEL) + if (options.language) form.append("language", options.language) + + let res: Response + try { + res = await doFetch(url, { + method: "POST", + headers: { Authorization: `Bearer ${options.apiKey}` }, + body: form, + }) + } catch (err) { + throw new SdkError( + `Smallest STT request failed: ${(err as Error).message}`, + { code: "VOICE_STT_FAILED" }, + ) + } + + const body = await res.text() + if (!res.ok) { + throw new SdkError(`Smallest STT error ${res.status}: ${body}`, { + code: "VOICE_STT_FAILED", + }) + } + + let data: { text?: string; transcription?: string } + try { + data = JSON.parse(body) as { text?: string; transcription?: string } + } catch { + throw new SdkError(`Smallest STT invalid JSON: ${body}`, { + code: "VOICE_STT_FAILED", + }) + } + + const text = data.text ?? data.transcription ?? "" + if (!text) { + throw new SdkError("Smallest STT returned empty text", { + code: "VOICE_STT_FAILED", + }) + } + return { text } +} + +export async function synthesizeSmallest( + text: string, + options: SmallestTtsOptions, +): Promise { + const baseURL = (options.baseURL ?? SMALLEST_BASE_URL).replace(/\/$/, "") + const url = `${baseURL}/lightning/speech-synthesis` + const doFetch = resolveFetch(options) + + let res: Response + try { + res = await doFetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${options.apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ text, model: options.model ?? SMALLEST_TTS_DEFAULT_MODEL }), + }) + } catch (err) { + throw new SdkError( + `Smallest TTS request failed: ${(err as Error).message}`, + { code: "VOICE_TTS_FAILED" }, + ) + } + + if (!res.ok) { + const body = await res.text() + throw new SdkError(`Smallest TTS error ${res.status}: ${body}`, { + code: "VOICE_TTS_FAILED", + }) + } + + const audio = Buffer.from(await res.arrayBuffer()) + if (audio.length === 0) { + throw new SdkError("Smallest TTS returned empty audio", { + code: "VOICE_TTS_FAILED", + }) + } + return { audio, format: "pcm" } +} diff --git a/packages/cortex-sdk/src/voice/stt.ts b/packages/cortex-sdk/src/voice/stt.ts new file mode 100644 index 0000000..d99b0c7 --- /dev/null +++ b/packages/cortex-sdk/src/voice/stt.ts @@ -0,0 +1,73 @@ +import { SdkError } from "../core/errors" +import type { FetchLike, SttProvider } from "../core/types" +import { transcribeElevenLabs } from "./elevenlabs" +import { transcribeGroq } from "./groq" +import { transcribeSmallest } from "./smallest" +import type { SttResult } from "./types" + +export interface SttConfig { + apiKey?: string + model?: string + language?: string + baseURL?: string + fetch?: FetchLike +} + +export const STT_PROVIDERS: SttProvider[] = ["smallest", "elevenlabs", "groq"] + +export function isSttProvider(value: unknown): value is SttProvider { + return typeof value === "string" && (STT_PROVIDERS as string[]).includes(value) +} + +export async function transcribe( + audio: Buffer, + provider: SttProvider, + config: SttConfig = {}, +): Promise { + switch (provider) { + case "smallest": + return transcribeSmallest(audio, { + apiKey: requireKey("smallest", config.apiKey, [ + "SMALLEST_API_KEY", + "SMALLEST_AI_API_KEY", + ]), + model: config.model ?? process.env.SMALLEST_MODEL ?? process.env.SMALLEST_AI_STT_MODEL, + language: config.language ?? process.env.STT_LANGUAGE ?? "en", + baseURL: config.baseURL, + fetch: config.fetch, + }) + case "elevenlabs": + return transcribeElevenLabs(audio, { + apiKey: requireKey("elevenlabs", config.apiKey, ["ELEVENLABS_API_KEY"]), + model: config.model ?? process.env.ELEVENLABS_STT_MODEL ?? "scribe_v1", + language: config.language ?? process.env.STT_LANGUAGE, + baseURL: config.baseURL, + fetch: config.fetch, + }) + case "groq": + return transcribeGroq(audio, { + apiKey: requireKey("groq", config.apiKey, ["GROQ_API_KEY"]), + model: config.model ?? process.env.GROQ_MODEL, + baseURL: config.baseURL, + fetch: config.fetch, + }) + default: + throw new SdkError(`Unknown STT provider: ${String(provider)}`, { + code: "UNKNOWN_PROVIDER", + }) + } +} + +function requireKey( + provider: string, + configKey: string | undefined, + envNames: string[], +): string { + const key = configKey ?? envNames.map((n) => process.env[n]).find(Boolean) + if (!key) { + throw new SdkError(`${provider} STT: no API key configured`, { + code: "NOT_CONFIGURED", + }) + } + return key +} diff --git a/packages/cortex-sdk/src/voice/tts.ts b/packages/cortex-sdk/src/voice/tts.ts new file mode 100644 index 0000000..ad8058d --- /dev/null +++ b/packages/cortex-sdk/src/voice/tts.ts @@ -0,0 +1,64 @@ +import { SdkError } from "../core/errors" +import type { FetchLike, TtsProvider } from "../core/types" +import { synthesizeElevenLabs } from "./elevenlabs" +import { synthesizeSmallest } from "./smallest" +import type { TtsResult } from "./types" + +export interface TtsConfig { + apiKey?: string + model?: string + voice?: string + baseURL?: string + fetch?: FetchLike +} + +export const TTS_PROVIDERS: TtsProvider[] = ["smallest", "elevenlabs"] + +export function isTtsProvider(value: unknown): value is TtsProvider { + return typeof value === "string" && (TTS_PROVIDERS as string[]).includes(value) +} + +export async function synthesize( + text: string, + provider: TtsProvider, + config: TtsConfig = {}, +): Promise { + switch (provider) { + case "smallest": + return synthesizeSmallest(text, { + apiKey: requireKey("smallest", config.apiKey, [ + "SMALLEST_API_KEY", + "SMALLEST_AI_API_KEY", + ]), + model: config.model ?? process.env.SMALLEST_AI_TTS_MODEL, + baseURL: config.baseURL, + fetch: config.fetch, + }) + case "elevenlabs": + return synthesizeElevenLabs(text, { + apiKey: requireKey("elevenlabs", config.apiKey, ["ELEVENLABS_API_KEY"]), + model: config.model ?? process.env.ELEVENLABS_TTS_MODEL ?? "eleven_turbo_v2_5", + voiceId: config.voice ?? process.env.ELEVENLABS_VOICE_ID, + baseURL: config.baseURL, + fetch: config.fetch, + }) + default: + throw new SdkError(`Unknown TTS provider: ${String(provider)}`, { + code: "UNKNOWN_PROVIDER", + }) + } +} + +function requireKey( + provider: string, + configKey: string | undefined, + envNames: string[], +): string { + const key = configKey ?? envNames.map((n) => process.env[n]).find(Boolean) + if (!key) { + throw new SdkError(`${provider} TTS: no API key configured`, { + code: "NOT_CONFIGURED", + }) + } + return key +} diff --git a/packages/cortex-sdk/src/voice/types.ts b/packages/cortex-sdk/src/voice/types.ts new file mode 100644 index 0000000..8258156 --- /dev/null +++ b/packages/cortex-sdk/src/voice/types.ts @@ -0,0 +1,20 @@ +import type { FetchLike, SttProvider, TtsProvider, VoiceConfig } from "../core/types" + +export type { FetchLike, SttProvider, TtsProvider, VoiceConfig } from "../core/types" + +export interface SttResult { + text: string + language?: string +} + +export interface TtsResult { + audio: Buffer + format: "pcm" | "mp3" +} + +export interface VoiceClient { + transcribe(audio: Buffer, provider?: SttProvider): Promise + synthesize(text: string, voice?: string, provider?: TtsProvider): Promise + listSttProviders(): SttProvider[] + listTtsProviders(): TtsProvider[] +} diff --git a/packages/cortex-sdk/src/voice/voice.test.ts b/packages/cortex-sdk/src/voice/voice.test.ts new file mode 100644 index 0000000..c30fd40 --- /dev/null +++ b/packages/cortex-sdk/src/voice/voice.test.ts @@ -0,0 +1,216 @@ +import { describe, expect, test } from "bun:test" +import { SdkError } from "../core/errors" +import type { VoiceConfig } from "../core/types" +import { createVoice } from "./index" +import { isSttProvider, STT_PROVIDERS, transcribe } from "./stt" +import { isTtsProvider, TTS_PROVIDERS, synthesize } from "./tts" + +function jsonResponse(data: unknown, status = 200): Response { + return new Response(JSON.stringify(data), { + status, + headers: { "Content-Type": "application/json" }, + }) +} + +function makeVoice(overrides: Partial = {}): ReturnType { + return createVoice({ + stt: "smallest", + tts: "elevenlabs", + sttApiKey: "stt-key", + ttsApiKey: "tts-key", + ...overrides, + }) +} + +const AUDIO = Buffer.from("fake-audio-bytes") + +describe("createVoice", () => { + test("listSttProviders and listTtsProviders return known providers", () => { + const voice = makeVoice() + expect(voice.listSttProviders()).toEqual(["smallest", "elevenlabs", "groq"]) + expect(voice.listTtsProviders()).toEqual(["smallest", "elevenlabs"]) + }) + + test("transcribe routes to smallest STT and returns text", async () => { + const voice = makeVoice({ + fetch: (async (url, init) => { + expect(String(url)).toBe( + "https://api.smallest.ai/v1/speech-to-text/transcribe", + ) + expect((init?.headers as Record).Authorization).toBe( + "Bearer stt-key", + ) + return jsonResponse({ text: "hello world" }) + }) as VoiceConfig["fetch"], + }) + const result = await voice.transcribe(AUDIO) + expect(result.text).toBe("hello world") + }) + + test("transcribe falls back to default smallest provider", async () => { + const voice = makeVoice({ + stt: undefined, + fetch: (async () => jsonResponse({ text: "default" })) as VoiceConfig["fetch"], + }) + const result = await voice.transcribe(AUDIO) + expect(result.text).toBe("default") + }) + + test("synthesize routes to elevenlabs TTS with voice id and returns mp3", async () => { + const voice = makeVoice({ + fetch: (async (url, init) => { + expect(String(url)).toBe( + "https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM/stream", + ) + expect((init?.headers as Record)["xi-api-key"]).toBe( + "tts-key", + ) + const body = JSON.parse(String(init?.body)) as { text: string; model_id: string } + expect(body.text).toBe("hi") + expect(body.model_id).toBe("eleven_turbo_v2_5") + return new Response(Buffer.from("mp3-bytes"), { + status: 200, + headers: { "Content-Type": "audio/mpeg" }, + }) + }) as VoiceConfig["fetch"], + }) + const result = await voice.synthesize("hi") + expect(result.format).toBe("mp3") + expect(result.audio.toString()).toBe("mp3-bytes") + }) + + test("synthesize accepts explicit provider and voice overrides", async () => { + const voice = makeVoice({ + fetch: (async (url) => { + expect(String(url)).toBe( + "https://api.smallest.ai/v1/lightning/speech-synthesis", + ) + return new Response(Buffer.from("pcm-bytes"), { + status: 200, + headers: { "Content-Type": "audio/pcm" }, + }) + }) as VoiceConfig["fetch"], + }) + const result = await voice.synthesize("hey", undefined, "smallest") + expect(result.format).toBe("pcm") + expect(result.audio.toString()).toBe("pcm-bytes") + }) + + test("transcribe throws SdkError when stt provider has no key", async () => { + const voice = createVoice({ + stt: "groq", + fetch: (async () => jsonResponse({ text: "x" })) as VoiceConfig["fetch"], + }) + try { + await voice.transcribe(AUDIO) + expect.unreachable() + } catch (err) { + expect(err).toBeInstanceOf(SdkError) + expect((err as SdkError).code).toBe("NOT_CONFIGURED") + } + }) + + test("transcribe throws SdkError on HTTP error", async () => { + const voice = makeVoice({ + fetch: (async () => jsonResponse({ error: "boom" }, 500)) as VoiceConfig["fetch"], + }) + try { + await voice.transcribe(AUDIO) + expect.unreachable() + } catch (err) { + expect(err).toBeInstanceOf(SdkError) + expect((err as SdkError).code).toBe("VOICE_STT_FAILED") + } + }) + + test("synthesize throws SdkError on empty audio", async () => { + const voice = makeVoice({ + fetch: (async () => new Response(new ArrayBuffer(0), { status: 200 })) as VoiceConfig["fetch"], + }) + try { + await voice.synthesize("hi") + expect.unreachable() + } catch (err) { + expect(err).toBeInstanceOf(SdkError) + expect((err as SdkError).code).toBe("VOICE_TTS_FAILED") + } + }) +}) + +describe("stt dispatcher", () => { + test("STT_PROVIDERS and isSttProvider work", () => { + expect(STT_PROVIDERS).toEqual(["smallest", "elevenlabs", "groq"]) + expect(isSttProvider("smallest")).toBe(true) + expect(isSttProvider("unknown")).toBe(false) + expect(isSttProvider(123)).toBe(false) + }) + + test("transcribe groq posts to openai-compatible endpoint", async () => { + const result = await transcribe( + AUDIO, + "groq", + { + apiKey: "g-key", + fetch: (async (url, init) => { + expect(String(url)).toBe( + "https://api.groq.com/openai/v1/audio/transcriptions", + ) + expect((init?.headers as Record).Authorization).toBe( + "Bearer g-key", + ) + return jsonResponse({ text: "transcribed" }) + }) as VoiceConfig["fetch"], + }, + ) + expect(result.text).toBe("transcribed") + }) + + test("transcribe with unknown provider throws UNKNOWN_PROVIDER", async () => { + try { + await transcribe(AUDIO, "bogus" as never) + expect.unreachable() + } catch (err) { + expect((err as SdkError).code).toBe("UNKNOWN_PROVIDER") + } + }) +}) + +describe("tts dispatcher", () => { + test("TTS_PROVIDERS and isTtsProvider work", () => { + expect(TTS_PROVIDERS).toEqual(["smallest", "elevenlabs"]) + expect(isTtsProvider("elevenlabs")).toBe(true) + expect(isTtsProvider("groq")).toBe(false) + }) + + test("synthesize elevenlabs posts with model_id and xi-api-key", async () => { + const result = await synthesize( + "hello", + "elevenlabs", + { + apiKey: "e-key", + fetch: (async (url, init) => { + expect(String(url)).toBe( + "https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM/stream", + ) + expect((init?.headers as Record)["xi-api-key"]).toBe( + "e-key", + ) + return new Response(Buffer.from("mp3"), { + status: 200, + headers: { "Content-Type": "audio/mpeg" }, + }) + }) as VoiceConfig["fetch"], + }, + ) + expect(result.format).toBe("mp3") + }) + + test("synthesize with unknown provider throws UNKNOWN_PROVIDER", async () => { + try { + await synthesize("x", "bogus" as never) + expect.unreachable() + } catch (err) { + expect((err as SdkError).code).toBe("UNKNOWN_PROVIDER") + } + }) +}) diff --git a/packages/cortex-sdk/src/web-search/client.test.ts b/packages/cortex-sdk/src/web-search/client.test.ts new file mode 100644 index 0000000..c3bef80 --- /dev/null +++ b/packages/cortex-sdk/src/web-search/client.test.ts @@ -0,0 +1,235 @@ +import { describe, expect, test } from "bun:test" +import { ConnectionError, SdkError } from "../core/errors" +import type { McpClientLike } from "../agent-handler/types" +import type { WebSearchClient, WebSearchConfig } from "./types" +import { createExaSearch } from "./exa" +import { createFirecrawlSearch } from "./firecrawl" + +function fakeMcpClient(tools: Record): McpClientLike { + return { + tools: async () => tools as never, + close: async () => {}, + } +} + +function jsonResponse(data: unknown, status = 200): Response { + return new Response(JSON.stringify(data), { status }) +} + +type Options = Omit + +describe("createExaSearch", () => { + test("provider and listProviders are correct", () => { + const client = createExaSearch({ apiKey: "k" }) + expect(client.provider).toBe("exa") + expect(client.listProviders()).toEqual(["exa"]) + }) + + test("connect() opens MCP and getTools() returns tools", async () => { + const seen: unknown[] = [] + const client = createExaSearch({ + apiKey: "k", + createMcpClient: (config) => { + seen.push(config) + return Promise.resolve(fakeMcpClient({ exa_search: { type: "function" } })) + }, + }) + await client.connect() + expect(client.isConnected).toBe(true) + const tools = await client.getTools() + expect(Object.keys(tools)).toEqual(["exa_search"]) + expect(seen[0]).toMatchObject({ + transport: { type: "http", url: "https://mcp.exa.ai/mcp", headers: { "x-api-key": "k" } }, + }) + }) + + test("connect() is idempotent", async () => { + let count = 0 + const client = createExaSearch({ + apiKey: "k", + createMcpClient: () => { + count++ + return Promise.resolve(fakeMcpClient({})) + }, + }) + await client.connect() + await client.connect() + expect(count).toBe(1) + }) + + test("connect() throws when apiKey missing", async () => { + const client = createExaSearch({}) + await expect(client.connect()).rejects.toMatchObject({ code: "NOT_CONFIGURED" }) + }) + + test("connect() maps MCP failures to ConnectionError", async () => { + const client = createExaSearch({ + apiKey: "k", + createMcpClient: async () => { + throw new Error("boom") + }, + }) + await expect(client.connect()).rejects.toBeInstanceOf(ConnectionError) + }) + + test("search() maps REST results to SearchResult", async () => { + let requested = "" + const client = createExaSearch({ + apiKey: "k", + fetch: (async (url, init) => { + requested = String(url) + expect(JSON.parse(String(init?.body))).toEqual({ query: "bun", numResults: 3 }) + return jsonResponse({ + results: [ + { title: "Bun", url: "https://bun.sh", publishedDate: "2024-01-01", text: "fast" }, + ], + }) + }) as Options["fetch"], + }) + const results = await client.search("bun", 3) + expect(requested).toBe("https://api.exa.ai/search") + expect(results[0]).toEqual({ + title: "Bun", + url: "https://bun.sh", + snippet: "fast", + publishedDate: "2024-01-01", + }) + }) + + test("search() throws SdkError on non-ok response", async () => { + const client = createExaSearch({ + apiKey: "k", + fetch: (async () => new Response("boom", { status: 500 })) as Options["fetch"], + }) + await expect(client.search("bun")).rejects.toBeInstanceOf(SdkError) + }) + + test("scrape() fetches page contents", async () => { + let requested = "" + const client = createExaSearch({ + apiKey: "k", + fetch: (async (url, init) => { + requested = String(url) + expect(JSON.parse(String(init?.body))).toEqual({ urls: ["https://bun.sh"] }) + return jsonResponse({ + results: [{ url: "https://bun.sh", text: "# Bun docs", title: "Bun" }], + }) + }) as Options["fetch"], + }) + const markdown = await client.scrape("https://bun.sh") + expect(requested).toBe("https://api.exa.ai/contents") + expect(markdown).toBe("# Bun docs") + }) + + test("disconnect() closes MCP client", async () => { + let closed = 0 + const client = createExaSearch({ + apiKey: "k", + createMcpClient: () => + Promise.resolve({ + tools: async () => ({}), + close: async () => { + closed++ + }, + }), + }) + await client.connect() + await client.disconnect() + expect(closed).toBe(1) + expect(client.isConnected).toBe(false) + }) +}) + +describe("createFirecrawlSearch", () => { + test("provider and listProviders are correct", () => { + const client = createFirecrawlSearch({ apiKey: "k" }) + expect(client.provider).toBe("firecrawl") + expect(client.listProviders()).toEqual(["firecrawl"]) + }) + + test("connect() uses Bearer auth against v2 MCP", async () => { + const seen: unknown[] = [] + const client = createFirecrawlSearch({ + apiKey: "k", + createMcpClient: (config) => { + seen.push(config) + return Promise.resolve(fakeMcpClient({})) + }, + }) + await client.connect() + expect(seen[0]).toMatchObject({ + transport: { + type: "http", + url: "https://mcp.firecrawl.dev/v2/mcp", + headers: { Authorization: "Bearer k" }, + }, + }) + }) + + test("connect() throws when apiKey missing", async () => { + const client = createFirecrawlSearch({}) + await expect(client.connect()).rejects.toMatchObject({ code: "NOT_CONFIGURED" }) + }) + + test("search() maps data to SearchResult", async () => { + const client = createFirecrawlSearch({ + apiKey: "k", + fetch: (async (url, init) => { + expect(String(url)).toBe("https://api.firecrawl.dev/v1/search") + expect(JSON.parse(String(init?.body))).toEqual({ query: "bun", limit: 2 }) + return jsonResponse({ + data: [{ title: "Bun", url: "https://bun.sh", description: "fast JS" }], + }) + }) as Options["fetch"], + }) + const results = await client.search("bun", 2) + expect(results[0]).toEqual({ + title: "Bun", + url: "https://bun.sh", + snippet: "fast JS", + }) + }) + + test("search() throws SdkError on non-ok response", async () => { + const client = createFirecrawlSearch({ + apiKey: "k", + fetch: (async () => new Response("boom", { status: 400 })) as Options["fetch"], + }) + await expect(client.search("bun")).rejects.toBeInstanceOf(SdkError) + }) + + test("scrape() returns markdown", async () => { + const client = createFirecrawlSearch({ + apiKey: "k", + fetch: (async (url, init) => { + expect(String(url)).toBe("https://api.firecrawl.dev/v1/scrape") + expect(JSON.parse(String(init?.body))).toEqual({ + url: "https://bun.sh", + formats: ["markdown"], + }) + return jsonResponse({ data: { markdown: "# Bun" } }) + }) as Options["fetch"], + }) + const markdown = await client.scrape("https://bun.sh") + expect(markdown).toBe("# Bun") + }) +}) + +describe("createWebSearch dispatcher", () => { + test("dispatches to the right provider", async () => { + const { createWebSearch } = await import("./index") + const exa = createWebSearch({ provider: "exa", apiKey: "k" }) + expect(exa.provider).toBe("exa") + const firecrawl = createWebSearch({ provider: "firecrawl", apiKey: "k" }) + expect(firecrawl.provider).toBe("firecrawl") + const contextdev = createWebSearch({ provider: "contextdev", apiKey: "k" }) + expect(contextdev.provider).toBe("contextdev") + }) + + test("unknown provider throws SdkError", async () => { + const { createWebSearch } = await import("./index") + expect(() => + createWebSearch({ provider: "unknown" as never }), + ).toThrow(SdkError) + }) +}) diff --git a/packages/cortex-sdk/src/web-search/contextdev.test.ts b/packages/cortex-sdk/src/web-search/contextdev.test.ts new file mode 100644 index 0000000..ee05795 --- /dev/null +++ b/packages/cortex-sdk/src/web-search/contextdev.test.ts @@ -0,0 +1,203 @@ +import { describe, expect, test } from "bun:test" +import { SdkError } from "../core/errors" +import type { WebSearchConfig } from "./types" +import { createContextDevSearch } from "./contextdev" + +type Options = Omit + +function jsonResponse(data: unknown, status = 200): Response { + return new Response(JSON.stringify(data), { + status, + headers: { "Content-Type": "application/json" }, + }) +} + +function makeContextDev(overrides: Partial = {}): ReturnType { + return createContextDevSearch({ + apiKey: "cd-key", + fetch: (async () => jsonResponse({})) as Options["fetch"], + ...overrides, + }) +} + +describe("createContextDevSearch", () => { + test("provider and listProviders are correct", () => { + const client = makeContextDev() + expect(client.provider).toBe("contextdev") + expect(client.listProviders()).toEqual(["contextdev"]) + }) + + test("isConnected is always true (REST, no session)", () => { + const client = makeContextDev() + expect(client.isConnected).toBe(true) + }) + + test("connect()/disconnect() are no-ops", async () => { + const client = makeContextDev() + await client.connect() + expect(client.isConnected).toBe(true) + await client.disconnect() + expect(client.isConnected).toBe(true) + }) + + test("throws NOT_CONFIGURED when no apiKey", () => { + expect(() => createContextDevSearch({})).toThrow(SdkError) + }) + + test("search() maps SDK results to SearchResult", async () => { + const client = createContextDevSearch({ + apiKey: "cd-key", + fetch: (async (url, init) => { + expect(String(url)).toBe("https://api.context.dev/v1/web/search") + const body = JSON.parse(String(init?.body)) as { query: string; numResults: number } + expect(body).toEqual({ query: "bun", numResults: 2 }) + return jsonResponse({ + query: "bun", + results: [ + { title: "Bun", url: "https://bun.sh", description: "fast JS", relevance: "high" }, + ], + }) + }) as Options["fetch"], + }) + const results = await client.search("bun", 2) + expect(results[0]).toEqual({ + title: "Bun", + url: "https://bun.sh", + snippet: "fast JS", + }) + }) + + test("scrape() returns markdown from webScrapeMd", async () => { + const client = createContextDevSearch({ + apiKey: "cd-key", + fetch: (async (url) => { + expect(String(url)).toBe( + "https://api.context.dev/v1/web/scrape/markdown?url=https%3A%2F%2Fbun.sh", + ) + return jsonResponse({ contentLength: 7, markdown: "# Bun" }) + }) as Options["fetch"], + }) + const markdown = await client.scrape("https://bun.sh") + expect(markdown).toBe("# Bun") + }) + + test("crawl() concatenates page markdown", async () => { + const client = createContextDevSearch({ + apiKey: "cd-key", + fetch: (async () => + jsonResponse({ + metadata: { numUrls: 2, numSucceeded: 2 }, + results: [ + { markdown: "Page A", metadata: {} }, + { markdown: "Page B", metadata: {} }, + ], + })) as Options["fetch"], + }) + const markdown = await client.crawl("https://bun.sh") + expect(markdown).toBe("Page A\n\nPage B") + }) + + test("extract() returns structured data", async () => { + const client = createContextDevSearch({ + apiKey: "cd-key", + fetch: (async () => + jsonResponse({ + data: { title: "Bun" }, + metadata: { status: "success" }, + })) as Options["fetch"], + }) + const data = await client.extract("https://bun.sh", { + type: "object", + properties: { title: { type: "string" } }, + }) + expect(data).toEqual({ title: "Bun" }) + }) + + test("getBrand() maps brand fields", async () => { + const client = createContextDevSearch({ + apiKey: "cd-key", + fetch: (async () => + jsonResponse({ + brand: { + domain: "bun.sh", + description: "A fast JS runtime", + logos: [{ url: "https://bun.sh/logo.png" }], + }, + })) as Options["fetch"], + }) + const brand = await client.getBrand("bun.sh") + expect(brand).toEqual({ + domain: "bun.sh", + name: "bun.sh", + logo: "https://bun.sh/logo.png", + description: "A fast JS runtime", + }) + }) + + test("createMonitor() posts page target", async () => { + const client = createContextDevSearch({ + apiKey: "cd-key", + fetch: (async (url, init) => { + expect(String(url)).toBe("https://api.context.dev/v1/monitors") + const body = JSON.parse(String(init?.body)) as { name: string; target: { type: string; url: string } } + expect(body).toEqual({ name: "pricing", target: { type: "page", url: "https://acme.com/pricing" } }) + return jsonResponse({ id: "mon_1", created_at: "2024-01-01", change_detection: { type: "exact" } }) + }) as Options["fetch"], + }) + const monitor = await client.createMonitor({ + name: "pricing", + url: "https://acme.com/pricing", + }) + expect(monitor).toMatchObject({ id: "mon_1" }) + }) + + test("createMonitor() parses interval schedule", async () => { + const client = createContextDevSearch({ + apiKey: "cd-key", + fetch: (async (_url, init) => { + const body = JSON.parse(String(init?.body)) as { schedule: { type: string; frequency: number; unit: string } } + expect(body.schedule).toEqual({ type: "interval", frequency: 6, unit: "hours" }) + return jsonResponse({ id: "mon_1" }) + }) as Options["fetch"], + }) + await client.createMonitor({ + name: "pricing", + url: "https://acme.com/pricing", + schedule: "6h", + }) + }) + + test("createMonitor() throws on invalid schedule", async () => { + const client = makeContextDev() + await expect( + client.createMonitor({ name: "p", url: "https://a.com", schedule: "fortnightly" }), + ).rejects.toBeInstanceOf(SdkError) + }) + + test("listMonitors() returns data array", async () => { + const client = createContextDevSearch({ + apiKey: "cd-key", + fetch: (async () => + jsonResponse({ + data: [{ id: "mon_1", name: "pricing" }], + has_more: false, + })) as Options["fetch"], + }) + const monitors = await client.listMonitors() + expect(monitors).toHaveLength(1) + expect(monitors[0]).toMatchObject({ id: "mon_1" }) + }) + + test("getTools() exposes all contextdev tools", async () => { + const client = makeContextDev() + const tools = await client.getTools() + expect(Object.keys(tools).sort()).toEqual([ + "create_monitor", + "get_brand", + "web_crawl", + "web_extract", + "web_scrape", + "web_search", + ]) + }) +}) diff --git a/packages/cortex-sdk/src/web-search/contextdev.ts b/packages/cortex-sdk/src/web-search/contextdev.ts new file mode 100644 index 0000000..ff8b236 --- /dev/null +++ b/packages/cortex-sdk/src/web-search/contextdev.ts @@ -0,0 +1,176 @@ +import ContextDev from "context.dev" +import { tool, type Tool } from "ai" +import { z } from "zod" +import { SdkError } from "../core/errors" +import type { FetchLike } from "../core/types" +import type { + BrandInfo, + ContextDevWebSearchClient, + MonitorConfig, + SearchResult, + WebSearchConfig, + WebSearchProvider, +} from "./types" + +export type ContextDevSearchOptions = Omit + +export function createContextDevSearch( + options: ContextDevSearchOptions = {}, +): ContextDevWebSearchClient { + return new ContextDevSearchClient(options) +} + +class ContextDevSearchClient implements ContextDevWebSearchClient { + readonly provider = "contextdev" as const + + private readonly client: ContextDev + + constructor(options: ContextDevSearchOptions) { + const apiKey = + options.apiKey ?? + process.env.CONTEXTDEV_API_KEY ?? + process.env.CONTEXT_DEV_API_KEY + if (!apiKey) { + throw new SdkError( + "createContextDevSearch: CONTEXTDEV_API_KEY not configured", + { code: "NOT_CONFIGURED" }, + ) + } + const opts: ConstructorParameters[0] = { apiKey } + if (options.baseURL) opts.baseURL = options.baseURL + if (options.fetch) opts.fetch = options.fetch as FetchLike + this.client = new ContextDev(opts) + } + + get isConnected(): boolean { + return true + } + + listProviders(): WebSearchProvider[] { + return ["contextdev"] + } + + async connect(): Promise {} + + async disconnect(): Promise {} + + async search(query: string, limit = 5): Promise { + const res = await this.client.web.search({ query, numResults: limit }) + return res.results.map((r) => ({ + title: r.title, + url: r.url, + snippet: r.description, + })) + } + + async scrape(url: string): Promise { + const res = await this.client.web.webScrapeMd({ url }) + return res.markdown + } + + async crawl(url: string): Promise { + const res = await this.client.web.webCrawlMd({ url }) + return res.results + .map((r) => (r.markdown ?? "").trim()) + .filter(Boolean) + .join("\n\n") + } + + async extract(url: string, schema: Record): Promise { + const res = await this.client.web.extract({ url, schema }) + return res.data + } + + async getBrand(domain: string): Promise { + const res = await this.client.brand.retrieve({ type: "by_domain", domain }) + const brand = res.brand + return { + domain: brand?.domain ?? domain, + name: brand?.slogan ?? brand?.domain ?? domain, + logo: brand?.logos?.[0]?.url, + description: brand?.description, + } + } + + async listMonitors(): Promise { + const res = await this.client.monitors.list() + return res.data + } + + async createMonitor(config: MonitorConfig): Promise { + return this.client.monitors.create({ + name: config.name, + target: { type: "page", url: config.url }, + ...(config.schedule ? { schedule: parseSchedule(config.schedule) } : {}), + }) + } + + async getTools(): Promise> { + return { + web_search: tool({ + description: "Search the web with Context.dev and return ranked results.", + inputSchema: z.object({ + query: z.string(), + limit: z.number().int().positive().default(5), + }), + execute: async ({ query, limit }) => this.search(query, limit), + }), + web_scrape: tool({ + description: "Scrape a URL into GitHub-Flavored Markdown with Context.dev.", + inputSchema: z.object({ url: z.string().url() }), + execute: async ({ url }) => this.scrape(url), + }), + web_crawl: tool({ + description: "Crawl a site starting from a URL and return page Markdown with Context.dev.", + inputSchema: z.object({ url: z.string().url() }), + execute: async ({ url }) => this.crawl(url), + }), + web_extract: tool({ + description: + "Extract structured data from a URL against a JSON schema with Context.dev.", + inputSchema: z.object({ + url: z.string().url(), + schema: z.record(z.any()), + }), + execute: async ({ url, schema }) => this.extract(url, schema), + }), + get_brand: tool({ + description: "Fetch brand info (name, logo, description) for a domain with Context.dev.", + inputSchema: z.object({ domain: z.string() }), + execute: async ({ domain }) => this.getBrand(domain), + }), + create_monitor: tool({ + description: "Create a change-detection monitor for a page with Context.dev.", + inputSchema: z.object({ + name: z.string(), + url: z.string().url(), + schedule: z.string().optional(), + }), + execute: async (args) => this.createMonitor(args), + }), + } + } +} + +function parseSchedule(schedule: string): { + type: "interval" + frequency: number + unit: "minutes" | "hours" | "days" +} { + const match = /^(\d+)\s*(min|minute|minutes|h|hr|hour|hours|d|day|days)?$/.exec( + schedule.trim(), + ) + if (!match) { + throw new SdkError( + `createMonitor: unsupported schedule "${schedule}" (expected e.g. "30m", "6h", "2d")`, + { code: "INVALID_SCHEDULE" }, + ) + } + const frequency = Number(match[1]) + const unitRaw = (match[2] ?? "h").toLowerCase() + const unit = + unitRaw.startsWith("min") ? "minutes" + : unitRaw.startsWith("d") ? "days" + : "hours" + return { type: "interval", frequency, unit } +} diff --git a/packages/cortex-sdk/src/web-search/exa.ts b/packages/cortex-sdk/src/web-search/exa.ts new file mode 100644 index 0000000..1453c42 --- /dev/null +++ b/packages/cortex-sdk/src/web-search/exa.ts @@ -0,0 +1,144 @@ +import { createMCPClient } from "@ai-sdk/mcp" +import type { Tool } from "ai" +import { ConnectionError, SdkError } from "../core/errors" +import type { FetchLike } from "../core/types" +import type { AgentHandlerTransportConfig, McpClientLike } from "../agent-handler/types" +import type { + SearchResult, + WebSearchClient, + WebSearchConfig, + WebSearchProvider, +} from "./types" + +const EXA_MCP_URL = "https://mcp.exa.ai/mcp" +const EXA_API_URL = "https://api.exa.ai" + +export type ExaSearchOptions = Omit + +export function createExaSearch(options: ExaSearchOptions = {}): WebSearchClient { + return new ExaSearchClient(options) +} + +class ExaSearchClient implements WebSearchClient { + readonly provider = "exa" as const + + private readonly apiKey?: string + private readonly baseURL: string + private readonly fetchImpl: FetchLike + private readonly createMcpClient: ( + config: AgentHandlerTransportConfig, + ) => Promise + private mcpClient: McpClientLike | null = null + + constructor(options: ExaSearchOptions) { + this.apiKey = options.apiKey ?? process.env.EXA_API_KEY + this.baseURL = options.baseURL ?? EXA_API_URL + this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init)) + this.createMcpClient = + options.createMcpClient ?? + ((config) => createMCPClient(config as never) as unknown as Promise) + } + + get isConnected(): boolean { + return this.mcpClient !== null + } + + listProviders(): WebSearchProvider[] { + return ["exa"] + } + + async connect(): Promise { + if (this.mcpClient) return + if (!this.apiKey) { + throw new SdkError("createExaSearch: EXA_API_KEY not configured", { + code: "NOT_CONFIGURED", + }) + } + const config: AgentHandlerTransportConfig = { + transport: { + type: "http", + url: EXA_MCP_URL, + headers: { "x-api-key": this.apiKey }, + }, + } + try { + this.mcpClient = await this.createMcpClient(config) + } catch (error) { + throw new ConnectionError("createExaSearch: failed to connect to Exa MCP", { + code: "EXA_MCP_CONNECT_FAILED", + cause: error, + }) + } + } + + async disconnect(): Promise { + const client = this.mcpClient + this.mcpClient = null + if (client) { + await client.close() + } + } + + async getTools(): Promise> { + if (!this.mcpClient) { + await this.connect() + } + return await this.mcpClient!.tools() + } + + async search(query: string, limit = 5): Promise { + if (!this.apiKey) { + throw new SdkError("createExaSearch: EXA_API_KEY not configured", { + code: "NOT_CONFIGURED", + }) + } + const res = await this.fetchImpl(`${this.baseURL}/search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": this.apiKey, + }, + body: JSON.stringify({ query, numResults: limit }), + }) + if (!res.ok) { + throw new SdkError(`createExaSearch: search failed (${res.status})`, { + code: "EXA_SEARCH_FAILED", + }) + } + const data = (await res.json()) as { + results?: Array<{ title?: string; url: string; publishedDate?: string; text?: string }> + } + return (data.results ?? []).map((r) => ({ + title: r.title ?? r.url, + url: r.url, + snippet: r.text, + publishedDate: r.publishedDate, + })) + } + + async scrape(url: string): Promise { + if (!this.apiKey) { + throw new SdkError("createExaSearch: EXA_API_KEY not configured", { + code: "NOT_CONFIGURED", + }) + } + const res = await this.fetchImpl(`${this.baseURL}/contents`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": this.apiKey, + }, + body: JSON.stringify({ urls: [url] }), + }) + if (!res.ok) { + throw new SdkError(`createExaSearch: scrape failed (${res.status})`, { + code: "EXA_SCRAPE_FAILED", + }) + } + const data = (await res.json()) as { + results?: Array<{ url: string; text?: string; title?: string }> + } + const hit = (data.results ?? []).find((r) => r.url === url) ?? (data.results ?? [])[0] + return hit?.text ?? "" + } +} diff --git a/packages/cortex-sdk/src/web-search/firecrawl.ts b/packages/cortex-sdk/src/web-search/firecrawl.ts new file mode 100644 index 0000000..6e8e33f --- /dev/null +++ b/packages/cortex-sdk/src/web-search/firecrawl.ts @@ -0,0 +1,140 @@ +import { createMCPClient } from "@ai-sdk/mcp" +import type { Tool } from "ai" +import { ConnectionError, SdkError } from "../core/errors" +import type { FetchLike } from "../core/types" +import type { AgentHandlerTransportConfig, McpClientLike } from "../agent-handler/types" +import type { + SearchResult, + WebSearchClient, + WebSearchConfig, + WebSearchProvider, +} from "./types" + +const FIRECRAWL_MCP_URL = "https://mcp.firecrawl.dev/v2/mcp" +const FIRECRAWL_API_URL = "https://api.firecrawl.dev/v1" + +export type FirecrawlSearchOptions = Omit + +export function createFirecrawlSearch(options: FirecrawlSearchOptions = {}): WebSearchClient { + return new FirecrawlSearchClient(options) +} + +class FirecrawlSearchClient implements WebSearchClient { + readonly provider = "firecrawl" as const + + private readonly apiKey?: string + private readonly baseURL: string + private readonly fetchImpl: FetchLike + private readonly createMcpClient: ( + config: AgentHandlerTransportConfig, + ) => Promise + private mcpClient: McpClientLike | null = null + + constructor(options: FirecrawlSearchOptions) { + this.apiKey = options.apiKey ?? process.env.FIRECRAWL_API_KEY + this.baseURL = options.baseURL ?? FIRECRAWL_API_URL + this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init)) + this.createMcpClient = + options.createMcpClient ?? + ((config) => createMCPClient(config as never) as unknown as Promise) + } + + get isConnected(): boolean { + return this.mcpClient !== null + } + + listProviders(): WebSearchProvider[] { + return ["firecrawl"] + } + + async connect(): Promise { + if (this.mcpClient) return + if (!this.apiKey) { + throw new SdkError("createFirecrawlSearch: FIRECRAWL_API_KEY not configured", { + code: "NOT_CONFIGURED", + }) + } + const config: AgentHandlerTransportConfig = { + transport: { + type: "http", + url: FIRECRAWL_MCP_URL, + headers: { Authorization: `Bearer ${this.apiKey}` }, + }, + } + try { + this.mcpClient = await this.createMcpClient(config) + } catch (error) { + throw new ConnectionError( + "createFirecrawlSearch: failed to connect to Firecrawl MCP", + { code: "FIRECRAWL_MCP_CONNECT_FAILED", cause: error }, + ) + } + } + + async disconnect(): Promise { + const client = this.mcpClient + this.mcpClient = null + if (client) { + await client.close() + } + } + + async getTools(): Promise> { + if (!this.mcpClient) { + await this.connect() + } + return await this.mcpClient!.tools() + } + + async search(query: string, limit = 5): Promise { + if (!this.apiKey) { + throw new SdkError("createFirecrawlSearch: FIRECRAWL_API_KEY not configured", { + code: "NOT_CONFIGURED", + }) + } + const res = await this.fetchImpl(`${this.baseURL}/search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ query, limit }), + }) + if (!res.ok) { + throw new SdkError(`createFirecrawlSearch: search failed (${res.status})`, { + code: "FIRECRAWL_SEARCH_FAILED", + }) + } + const data = (await res.json()) as { + data?: Array<{ title?: string; url: string; description?: string }> + } + return (data.data ?? []).map((r) => ({ + title: r.title ?? r.url, + url: r.url, + snippet: r.description, + })) + } + + async scrape(url: string): Promise { + if (!this.apiKey) { + throw new SdkError("createFirecrawlSearch: FIRECRAWL_API_KEY not configured", { + code: "NOT_CONFIGURED", + }) + } + const res = await this.fetchImpl(`${this.baseURL}/scrape`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ url, formats: ["markdown"] }), + }) + if (!res.ok) { + throw new SdkError(`createFirecrawlSearch: scrape failed (${res.status})`, { + code: "FIRECRAWL_SCRAPE_FAILED", + }) + } + const data = (await res.json()) as { data?: { markdown?: string } } + return data.data?.markdown ?? "" + } +} diff --git a/packages/cortex-sdk/src/web-search/index.ts b/packages/cortex-sdk/src/web-search/index.ts new file mode 100644 index 0000000..790cecc --- /dev/null +++ b/packages/cortex-sdk/src/web-search/index.ts @@ -0,0 +1,35 @@ +import { SdkError } from "../core/errors" +import { createContextDevSearch } from "./contextdev" +import { createExaSearch } from "./exa" +import { createFirecrawlSearch } from "./firecrawl" +import type { WebSearchClient, WebSearchConfig, WebSearchProvider } from "./types" + +export type { + BrandInfo, + ContextDevWebSearchClient, + MonitorConfig, + SearchResult, + WebSearchClient, + WebSearchConfig, + WebSearchProvider, +} from "./types" +export { WEB_SEARCH_PROVIDERS } from "./types" +export { createContextDevSearch } from "./contextdev" +export { createExaSearch } from "./exa" +export { createFirecrawlSearch } from "./firecrawl" + +export function createWebSearch(options: WebSearchConfig): WebSearchClient { + switch (options.provider) { + case "exa": + return createExaSearch(options) + case "firecrawl": + return createFirecrawlSearch(options) + case "contextdev": + return createContextDevSearch(options) + default: + throw new SdkError( + `createWebSearch: unknown provider "${(options as WebSearchConfig).provider}"`, + { code: "UNKNOWN_PROVIDER" }, + ) + } +} diff --git a/packages/cortex-sdk/src/web-search/types.ts b/packages/cortex-sdk/src/web-search/types.ts new file mode 100644 index 0000000..3f23a04 --- /dev/null +++ b/packages/cortex-sdk/src/web-search/types.ts @@ -0,0 +1,58 @@ +import type { Tool } from "ai" +import type { FetchLike } from "../core/types" +import type { AgentHandlerTransportConfig, McpClientLike } from "../agent-handler/types" + +export type WebSearchProvider = "exa" | "firecrawl" | "contextdev" + +export interface SearchResult { + title: string + url: string + snippet?: string + publishedDate?: string +} + +export interface WebSearchConfig { + provider: WebSearchProvider + apiKey?: string + baseURL?: string + fetch?: FetchLike + createMcpClient?: (config: AgentHandlerTransportConfig) => Promise +} + +export interface WebSearchClient { + search(query: string, limit?: number): Promise + scrape(url: string): Promise + getTools(): Promise> + listProviders(): WebSearchProvider[] + connect(): Promise + disconnect(): Promise + readonly provider: WebSearchProvider + readonly isConnected: boolean +} + +export interface BrandInfo { + domain: string + name?: string + logo?: string + description?: string +} + +export interface MonitorConfig { + name: string + url: string + schedule?: string +} + +export interface ContextDevWebSearchClient extends WebSearchClient { + crawl(url: string): Promise + extract(url: string, schema: Record): Promise + getBrand(domain: string): Promise + listMonitors(): Promise + createMonitor(config: MonitorConfig): Promise +} + +export const WEB_SEARCH_PROVIDERS: readonly WebSearchProvider[] = [ + "exa", + "firecrawl", + "contextdev", +] diff --git a/packages/cortex-sdk/tsconfig.json b/packages/cortex-sdk/tsconfig.json new file mode 100644 index 0000000..183d812 --- /dev/null +++ b/packages/cortex-sdk/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "verbatimModuleSyntax": true, + "strict": true, + "skipLibCheck": true, + "noUncheckedIndexedAccess": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["bun"] + }, + "include": ["src/**/*.ts", "cli/src/**/*.ts", "tsup.config.ts", "tsup.cli.config.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/cortex-sdk/tsup.cli.config.ts b/packages/cortex-sdk/tsup.cli.config.ts new file mode 100644 index 0000000..8bf2f5b --- /dev/null +++ b/packages/cortex-sdk/tsup.cli.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from "tsup" + +export default defineConfig({ + entry: { + "cli/index": "cli/src/index.ts", + }, + format: ["esm"], + dts: false, + clean: false, + sourcemap: false, + treeshake: true, + outDir: "dist", + banner: { + js: "#!/usr/bin/env bun", + }, + external: [ + "@opentui/core", + "ai", + "@ai-sdk/openai-compatible", + "@ai-sdk/google", + "@ai-sdk/mcp", + "@ai-sdk/provider", + "vercel-minimax-ai-provider", + "context.dev", + "zod", + "@composio/core", + "@modelcontextprotocol/sdk", + ], +}) diff --git a/packages/cortex-sdk/tsup.config.ts b/packages/cortex-sdk/tsup.config.ts new file mode 100644 index 0000000..2eb96d6 --- /dev/null +++ b/packages/cortex-sdk/tsup.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from "tsup" + +export default defineConfig({ + entry: { + index: "src/index.ts", + "gateway/index": "src/gateway/index.ts", + "agent-handler/index": "src/agent-handler/index.ts", + "composio/index": "src/composio/index.ts", + "web-search/index": "src/web-search/index.ts", + "mcp/index": "src/mcp/index.ts", + "voice/index": "src/voice/index.ts", + }, + format: ["esm", "cjs"], + dts: true, + clean: true, + sourcemap: true, + treeshake: true, + outDir: "dist", + external: [ + "ai", + "@ai-sdk/openai-compatible", + "@ai-sdk/google", + "@ai-sdk/mcp", + "@ai-sdk/provider", + "vercel-minimax-ai-provider", + "context.dev", + "zod", + "@composio/core", + "@modelcontextprotocol/sdk", + ], +})