diff --git a/README.md b/README.md index 13a377e6..fd940e3e 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,32 @@ users of the wizard, no training delays or other ambiguity. ## Running locally +### Experimental OpenAI runner + +The wizard uses the Claude runner by default. To use the experimental OpenAI +Agents SDK path instead, set: + +```bash +export WIZARD_AGENT_PROVIDER=openai +export OPENAI_MODEL=gpt-5.4 +``` + +By default this uses the same PostHog login or personal API key flow as the +Claude runner and routes requests through the PostHog LLM gateway. You do not +need to set an OpenAI API key for that path. + +Optional direct OpenAI-compatible override: + +```bash +export WIZARD_OPENAI_API_KEY=your_openai_api_key +export OPENAI_BASE_URL=https://your-openai-compatible-endpoint +``` + +You can use `OPENAI_API_KEY` instead of `WIZARD_OPENAI_API_KEY`. If either +OpenAI API key variable is set, the wizard will use that credential directly +instead of the PostHog gateway. `WIZARD_OPENAI_API_KEY` overrides +`OPENAI_API_KEY` when both are set. + ### Quick test without linking ```bash diff --git a/package.json b/package.json index 996f11ba..b0c14c22 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@anthropic-ai/claude-agent-sdk": "0.2.73", "@inkjs/ui": "^2.0.0", "@langchain/core": "^0.3.40", + "@openai/agents": "0.8.0", "axios": "1.7.4", "fast-glob": "^3.3.3", "glob": "9.3.5", @@ -44,6 +45,7 @@ "lodash": "^4.17.21", "magicast": "^0.2.10", "nanostores": "^1.1.1", + "openai": "6.31.0", "opn": "^5.4.0", "posthog-node": "^5.24.17", "react": "^19.2.4", @@ -54,8 +56,8 @@ "xcode": "3.0.1", "xml-js": "^1.6.11", "yargs": "^16.2.0", - "zod": "^3.24.2", - "zod-to-json-schema": "^3.24.3" + "zod": "^4.3.5", + "zod-to-json-schema": "^3.25.1" }, "devDependencies": { "@babel/core": "^7.29.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d996f11..dfc32fee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,13 +10,16 @@ importers: dependencies: '@anthropic-ai/claude-agent-sdk': specifier: 0.2.73 - version: 0.2.73(zod@3.24.2) + version: 0.2.73(zod@4.3.5) '@inkjs/ui': specifier: ^2.0.0 version: 2.0.0(ink@6.8.0(@types/react@19.2.14)(react@19.2.4)) '@langchain/core': specifier: ^0.3.40 - version: 0.3.40(openai@6.7.0(ws@8.18.1)(zod@3.24.2)) + version: 0.3.40(openai@6.31.0(ws@8.18.1)(zod@4.3.5)) + '@openai/agents': + specifier: 0.8.0 + version: 0.8.0(@cfworker/json-schema@4.1.1)(ws@8.18.1)(zod@4.3.5) axios: specifier: 1.7.4 version: 1.7.4 @@ -44,6 +47,9 @@ importers: nanostores: specifier: ^1.1.1 version: 1.1.1 + openai: + specifier: 6.31.0 + version: 6.31.0(ws@8.18.1)(zod@4.3.5) opn: specifier: ^5.4.0 version: 5.5.0 @@ -75,11 +81,11 @@ importers: specifier: ^16.2.0 version: 16.2.0 zod: - specifier: ^3.24.2 - version: 3.24.2 + specifier: ^4.3.5 + version: 4.3.5 zod-to-json-schema: - specifier: ^3.24.3 - version: 3.24.3(zod@3.24.2) + specifier: ^3.25.1 + version: 3.25.1(zod@4.3.5) devDependencies: '@babel/core': specifier: ^7.29.0 @@ -1002,6 +1008,12 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1259,6 +1271,16 @@ packages: resolution: {integrity: sha512-RGhJOTzJv6H+3veBAnDlH2KXuZ68CXMEg6B6DPTzL3IGDyd+vLxXG4FIttzUwjdeQKjrrFBwlXpJDl7bkoApzQ==} engines: {node: '>=18'} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@mswjs/interceptors@0.39.2': resolution: {integrity: sha512-RuzCup9Ct91Y7V79xwCb146RaBRHZ7NBbrIUySumd1rpKqHL5OonaqrGIbug5hNwP/fRyxFMA6ISgw4FTtYFYg==} engines: {node: '>=18'} @@ -1284,6 +1306,29 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@openai/agents-core@0.8.0': + resolution: {integrity: sha512-HG2e8TjKihra14R4W32imV4rbt7z/ZOM82zW8Mwb8qREboGySuk13iUlnbrbrUgPRzw9+yBMVoLHVmjO6BJFcw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@openai/agents-openai@0.8.0': + resolution: {integrity: sha512-mDtCQDIya5ay2Pf869p1+CiwnH2VAr1Dqpz9Lbct3KCFggcYq4rZPKEi+Gsg09tGBrc5Z2u375xTmWb1/9khkg==} + peerDependencies: + zod: ^4.0.0 + + '@openai/agents-realtime@0.8.0': + resolution: {integrity: sha512-GbKmM1/GpfVkBI0QIXZqjnA8eDfPz2YzQIIpnY15DwlIrKoDhshmKYA7UfjiuuGSh/iM/lJOA/VZUcs4Z82skA==} + peerDependencies: + zod: ^4.0.0 + + '@openai/agents@0.8.0': + resolution: {integrity: sha512-/uu0eece5IozgfGOQ0Rq5T6DmDkOkpq/ThJGW26BkU0iSNgSngcL1JkO8/9VmUi2qs3SyKizv3ipQrvH2F8IzA==} + peerDependencies: + zod: ^4.0.0 + '@posthog/core@1.23.1': resolution: {integrity: sha512-GViD5mOv/mcbZcyzz3z9CS0R79JzxVaqEz4sP5Dsea178M/j3ZWe6gaHDZB9yuyGfcmIMQ/8K14yv+7QrK4sQQ==} @@ -1428,6 +1473,9 @@ packages: '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1508,6 +1556,10 @@ packages: resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} engines: {node: '>=10.0.0'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1522,9 +1574,20 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-escapes@3.2.0: resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} engines: {node: '>=4'} @@ -1670,6 +1733,10 @@ packages: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + bplist-creator@0.1.0: resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} @@ -1707,10 +1774,18 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1846,6 +1921,14 @@ packages: console-table-printer@2.12.1: resolution: {integrity: sha512-wKGOQRRvdnd89pCeH96e2Fn4wkbenSP6LMHfjfyNLMbGuHEFbMqQNuxXqd0oXG9caIOQ1FTvc5Uijp9/4jujnQ==} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1853,6 +1936,10 @@ packages: resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -1860,6 +1947,10 @@ packages: core-js-compat@3.48.0: resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1916,6 +2007,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -1944,6 +2039,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} @@ -1965,6 +2063,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -2000,6 +2102,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -2078,12 +2183,24 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2100,6 +2217,16 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + express-rate-limit@8.3.1: + resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -2117,6 +2244,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.19.0: resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==} @@ -2142,6 +2272,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -2170,6 +2304,14 @@ packages: resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} engines: {node: '>= 6'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2281,9 +2423,17 @@ packages: headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hono@4.12.9: + resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==} + engines: {node: '>=16.9.0'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2301,6 +2451,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2346,6 +2500,14 @@ packages: resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} engines: {node: '>=6.0.0'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -2401,6 +2563,9 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -2578,6 +2743,9 @@ packages: node-notifier: optional: true + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + js-tiktoken@1.0.19: resolution: {integrity: sha512-XC63YQeEcS47Y53gg950xiZ4IWmkfMe4p2V9OSaBt26q+p47WHn18izuXzSclCI73B7yGqtfRsT6jcZQI0y08g==} @@ -2606,6 +2774,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2703,6 +2877,14 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2718,10 +2900,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-fn@1.2.0: resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} engines: {node: '>=4'} @@ -2791,6 +2981,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -2812,6 +3006,18 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2831,8 +3037,8 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - openai@6.7.0: - resolution: {integrity: sha512-mgSQXa3O/UXTbA8qFzoa7aydbXBJR5dbLQXCRapAOtoNT+v69sLdKMZzgiakpqhclRnhPggPAXoniVGn2kMY2A==} + openai@6.31.0: + resolution: {integrity: sha512-iqEn3QY1NnVrDOynUWA2Fjs04gzZ0m7fbwR9xZVFmcN1cqQwslMQM2sjQBNkTVFK/rrSvV78SXiBgU+Pyqz1Aw==} hasBin: true peerDependencies: ws: ^8.18.0 @@ -2902,6 +3108,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + patch-console@2.0.0: resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2932,6 +3142,9 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2952,6 +3165,10 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -2981,6 +3198,10 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -2994,12 +3215,24 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -3042,6 +3275,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -3102,6 +3339,10 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -3131,6 +3372,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3139,6 +3391,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3308,6 +3576,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -3391,6 +3663,10 @@ packages: resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} engines: {node: '>=20'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript@5.7.3: resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} engines: {node: '>=14.17'} @@ -3419,6 +3695,10 @@ packages: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + update-browserslist-db@1.1.2: resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} hasBin: true @@ -3456,6 +3736,10 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -3558,14 +3842,17 @@ packages: yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} - zod-to-json-schema@3.24.3: - resolution: {integrity: sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: - zod: ^3.24.1 + zod: ^3.25 || ^4 zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} + snapshots: '@alcalzone/ansi-tokenize@0.2.5': @@ -3573,9 +3860,9 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 - '@anthropic-ai/claude-agent-sdk@0.2.73(zod@3.24.2)': + '@anthropic-ai/claude-agent-sdk@0.2.73(zod@4.3.5)': dependencies: - zod: 3.24.2 + zod: 4.3.5 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -4487,6 +4774,11 @@ snapshots: '@eslint/js@8.57.1': {} + '@hono/node-server@1.19.11(hono@4.12.9)': + dependencies: + hono: 4.12.9 + optional: true + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -4804,23 +5096,48 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@langchain/core@0.3.40(openai@6.7.0(ws@8.18.1)(zod@3.24.2))': + '@langchain/core@0.3.40(openai@6.31.0(ws@8.18.1)(zod@4.3.5))': dependencies: '@cfworker/json-schema': 4.1.1 ansi-styles: 5.2.0 camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.19 - langsmith: 0.3.11(openai@6.7.0(ws@8.18.1)(zod@3.24.2)) + langsmith: 0.3.11(openai@6.31.0(ws@8.18.1)(zod@4.3.5)) mustache: 4.2.0 p-queue: 6.6.2 p-retry: 4.6.2 uuid: 10.0.0 zod: 3.24.2 - zod-to-json-schema: 3.24.3(zod@3.24.2) + zod-to-json-schema: 3.25.1(zod@3.24.2) transitivePeerDependencies: - openai + '@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.5)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.9) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.1(express@5.2.1) + hono: 4.12.9 + jose: 6.2.2 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.5 + zod-to-json-schema: 3.25.1(zod@4.3.5) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - supports-color + optional: true + '@mswjs/interceptors@0.39.2': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -4851,6 +5168,57 @@ snapshots: '@open-draft/until@2.1.0': {} + '@openai/agents-core@0.8.0(@cfworker/json-schema@4.1.1)(ws@8.18.1)(zod@4.3.5)': + dependencies: + debug: 4.4.3 + openai: 6.31.0(ws@8.18.1)(zod@4.3.5) + optionalDependencies: + '@modelcontextprotocol/sdk': 1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.5) + zod: 4.3.5 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + + '@openai/agents-openai@0.8.0(@cfworker/json-schema@4.1.1)(ws@8.18.1)(zod@4.3.5)': + dependencies: + '@openai/agents-core': 0.8.0(@cfworker/json-schema@4.1.1)(ws@8.18.1)(zod@4.3.5) + debug: 4.4.3 + openai: 6.31.0(ws@8.18.1)(zod@4.3.5) + zod: 4.3.5 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + + '@openai/agents-realtime@0.8.0(@cfworker/json-schema@4.1.1)(zod@4.3.5)': + dependencies: + '@openai/agents-core': 0.8.0(@cfworker/json-schema@4.1.1)(ws@8.18.1)(zod@4.3.5) + '@types/ws': 8.18.1 + debug: 4.4.3 + ws: 8.18.1 + zod: 4.3.5 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + + '@openai/agents@0.8.0(@cfworker/json-schema@4.1.1)(ws@8.18.1)(zod@4.3.5)': + dependencies: + '@openai/agents-core': 0.8.0(@cfworker/json-schema@4.1.1)(ws@8.18.1)(zod@4.3.5) + '@openai/agents-openai': 0.8.0(@cfworker/json-schema@4.1.1)(ws@8.18.1)(zod@4.3.5) + '@openai/agents-realtime': 0.8.0(@cfworker/json-schema@4.1.1)(zod@4.3.5) + debug: 4.4.3 + openai: 6.31.0(ws@8.18.1)(zod@4.3.5) + zod: 4.3.5 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + - ws + '@posthog/core@1.23.1': dependencies: cross-spawn: 7.0.6 @@ -5028,6 +5396,10 @@ snapshots: '@types/uuid@10.0.0': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 18.19.76 + '@types/yargs-parser@21.0.3': {} '@types/yargs@16.0.9': @@ -5134,6 +5506,12 @@ snapshots: '@xmldom/xmldom@0.8.10': {} + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + optional: true + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -5144,6 +5522,11 @@ snapshots: acorn@8.14.0: {} + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + optional: true + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -5151,6 +5534,14 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + optional: true + ansi-escapes@3.2.0: {} ansi-escapes@4.3.2: @@ -5309,6 +5700,21 @@ snapshots: big-integer@1.6.52: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + optional: true + bplist-creator@0.1.0: dependencies: stream-buffers: 2.2.0 @@ -5355,11 +5761,20 @@ snapshots: buffer-from@1.1.2: {} + bytes@3.1.2: + optional: true + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + optional: true + callsites@3.1.0: {} camelcase@5.0.0: {} @@ -5471,16 +5886,31 @@ snapshots: dependencies: simple-wcswidth: 1.0.1 + content-disposition@1.0.1: + optional: true + + content-type@1.0.5: + optional: true + convert-source-map@2.0.0: {} convert-to-spaces@2.0.1: {} + cookie-signature@1.2.2: + optional: true + cookie@0.7.2: {} core-js-compat@3.48.0: dependencies: browserslist: 4.28.1 + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + optional: true + create-jest@29.7.0(@types/node@18.19.76)(ts-node@10.9.2(@types/node@18.19.76)(typescript@5.7.3)): dependencies: '@jest/types': 29.6.3 @@ -5524,6 +5954,9 @@ snapshots: delayed-stream@1.0.0: {} + depd@2.0.0: + optional: true + detect-newline@3.1.0: {} diff-sequences@29.6.3: {} @@ -5546,6 +5979,9 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + ee-first@1.1.1: + optional: true + ejs@3.1.10: dependencies: jake: 10.9.2 @@ -5560,6 +5996,9 @@ snapshots: emoji-regex@8.0.0: {} + encodeurl@2.0.0: + optional: true + environment@1.1.0: {} error-ex@1.3.2: @@ -5614,6 +6053,9 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: + optional: true + escape-string-regexp@1.0.5: {} escape-string-regexp@2.0.0: {} @@ -5712,10 +6154,21 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: + optional: true + eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} + eventsource-parser@3.0.6: + optional: true + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + optional: true + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -5750,6 +6203,46 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + express-rate-limit@8.3.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + optional: true + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + optional: true + external-editor@3.1.0: dependencies: chardet: 0.7.0 @@ -5770,6 +6263,9 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: + optional: true + fastq@1.19.0: dependencies: reusify: 1.0.4 @@ -5798,6 +6294,18 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + optional: true + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -5825,6 +6333,12 @@ snapshots: es-set-tostringtag: 2.1.0 mime-types: 2.1.35 + forwarded@0.2.0: + optional: true + + fresh@2.0.0: + optional: true + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -5929,8 +6443,20 @@ snapshots: headers-polyfill@4.0.3: {} + hono@4.12.9: + optional: true + html-escaper@2.0.2: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + optional: true + human-signals@2.1.0: {} human-signals@5.0.0: {} @@ -5941,6 +6467,11 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + optional: true + ignore@5.3.2: {} import-fresh@3.3.1: @@ -6014,6 +6545,12 @@ snapshots: strip-ansi: 5.2.0 through: 2.3.8 + ip-address@10.1.0: + optional: true + + ipaddr.js@1.9.1: + optional: true + is-arrayish@0.2.1: {} is-core-module@2.16.1: @@ -6050,6 +6587,9 @@ snapshots: is-path-inside@3.0.3: {} + is-promise@4.0.0: + optional: true + is-stream@2.0.1: {} is-stream@3.0.0: {} @@ -6417,6 +6957,9 @@ snapshots: - supports-color - ts-node + jose@6.2.2: + optional: true + js-tiktoken@1.0.19: dependencies: base64-js: 1.5.1 @@ -6440,6 +6983,12 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: + optional: true + + json-schema-typed@8.0.2: + optional: true + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} @@ -6452,7 +7001,7 @@ snapshots: kleur@3.0.3: {} - langsmith@0.3.11(openai@6.7.0(ws@8.18.1)(zod@3.24.2)): + langsmith@0.3.11(openai@6.31.0(ws@8.18.1)(zod@4.3.5)): dependencies: '@types/uuid': 10.0.0 chalk: 4.1.2 @@ -6462,7 +7011,7 @@ snapshots: semver: 7.7.1 uuid: 10.0.0 optionalDependencies: - openai: 6.7.0(ws@8.18.1)(zod@3.24.2) + openai: 6.31.0(ws@8.18.1)(zod@4.3.5) leven@3.1.0: {} @@ -6547,6 +7096,12 @@ snapshots: math-intrinsics@1.1.0: {} + media-typer@1.1.0: + optional: true + + merge-descriptors@2.0.0: + optional: true + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -6558,10 +7113,18 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: + optional: true + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + optional: true + mimic-fn@1.2.0: {} mimic-fn@2.1.0: {} @@ -6625,6 +7188,9 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: + optional: true + node-int64@0.4.0: {} node-releases@2.0.19: {} @@ -6641,6 +7207,17 @@ snapshots: dependencies: path-key: 4.0.0 + object-assign@4.1.1: + optional: true + + object-inspect@1.13.4: + optional: true + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + optional: true + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -6661,11 +7238,10 @@ snapshots: dependencies: mimic-function: 5.0.1 - openai@6.7.0(ws@8.18.1)(zod@3.24.2): + openai@6.31.0(ws@8.18.1)(zod@4.3.5): optionalDependencies: ws: 8.18.1 - zod: 3.24.2 - optional: true + zod: 4.3.5 opn@5.5.0: dependencies: @@ -6729,6 +7305,9 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parseurl@1.3.3: + optional: true + patch-console@2.0.0: {} path-exists@4.0.0: {} @@ -6748,6 +7327,9 @@ snapshots: path-to-regexp@6.3.0: {} + path-to-regexp@8.3.0: + optional: true + path-type@4.0.0: {} picocolors@1.1.1: {} @@ -6758,6 +7340,9 @@ snapshots: pirates@4.0.6: {} + pkce-challenge@5.0.1: + optional: true + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 @@ -6787,6 +7372,12 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + optional: true + proxy-from-env@1.1.0: {} psl@1.15.0: @@ -6797,10 +7388,26 @@ snapshots: pure-rand@6.1.0: {} + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + optional: true + querystringify@2.2.0: {} queue-microtask@1.2.3: {} + range-parser@1.2.1: + optional: true + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + optional: true + react-is@18.3.1: {} react-reconciler@0.33.0(react@19.2.4): @@ -6845,6 +7452,9 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: + optional: true + requires-port@1.0.0: {} resolve-cwd@3.0.0: @@ -6896,6 +7506,17 @@ snapshots: dependencies: glob: 7.2.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + optional: true + run-async@2.4.1: {} run-parallel@1.2.0: @@ -6916,12 +7537,74 @@ snapshots: semver@7.7.1: {} + 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 + transitivePeerDependencies: + - supports-color + optional: true + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + optional: true + + setprototypeof@1.2.0: + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + optional: true + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + optional: true + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + optional: true + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + optional: true + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -7071,6 +7754,9 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: + optional: true + tough-cookie@4.1.4: dependencies: psl: 1.15.0 @@ -7147,6 +7833,13 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + optional: true + typescript@5.7.3: {} undici-types@5.26.5: {} @@ -7164,6 +7857,9 @@ snapshots: universalify@0.2.0: {} + unpipe@1.0.0: + optional: true + update-browserslist-db@1.1.2(browserslist@4.24.4): dependencies: browserslist: 4.24.4 @@ -7199,6 +7895,9 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + vary@1.1.2: + optional: true + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -7289,8 +7988,14 @@ snapshots: yoga-layout@3.2.1: {} - zod-to-json-schema@3.24.3(zod@3.24.2): + zod-to-json-schema@3.25.1(zod@3.24.2): dependencies: zod: 3.24.2 + zod-to-json-schema@3.25.1(zod@4.3.5): + dependencies: + zod: 4.3.5 + zod@3.24.2: {} + + zod@4.3.5: {} diff --git a/src/lib/__tests__/agent-interface.test.ts b/src/lib/__tests__/agent-interface.test.ts index e1ba0d83..5f98e021 100644 --- a/src/lib/__tests__/agent-interface.test.ts +++ b/src/lib/__tests__/agent-interface.test.ts @@ -1,4 +1,9 @@ -import { runAgent, createStopHook } from '../agent-interface'; +import { + runAgent, + createStopHook, + getOpenAIRunnerConfig, + getOpenAIMaxTurns, +} from '../agent-interface'; import type { WizardOptions } from '../../utils/types'; import type { SpinnerHandle } from '../../ui'; import { @@ -282,6 +287,102 @@ describe('runAgent', () => { }); }); +describe('getOpenAIRunnerConfig', () => { + const envSnapshot = { ...process.env }; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...envSnapshot }; + delete process.env.WIZARD_OPENAI_API_KEY; + delete process.env.OPENAI_API_KEY; + delete process.env.OPENAI_BASE_URL; + delete process.env.OPENAI_MODEL; + delete process.env.WIZARD_OPENAI_MAX_TURNS; + delete process.env.OPENAI_MAX_TURNS; + }); + + afterAll(() => { + process.env = envSnapshot; + }); + + it('defaults to the PostHog gateway using the wizard access token', () => { + const result = getOpenAIRunnerConfig({ + posthogApiKey: 'phx_test_token', + posthogApiHost: 'https://eu.i.posthog.com', + }); + + expect(result).toEqual({ + apiKey: 'phx_test_token', + baseURL: 'https://gateway.eu.posthog.com/wizard', + model: 'gpt-5.4', + maxTurns: Number.MAX_SAFE_INTEGER, + authMode: 'posthog_gateway', + }); + }); + + it('uses direct OpenAI credentials when explicitly configured', () => { + process.env.WIZARD_OPENAI_API_KEY = 'sk-test'; + process.env.OPENAI_BASE_URL = 'https://example.com/v1'; + process.env.OPENAI_MODEL = 'gpt-5.4-mini'; + + const result = getOpenAIRunnerConfig({ + posthogApiKey: 'phx_test_token', + posthogApiHost: 'https://us.i.posthog.com', + }); + + expect(result).toEqual({ + apiKey: 'sk-test', + baseURL: 'https://example.com/v1', + model: 'gpt-5.4-mini', + maxTurns: Number.MAX_SAFE_INTEGER, + authMode: 'direct_openai', + }); + }); + + it('rejects OPENAI_BASE_URL without an explicit OpenAI API key override', () => { + process.env.OPENAI_BASE_URL = 'https://example.com/v1'; + + expect(() => + getOpenAIRunnerConfig({ + posthogApiKey: 'phx_test_token', + posthogApiHost: 'https://us.i.posthog.com', + }), + ).toThrow( + 'OPENAI_BASE_URL was provided without OPENAI_API_KEY or WIZARD_OPENAI_API_KEY.', + ); + }); + + it('uses an explicit OpenAI max turns override when configured', () => { + process.env.WIZARD_OPENAI_MAX_TURNS = '80'; + + expect(getOpenAIMaxTurns()).toBe(80); + expect( + getOpenAIRunnerConfig({ + posthogApiKey: 'phx_test_token', + posthogApiHost: 'https://eu.i.posthog.com', + }).maxTurns, + ).toBe(80); + }); + + it('falls back to the default max turns when the override is invalid', () => { + process.env.OPENAI_MAX_TURNS = 'not-a-number'; + + expect(getOpenAIMaxTurns()).toBe(Number.MAX_SAFE_INTEGER); + }); + + it('supports an unlimited max turns override', () => { + process.env.WIZARD_OPENAI_MAX_TURNS = 'unlimited'; + + expect(getOpenAIMaxTurns()).toBe(Number.MAX_SAFE_INTEGER); + }); + + it('treats 0 as unlimited max turns', () => { + process.env.OPENAI_MAX_TURNS = '0'; + + expect(getOpenAIMaxTurns()).toBe(Number.MAX_SAFE_INTEGER); + }); +}); + describe('createStopHook', () => { const hookInput = { stop_hook_active: false }; diff --git a/src/lib/__tests__/api.test.ts b/src/lib/__tests__/api.test.ts new file mode 100644 index 00000000..0ed432a5 --- /dev/null +++ b/src/lib/__tests__/api.test.ts @@ -0,0 +1,88 @@ +import { fetchProjectData, fetchUserData } from '../api'; +import { WIZARD_USER_AGENT } from '../constants'; + +jest.mock('axios', () => { + const get = jest.fn(); + const isAxiosError = jest.fn((error: unknown) => { + return ( + typeof error === 'object' && + error !== null && + 'isAxiosError' in error && + Boolean((error as { isAxiosError?: unknown }).isAxiosError) + ); + }); + + return { + __esModule: true, + default: { get }, + isAxiosError, + }; +}); + +jest.mock('../../utils/analytics', () => ({ + analytics: { + captureException: jest.fn(), + }, +})); + +const axiosMock = jest.requireMock('axios'); + +describe('api', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('fetchProjectData accepts project payloads with only the fields the wizard uses', async () => { + axiosMock.default.get.mockResolvedValue({ + data: { + id: 123, + api_token: 'phc_test_token', + organization: { id: 'not-a-uuid-anymore' }, + name: null, + }, + }); + + await expect( + fetchProjectData('oauth-token', 123, 'https://eu.posthog.com'), + ).resolves.toEqual({ + id: 123, + api_token: 'phc_test_token', + }); + + expect(axiosMock.default.get).toHaveBeenCalledWith( + 'https://eu.posthog.com/api/projects/123/', + { + headers: { + Authorization: 'Bearer oauth-token', + 'User-Agent': WIZARD_USER_AGENT, + }, + }, + ); + }); + + it('fetchUserData accepts user payloads without organization metadata', async () => { + axiosMock.default.get.mockResolvedValue({ + data: { + distinct_id: 'user_123', + team: { id: 456 }, + }, + }); + + await expect( + fetchUserData('oauth-token', 'https://us.posthog.com'), + ).resolves.toEqual({ + distinct_id: 'user_123', + team: { id: 456 }, + }); + + expect(axiosMock.default.get).toHaveBeenCalledWith( + 'https://us.posthog.com/api/users/@me/', + { + headers: { + Authorization: 'Bearer oauth-token', + 'User-Agent': WIZARD_USER_AGENT, + }, + }, + ); + }); +}); diff --git a/src/lib/__tests__/openai-function-tools.test.ts b/src/lib/__tests__/openai-function-tools.test.ts new file mode 100644 index 00000000..d8fb1193 --- /dev/null +++ b/src/lib/__tests__/openai-function-tools.test.ts @@ -0,0 +1,115 @@ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { createOpenAIFunctionTools } from '../openai-function-tools'; + +jest.mock('../../ui', () => ({ + getUI: jest.fn().mockReturnValue({ + syncTodos: jest.fn(), + }), +})); + +jest.mock('../wizard-tools', () => ({ + fetchSkillMenu: jest.fn().mockResolvedValue({ categories: {} }), + downloadSkill: jest.fn(), + resolveEnvPath: jest.requireActual('../wizard-tools').resolveEnvPath, + parseEnvKeys: jest.requireActual('../wizard-tools').parseEnvKeys, + mergeEnvValues: jest.requireActual('../wizard-tools').mergeEnvValues, + ensureGitignoreCoverage: + jest.requireActual('../wizard-tools').ensureGitignoreCoverage, +})); + +describe('createOpenAIFunctionTools', () => { + const defineTools = async (workingDirectory: string) => { + return createOpenAIFunctionTools({ + tool: (definition) => definition, + workingDirectory, + detectPackageManager: jest.fn(), + skillsBaseUrl: 'https://example.com/skills', + checkToolAccess: jest.fn(), + mcpMissingSignal: '[ERROR-MCP-MISSING]', + }) as Promise>>; + }; + + it('emits OpenAI-strict schemas for optional fields by requiring all keys and allowing nulls', async () => { + const tools = await defineTools(process.cwd()); + const readTool = tools.find((tool) => tool.name === 'Read'); + + expect(readTool?.parameters).toEqual({ + type: 'object', + properties: { + file_path: { + type: 'string', + description: 'Path to the file, relative to the project root.', + }, + offset: { + type: ['integer', 'null'], + description: 'Optional zero-based line offset.', + }, + limit: { + type: ['integer', 'null'], + description: 'Optional maximum number of lines to return.', + }, + }, + required: ['file_path', 'offset', 'limit'], + additionalProperties: false, + }); + }); + + it('uses a strict-compatible array shape for set_env_values instead of a dynamic object', async () => { + const tools = await defineTools(process.cwd()); + const setEnvValuesTool = tools.find( + (tool) => tool.name === 'set_env_values', + ); + + expect(setEnvValuesTool?.parameters).toEqual({ + type: 'object', + properties: { + filePath: { + type: 'string', + description: 'Path to the .env file, relative to the project root.', + }, + values: { + type: 'array', + items: { + type: 'object', + properties: { + key: { + type: 'string', + description: 'Environment variable key to set.', + }, + value: { + type: 'string', + description: 'Environment variable value to write.', + }, + }, + required: ['key', 'value'], + additionalProperties: false, + }, + description: 'Key-value pairs to set.', + }, + }, + required: ['filePath', 'values'], + additionalProperties: false, + }); + }); + + it('normalizes nullable optional inputs before executing the tool', async () => { + const tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'wizard-openai-tools-'), + ); + const filePath = path.join(tempDir, 'example.txt'); + fs.writeFileSync(filePath, 'line 1\nline 2\nline 3\n', 'utf8'); + + const tools = await defineTools(tempDir); + const readTool = tools.find((tool) => tool.name === 'Read'); + + const output = readTool?.execute({ + file_path: 'example.txt', + offset: null, + limit: null, + }); + + expect(output).toBe('line 1\nline 2\nline 3\n'); + }); +}); diff --git a/src/lib/__tests__/openai-sdk-compat.test.ts b/src/lib/__tests__/openai-sdk-compat.test.ts new file mode 100644 index 00000000..cf705173 --- /dev/null +++ b/src/lib/__tests__/openai-sdk-compat.test.ts @@ -0,0 +1,116 @@ +import { + applyOpenAIResponsesUsageCompatPatch, + sanitizeOpenAIResponseDoneEventUsage, +} from '../openai-sdk-compat'; + +describe('openai-sdk-compat', () => { + it('sanitizes null token detail values in response_done usage payloads', () => { + expect( + sanitizeOpenAIResponseDoneEventUsage({ + type: 'response_done', + response: { + id: 'resp_123', + usage: { + inputTokens: 10, + outputTokens: 5, + totalTokens: 15, + inputTokensDetails: { + audio_tokens: null, + text_tokens: null, + cached_tokens: 4, + }, + outputTokensDetails: { + text_tokens: null, + reasoning_tokens: 2, + }, + requestUsageEntries: [ + { + inputTokens: 10, + outputTokens: 5, + totalTokens: 15, + inputTokensDetails: { + audio_tokens: null, + text_tokens: 8, + }, + outputTokensDetails: { + text_tokens: null, + }, + }, + ], + }, + }, + }), + ).toEqual({ + type: 'response_done', + response: { + id: 'resp_123', + usage: { + inputTokens: 10, + outputTokens: 5, + totalTokens: 15, + inputTokensDetails: { + cached_tokens: 4, + }, + outputTokensDetails: { + reasoning_tokens: 2, + }, + requestUsageEntries: [ + { + inputTokens: 10, + outputTokens: 5, + totalTokens: 15, + inputTokensDetails: { + text_tokens: 8, + }, + outputTokensDetails: {}, + }, + ], + }, + }, + }); + }); + + it('patches the SDK response_done parser to sanitize usage details first', () => { + const parse = jest.fn((input) => input); + const sdkModule = { + protocol: { + StreamEventResponseCompleted: { + parse, + }, + }, + }; + + applyOpenAIResponsesUsageCompatPatch(sdkModule); + + sdkModule.protocol.StreamEventResponseCompleted.parse({ + type: 'response_done', + response: { + usage: { + inputTokens: 1, + outputTokens: 1, + totalTokens: 2, + inputTokensDetails: { + audio_tokens: null, + text_tokens: 1, + }, + }, + }, + }); + + expect(parse).toHaveBeenCalledWith({ + type: 'response_done', + response: { + usage: { + inputTokens: 1, + outputTokens: 1, + totalTokens: 2, + inputTokensDetails: { + text_tokens: 1, + }, + outputTokensDetails: undefined, + requestUsageEntries: undefined, + }, + }, + }); + }); +}); diff --git a/src/lib/agent-interface.ts b/src/lib/agent-interface.ts index 394a4e52..140e0d0b 100644 --- a/src/lib/agent-interface.ts +++ b/src/lib/agent-interface.ts @@ -1,6 +1,6 @@ /** * Shared agent interface for PostHog wizards - * Uses Claude Agent SDK directly with PostHog LLM gateway + * Uses Claude Agent SDK by default, with an experimental OpenAI Agents SDK path. */ import path from 'path'; @@ -29,6 +29,8 @@ import { createCustomHeaders } from '../utils/custom-headers'; import { getLlmGatewayUrlFromHost } from '../utils/urls'; import { LINTING_TOOLS } from './safe-tools'; import { createWizardToolsServer, WIZARD_TOOL_NAMES } from './wizard-tools'; +import { createOpenAIFunctionTools } from './openai-function-tools'; +import { applyOpenAIResponsesUsageCompatPatch } from './openai-sdk-compat'; import { createPreToolUseYaraHooks, createPostToolUseYaraHooks, @@ -45,6 +47,22 @@ async function getSDKModule(): Promise { return _sdkModule; } +let _openaiSdkModule: any = null; +async function getOpenAISDKModule(): Promise { + if (!_openaiSdkModule) { + _openaiSdkModule = await import('@openai/agents'); + } + return _openaiSdkModule; +} + +let _openaiClientModule: any = null; +async function getOpenAIClientModule(): Promise { + if (!_openaiClientModule) { + _openaiClientModule = await import('openai'); + } + return _openaiClientModule; +} + /** * Get the path to the bundled Claude Code CLI from the SDK package. * This ensures we use the SDK's bundled version rather than the user's installed Claude Code. @@ -60,6 +78,8 @@ function getClaudeCodeExecutablePath(): string { type SDKMessage = any; type McpServersConfig = any; +export type AgentProvider = 'claude' | 'openai'; + export const AgentSignals = { /** Signal emitted when the agent reports progress to the user */ STATUS: '[STATUS]', @@ -92,6 +112,77 @@ export enum AgentErrorType { YARA_VIOLATION = 'WIZARD_YARA_VIOLATION', } +export function getWizardAgentProvider(): AgentProvider { + const provider = process.env.WIZARD_AGENT_PROVIDER?.toLowerCase(); + return provider === 'openai' ? 'openai' : 'claude'; +} + +export function getOpenAIRunnerConfig(config: { + posthogApiKey: string; + posthogApiHost: string; +}): { + apiKey: string; + baseURL?: string; + model: string; + maxTurns: number; + authMode: 'posthog_gateway' | 'direct_openai'; +} { + const directApiKey = + process.env.WIZARD_OPENAI_API_KEY || process.env.OPENAI_API_KEY; + const directBaseURL = process.env.OPENAI_BASE_URL; + const maxTurns = getOpenAIMaxTurns(); + + if (directApiKey) { + return { + apiKey: directApiKey, + baseURL: directBaseURL, + model: process.env.OPENAI_MODEL || 'gpt-5.4', + maxTurns, + authMode: 'direct_openai', + }; + } + + if (directBaseURL) { + throw new Error( + 'OPENAI_BASE_URL was provided without OPENAI_API_KEY or WIZARD_OPENAI_API_KEY. Set both to bypass the PostHog gateway, or unset OPENAI_BASE_URL to use the default PostHog-authenticated OpenAI path.', + ); + } + + return { + apiKey: config.posthogApiKey, + baseURL: getLlmGatewayUrlFromHost(config.posthogApiHost), + model: process.env.OPENAI_MODEL || 'gpt-5.4', + maxTurns, + authMode: 'posthog_gateway', + }; +} + +export function getOpenAIMaxTurns(): number { + const rawValue = + process.env.WIZARD_OPENAI_MAX_TURNS || process.env.OPENAI_MAX_TURNS; + if (!rawValue) { + return Number.MAX_SAFE_INTEGER; + } + + const normalized = rawValue.trim().toLowerCase(); + if ( + normalized === '0' || + normalized === 'none' || + normalized === 'unlimited' || + normalized === 'infinite' || + normalized === 'infinity' + ) { + return Number.MAX_SAFE_INTEGER; + } + + const parsed = Number(rawValue); + if (!Number.isInteger(parsed) || parsed < 1) { + return Number.MAX_SAFE_INTEGER; + } + + return parsed; +} + const BLOCKING_ENV_KEYS = [ 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', @@ -345,9 +436,13 @@ export function createStopHook( * Internal configuration object returned by initializeAgent */ type AgentRunConfig = { + provider?: AgentProvider; workingDirectory: string; - mcpServers: McpServersConfig; model: string; + maxTurns?: number; + mcpServers: McpServersConfig; + agent?: any; + runner?: any; wizardFlags?: Record; wizardMetadata?: Record; }; @@ -365,13 +460,10 @@ export function buildWizardMetadata( return { ...variant }; } -/** - * Build env for the SDK subprocess: process.env plus ANTHROPIC_CUSTOM_HEADERS from wizard metadata/flags. - */ -function buildAgentEnv( +function buildWizardHeaders( wizardMetadata: Record, wizardFlags: Record, -): string { +): ReturnType { const headers = createCustomHeaders(); for (const [key, value] of Object.entries(wizardMetadata)) { headers.add( @@ -385,11 +477,59 @@ function buildAgentEnv( if (!flagKey.toLowerCase().startsWith('wizard')) continue; headers.addFlag(flagKey, variant); } + return headers; +} + +/** + * Build env for the SDK subprocess: process.env plus ANTHROPIC_CUSTOM_HEADERS from wizard metadata/flags. + */ +function buildAgentEnv( + wizardMetadata: Record, + wizardFlags: Record, +): string { + const headers = buildWizardHeaders(wizardMetadata, wizardFlags); const encoded = headers.encode(); logToFile('ANTHROPIC_CUSTOM_HEADERS', encoded); return encoded; } +function buildOpenAIDefaultHeaders( + wizardMetadata: Record, + wizardFlags: Record, +): Record { + const headers = buildWizardHeaders(wizardMetadata, wizardFlags); + const objectHeaders = headers.toObject(); + logToFile('OPENAI_DEFAULT_HEADERS', JSON.stringify(objectHeaders, null, 2)); + return objectHeaders; +} + +function buildOpenAIInstructions(): string { + return [ + 'You are the PostHog Wizard coding agent.', + 'Use the provided file tools for reading, searching, and editing the local project.', + 'Prefer Read, Grep, and Glob over Bash for code inspection.', + `Report notable phase changes using lines that begin exactly with ${AgentSignals.STATUS}.`, + getWizardCommandments(), + ].join('\n'); +} + +function captureWizardRemark(outputText: string): void { + const remarkRegex = new RegExp( + `${AgentSignals.WIZARD_REMARK.replace( + /[.*+?^${}()|[\]\\]/g, + '\\$&', + )}\\s*(.+?)(?:\\n|$)`, + 's', + ); + const remarkMatch = outputText.match(remarkRegex); + if (remarkMatch && remarkMatch[1]) { + const remark = remarkMatch[1].trim(); + if (remark) { + analytics.capture(WIZARD_REMARK_EVENT_NAME, { remark }); + } + } +} + /** * Package managers that can be used to run commands. */ @@ -595,80 +735,209 @@ export function wizardCanUseTool( }; } -/** - * Initialize agent configuration for the LLM gateway - */ -export async function initializeAgent( +async function initializeClaudeAgent( config: AgentConfig, options: WizardOptions, ): Promise { - // Initialize log file for this run - initLogFile(); - logToFile('Agent initialization starting'); - logToFile('Install directory:', options.installDir); - getUI().log.step('Initializing Claude agent...'); - try { - // Configure LLM gateway environment variables (inherited by SDK subprocess) - const gatewayUrl = getLlmGatewayUrlFromHost(config.posthogApiHost); - process.env.ANTHROPIC_BASE_URL = gatewayUrl; - process.env.ANTHROPIC_AUTH_TOKEN = config.posthogApiKey; - // Use CLAUDE_CODE_OAUTH_TOKEN to override any stored /login credentials - process.env.CLAUDE_CODE_OAUTH_TOKEN = config.posthogApiKey; - // Disable experimental betas (like input_examples) that the LLM gateway doesn't support - process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = 'true'; - - logToFile('Configured LLM gateway:', gatewayUrl); - - // Configure MCP server with PostHog authentication - const mcpServers: McpServersConfig = { - 'posthog-wizard': { - type: 'http', - url: config.posthogMcpUrl, + const gatewayUrl = getLlmGatewayUrlFromHost(config.posthogApiHost); + process.env.ANTHROPIC_BASE_URL = gatewayUrl; + process.env.ANTHROPIC_AUTH_TOKEN = config.posthogApiKey; + process.env.CLAUDE_CODE_OAUTH_TOKEN = config.posthogApiKey; + process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = 'true'; + + logToFile('Configured LLM gateway:', gatewayUrl); + + const mcpServers: McpServersConfig = { + 'posthog-wizard': { + type: 'http', + url: config.posthogMcpUrl, + headers: { + Authorization: `Bearer ${config.posthogApiKey}`, + 'User-Agent': WIZARD_USER_AGENT, + }, + }, + ...Object.fromEntries( + Object.entries(config.additionalMcpServers ?? {}).map( + ([name, { url }]) => [name, { type: 'http', url }], + ), + ), + }; + + const wizardToolsServer = await createWizardToolsServer({ + workingDirectory: config.workingDirectory, + detectPackageManager: config.detectPackageManager, + skillsBaseUrl: config.skillsBaseUrl, + }); + mcpServers['wizard-tools'] = wizardToolsServer; + + const agentRunConfig: AgentRunConfig = { + provider: 'claude', + workingDirectory: config.workingDirectory, + mcpServers, + model: 'anthropic/claude-sonnet-4-6', + wizardFlags: config.wizardFlags, + wizardMetadata: config.wizardMetadata, + }; + + logToFile('Claude agent config:', { + workingDirectory: agentRunConfig.workingDirectory, + posthogMcpUrl: config.posthogMcpUrl, + gatewayUrl, + apiKeyPresent: !!config.posthogApiKey, + }); + + if (options.debug) { + debug('Claude agent config:', { + workingDirectory: agentRunConfig.workingDirectory, + posthogMcpUrl: config.posthogMcpUrl, + gatewayUrl, + apiKeyPresent: !!config.posthogApiKey, + }); + } + + return agentRunConfig; +} + +async function initializeOpenAIAgent( + config: AgentConfig, + options: WizardOptions, +): Promise { + const openAISDK = await getOpenAISDKModule(); + applyOpenAIResponsesUsageCompatPatch(openAISDK); + const { Agent, Runner, MCPServerStreamableHttp, OpenAIProvider, tool } = + openAISDK; + const { default: OpenAI } = await getOpenAIClientModule(); + const openaiConfig = getOpenAIRunnerConfig(config); + + getUI().log.step(`Initializing OpenAI agent (${openaiConfig.model})...`); + + const mcpServers = [ + new MCPServerStreamableHttp({ + name: 'posthog-wizard', + url: config.posthogMcpUrl, + requestInit: { headers: { Authorization: `Bearer ${config.posthogApiKey}`, 'User-Agent': WIZARD_USER_AGENT, }, }, - ...Object.fromEntries( - Object.entries(config.additionalMcpServers ?? {}).map( - ([name, { url }]) => [name, { type: 'http', url }], - ), - ), - }; + }), + ...Object.entries(config.additionalMcpServers ?? {}).map( + ([name, { url }]) => + new MCPServerStreamableHttp({ + name, + url, + }), + ), + ]; - // Add in-process wizard tools (env files, package manager detection, skill loading) - const wizardToolsServer = await createWizardToolsServer({ - workingDirectory: config.workingDirectory, - detectPackageManager: config.detectPackageManager, - skillsBaseUrl: config.skillsBaseUrl, - }); - mcpServers['wizard-tools'] = wizardToolsServer; - - const agentRunConfig: AgentRunConfig = { - workingDirectory: config.workingDirectory, - mcpServers, - model: 'anthropic/claude-sonnet-4-6', - wizardFlags: config.wizardFlags, - wizardMetadata: config.wizardMetadata, - }; + await Promise.all(mcpServers.map((server: any) => server.connect())); + + const tools = await createOpenAIFunctionTools({ + tool, + workingDirectory: config.workingDirectory, + detectPackageManager: config.detectPackageManager, + skillsBaseUrl: config.skillsBaseUrl, + checkToolAccess: (toolName, input) => { + const result = wizardCanUseTool(toolName, input); + if (result.behavior === 'deny') { + throw new Error(result.message); + } + }, + mcpMissingSignal: AgentSignals.ERROR_MCP_MISSING, + }); + + const openaiClient = new OpenAI({ + apiKey: openaiConfig.apiKey, + baseURL: openaiConfig.baseURL, + defaultHeaders: + openaiConfig.authMode === 'posthog_gateway' + ? buildOpenAIDefaultHeaders( + config.wizardMetadata ?? {}, + config.wizardFlags ?? {}, + ) + : undefined, + }); + + const provider = new OpenAIProvider({ + openAIClient: openaiClient, + useResponses: true, + }); + + const agent = new Agent({ + name: 'PostHog Wizard', + instructions: buildOpenAIInstructions(), + model: openaiConfig.model, + modelSettings: { + parallelToolCalls: false, + reasoning: { effort: 'low' }, + text: { verbosity: 'low' }, + }, + tools, + mcpServers, + }); - logToFile('Agent config:', { + const runner = new Runner({ + modelProvider: provider, + tracingDisabled: true, + workflowName: 'posthog-wizard', + }); + + const agentRunConfig: AgentRunConfig = { + provider: 'openai', + workingDirectory: config.workingDirectory, + mcpServers, + agent, + runner, + model: openaiConfig.model, + maxTurns: openaiConfig.maxTurns, + wizardFlags: config.wizardFlags, + wizardMetadata: config.wizardMetadata, + }; + + logToFile('OpenAI agent config:', { + workingDirectory: agentRunConfig.workingDirectory, + posthogMcpUrl: config.posthogMcpUrl, + model: openaiConfig.model, + maxTurns: openaiConfig.maxTurns, + apiKeyPresent: !!openaiConfig.apiKey, + baseURL: openaiConfig.baseURL, + authMode: openaiConfig.authMode, + }); + + if (options.debug) { + debug('OpenAI agent config:', { workingDirectory: agentRunConfig.workingDirectory, posthogMcpUrl: config.posthogMcpUrl, - gatewayUrl, - apiKeyPresent: !!config.posthogApiKey, + model: openaiConfig.model, + maxTurns: openaiConfig.maxTurns, + apiKeyPresent: !!openaiConfig.apiKey, + baseURL: openaiConfig.baseURL, + authMode: openaiConfig.authMode, }); + } - if (options.debug) { - debug('Agent config:', { - workingDirectory: agentRunConfig.workingDirectory, - posthogMcpUrl: config.posthogMcpUrl, - gatewayUrl, - apiKeyPresent: !!config.posthogApiKey, - }); - } + return agentRunConfig; +} + +/** + * Initialize agent configuration for the selected provider. + */ +export async function initializeAgent( + config: AgentConfig, + options: WizardOptions, +): Promise { + initLogFile(); + logToFile('Agent initialization starting'); + logToFile('Install directory:', options.installDir); + + try { + const agentRunConfig = + getWizardAgentProvider() === 'openai' + ? await initializeOpenAIAgent(config, options) + : await initializeClaudeAgent(config, options); getUI().log.step(`Verbose logs: ${getLogFilePath()}`); getUI().log.success("Agent initialized. Let's get cooking!"); @@ -693,6 +962,7 @@ function checkYaraViolation( ): { error: AgentErrorType } | null { if ( outputText.includes('[YARA CRITICAL]') || + outputText.includes('[YARA VIOLATION]') || outputText.includes('[YARA] Scanner error') ) { logToFile('Agent error: YARA_VIOLATION'); @@ -708,6 +978,236 @@ function checkYaraViolation( * * @returns An object containing any error detected in the agent's output */ +function handleOpenAIStreamEvent( + event: any, + options: WizardOptions, + spinner: SpinnerHandle, + collectedText: string[], +): void { + logToFile( + `OpenAI Stream Event: ${event.type}`, + JSON.stringify(event, null, 2), + ); + + if (options.debug) { + debug(`OpenAI stream event type: ${event.type}`); + } + + if (event.type !== 'run_item_stream_event') { + return; + } + + if (event.name !== 'message_output_created') { + return; + } + + const rawItem = event.item?.rawItem; + const content = rawItem?.content; + if (!Array.isArray(content)) { + return; + } + + for (const block of content) { + if (block.type !== 'output_text' || typeof block.text !== 'string') { + continue; + } + + collectedText.push(block.text); + + const statusRegex = new RegExp( + `^.*${AgentSignals.STATUS.replace( + /[.*+?^${}()|[\]\\]/g, + '\\$&', + )}\\s*(.+?)$`, + 'm', + ); + const statusMatch = block.text.match(statusRegex); + if (statusMatch) { + const statusText = statusMatch[1].trim(); + getUI().pushStatus(statusText); + spinner.message(statusText); + } + } +} + +async function runOpenAIAgent( + agentConfig: AgentRunConfig, + prompt: string, + options: WizardOptions, + spinner: SpinnerHandle, + config?: { + estimatedDurationMinutes?: number; + spinnerMessage?: string; + successMessage?: string; + errorMessage?: string; + additionalFeatureQueue?: readonly AdditionalFeature[]; + }, + middleware?: { + onMessage(message: any): void; + finalize(resultMessage: any, totalDurationMs: number): any; + }, +): Promise<{ error?: AgentErrorType; message?: string }> { + const { + spinnerMessage = 'Customizing your PostHog setup...', + successMessage = 'PostHog integration complete', + errorMessage = 'Integration failed', + } = config ?? {}; + + spinner.start(spinnerMessage); + logToFile('Starting OpenAI agent run'); + logToFile('Prompt:', prompt); + + const startTime = Date.now(); + const collectedText: string[] = []; + let streamResult: any = null; + + let eventPlanWatcher: fs.FSWatcher | undefined; + let eventPlanInterval: ReturnType | undefined; + + const eventPlanPath = path.join( + agentConfig.workingDirectory, + '.posthog-events.json', + ); + const readEventPlan = () => { + try { + const content = fs.readFileSync(eventPlanPath, 'utf-8'); + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + getUI().setEventPlan( + parsed.map((e: Record) => ({ + name: (e.name ?? e.event ?? '') as string, + description: (e.description ?? '') as string, + })), + ); + } + } catch { + // File doesn't exist or isn't valid JSON yet. + } + }; + + try { + try { + eventPlanWatcher = fs.watch(eventPlanPath, () => readEventPlan()); + readEventPlan(); + } catch { + eventPlanInterval = setInterval(() => { + try { + fs.accessSync(eventPlanPath); + readEventPlan(); + clearInterval(eventPlanInterval); + eventPlanInterval = undefined; + eventPlanWatcher = fs.watch(eventPlanPath, () => readEventPlan()); + } catch { + // Still waiting for the plan file. + } + }, 1000); + } + + const stream = await agentConfig.runner.run(agentConfig.agent, prompt, { + stream: true, + maxTurns: agentConfig.maxTurns, + }); + + for await (const event of stream) { + handleOpenAIStreamEvent(event, options, spinner, collectedText); + try { + middleware?.onMessage(event); + } catch (e) { + logToFile(`${AgentSignals.BENCHMARK} Middleware onMessage error:`, e); + } + } + + await stream.completed; + if (stream.error) { + throw stream.error; + } + + streamResult = stream; + + if ( + typeof stream.finalOutput === 'string' && + !collectedText.includes(stream.finalOutput) + ) { + collectedText.push(stream.finalOutput); + } + + const outputText = collectedText.join('\n'); + const yaraResult = checkYaraViolation(outputText, spinner); + if (yaraResult) return yaraResult; + + if (outputText.includes(AgentSignals.ERROR_MCP_MISSING)) { + logToFile('Agent error: MCP_MISSING'); + spinner.stop('Agent could not access PostHog MCP'); + return { error: AgentErrorType.MCP_MISSING }; + } + + if (outputText.includes(AgentSignals.ERROR_RESOURCE_MISSING)) { + logToFile('Agent error: RESOURCE_MISSING'); + spinner.stop('Agent could not access setup resource'); + return { error: AgentErrorType.RESOURCE_MISSING }; + } + + captureWizardRemark(outputText); + + const durationMs = Date.now() - startTime; + analytics.wizardCapture('agent completed', { + duration_ms: durationMs, + duration_seconds: Math.round(durationMs / 1000), + }); + try { + middleware?.finalize(streamResult, durationMs); + } catch (e) { + logToFile(`${AgentSignals.BENCHMARK} Middleware finalize error:`, e); + } + spinner.stop(successMessage); + return {}; + } catch (error) { + const outputText = collectedText.join('\n'); + const yaraResult = checkYaraViolation(outputText, spinner); + if (yaraResult) return yaraResult; + + const message = (error as Error)?.message || String(error); + if (message.includes(AgentSignals.ERROR_MCP_MISSING)) { + spinner.stop('Agent could not access PostHog MCP'); + return { error: AgentErrorType.MCP_MISSING }; + } + if (message.includes(AgentSignals.ERROR_RESOURCE_MISSING)) { + spinner.stop('Agent could not access setup resource'); + return { error: AgentErrorType.RESOURCE_MISSING }; + } + if (message.includes('429')) { + spinner.stop('Rate limit exceeded'); + return { error: AgentErrorType.RATE_LIMIT, message }; + } + + spinner.stop(errorMessage); + logToFile('OpenAI agent run failed:', error); + getUI().log.error(`Error: ${message}`); + return { error: AgentErrorType.API_ERROR, message }; + } finally { + eventPlanWatcher?.close(); + if (eventPlanInterval) clearInterval(eventPlanInterval); + + await Promise.all( + (agentConfig.mcpServers ?? []).map(async (server: any) => { + if (typeof server?.close === 'function') { + try { + await server.close(); + } catch (error) { + logToFile('Failed to close OpenAI MCP server:', error); + } + } + }), + ); + + try { + await agentConfig.runner?.config?.modelProvider?.close?.(); + } catch (error) { + logToFile('Failed to close OpenAI model provider:', error); + } + } +} + export async function runAgent( agentConfig: AgentRunConfig, prompt: string, @@ -724,6 +1224,44 @@ export async function runAgent( onMessage(message: any): void; finalize(resultMessage: any, totalDurationMs: number): any; }, +): Promise<{ error?: AgentErrorType; message?: string }> { + if (agentConfig.provider === 'openai') { + return runOpenAIAgent( + agentConfig, + prompt, + options, + spinner, + config, + middleware, + ); + } + + return runClaudeAgent( + agentConfig, + prompt, + options, + spinner, + config, + middleware, + ); +} + +async function runClaudeAgent( + agentConfig: AgentRunConfig, + prompt: string, + options: WizardOptions, + spinner: SpinnerHandle, + config?: { + estimatedDurationMinutes?: number; + spinnerMessage?: string; + successMessage?: string; + errorMessage?: string; + additionalFeatureQueue?: readonly AdditionalFeature[]; + }, + middleware?: { + onMessage(message: any): void; + finalize(resultMessage: any, totalDurationMs: number): any; + }, ): Promise<{ error?: AgentErrorType; message?: string }> { const { spinnerMessage = 'Customizing your PostHog setup...', @@ -782,22 +1320,8 @@ export async function runAgent( logToFile(`Agent run completed in ${durationSeconds}s`); } - // Extract and capture the agent's reflection on the run const outputText = collectedText.join('\n'); - const remarkRegex = new RegExp( - `${AgentSignals.WIZARD_REMARK.replace( - /[.*+?^${}()|[\]\\]/g, - '\\$&', - )}\\s*(.+?)(?:\\n|$)`, - 's', - ); - const remarkMatch = outputText.match(remarkRegex); - if (remarkMatch && remarkMatch[1]) { - const remark = remarkMatch[1].trim(); - if (remark) { - analytics.capture(WIZARD_REMARK_EVENT_NAME, { remark }); - } - } + captureWizardRemark(outputText); analytics.wizardCapture('agent completed', { duration_ms: durationMs, diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts index d8686ae7..a5847f10 100644 --- a/src/lib/agent-runner.ts +++ b/src/lib/agent-runner.ts @@ -19,6 +19,7 @@ import { runAgent, AgentSignals, AgentErrorType, + getWizardAgentProvider, buildWizardMetadata, checkAllSettingsConflicts, backupAndFixClaudeSettings, @@ -29,6 +30,7 @@ import { getCloudUrlFromRegion } from '../utils/urls'; import * as semver from 'semver'; import { evaluateWizardReadiness, + getReadinessConfigForProvider, WizardReadiness, } from './health-checks/readiness'; import { enableDebugLogs, initLogFile, logToFile } from '../utils/debug'; @@ -106,11 +108,16 @@ export async function runAgentWizard( const skillsBaseUrl = session.localMcp ? 'http://localhost:8765' : 'https://github.com/PostHog/context-mill/releases/latest/download'; + const agentProvider = getWizardAgentProvider(); // Check all external service health (skip if TUI already ran it in bin.ts) if (!session.readinessResult) { logToFile('[agent-runner] evaluating wizard readiness'); - const readiness = await evaluateWizardReadiness(); + const readiness = await evaluateWizardReadiness( + getReadinessConfigForProvider(agentProvider, { + openaiMode: session.openaiAuthMode, + }), + ); logToFile(`[agent-runner] readiness=${readiness.decision}`); if (readiness.decision === WizardReadiness.No) { await getUI().showBlockingOutage(readiness); @@ -120,7 +127,10 @@ export async function runAgentWizard( } // Check ALL settings sources for blocking overrides before login. - const settingsConflicts = checkAllSettingsConflicts(session.installDir); + const settingsConflicts = + agentProvider === 'claude' + ? checkAllSettingsConflicts(session.installDir) + : []; logToFile( `[agent-runner] settings conflicts: ${ settingsConflicts.length > 0 @@ -237,8 +247,10 @@ export async function runAgentWizard( ? 'https://mcp-eu.posthog.com/mcp' : 'https://mcp.posthog.com/mcp'); - const restoreSettings = () => restoreClaudeSettings(session.installDir); - getUI().onEnterScreen('outro', restoreSettings); + if (agentProvider === 'claude') { + const restoreSettings = () => restoreClaudeSettings(session.installDir); + getUI().onEnterScreen('outro', restoreSettings); + } // Register YARA report as cleanup so it fires on any exit path (including wizardAbort) if (session.yaraReport) { @@ -268,9 +280,15 @@ export async function runAgentWizard( sessionToOptions(session), ); - const middleware = session.benchmark - ? createBenchmarkPipeline(spinner, sessionToOptions(session)) - : undefined; + const middleware = + session.benchmark && agentProvider === 'claude' + ? createBenchmarkPipeline(spinner, sessionToOptions(session)) + : undefined; + if (session.benchmark && agentProvider === 'openai') { + getUI().log.warn( + 'Benchmark mode is currently only wired for the Claude runner. Continuing without benchmark output.', + ); + } const agentResult = await runAgent( agent, @@ -417,7 +435,7 @@ function buildIntegrationPrompt( ? '\n' + additionalLines.map((line) => `- ${line}`).join('\n') : ''; - return `You have access to the PostHog MCP server which provides skills to integrate PostHog into this ${ + return `You have access to PostHog tools and local wizard tools to integrate PostHog into this ${ config.metadata.name } project. @@ -434,7 +452,7 @@ Project context: Instructions (follow these steps IN ORDER - do not skip or reorder): -STEP 1: Call load_skill_menu (from the wizard-tools MCP server) to see available skills. +STEP 1: Call load_skill_menu from the wizard tools to see available skills. If the tool fails, emit: ${ AgentSignals.ERROR_MCP_MISSING } Could not load skill menu and halt. @@ -444,21 +462,21 @@ STEP 1: Call load_skill_menu (from the wizard-tools MCP server) to see available AgentSignals.ERROR_RESOURCE_MISSING } Could not find a suitable skill for this project. -STEP 2: Call install_skill (from the wizard-tools MCP server) with the chosen skill ID (e.g., "integration-nextjs-app-router"). +STEP 2: Call install_skill from the wizard tools with the chosen skill ID (e.g., "integration-nextjs-app-router"). Do NOT run any shell commands to install skills. STEP 3: Load the installed skill's SKILL.md file to understand what references are available. STEP 4: Follow the skill's workflow files in sequence. Look for numbered workflow files in the references (e.g., files with patterns like "1.0-", "1.1-", "1.2-"). Start with the first one and proceed through each step until completion. Each workflow file will tell you what to do and which file comes next. Never directly write PostHog tokens directly to code files; always use environment variables. -STEP 5: Set up environment variables for PostHog using the wizard-tools MCP server (this runs locally — secret values never leave the machine): +STEP 5: Set up environment variables for PostHog using the wizard tools (this runs locally — secret values never leave the machine): - Use check_env_keys to see which keys already exist in the project's .env file (e.g. .env.local or .env). - Use set_env_values to create or update the PostHog public token and host, using the appropriate environment variable naming convention for ${ config.metadata.name }, which you'll find in example code. The tool will also ensure .gitignore coverage. Don't assume the presence of keys means the value is up to date. Write the correct value each time. - Reference these environment variables in the code files you create instead of hardcoding the public token and host. -Important: Use the detect_package_manager tool (from the wizard-tools MCP server) to determine which package manager the project uses. Do not manually search for lockfiles or config files. Always install packages as a background task. Don't await completion; proceed with other work immediately after starting the installation. You must read a file immediately before attempting to write it, even if you have previously read it; failure to do so will cause a tool failure. +Important: Use the detect_package_manager tool from the wizard tools to determine which package manager the project uses. Do not manually search for lockfiles or config files. Always install packages as a background task. Don't await completion; proceed with other work immediately after starting the installation. You must read a file immediately before attempting to write it, even if you have previously read it; failure to do so will cause a tool failure. `; diff --git a/src/lib/api.ts b/src/lib/api.ts index 5b1ec823..997798a2 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -5,26 +5,16 @@ import { WIZARD_USER_AGENT } from './constants'; export const ApiUserSchema = z.object({ distinct_id: z.string(), - organizations: z.array( - z.object({ - id: z.string().uuid(), - }), - ), - team: z.object({ - id: z.number(), - organization: z.string().uuid(), - }), - organization: z.object({ - id: z.string().uuid(), - }), + team: z + .object({ + id: z.number(), + }) + .optional(), }); export const ApiProjectSchema = z.object({ id: z.number(), - uuid: z.string().uuid(), - organization: z.string().uuid(), api_token: z.string(), - name: z.string(), }); export type ApiUser = z.infer; diff --git a/src/lib/commandments.ts b/src/lib/commandments.ts index c07e87b5..38b873f6 100644 --- a/src/lib/commandments.ts +++ b/src/lib/commandments.ts @@ -7,9 +7,9 @@ const WIZARD_COMMANDMENTS = [ 'Never hallucinate a PostHog API key, host, or any other secret. Always use the real values that have been configured for this project (for example via environment variables).', - 'Never write API keys, access tokens, or other secrets directly into source code. Always reference environment variables instead, and rely on the wizard-tools MCP server (check_env_keys / set_env_values) to create or update .env files.', + 'Never write API keys, access tokens, or other secrets directly into source code. Always reference environment variables instead, and rely on the wizard tools (check_env_keys / set_env_values) to create or update .env files.', - 'Always use the detect_package_manager tool from the wizard-tools MCP server to determine the package manager. Do not guess based on lockfiles or hard-code npm, yarn, pnpm, bun, pip, etc.', + 'Always use the detect_package_manager tool from the wizard tools to determine the package manager. Do not guess based on lockfiles or hard-code npm, yarn, pnpm, bun, pip, etc.', 'When installing packages, start the installation as a background task and then continue with other work. Do not block waiting for installs to finish unless explicitly instructed.', diff --git a/src/lib/health-checks/__tests__/health-checks.test.ts b/src/lib/health-checks/__tests__/health-checks.test.ts index 3bec283d..fada20a1 100644 --- a/src/lib/health-checks/__tests__/health-checks.test.ts +++ b/src/lib/health-checks/__tests__/health-checks.test.ts @@ -26,10 +26,12 @@ import { checkMcpHealth, checkNpmComponentHealth, checkNpmOverallHealth, + checkOpenAIHealth, checkPosthogComponentHealth, checkPosthogOverallHealth, DEFAULT_WIZARD_READINESS_CONFIG, evaluateWizardReadiness, + getReadinessConfigForProvider, ServiceHealthStatus, WizardReadiness, } from '../index'; @@ -112,6 +114,14 @@ const ANTHROPIC_STATUS_HEALTHY = makeStatuspageStatus({ description: 'All Systems Operational', }); +const OPENAI_STATUS_HEALTHY = makeStatuspageStatus({ + pageId: '01JMDK9XYNY6RXSED6SDWW50WY', + pageName: 'OpenAI', + pageUrl: 'https://status.openai.com/', + indicator: 'none', + description: 'All Systems Operational', +}); + const GITHUB_STATUS_HEALTHY = makeStatuspageStatus({ pageId: 'kctbh9vrtdwd', pageName: 'GitHub', @@ -232,6 +242,7 @@ const MCP_LANDING_HTML = const URLS = { anthropicStatus: 'https://status.claude.com/api/v2/status.json', + openaiStatus: 'https://status.openai.com/api/v2/status.json', posthogStatus: 'https://www.posthogstatus.com/api/v2/status.json', posthogSummary: 'https://www.posthogstatus.com/api/v2/summary.json', githubStatus: 'https://www.githubstatus.com/api/v2/status.json', @@ -255,6 +266,10 @@ const HEALTHY_RESPONSES: Record = body: JSON.stringify(ANTHROPIC_STATUS_HEALTHY), contentType: 'application/json', }, + [URLS.openaiStatus]: { + body: JSON.stringify(OPENAI_STATUS_HEALTHY), + contentType: 'application/json', + }, [URLS.posthogStatus]: { body: JSON.stringify(POSTHOG_STATUS_HEALTHY), contentType: 'application/json', @@ -446,6 +461,14 @@ describe('health-checks', () => { }); }); + describe('checkOpenAIHealth', () => { + it('returns healthy for indicator=none ("All Systems Operational")', async () => { + const result = await checkOpenAIHealth(); + expect(result.status).toBe(ServiceHealthStatus.Healthy); + expect(result.rawIndicator).toBe('none'); + }); + }); + describe('checkPosthogOverallHealth', () => { it('returns healthy for indicator=none', async () => { const result = await checkPosthogOverallHealth(); @@ -845,12 +868,13 @@ describe('health-checks', () => { // ----------------------------------------------------------------------- describe('checkAllExternalServices', () => { - it('returns all 11 service keys when everything is healthy', async () => { + it('returns all 12 service keys when everything is healthy', async () => { const health = await checkAllExternalServices(); const keys = Object.keys(health); expect(keys).toEqual( expect.arrayContaining([ 'anthropic', + 'openai', 'posthogOverall', 'posthogComponents', 'github', @@ -863,19 +887,20 @@ describe('health-checks', () => { 'githubReleases', ]), ); - expect(keys).toHaveLength(11); + expect(keys).toHaveLength(12); for (const val of Object.values(health)) { expect(val.status).toBe(ServiceHealthStatus.Healthy); } }); - it('fires all 11 fetch calls in parallel', async () => { + it('fires all 12 fetch calls in parallel', async () => { await checkAllExternalServices(); const calledUrls = (global.fetch as jest.Mock).mock.calls.map( (c: unknown[]) => typeof c[0] === 'string' ? c[0] : (c[0] as URL).toString(), ); - expect(calledUrls).toHaveLength(11); + expect(calledUrls).toHaveLength(12); + expect(calledUrls).toContain(URLS.openaiStatus); expect(calledUrls).toContain(URLS.llmGatewayLiveness); expect(calledUrls).toContain(URLS.mcpLanding); expect(calledUrls).toContain(URLS.githubReleasesSkillMenu); @@ -917,6 +942,29 @@ describe('health-checks', () => { expect(result.health.anthropic.status).toBe(ServiceHealthStatus.Degraded); }); + it('returns No when OpenAI is degraded for the OpenAI provider config', async () => { + const body = makeStatuspageStatus({ + pageId: '01JMDK9XYNY6RXSED6SDWW50WY', + pageName: 'OpenAI', + pageUrl: 'https://status.openai.com/', + indicator: 'minor', + description: 'Minor Service Outage', + }); + (global.fetch as jest.Mock).mockImplementation( + overrideFetch({ + [URLS.openaiStatus]: () => + Promise.resolve( + new Response(JSON.stringify(body), { status: 200 }), + ), + }), + ); + const result = await evaluateWizardReadiness( + getReadinessConfigForProvider('openai'), + ); + expect(result.decision).toBe(WizardReadiness.No); + expect(result.health.openai.status).toBe(ServiceHealthStatus.Degraded); + }); + it('returns No when LLM Gateway is down (downBlocksRun)', async () => { (global.fetch as jest.Mock).mockImplementation( overrideFetch({ @@ -998,6 +1046,7 @@ describe('health-checks', () => { ); expect(result.reasons.length).toBeGreaterThan(0); expect(result.reasons.some((r) => r.includes('Anthropic'))).toBe(true); + expect(result.reasons.some((r) => r.includes('OpenAI'))).toBe(true); expect(result.reasons.some((r) => r.includes('PostHog'))).toBe(true); expect(result.reasons.some((r) => r.includes('GitHub'))).toBe(true); expect(result.reasons.some((r) => r.includes('npm'))).toBe(true); diff --git a/src/lib/health-checks/index.ts b/src/lib/health-checks/index.ts index f0e336c1..945c7224 100644 --- a/src/lib/health-checks/index.ts +++ b/src/lib/health-checks/index.ts @@ -9,6 +9,7 @@ export { export { checkAnthropicHealth, + checkOpenAIHealth, checkPosthogOverallHealth, checkPosthogComponentHealth, checkGithubHealth, @@ -26,7 +27,10 @@ export { export { type WizardReadinessConfig, + type ReadinessProvider, + type OpenAIReadinessMode, DEFAULT_WIZARD_READINESS_CONFIG, + getReadinessConfigForProvider, checkAllExternalServices, WizardReadiness, type WizardReadinessResult, diff --git a/src/lib/health-checks/readiness.ts b/src/lib/health-checks/readiness.ts index 3263d1fe..97af543f 100644 --- a/src/lib/health-checks/readiness.ts +++ b/src/lib/health-checks/readiness.ts @@ -7,6 +7,7 @@ import { } from './types'; import { checkAnthropicHealth, + checkOpenAIHealth, checkPosthogOverallHealth, checkPosthogComponentHealth, checkGithubHealth, @@ -28,6 +29,7 @@ import { logToFile } from '../../utils/debug'; export const SERVICE_LABELS: Record = { anthropic: 'Anthropic', + openai: 'OpenAI', posthogOverall: 'PostHog', posthogComponents: 'PostHog (components)', github: 'GitHub', @@ -51,6 +53,9 @@ export interface WizardReadinessConfig { degradedBlocksRun?: HealthCheckKey[]; } +export type ReadinessProvider = 'claude' | 'openai'; +export type OpenAIReadinessMode = 'posthog_gateway' | 'direct_openai'; + /** * See README section "Health checks" for the full rationale. * Adjust these arrays to change what blocks a wizard run. @@ -67,6 +72,30 @@ export const DEFAULT_WIZARD_READINESS_CONFIG: WizardReadinessConfig = { degradedBlocksRun: ['anthropic'], }; +export function getReadinessConfigForProvider( + provider: ReadinessProvider, + options?: { + openaiMode?: OpenAIReadinessMode; + }, +): WizardReadinessConfig { + if (provider !== 'openai') { + return DEFAULT_WIZARD_READINESS_CONFIG; + } + + const useGateway = options?.openaiMode !== 'direct_openai'; + return { + downBlocksRun: [ + 'openai', + 'posthogOverall', + 'npmOverall', + ...(useGateway ? (['llmGateway'] as HealthCheckKey[]) : []), + 'mcp', + 'githubReleases', + ], + degradedBlocksRun: ['openai'], + }; +} + // --------------------------------------------------------------------------- // Aggregate check // --------------------------------------------------------------------------- @@ -74,6 +103,7 @@ export const DEFAULT_WIZARD_READINESS_CONFIG: WizardReadinessConfig = { export async function checkAllExternalServices(): Promise { const [ anthropic, + openai, posthogOverall, posthogComponents, github, @@ -86,6 +116,7 @@ export async function checkAllExternalServices(): Promise { githubReleases, ] = await Promise.all([ checkAnthropicHealth(), + checkOpenAIHealth(), checkPosthogOverallHealth(), checkPosthogComponentHealth(), checkGithubHealth(), @@ -100,6 +131,7 @@ export async function checkAllExternalServices(): Promise { return { anthropic, + openai, posthogOverall, posthogComponents, github, @@ -254,6 +286,7 @@ function allUnknown(error: string): AllServicesHealth { }; return { anthropic: base, + openai: base, posthogOverall: base, posthogComponents: { ...base }, github: base, diff --git a/src/lib/health-checks/statuspage.ts b/src/lib/health-checks/statuspage.ts index 877d713f..f8a21b5b 100644 --- a/src/lib/health-checks/statuspage.ts +++ b/src/lib/health-checks/statuspage.ts @@ -123,6 +123,9 @@ async function fetchStatuspageSummary( export const checkAnthropicHealth = (): Promise => fetchStatuspageIndicator('https://status.claude.com/api/v2/status.json'); +export const checkOpenAIHealth = (): Promise => + fetchStatuspageIndicator('https://status.openai.com/api/v2/status.json'); + export const checkPosthogOverallHealth = (): Promise => fetchStatuspageIndicator('https://www.posthogstatus.com/api/v2/status.json'); diff --git a/src/lib/health-checks/types.ts b/src/lib/health-checks/types.ts index e4c5215d..a811f765 100644 --- a/src/lib/health-checks/types.ts +++ b/src/lib/health-checks/types.ts @@ -22,6 +22,7 @@ export interface ComponentHealthResult extends BaseHealthResult { export interface AllServicesHealth { anthropic: BaseHealthResult; + openai: BaseHealthResult; posthogOverall: BaseHealthResult; posthogComponents: ComponentHealthResult; github: BaseHealthResult; diff --git a/src/lib/openai-function-tools.ts b/src/lib/openai-function-tools.ts new file mode 100644 index 00000000..d1ca136a --- /dev/null +++ b/src/lib/openai-function-tools.ts @@ -0,0 +1,783 @@ +import fs from 'fs'; +import path from 'path'; +import { execFile, spawn } from 'child_process'; +import fg from 'fast-glob'; +import { promisify } from 'util'; +import { getUI } from '../ui'; +import { logToFile } from '../utils/debug'; +import { scan, scanSkillDirectory } from './yara-scanner'; +import { + fetchSkillMenu, + downloadSkill, + resolveEnvPath, + parseEnvKeys, + mergeEnvValues, + ensureGitignoreCoverage, + type SkillEntry, +} from './wizard-tools'; +import type { PackageManagerDetector } from './package-manager-detection'; + +const execFileAsync = promisify(execFile); +const RECENT_READ_WINDOW_MS = 60_000; +const DEFAULT_BASH_TIMEOUT_MS = 60_000; +const DEFAULT_BASH_MAX_OUTPUT = 12_000; +const DEFAULT_READ_LIMIT = 400; +const DEFAULT_GREP_LIMIT = 200; + +type RecentReadTracker = Map; + +export interface OpenAIFunctionToolsOptions { + tool: (definition: Record) => unknown; + workingDirectory: string; + detectPackageManager: PackageManagerDetector; + skillsBaseUrl: string; + checkToolAccess: (toolName: string, input: Record) => void; + mcpMissingSignal: string; +} + +function resolveProjectPath( + workingDirectory: string, + filePath: string, +): string { + const resolved = path.resolve(workingDirectory, filePath); + if ( + !resolved.startsWith(workingDirectory + path.sep) && + resolved !== workingDirectory + ) { + throw new Error( + `Path traversal rejected: "${filePath}" resolves outside working directory`, + ); + } + return resolved; +} + +function rememberRead(recentReads: RecentReadTracker, filePath: string): void { + recentReads.set(filePath, Date.now()); +} + +function assertRecentlyRead( + recentReads: RecentReadTracker, + filePath: string, +): void { + const lastRead = recentReads.get(filePath); + if (!lastRead || Date.now() - lastRead > RECENT_READ_WINDOW_MS) { + throw new Error( + `You must read "${filePath}" immediately before writing it.`, + ); + } +} + +function checkPreCommandScan(command: string): void { + const result = scan(command, 'PreToolUse', 'Bash'); + if (result.matched) { + throw new Error( + `[YARA] ${result.matches[0].rule.name}: ${result.matches[0].rule.description}`, + ); + } +} + +function checkPostReadScan(toolName: 'Read' | 'Grep', content: string): void { + const result = scan(content, 'PostToolUse', toolName); + if (!result.matched) return; + + const match = result.matches[0]; + if (match.rule.severity === 'critical') { + throw new Error( + `[YARA CRITICAL] ${match.rule.name}: ${match.rule.description}`, + ); + } + + logToFile( + `[YARA] ${toolName} warning ${match.rule.name}: ${match.rule.description}`, + ); +} + +function checkWriteScan(toolName: 'Write' | 'Edit', content: string): void { + const result = scan(content, 'PostToolUse', toolName); + if (!result.matched) return; + + const match = result.matches[0]; + throw new Error( + `[YARA VIOLATION] ${match.rule.name}: ${match.rule.description}`, + ); +} + +async function scanInstalledSkillDirectory(absoluteDir: string): Promise { + if (!fs.existsSync(absoluteDir)) return; + + const files = await fg('**/*.{md,txt,yaml,yml,json,js,ts,py,rb,sh}', { + cwd: absoluteDir, + absolute: true, + }); + + const fileContents: Array<{ path: string; content: string }> = []; + for (const filePath of files) { + try { + fileContents.push({ + path: filePath, + content: fs.readFileSync(filePath, 'utf8'), + }); + } catch { + // Ignore unreadable skill files and scan what we can. + } + } + + const result = scanSkillDirectory(fileContents); + if (result.matched) { + const match = result.matches[0]; + throw new Error( + `[YARA CRITICAL] ${match.rule.name}: ${match.rule.description}`, + ); + } +} + +function buildStrictObjectSchema( + properties: Record, + required: string[], +): Record { + const normalizedProperties = Object.fromEntries( + Object.entries(properties).map(([key, schema]) => [ + key, + required.includes(key) ? schema : makeNullableSchema(schema), + ]), + ); + + return { + type: 'object', + properties: normalizedProperties, + required: Object.keys(properties), + additionalProperties: false, + }; +} + +function makeNullableSchema(schema: unknown): unknown { + if (!schema || typeof schema !== 'object' || !('type' in schema)) { + return { + anyOf: [schema, { type: 'null' }], + }; + } + + const typedSchema = schema as { type: string | string[] }; + const schemaType = typedSchema.type; + if (Array.isArray(schemaType)) { + return schemaType.includes('null') + ? schema + : { ...typedSchema, type: [...schemaType, 'null'] }; + } + + return { ...typedSchema, type: [schemaType, 'null'] }; +} + +function normalizeNullableInput(value: unknown): unknown { + if (value === null) return undefined; + if (Array.isArray(value)) { + return value.map((item) => normalizeNullableInput(item)); + } + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value).map(([key, nested]) => [ + key, + normalizeNullableInput(nested), + ]), + ); + } + return value; +} + +function truncateOutput(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + return `${text.slice( + 0, + maxLength, + )}\n[output truncated to ${maxLength} chars]`; +} + +async function runCommand( + command: string, + workingDirectory: string, + timeoutMs = DEFAULT_BASH_TIMEOUT_MS, + maxOutputLength = DEFAULT_BASH_MAX_OUTPUT, +): Promise { + return new Promise((resolve, reject) => { + const child = spawn('/bin/zsh', ['-lc', command], { + cwd: workingDirectory, + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let stdout = ''; + let stderr = ''; + let settled = false; + + const timer = setTimeout(() => { + if (settled) return; + settled = true; + child.kill('SIGTERM'); + reject(new Error(`Command timed out after ${timeoutMs}ms`)); + }, timeoutMs); + + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + if (stdout.length > maxOutputLength) { + stdout = stdout.slice(0, maxOutputLength); + } + }); + + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + if (stderr.length > maxOutputLength) { + stderr = stderr.slice(0, maxOutputLength); + } + }); + + child.on('error', (error) => { + if (settled) return; + settled = true; + clearTimeout(timer); + reject(error); + }); + + child.on('close', (code) => { + if (settled) return; + settled = true; + clearTimeout(timer); + const output = truncateOutput( + [stdout, stderr].filter(Boolean).join(stderr ? '\n' : ''), + maxOutputLength, + ); + if (code === 0) { + resolve(output || 'Command completed successfully with no output.'); + } else { + reject( + new Error(output || `Command exited with code ${code ?? 'unknown'}`), + ); + } + }); + }); +} + +function formatFileSlice( + content: string, + offset = 0, + limit = DEFAULT_READ_LIMIT, +): string { + const lines = content.split('\n'); + const start = Math.max(0, offset); + const end = Math.min(lines.length, start + Math.max(1, limit)); + return lines.slice(start, end).join('\n'); +} + +async function grepWithRipgrep( + workingDirectory: string, + grepPath: string, + pattern: string, + glob?: string, +): Promise { + const args = ['-n', '-S', pattern]; + if (glob) { + args.push('--glob', glob); + } + args.push(grepPath); + + try { + const { stdout } = await execFileAsync('rg', args, { + cwd: workingDirectory, + maxBuffer: 1024 * 1024, + }); + return stdout.trim(); + } catch (error: any) { + if (error.code === 'ENOENT') { + const fallbackArgs = ['-RIn', pattern, grepPath]; + const { stdout } = await execFileAsync('grep', fallbackArgs, { + cwd: workingDirectory, + maxBuffer: 1024 * 1024, + }); + return stdout.trim(); + } + + if (error.code === 1 && typeof error.stdout === 'string') { + return error.stdout.trim(); + } + throw error; + } +} + +export async function createOpenAIFunctionTools( + options: OpenAIFunctionToolsOptions, +): Promise { + const { + tool, + workingDirectory, + detectPackageManager, + skillsBaseUrl, + checkToolAccess, + mcpMissingSignal, + } = options; + const recentReads: RecentReadTracker = new Map(); + const defineTool = (definition: Record) => + tool({ + ...definition, + execute: (input: unknown, ...args: unknown[]) => + (definition.execute as (...toolArgs: unknown[]) => unknown)( + normalizeNullableInput(input), + ...args, + ), + }); + + let cachedSkillMenu = await fetchSkillMenu(skillsBaseUrl); + + const ensureSkillMenu = async () => { + if (!cachedSkillMenu) { + cachedSkillMenu = await fetchSkillMenu(skillsBaseUrl); + } + return cachedSkillMenu; + }; + + const readTool = defineTool({ + name: 'Read', + description: + 'Read a file from the project. Use this immediately before writing any existing file.', + parameters: buildStrictObjectSchema( + { + file_path: { + type: 'string', + description: 'Path to the file, relative to the project root.', + }, + offset: { + type: 'integer', + description: 'Optional zero-based line offset.', + }, + limit: { + type: 'integer', + description: 'Optional maximum number of lines to return.', + }, + }, + ['file_path'], + ), + execute: (input: any) => { + checkToolAccess('Read', input); + + const absolutePath = resolveProjectPath( + workingDirectory, + input.file_path, + ); + const content = fs.readFileSync(absolutePath, 'utf8'); + rememberRead(recentReads, absolutePath); + checkPostReadScan('Read', content); + return formatFileSlice(content, input.offset, input.limit); + }, + }); + + const writeTool = defineTool({ + name: 'Write', + description: + 'Write an entire file. Use this for new files or when replacing all content in an existing file.', + parameters: buildStrictObjectSchema( + { + file_path: { + type: 'string', + description: 'Path to the file, relative to the project root.', + }, + content: { + type: 'string', + description: 'The full content to write.', + }, + }, + ['file_path', 'content'], + ), + execute: (input: any) => { + checkToolAccess('Write', input); + + const absolutePath = resolveProjectPath( + workingDirectory, + input.file_path, + ); + if (fs.existsSync(absolutePath)) { + assertRecentlyRead(recentReads, absolutePath); + } + checkWriteScan('Write', input.content); + + fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); + fs.writeFileSync(absolutePath, input.content, 'utf8'); + rememberRead(recentReads, absolutePath); + return `Wrote ${input.file_path}`; + }, + }); + + const editTool = defineTool({ + name: 'Edit', + description: + 'Edit a file by replacing exact text. Read the file immediately beforehand and prefer precise, minimal replacements.', + parameters: buildStrictObjectSchema( + { + file_path: { + type: 'string', + description: 'Path to the file, relative to the project root.', + }, + old_str: { + type: 'string', + description: 'Exact existing text to replace.', + }, + new_str: { + type: 'string', + description: 'Replacement text.', + }, + replace_all: { + type: 'boolean', + description: 'Replace all matches instead of exactly one match.', + }, + }, + ['file_path', 'old_str', 'new_str'], + ), + execute: (input: any) => { + checkToolAccess('Edit', input); + + const absolutePath = resolveProjectPath( + workingDirectory, + input.file_path, + ); + assertRecentlyRead(recentReads, absolutePath); + + const existing = fs.readFileSync(absolutePath, 'utf8'); + if (!existing.includes(input.old_str)) { + throw new Error( + `Could not find the text to replace in ${input.file_path}`, + ); + } + + const matchCount = existing.split(input.old_str).length - 1; + if (matchCount > 1 && !input.replace_all) { + throw new Error( + `Found ${matchCount} matches in ${input.file_path}; set replace_all=true or make old_str more specific.`, + ); + } + + checkWriteScan('Edit', input.new_str); + + const nextContent = input.replace_all + ? existing.split(input.old_str).join(input.new_str) + : existing.replace(input.old_str, input.new_str); + + fs.writeFileSync(absolutePath, nextContent, 'utf8'); + rememberRead(recentReads, absolutePath); + return `Edited ${input.file_path}`; + }, + }); + + const globTool = defineTool({ + name: 'Glob', + description: 'Find files by glob pattern.', + parameters: buildStrictObjectSchema( + { + pattern: { + type: 'string', + description: 'Glob pattern, for example src/**/*.ts', + }, + path: { + type: 'string', + description: + 'Optional search root relative to the project root. Defaults to the project root.', + }, + }, + ['pattern'], + ), + execute: async (input: any) => { + const searchRoot = input.path + ? resolveProjectPath(workingDirectory, input.path) + : workingDirectory; + + const matches = await fg(input.pattern, { + cwd: searchRoot, + dot: false, + onlyFiles: false, + }); + return matches.join('\n'); + }, + }); + + const grepTool = defineTool({ + name: 'Grep', + description: 'Search file contents with ripgrep and return matching lines.', + parameters: buildStrictObjectSchema( + { + pattern: { + type: 'string', + description: 'Text or regex pattern to search for.', + }, + path: { + type: 'string', + description: + 'Optional file or directory relative to the project root. Defaults to the project root.', + }, + glob: { + type: 'string', + description: 'Optional glob filter, for example *.tsx.', + }, + }, + ['pattern'], + ), + execute: async (input: any) => { + checkToolAccess('Grep', { path: input.path ?? '' }); + + const searchPath = input.path + ? resolveProjectPath(workingDirectory, input.path) + : workingDirectory; + const output = await grepWithRipgrep( + workingDirectory, + searchPath, + input.pattern, + input.glob, + ); + checkPostReadScan('Grep', output); + + const lines = output.split('\n'); + return lines.slice(0, DEFAULT_GREP_LIMIT).join('\n'); + }, + }); + + const bashTool = defineTool({ + name: 'Bash', + description: + 'Run an allowed shell command for installs, builds, type checks, linting, or formatting. Use other tools for file inspection and edits.', + parameters: buildStrictObjectSchema( + { + command: { + type: 'string', + description: 'The shell command to run.', + }, + timeout_ms: { + type: 'integer', + description: 'Optional timeout in milliseconds.', + }, + }, + ['command'], + ), + execute: (input: any) => { + checkToolAccess('Bash', { command: input.command }); + checkPreCommandScan(input.command); + + return runCommand( + input.command, + workingDirectory, + input.timeout_ms, + DEFAULT_BASH_MAX_OUTPUT, + ); + }, + }); + + const todoWriteTool = defineTool({ + name: 'TodoWrite', + description: + 'Update the wizard todo list to reflect high-level progress for the current integration.', + parameters: buildStrictObjectSchema( + { + todos: { + type: 'array', + items: buildStrictObjectSchema( + { + content: { type: 'string' }, + status: { type: 'string' }, + activeForm: { type: 'string' }, + }, + ['content', 'status'], + ), + }, + }, + ['todos'], + ), + execute: (input: any) => { + getUI().syncTodos(input.todos); + return `Updated ${input.todos.length} todos.`; + }, + }); + + const checkEnvKeysTool = defineTool({ + name: 'check_env_keys', + description: + 'Check whether environment variable keys exist in a local .env file without revealing values.', + parameters: buildStrictObjectSchema( + { + filePath: { + type: 'string', + description: 'Path to the .env file, relative to the project root.', + }, + keys: { + type: 'array', + items: { type: 'string' }, + description: 'Environment variable keys to check.', + }, + }, + ['filePath', 'keys'], + ), + execute: (input: any) => { + const resolved = resolveEnvPath(workingDirectory, input.filePath); + const existingKeys = fs.existsSync(resolved) + ? parseEnvKeys(fs.readFileSync(resolved, 'utf8')) + : new Set(); + + const result: Record = {}; + for (const key of input.keys) { + result[key] = existingKeys.has(key) ? 'present' : 'missing'; + } + return JSON.stringify(result, null, 2); + }, + }); + + const setEnvValuesTool = defineTool({ + name: 'set_env_values', + description: + 'Create or update values in a local .env file and ensure .gitignore covers that file.', + parameters: buildStrictObjectSchema( + { + filePath: { + type: 'string', + description: 'Path to the .env file, relative to the project root.', + }, + values: { + type: 'array', + items: buildStrictObjectSchema( + { + key: { + type: 'string', + description: 'Environment variable key to set.', + }, + value: { + type: 'string', + description: 'Environment variable value to write.', + }, + }, + ['key', 'value'], + ), + description: 'Key-value pairs to set.', + }, + }, + ['filePath', 'values'], + ), + execute: (input: any) => { + const values = Object.fromEntries( + input.values.map((entry: { key: string; value: string }) => [ + entry.key, + entry.value, + ]), + ); + + const forbidden = Object.keys(values).find( + (key) => key.toUpperCase() === 'POSTHOG_API_KEY', + ); + if (forbidden) { + throw new Error( + `"${forbidden}" is not a valid PostHog env var name. Use the framework-specific key name, such as NEXT_PUBLIC_POSTHOG_KEY.`, + ); + } + + const resolved = resolveEnvPath(workingDirectory, input.filePath); + const existing = fs.existsSync(resolved) + ? fs.readFileSync(resolved, 'utf8') + : ''; + const content = mergeEnvValues(existing, values); + + fs.mkdirSync(path.dirname(resolved), { recursive: true }); + fs.writeFileSync(resolved, content, 'utf8'); + ensureGitignoreCoverage(workingDirectory, path.basename(resolved)); + + return `Updated ${Object.keys(values).length} key(s) in ${ + input.filePath + }`; + }, + }); + + const detectPackageManagerTool = defineTool({ + name: 'detect_package_manager', + description: + 'Detect which package manager the project uses. Call this before any install commands.', + parameters: buildStrictObjectSchema({}, []), + execute: async () => { + const result = await detectPackageManager(workingDirectory); + return JSON.stringify(result, null, 2); + }, + }); + + const loadSkillMenuTool = defineTool({ + name: 'load_skill_menu', + description: + 'List available PostHog skills for a category so you can pick the correct integration skill.', + parameters: buildStrictObjectSchema( + { + category: { + type: 'string', + description: 'Skill category to list, usually "integration".', + }, + }, + ['category'], + ), + execute: async (input: any) => { + const menu = await ensureSkillMenu(); + if (!menu) { + throw new Error(`${mcpMissingSignal} Could not load skill menu`); + } + + const skills = menu.categories[input.category]; + if (!skills || skills.length === 0) { + throw new Error(`No skills found for category "${input.category}".`); + } + + return skills + .map((skill: SkillEntry) => `- ${skill.id}: ${skill.name}`) + .join('\n'); + }, + }); + + const installSkillTool = defineTool({ + name: 'install_skill', + description: + 'Download and install a PostHog skill by ID into the project skill directory.', + parameters: buildStrictObjectSchema( + { + skillId: { + type: 'string', + description: 'Skill ID from load_skill_menu.', + }, + }, + ['skillId'], + ), + execute: async (input: any) => { + const menu = await ensureSkillMenu(); + const skills = Object.values(menu?.categories ?? {}).flat(); + const skill = skills.find((entry) => entry.id === input.skillId); + if (!skill) { + throw new Error( + `Skill "${input.skillId}" not found. Use load_skill_menu first.`, + ); + } + + const result = downloadSkill(skill, workingDirectory); + if (!result.success) { + throw new Error(result.error || 'Failed to install skill.'); + } + + const skillDir = path.join( + workingDirectory, + '.claude', + 'skills', + input.skillId, + ); + await scanInstalledSkillDirectory(skillDir); + return `Skill installed to .claude/skills/${input.skillId}/`; + }, + }); + + return [ + readTool, + writeTool, + editTool, + globTool, + grepTool, + bashTool, + todoWriteTool, + checkEnvKeysTool, + setEnvValuesTool, + detectPackageManagerTool, + loadSkillMenuTool, + installSkillTool, + ]; +} diff --git a/src/lib/openai-sdk-compat.ts b/src/lib/openai-sdk-compat.ts new file mode 100644 index 00000000..6977e527 --- /dev/null +++ b/src/lib/openai-sdk-compat.ts @@ -0,0 +1,141 @@ +type UsageDetailsRecord = Record; + +type ParseMethod = (input: unknown, ...args: any[]) => T; + +interface SchemaLike { + parse: ParseMethod; + safeParse?: ParseMethod; + parseAsync?: ParseMethod>; + safeParseAsync?: ParseMethod>; +} + +interface OpenAIProtocolModule { + protocol?: { + StreamEventResponseCompleted?: SchemaLike; + }; +} + +let responseUsageCompatPatchApplied = false; + +function sanitizeUsageDetailsRecord(value: unknown): UsageDetailsRecord { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return {}; + } + + return Object.fromEntries( + Object.entries(value).filter( + ([, entryValue]) => + typeof entryValue === 'number' && Number.isFinite(entryValue), + ), + ); +} + +function sanitizeUsageDetailsCollection( + value: unknown, +): UsageDetailsRecord | UsageDetailsRecord[] | undefined { + if (Array.isArray(value)) { + return value.map((entry) => sanitizeUsageDetailsRecord(entry)); + } + + if (value && typeof value === 'object') { + return sanitizeUsageDetailsRecord(value); + } + + return undefined; +} + +function sanitizeRequestUsageEntry(entry: unknown): unknown { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) { + return entry; + } + + const candidate = entry as Record; + return { + ...candidate, + inputTokensDetails: sanitizeUsageDetailsRecord( + candidate.inputTokensDetails, + ), + outputTokensDetails: sanitizeUsageDetailsRecord( + candidate.outputTokensDetails, + ), + }; +} + +export function sanitizeOpenAIResponseDoneEventUsage(event: unknown): unknown { + if (!event || typeof event !== 'object' || Array.isArray(event)) { + return event; + } + + const candidate = event as Record; + if (candidate.type !== 'response_done') { + return event; + } + + const response = + candidate.response && typeof candidate.response === 'object' + ? (candidate.response as Record) + : null; + const usage = + response?.usage && typeof response.usage === 'object' + ? (response.usage as Record) + : null; + + if (!response || !usage) { + return event; + } + + return { + ...candidate, + response: { + ...response, + usage: { + ...usage, + inputTokensDetails: sanitizeUsageDetailsCollection( + usage.inputTokensDetails, + ), + outputTokensDetails: sanitizeUsageDetailsCollection( + usage.outputTokensDetails, + ), + requestUsageEntries: Array.isArray(usage.requestUsageEntries) + ? usage.requestUsageEntries.map((entry) => + sanitizeRequestUsageEntry(entry), + ) + : usage.requestUsageEntries, + }, + }, + }; +} + +function patchSchemaParseMethod( + schema: SchemaLike, + methodName: keyof SchemaLike, +): void { + const original = schema[methodName]; + if (typeof original !== 'function') { + return; + } + + const boundOriginal = original.bind(schema) as ParseMethod; + schema[methodName] = ((input: unknown, ...args: any[]) => + boundOriginal(sanitizeOpenAIResponseDoneEventUsage(input), ...args)) as any; +} + +export function applyOpenAIResponsesUsageCompatPatch( + sdkModule: OpenAIProtocolModule, +): void { + if (responseUsageCompatPatchApplied) { + return; + } + + const schema = sdkModule.protocol?.StreamEventResponseCompleted; + if (!schema) { + return; + } + + patchSchemaParseMethod(schema, 'parse'); + patchSchemaParseMethod(schema, 'safeParse'); + patchSchemaParseMethod(schema, 'parseAsync'); + patchSchemaParseMethod(schema, 'safeParseAsync'); + + responseUsageCompatPatchApplied = true; +} diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index df07fcde..d7a1f1e0 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -22,6 +22,20 @@ function parseProjectIdArg(value: string | undefined): number | undefined { } export type CloudRegion = 'us' | 'eu'; +export type AgentProvider = 'claude' | 'openai'; +export type OpenAIAuthMode = 'posthog_gateway' | 'direct_openai'; + +function parseAgentProvider(): AgentProvider { + return process.env.WIZARD_AGENT_PROVIDER?.toLowerCase() === 'openai' + ? 'openai' + : 'claude'; +} + +function parseOpenAIAuthMode(): OpenAIAuthMode { + return process.env.WIZARD_OPENAI_API_KEY || process.env.OPENAI_API_KEY + ? 'direct_openai' + : 'posthog_gateway'; +} /** Lifecycle phase of the main work (agent run, MCP install, etc.) */ export enum RunPhase { @@ -93,6 +107,8 @@ export interface WizardSession { benchmark: boolean; yaraReport: boolean; projectId?: number; + agentProvider: AgentProvider; + openaiAuthMode: OpenAIAuthMode; // From detection + screens setupConfirmed: boolean; @@ -180,6 +196,8 @@ export function buildSession(args: { benchmark: args.benchmark ?? false, yaraReport: args.yaraReport ?? false, projectId: parseProjectIdArg(args.projectId), + agentProvider: parseAgentProvider(), + openaiAuthMode: parseOpenAIAuthMode(), setupConfirmed: false, integration: args.integration ?? null, diff --git a/src/ui/tui/__tests__/store.test.ts b/src/ui/tui/__tests__/store.test.ts index d40e8344..c53290b0 100644 --- a/src/ui/tui/__tests__/store.test.ts +++ b/src/ui/tui/__tests__/store.test.ts @@ -23,19 +23,34 @@ jest.mock('../../../utils/analytics.js', () => ({ sessionProperties: jest.fn(() => ({})), })); -jest.mock('../../../lib/health-checks/readiness.js', () => ({ - evaluateWizardReadiness: jest.fn().mockResolvedValue({ +jest.mock('../../../lib/health-checks/readiness.js', () => { + const evaluateWizardReadiness = jest.fn().mockResolvedValue({ decision: 'yes', health: {}, reasons: [], - }), - WizardReadiness: { - Yes: 'yes', - No: 'no', - YesWithWarnings: 'yes-with-warnings', - }, - SERVICE_LABELS: {}, -})); + }); + const getReadinessConfigForProvider = jest.fn(() => ({ + downBlocksRun: [], + })); + + return { + evaluateWizardReadiness, + getReadinessConfigForProvider, + WizardReadiness: { + Yes: 'yes', + No: 'no', + YesWithWarnings: 'yes-with-warnings', + }, + SERVICE_LABELS: {}, + }; +}); + +const readinessMocks = jest.requireMock( + '../../../lib/health-checks/readiness.js', +); +const mockEvaluateWizardReadiness = readinessMocks.evaluateWizardReadiness; +const mockGetReadinessConfigForProvider = + readinessMocks.getReadinessConfigForProvider; function createStore(flow?: Flow): WizardStore { return new WizardStore(flow); @@ -46,6 +61,9 @@ const wizardCaptureMock = analytics.wizardCapture as jest.Mock; describe('WizardStore', () => { beforeEach(() => { jest.clearAllMocks(); + delete process.env.WIZARD_AGENT_PROVIDER; + delete process.env.WIZARD_OPENAI_API_KEY; + delete process.env.OPENAI_API_KEY; }); // ── Construction ───────────────────────────────────────────────── @@ -73,6 +91,16 @@ describe('WizardStore', () => { expect(store.getVersion()).toBe(0); expect(store.getSnapshot()).toBe(0); }); + + it('uses provider-aware readiness config during startup health checks', () => { + process.env.WIZARD_AGENT_PROVIDER = 'openai'; + createStore(); + + expect(mockGetReadinessConfigForProvider).toHaveBeenCalledWith('openai', { + openaiMode: 'posthog_gateway', + }); + expect(mockEvaluateWizardReadiness).toHaveBeenCalled(); + }); }); // ── Change notification ────────────────────────────────────────── diff --git a/src/ui/tui/components/TitleBar.tsx b/src/ui/tui/components/TitleBar.tsx index 87f97afd..47fc0f37 100644 --- a/src/ui/tui/components/TitleBar.tsx +++ b/src/ui/tui/components/TitleBar.tsx @@ -1,5 +1,6 @@ import { Box, Text } from 'ink'; import { Colors } from '../styles.js'; +import type { AgentProvider } from '../../../lib/wizard-session.js'; const FEEDBACK = 'Feedback: wizard@posthog.com '; const FEEDBACK_SHORT = ' wizard@posthog.com '; @@ -7,15 +8,17 @@ const FEEDBACK_SHORT = ' wizard@posthog.com '; interface TitleBarProps { version: string; width: number; + provider: AgentProvider; } -export const TitleBar = ({ version, width }: TitleBarProps) => { - const fullTitle = ` PostHog Wizard v${version}`; +export const TitleBar = ({ version, width, provider }: TitleBarProps) => { + const providerLabel = provider === 'openai' ? 'OpenAI' : 'Claude'; + const fullTitle = ` PostHog Wizard v${version} · ${providerLabel}`; const needShort = width < fullTitle.length + FEEDBACK.length; const feedback = needShort ? FEEDBACK_SHORT : FEEDBACK; const title = needShort && fullTitle.length + feedback.length > width - ? ` Wizard v${version}` + ? ` Wizard v${version} · ${providerLabel}` : fullTitle; const gap = Math.max(0, width - title.length - feedback.length); const padding = ' '.repeat(gap); diff --git a/src/ui/tui/playground/demos/HealthCheckDemo.tsx b/src/ui/tui/playground/demos/HealthCheckDemo.tsx index 0ea96e34..a51c6a4d 100644 --- a/src/ui/tui/playground/demos/HealthCheckDemo.tsx +++ b/src/ui/tui/playground/demos/HealthCheckDemo.tsx @@ -22,6 +22,7 @@ const HEALTHY = { status: ServiceHealthStatus.Healthy } as const; const MOCK_HEALTH: AllServicesHealth = { anthropic: { status: ServiceHealthStatus.Down, rawIndicator: 'major' }, + openai: HEALTHY, posthogOverall: HEALTHY, posthogComponents: { status: ServiceHealthStatus.Healthy }, github: HEALTHY, diff --git a/src/ui/tui/primitives/ScreenContainer.tsx b/src/ui/tui/primitives/ScreenContainer.tsx index 5153b81c..20325493 100644 --- a/src/ui/tui/primitives/ScreenContainer.tsx +++ b/src/ui/tui/primitives/ScreenContainer.tsx @@ -45,7 +45,11 @@ export const ScreenContainer = ({ store, screens }: ScreenContainerProps) => { const inner = ( - + { // Healthy or warnings — isComplete returns true, router skips past. // This branch only renders for a single frame before advancing. - const blockingKeys = getBlockingServiceKeys(result.health); + const readinessConfig = getReadinessConfigForProvider( + store.session.agentProvider, + { + openaiMode: store.session.openaiAuthMode, + }, + ); + const blockingKeys = getBlockingServiceKeys(result.health, readinessConfig); if (blockingKeys.length === 0) return null; const isGithubReleasesDown = blockingKeys.includes('githubReleases'); const canDownloadSkills = result.health.githubReleases.status === ServiceHealthStatus.Healthy; const integration = store.session.integration; + const providerServiceKey = + store.session.agentProvider === 'openai' ? 'openai' : 'anthropic'; + const isProviderDown = blockingKeys.includes(providerServiceKey); + const providerLabel = + store.session.agentProvider === 'openai' ? 'OpenAI' : 'Anthropic'; - const title = `Ongoing service disruptions`; + const title = isProviderDown + ? `${providerLabel} service disruption` + : 'Ongoing service disruptions'; const docsUrl = store.session.frameworkConfig?.metadata.docsUrl; const description = isGithubReleasesDown ? "The Wizard can't download necessary skills from GitHub Releases right now." + : isProviderDown + ? `${providerLabel} is reporting an ongoing disruption, so the Wizard may not work reliably right now.` : 'The Wizard may not work reliably while services are affected.'; const handleDownloadAndExit = async () => { diff --git a/src/ui/tui/store.ts b/src/ui/tui/store.ts index b5c3f1d2..7e484045 100644 --- a/src/ui/tui/store.ts +++ b/src/ui/tui/store.ts @@ -32,6 +32,7 @@ import { import { analytics, sessionProperties } from '../../utils/analytics.js'; import { evaluateWizardReadiness, + getReadinessConfigForProvider, WizardReadiness, } from '../../lib/health-checks/readiness.js'; @@ -115,7 +116,11 @@ export class WizardStore { * health gate if non-blocking. */ private _initHealthCheck(): void { - evaluateWizardReadiness() + evaluateWizardReadiness( + getReadinessConfigForProvider(this.session.agentProvider, { + openaiMode: this.session.openaiAuthMode, + }), + ) .then((readiness) => { this.setReadinessResult(readiness); if (readiness.decision !== WizardReadiness.No) { diff --git a/src/utils/custom-headers.ts b/src/utils/custom-headers.ts index b7102eaf..bed7b0e9 100644 --- a/src/utils/custom-headers.ts +++ b/src/utils/custom-headers.ts @@ -1,13 +1,14 @@ import { POSTHOG_FLAG_HEADER_PREFIX } from '../lib/constants'; /** - * Builds a list of custom headers for ANTHROPIC_CUSTOM_HEADERS. + * Builds a list of custom headers for wizard provider requests. */ export function createCustomHeaders(): { add(key: string, value: string): void; /** Add a feature flag for PostHog ($feature/: variant). */ addFlag(flagKey: string, variant: string): void; encode(): string; + toObject(): Record; } { const entries: Array<{ key: string; value: string }> = []; @@ -26,5 +27,9 @@ export function createCustomHeaders(): { encode(): string { return entries.map(({ key, value }) => `${key}: ${value}`).join('\n'); }, + + toObject(): Record { + return Object.fromEntries(entries.map(({ key, value }) => [key, value])); + }, }; } diff --git a/src/utils/setup-utils.ts b/src/utils/setup-utils.ts index f24ab546..a540bd55 100644 --- a/src/utils/setup-utils.ts +++ b/src/utils/setup-utils.ts @@ -327,9 +327,9 @@ export async function getOrAskForProjectData( projectId: number; cloudRegion: CloudRegion; }> { - // CI mode: bypass OAuth, use personal API key for LLM gateway - if (_options.ci && _options.apiKey) { - getUI().log.info('Using provided API key (CI mode - OAuth bypassed)'); + // Bypass OAuth when a personal API key is provided + if (_options.apiKey) { + getUI().log.info('Using provided API key (OAuth bypassed)'); const cloudRegion = await detectRegionFromToken(_options.apiKey); const host = getHostFromRegion(cloudRegion);