diff --git a/.gitignore b/.gitignore index d90cab7e5e..2fb88c5542 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,8 @@ Thumbs.db # Temporary files *.tmp -*.temp \ No newline at end of file +*.temp + +AGENTS.md +.claude/ +.claude-config/ \ No newline at end of file diff --git a/package.json b/package.json index 8a3319ea87..de3238600c 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "yaml": "^2.8.1" }, "dependencies": { - "@posthog/code-agent": "^0.2.0", + "@posthog/agent": "^1.2.0", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/themes": "^3.2.1", "axios": "^1.6.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb43e23243..cb29e64a7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - '@posthog/code-agent': - specifier: ^0.2.0 - version: 0.2.0 + '@posthog/agent': + specifier: ^1.2.0 + version: 1.2.0(zod@3.25.76) '@radix-ui/react-icons': specifier: ^1.3.2 version: 1.3.2(react@18.3.1) @@ -130,13 +130,11 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@anthropic-ai/claude-code@1.0.128': - resolution: {integrity: sha512-uUg5cFMJfeQetQzFw76Vpbro6DAXst2Lpu8aoZWRFSoQVYu5ZSAnbBoxaWmW/IgnHSqIIvtMwzCoqmcA9j9rNQ==} + '@anthropic-ai/claude-agent-sdk@0.1.10': + resolution: {integrity: sha512-FDcJ0VI4x5u8/QRNJsYn+UnNqbVZMaf/DTHwJSaq+v3CqW8m+rW529H/DzGraw2F8YT9WqnJ9LCQdtPf9J5BNQ==} engines: {node: '>=18.0.0'} - hasBin: true - - '@anthropic-ai/sdk@0.20.9': - resolution: {integrity: sha512-Lq74+DhiEQO6F9/gdVOLmHx57pX45ebK2Q/zH14xYe1157a7QeUVknRqIp0Jz5gQI01o7NKbuv9Dag2uQsLjDg==} + peerDependencies: + zod: ^3.24.1 '@apidevtools/json-schema-ref-parser@11.7.2': resolution: {integrity: sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==} @@ -772,17 +770,13 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@openai/codex@0.42.0': - resolution: {integrity: sha512-jLpMrQuq1gIBzBKbKMwAzXOh+5uwE+ht3RHUb2Ov7P50fjAxPKDZa0+zpqkhHTspm8Rw6Vdrm4I4L+Z03usCkg==} - engines: {node: '>=20'} - hasBin: true - '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@posthog/code-agent@0.2.0': - resolution: {integrity: sha512-BeumrJYLxZxXBGvBiOWR6mjdkxyOHJBn8YTJvFY0Q4JXPMoc8q1cGI2thr0WCA5WwYca/9TLxElVzQoUW95vtg==} + '@posthog/agent@1.2.0': + resolution: {integrity: sha512-kgprtfv48kKmSIerIbe+bvpUPiCS2DAqPNRc/SSXms+HpCT0OB8/5mw8SsM6HKUEqUwzPjxruacekaZAdhalog==} + engines: {node: '>=20.0.0'} '@radix-ui/colors@3.0.0': resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==} @@ -1674,9 +1668,6 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node-fetch@2.6.13': - resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} - '@types/node@18.19.127': resolution: {integrity: sha512-gSjxjrnKXML/yo0BO099uPixMqfpJU0TKYjpfLU7TrtA2WWDki412Np/RSTPRil1saKBhvVVKzVx/p/6p94nVA==} @@ -1783,10 +1774,6 @@ packages: resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1801,10 +1788,6 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} - agentkeepalive@4.6.0: - resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} - engines: {node: '>= 8.0.0'} - ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -2228,10 +2211,6 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - diff@5.2.0: - resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} - engines: {node: '>=0.3.1'} - dir-compare@3.3.0: resolution: {integrity: sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==} @@ -2262,6 +2241,10 @@ packages: dotenv-expand@5.1.0: resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dotenv@9.0.2: resolution: {integrity: sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==} engines: {node: '>=10'} @@ -2419,10 +2402,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -2493,17 +2472,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data-encoder@1.7.2: - resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} - form-data@4.0.4: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} - formdata-node@4.4.1: - resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} - engines: {node: '>= 12.20'} - fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -2667,9 +2639,6 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} - humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - iconv-corefoundation@1.1.7: resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} engines: {node: ^8.11.2 || >=10} @@ -3061,19 +3030,6 @@ packages: node-addon-api@1.7.2: resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-releases@2.0.21: resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} @@ -3124,18 +3080,6 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - openai@4.104.0: - resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} - hasBin: true - peerDependencies: - ws: ^8.18.0 - zod: ^3.23.8 - peerDependenciesMeta: - ws: - optional: true - zod: - optional: true - openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -3705,9 +3649,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - truncate-utf8-bytes@1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} @@ -3877,20 +3818,6 @@ packages: engines: {node: '>=12.0.0'} hasBin: true - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - - web-streams-polyfill@4.0.0-beta.3: - resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} - engines: {node: '>= 14'} - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -3981,6 +3908,9 @@ packages: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} @@ -4002,7 +3932,9 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@anthropic-ai/claude-code@1.0.128': + '@anthropic-ai/claude-agent-sdk@0.1.10(zod@3.25.76)': + dependencies: + zod: 3.25.76 optionalDependencies: '@img/sharp-darwin-arm64': 0.33.5 '@img/sharp-darwin-x64': 0.33.5 @@ -4011,19 +3943,6 @@ snapshots: '@img/sharp-linux-x64': 0.33.5 '@img/sharp-win32-x64': 0.33.5 - '@anthropic-ai/sdk@0.20.9': - dependencies: - '@types/node': 18.19.127 - '@types/node-fetch': 2.6.13 - abort-controller: 3.0.0 - agentkeepalive: 4.6.0 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0 - web-streams-polyfill: 3.3.3 - transitivePeerDependencies: - - encoding - '@apidevtools/json-schema-ref-parser@11.7.2': dependencies: '@jsdevtools/ono': 7.1.3 @@ -4557,21 +4476,14 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@openai/codex@0.42.0': {} - '@pkgjs/parseargs@0.11.0': optional: true - '@posthog/code-agent@0.2.0': + '@posthog/agent@1.2.0(zod@3.25.76)': dependencies: - '@anthropic-ai/claude-code': 1.0.128 - '@anthropic-ai/sdk': 0.20.9 - '@openai/codex': 0.42.0 - diff: 5.2.0 - openai: 4.104.0 + '@anthropic-ai/claude-agent-sdk': 0.1.10(zod@3.25.76) + dotenv: 17.2.3 transitivePeerDependencies: - - encoding - - ws - zod '@radix-ui/colors@3.0.0': {} @@ -5481,11 +5393,6 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node-fetch@2.6.13': - dependencies: - '@types/node': 20.19.17 - form-data: 4.0.4 - '@types/node@18.19.127': dependencies: undici-types: 5.26.5 @@ -5629,10 +5536,6 @@ snapshots: '@xmldom/xmldom@0.8.11': {} - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -5645,10 +5548,6 @@ snapshots: transitivePeerDependencies: - supports-color - agentkeepalive@4.6.0: - dependencies: - humanize-ms: 1.2.1 - ajv-draft-04@1.0.0(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -6163,8 +6062,6 @@ snapshots: didyoumean@1.2.2: {} - diff@5.2.0: {} - dir-compare@3.3.0: dependencies: buffer-equal: 1.0.1 @@ -6212,6 +6109,8 @@ snapshots: dotenv-expand@5.1.0: {} + dotenv@17.2.3: {} + dotenv@9.0.2: {} dunder-proto@1.0.1: @@ -6558,8 +6457,6 @@ snapshots: esutils@2.0.3: {} - event-target-shim@5.0.1: {} - extract-zip@2.0.1: dependencies: debug: 4.4.3 @@ -6633,8 +6530,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data-encoder@1.7.2: {} - form-data@4.0.4: dependencies: asynckit: 0.4.0 @@ -6643,11 +6538,6 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - formdata-node@4.4.1: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 4.0.0-beta.3 - fraction.js@4.3.7: {} fs-constants@1.0.0: {} @@ -6853,10 +6743,6 @@ snapshots: transitivePeerDependencies: - supports-color - humanize-ms@1.2.1: - dependencies: - ms: 2.1.3 - iconv-corefoundation@1.1.7: dependencies: cli-truncate: 2.1.0 @@ -7218,12 +7104,6 @@ snapshots: node-addon-api@1.7.2: optional: true - node-domexception@1.0.0: {} - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - node-releases@2.0.21: {} normalize-path@3.0.0: {} @@ -7274,18 +7154,6 @@ snapshots: dependencies: wrappy: 1.0.2 - openai@4.104.0: - dependencies: - '@types/node': 18.19.127 - '@types/node-fetch': 2.6.13 - abort-controller: 3.0.0 - agentkeepalive: 4.6.0 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - openapi-types@12.1.3: {} openapi3-ts@4.5.0: @@ -8010,8 +7878,6 @@ snapshots: dependencies: is-number: 7.0.0 - tr46@0.0.3: {} - truncate-utf8-bytes@1.0.2: dependencies: utf8-byte-length: 1.0.5 @@ -8174,17 +8040,6 @@ snapshots: transitivePeerDependencies: - debug - web-streams-polyfill@3.3.3: {} - - web-streams-polyfill@4.0.0-beta.3: {} - - webidl-conversions@3.0.1: {} - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -8285,6 +8140,8 @@ snapshots: compress-commons: 4.1.2 readable-stream: 3.6.2 + zod@3.25.76: {} + zustand@4.5.7(@types/react@18.3.24)(react@18.3.1): dependencies: use-sync-external-store: 1.5.0(react@18.3.1) diff --git a/src/api/fetcher.ts b/src/api/fetcher.ts index 854eadd56f..4b8961c2c6 100644 --- a/src/api/fetcher.ts +++ b/src/api/fetcher.ts @@ -30,12 +30,22 @@ export const buildApiFetcher: (config: { } } - const response = await fetch(input.url, { - method: input.method.toUpperCase(), - ...(body && { body }), - headers, - ...input.overrides, - }); + let response: Response; + try { + response = await fetch(input.url, { + method: input.method.toUpperCase(), + ...(body && { body }), + headers, + ...input.overrides, + }); + } catch (err) { + throw new Error( + `Network request failed for ${input.method.toUpperCase()} ${input.url}: ${ + err instanceof Error ? err.message : String(err) + }`, + { cause: err instanceof Error ? err : undefined }, + ); + } if (!response.ok) { const errorResponse = await response.json(); diff --git a/src/main/index.ts b/src/main/index.ts index 6857934c71..29c5742ef5 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { mkdirSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { app, @@ -6,6 +7,7 @@ import { Menu, type MenuItemConstructorOptions, } from "electron"; +import dns from "node:dns"; import { registerAgentIpc, type TaskController } from "./services/agent.js"; import { registerOsIpc } from "./services/os.js"; import { registerPosthogIpc } from "./services/posthog.js"; @@ -19,6 +21,21 @@ const isDev = let mainWindow: BrowserWindow | null = null; const taskControllers = new Map(); +// Force IPv4 resolution when "localhost" is used so the agent hits 127.0.0.1 +// instead of ::1. This matches how the renderer already reaches the PostHog API. +dns.setDefaultResultOrder("ipv4first"); + +function ensureClaudeConfigDir(): void { + const existing = process.env.CLAUDE_CONFIG_DIR; + if (existing) return; + + const userDataDir = app.getPath("userData"); + const claudeDir = path.join(userDataDir, "claude"); + + mkdirSync(claudeDir, { recursive: true }); + process.env.CLAUDE_CONFIG_DIR = claudeDir; +} + function createWindow(): void { mainWindow = new BrowserWindow({ width: 900, @@ -89,6 +106,7 @@ function createWindow(): void { } app.whenReady().then(createWindow); +app.whenReady().then(ensureClaudeConfigDir); app.on("window-all-closed", () => { if (process.platform !== "darwin") { diff --git a/src/main/preload.ts b/src/main/preload.ts index df65110016..ba50ba45e1 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -11,8 +11,13 @@ interface MessageBoxOptions { } interface AgentStartParams { - prompt: string; + taskId: string; + workflowId: string; repoPath: string; + apiKey: string; + apiHost: string; + permissionMode?: string; + autoProgress?: boolean; model?: string; } diff --git a/src/main/services/agent.ts b/src/main/services/agent.ts index 69e107350a..08836c4176 100644 --- a/src/main/services/agent.ts +++ b/src/main/services/agent.ts @@ -1,10 +1,15 @@ -import { type Agent, ClaudeCodeAgent, createAgent } from "@posthog/code-agent"; +import { Agent, PermissionMode } from "@posthog/agent"; import { type BrowserWindow, type IpcMainInvokeEvent, ipcMain } from "electron"; -import { getCurrentBranch } from "./git.js"; +import { randomUUID } from "node:crypto"; interface AgentStartParams { - prompt: string; + taskId: string; + workflowId: string; repoPath: string; + apiKey: string; + apiHost: string; + permissionMode?: PermissionMode | string; + autoProgress?: boolean; model?: string; } @@ -12,6 +17,22 @@ export interface TaskController { abortController: AbortController; agent: Agent; channel: string; + taskId: string; + poller?: NodeJS.Timeout; +} + +function resolvePermissionMode( + mode: AgentStartParams["permissionMode"], +): PermissionMode { + if (!mode) return PermissionMode.ACCEPT_EDITS; + if (typeof mode !== "string") return mode; + + const normalized = mode.trim().toLowerCase(); + const match = (Object.values(PermissionMode) as string[]).find( + (value) => value.toLowerCase() === normalized, + ); + + return (match as PermissionMode | undefined) ?? PermissionMode.ACCEPT_EDITS; } export function registerAgentIpc( @@ -22,113 +43,167 @@ export function registerAgentIpc( "agent-start", async ( _event: IpcMainInvokeEvent, - { prompt, repoPath }: AgentStartParams, + { + taskId: posthogTaskId, + workflowId, + repoPath, + apiKey, + apiHost, + permissionMode, + autoProgress, + model, + }: AgentStartParams, ): Promise<{ taskId: string; channel: string }> => { - if (!prompt || !repoPath) { - throw new Error("prompt and repoPath are required"); + if (!posthogTaskId || !workflowId || !repoPath) { + throw new Error("taskId, workflowId, and repoPath are required"); } - const agent = createAgent(new ClaudeCodeAgent()); + if (!apiKey || !apiHost) { + throw new Error("PostHog API credentials are required"); + } - const abortController = new AbortController(); + // Provide credentials to the PostHog MCP server used inside the agent runtime. + process.env.POSTHOG_API_KEY = apiKey; + process.env.POSTHOG_API_HOST = apiHost; - const currentBranch = (await getCurrentBranch(repoPath)) || "unknown"; - - const fullPrompt = ` - - Repository: ${repoPath} - Branch: ${currentBranch} - You have access to the local repository files for fast read/write operations. - You also have access to GitHub via the GitHub MCP server for additional repository operations. - Work with local files for your main implementation, and use GitHub MCP for any additional repository queries. - Commit changes to the repository regularly. - - - - PostHog AI Coding Agent โ€” autonomously transform a ticket into a merge-ready pull request that follows existing project conventions. - - - - Local file system (for main implementation work) - PostHog MCP server (for PostHog operations) - - - - - Follow existing style and patterns you discover in the repo. - - Try not to add new external dependencies, only if needed. - - Implement structured logging and error handling; never log secrets. - - Avoid destructive shell commands. - - ALWAYS create appropriate .gitignore files to exclude build artifacts, dependencies, and temporary files. - - - - - Created or updated .gitignore file with appropriate exclusions - - Created dependency files (requirements.txt, package.json, etc.) with exact versions - - Added clear setup/installation instructions to README.md - - Code compiles and tests pass. - - Added or updated tests. - - Captured meaningful events with PostHog SDK. - - Wrapped new logic in an PostHog feature flag. - - Updated docs, readme or type hints if needed. - - Verified no build artifacts or dependencies are being committed - - - - ${prompt} - - - - Complete the ticket in a thoughtful step by step manner. Plan thoroughly and make sure to add logging and error handling as well as cover edge cases. - - - - - first make a plan and create a todo list - - execute the todo list one by one - - test the changes - - - - Once finished respond with a summary of changes made - - - - Use this area as a private scratch-pad for step-by-step reasoning; erase before final output. - - `; - - const { taskId, stream } = await agent.run({ - prompt: fullPrompt, - repoPath, - permissionMode: "permissive", + const taskId = randomUUID(); + const channel = `agent-event:${taskId}`; + + const abortController = new AbortController(); + const stderrBuffer: string[] = []; + + const emitToRenderer = (payload: unknown) => { + const win = getMainWindow?.(); + if (!win || win.isDestroyed()) return; + win.webContents.send(channel, payload); + }; + + const forwardClaudeStderr = (chunk: string | Buffer) => { + const text = chunk.toString().trim(); + if (!text) return; + stderrBuffer.push(text); + if (stderrBuffer.length > 50) { + stderrBuffer.shift(); + } + console.error(`[agent][claude-stderr] ${text}`); + emitToRenderer({ + type: "status", + ts: Date.now(), + message: `[Claude stderr] ${text}`, + }); + }; + + const agent = new Agent({ + workingDirectory: repoPath, + posthogApiKey: apiKey, + posthogApiUrl: apiHost, + onEvent: (event) => { + console.log("agent event", event); + if (!event || abortController.signal.aborted) return; + const payload = + event.type === "done" ? { ...event, success: true } : event; + emitToRenderer(payload); + }, + debug: true, }); - const channel = `agent-event:${taskId}`; + const controllerEntry: TaskController = { + abortController, + agent, + channel, + taskId: posthogTaskId, + }; - taskControllers.set(taskId, { abortController, agent, channel }); + taskControllers.set(taskId, controllerEntry); - // Forward streaming events to renderer - (async () => { - try { - for await (const ev of stream) { - const win = getMainWindow?.(); - if (win && !win.isDestroyed()) { - win.webContents.send(channel, ev); + const posthogClient = agent.getPostHogClient(); + if (posthogClient) { + const pollTaskProgress = async () => { + if (abortController.signal.aborted) return; + try { + const progress = await posthogClient.getTaskProgress(posthogTaskId); + if (progress?.has_progress) { + emitToRenderer({ + type: "progress", + ts: Date.now(), + progress, + }); } + } catch (err) { + console.warn("[agent] failed to fetch task progress", err); } - const win = getMainWindow?.(); - if (win && !win.isDestroyed()) { - win.webContents.send(channel, { type: "done", success: true }); - } + }; + + void pollTaskProgress(); + controllerEntry.poller = setInterval(() => { + void pollTaskProgress(); + }, 5000); + } + + emitToRenderer({ + type: "status", + ts: Date.now(), + phase: "workflow_start", + workflowId, + taskId: posthogTaskId, + }); + + (async () => { + const resolvedPermission = resolvePermissionMode(permissionMode); + try { + const envOverrides = { + ...process.env, + POSTHOG_API_KEY: apiKey, + POSTHOG_API_HOST: apiHost, + POSTHOG_AUTH_HEADER: `Bearer ${apiKey}`, + }; + + const mcpOverrides = {}; + + await agent.runWorkflow(posthogTaskId, workflowId, { + repositoryPath: repoPath, + permissionMode: resolvedPermission, + autoProgress: autoProgress ?? true, + queryOverrides: { + abortController, + ...(model ? { model } : {}), + stderr: forwardClaudeStderr, + env: envOverrides, + mcpServers: mcpOverrides, + }, + }); + emitToRenderer({ type: "done", success: true, ts: Date.now() }); } catch (err) { - const win = getMainWindow?.(); - if (win && !win.isDestroyed()) { - win.webContents.send(channel, { + console.error("[agent] workflow execution failed", err); + let errorMessage = + err instanceof Error ? err.message : String(err); + const cause = + err instanceof Error && "cause" in err && err.cause + ? ` (cause: ${String(err.cause)})` + : ""; + if (!abortController.signal.aborted) { + if (stderrBuffer.length > 0) { + const stderrSummary = stderrBuffer.slice(-5).join("\n"); + errorMessage += `\nLast Claude stderr:\n${stderrSummary}`; + } + emitToRenderer({ type: "error", - message: err instanceof Error ? err.message : String(err), + message: `${errorMessage}${cause}`, + ts: Date.now(), }); - win.webContents.send(channel, { type: "done", success: false }); + emitToRenderer({ type: "done", success: false, ts: Date.now() }); + } else { + emitToRenderer({ + type: "status", + ts: Date.now(), + phase: "canceled", + }); + emitToRenderer({ type: "done", success: false, ts: Date.now() }); } } finally { + if (controllerEntry.poller) { + clearInterval(controllerEntry.poller); + } taskControllers.delete(taskId); } })(); @@ -144,10 +219,9 @@ export function registerAgentIpc( if (!entry) return false; try { entry.abortController.abort(); - if (entry.agent && typeof entry.agent.cancel === "function") { - try { - await entry.agent.cancel(taskId); - } catch {} + entry.agent.cancelTask(entry.taskId); + if (entry.poller) { + clearInterval(entry.poller); } return true; } finally { diff --git a/src/renderer/components/TaskDetail.tsx b/src/renderer/components/TaskDetail.tsx index 391e9dccc1..48fd84f28e 100644 --- a/src/renderer/components/TaskDetail.tsx +++ b/src/renderer/components/TaskDetail.tsx @@ -8,15 +8,17 @@ import { Flex, Link, SegmentedControl, + Text, } from "@radix-ui/themes"; import type { Task } from "@shared/types"; import { format } from "date-fns"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useIntegrationStore } from "../stores/integrationStore"; import { useStatusBarStore } from "../stores/statusBarStore"; import { useTabStore } from "../stores/tabStore"; import { useTaskExecutionStore } from "../stores/taskExecutionStore"; import { useTaskStore } from "../stores/taskStore"; +import { useWorkflowStore } from "../stores/workflowStore"; import { AsciiArt } from "./AsciiArt"; import { Combobox } from "./Combobox"; import { LogView } from "./LogView"; @@ -38,13 +40,23 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) { const { repositories } = useIntegrationStore(); const { updateTask, tasks } = useTaskStore(); const { updateTabTitle, activeTabId } = useTabStore(); + const workflows = useWorkflowStore((state) => state.workflows); + const fetchWorkflows = useWorkflowStore((state) => state.fetchWorkflows); + const workflowOptions = useMemo( + () => + workflows.map((workflow) => ({ + value: workflow.id, + label: workflow.name, + })), + [workflows], + ); // Get the latest task data from the store const task = tasks.find((t) => t.id === initialTask.id) || initialTask; // Get persistent state for this task const taskState = getTaskState(task.id); - const { isRunning, logs, repoPath, runMode } = taskState; + const { isRunning, logs, repoPath, runMode, progress } = taskState; const [selectedRepo, setSelectedRepo] = useState(() => { if (task.repository_config) { @@ -73,6 +85,12 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) { } }, [task.description]); + useEffect(() => { + if (!workflows.length) { + void fetchWorkflows({ skipLoadingState: true }); + } + }, [workflows.length, fetchWorkflows]); + useEffect(() => { setStatusBar({ statusText: isRunning ? "Agent running..." : "Task details", @@ -115,6 +133,12 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) { clearTaskLogs(task.id); }; + const handleWorkflowChange = async (value: string) => { + const nextWorkflow = + !value || value === "__none__" ? null : (value as string); + await updateTask(task.id, { workflow: nextWorkflow }); + }; + const handleRepositoryChange = async (value: string) => { setSelectedRepo(value); @@ -209,6 +233,67 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) { + + Progress + + {progress?.has_progress ? ( + + + {progress.status?.replace(/_/g, " ") ?? "in progress"} + + + Steps {progress.completed_steps ?? 0}/ + {typeof progress.total_steps === "number" + ? progress.total_steps + : "?"} + {typeof progress.progress_percentage === "number" + ? ` ยท ${Math.round(progress.progress_percentage)}%` + : ""} + + {progress.current_step && ( + + Current step: {progress.current_step} + + )} + + ) : ( + + No progress yet + + )} + + + + + Workflow + + {workflowOptions.length > 0 ? ( + + ) : ( + + No workflows available + + )} + + + Repository diff --git a/src/renderer/stores/taskExecutionStore.ts b/src/renderer/stores/taskExecutionStore.ts index 8f0853caca..fcc9d73735 100644 --- a/src/renderer/stores/taskExecutionStore.ts +++ b/src/renderer/stores/taskExecutionStore.ts @@ -1,6 +1,54 @@ +import type { Task } from "@shared/types"; import { create } from "zustand"; import { persist } from "zustand/middleware"; import type { LogEntry } from "../types/log"; +import { useAuthStore } from "./authStore"; +import { useTaskStore } from "./taskStore"; +import { useWorkflowStore } from "./workflowStore"; + +type AgentTaskProgress = { + has_progress?: boolean; + status?: "started" | "in_progress" | "completed" | "failed"; + current_step?: string | null; + completed_steps?: number | null; + total_steps?: number | null; + progress_percentage?: number | null; + message?: string | null; + output_log?: string | null; +}; + +const createProgressSignature = (progress: AgentTaskProgress): string => + [ + progress.status ?? "", + progress.current_step ?? "", + progress.completed_steps ?? "", + progress.total_steps ?? "", + progress.progress_percentage ?? "", + progress.message ?? "", + progress.output_log ?? "", + ].join("|"); + +const formatProgressLog = (progress: AgentTaskProgress): string => { + const statusLabel = + progress.status?.replace(/_/g, " ") ?? "in progress"; + const completed = + typeof progress.completed_steps === "number" + ? progress.completed_steps + : 0; + const total = + typeof progress.total_steps === "number" && progress.total_steps >= 0 + ? progress.total_steps + : "?"; + const step = + typeof progress.current_step === "string" && progress.current_step.length > 0 + ? progress.current_step + : "-"; + const percentage = + typeof progress.progress_percentage === "number" + ? ` ${Math.round(progress.progress_percentage)}%` + : ""; + return `๐Ÿ“Š Progress: ${statusLabel} | step=${step} (${completed}/${total})${percentage}`; +}; interface TaskExecutionState { isRunning: boolean; @@ -9,6 +57,8 @@ interface TaskExecutionState { currentTaskId: string | null; runMode: "local" | "cloud"; unsubscribe: (() => void) | null; + progress: AgentTaskProgress | null; + progressSignature: string | null; } interface TaskExecutionStore { @@ -28,14 +78,12 @@ interface TaskExecutionStore { setCurrentTaskId: (taskId: string, currentTaskId: string | null) => void; setRunMode: (taskId: string, runMode: "local" | "cloud") => void; setUnsubscribe: (taskId: string, unsubscribe: (() => void) | null) => void; + setProgress: (taskId: string, progress: AgentTaskProgress | null) => void; clearTaskState: (taskId: string) => void; // High-level task execution actions selectRepositoryForTask: (taskId: string) => Promise; - runTask: ( - taskId: string, - task: { id: string; title: string; description?: string }, - ) => Promise; + runTask: (taskId: string, task: Task) => Promise; cancelTask: (taskId: string) => Promise; clearTaskLogs: (taskId: string) => void; @@ -51,6 +99,8 @@ const defaultTaskState: TaskExecutionState = { currentTaskId: null, runMode: "local", unsubscribe: null, + progress: null, + progressSignature: null, }; export const useTaskExecutionStore = create()( @@ -109,6 +159,13 @@ export const useTaskExecutionStore = create()( get().updateTaskState(taskId, { unsubscribe }); }, + setProgress: (taskId: string, progress: AgentTaskProgress | null) => { + get().updateTaskState(taskId, { + progress, + progressSignature: progress ? createProgressSignature(progress) : null, + }); + }, + clearTaskState: (taskId: string) => { const state = get(); const taskState = state.taskStates[taskId]; @@ -151,6 +208,26 @@ export const useTaskExecutionStore = create()( }); } break; + case "progress": { + const rawProgress = ev.progress as AgentTaskProgress | undefined; + if ( + rawProgress && + typeof rawProgress === "object" && + rawProgress.has_progress + ) { + const previousState = currentStore.getTaskState(taskId); + const nextSignature = createProgressSignature(rawProgress); + if (previousState.progressSignature !== nextSignature) { + currentStore.setProgress(taskId, rawProgress); + currentStore.addLog(taskId, { + type: "text", + ts: ev.ts || Date.now(), + content: formatProgressLog(rawProgress), + }); + } + } + break; + } case "status": if (ev.phase) { currentStore.addLog(taskId, { @@ -307,15 +384,35 @@ export const useTaskExecutionStore = create()( } }, - runTask: async ( - taskId: string, - task: { id: string; title: string; description?: string }, - ) => { + runTask: async (taskId: string, fallbackTask: Task) => { const store = get(); const taskState = store.getTaskState(taskId); if (taskState.isRunning) return; + const currentTask = + useTaskStore + .getState() + .tasks.find((candidate) => candidate.id === taskId) ?? + fallbackTask; + + if (!currentTask.workflow) { + store.addLog( + taskId, + "Select a PostHog workflow before running the agent.", + ); + return; + } + + const { apiKey, apiHost } = useAuthStore.getState(); + if (!apiKey) { + store.addLog( + taskId, + "No PostHog API key found. Sign in to PostHog to run workflows.", + ); + return; + } + // Ensure repo path is selected let effectiveRepoPath = taskState.repoPath; if (!effectiveRepoPath) { @@ -363,42 +460,52 @@ export const useTaskExecutionStore = create()( return; } - // Build prompt - const promptLines: string[] = []; - promptLines.push(`Task: ${task.title}`); - if (task.description) { - promptLines.push("Description:"); - promptLines.push(task.description); - } - const prompt = promptLines.join("\n"); - const currentTaskState = store.getTaskState(taskId); - + const workflowName = + useWorkflowStore + .getState() + .workflows.find((w) => w.id === currentTask.workflow)?.name ?? + currentTask.workflow; + const permissionMode = + currentTaskState.runMode === "cloud" ? "default" : "acceptEdits"; + + store.setProgress(taskId, null); store.setRunning(taskId, true); + const startTs = Date.now(); store.setLogs(taskId, [ { type: "text", - ts: Date.now(), - content: `Starting ${currentTaskState.runMode} Claude Code agent...`, + ts: startTs, + content: `Starting workflow run (${workflowName})...`, + }, + { + type: "text", + ts: startTs, + content: `Permission mode: ${permissionMode}`, }, { type: "text", - ts: Date.now(), + ts: startTs, content: `Repo: ${effectiveRepoPath}`, }, ]); try { const result = await window.electronAPI?.agentStart({ - prompt, + taskId: currentTask.id, + workflowId: currentTask.workflow, repoPath: effectiveRepoPath, - model: "claude-4-sonnet", + apiKey, + apiHost, + permissionMode, + autoProgress: true, }); if (!result) { store.addLog( taskId, "Failed to start agent: electronAPI not available", ); + store.setRunning(taskId, false); return; } const { taskId: executionTaskId, channel } = result; diff --git a/src/renderer/types/electron.d.ts b/src/renderer/types/electron.d.ts index 0db06cf90c..23eba63d6f 100644 --- a/src/renderer/types/electron.d.ts +++ b/src/renderer/types/electron.d.ts @@ -15,8 +15,13 @@ export interface IElectronAPI { }) => Promise<{ response: number }>; openExternal: (url: string) => Promise; agentStart: (params: { - prompt: string; + taskId: string; + workflowId: string; repoPath: string; + apiKey: string; + apiHost: string; + permissionMode?: string; + autoProgress?: boolean; model?: string; }) => Promise<{ taskId: string; channel: string }>; agentCancel: (taskId: string) => Promise;