diff --git a/package-lock.json b/package-lock.json index 5422befeb..75deea26a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "license": "Apache-2.0", "workspaces": [ "typescript/agentkit", + "typescript/create-onchain-agent", "typescript/framework-extensions/langchain", "typescript/examples/langchain-cdp-chatbot", "typescript/examples/langchain-twitter-chatbot", @@ -1852,6 +1853,16 @@ "@types/node": "*" } }, + "node_modules/@types/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -1962,6 +1973,17 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/prompts": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", + "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "kleur": "^3.0.3" + } + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -2979,6 +3001,15 @@ "node": ">=6.14.2" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3206,6 +3237,33 @@ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "dev": true }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3339,6 +3397,10 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/create-onchain-agent": { + "resolved": "typescript/create-onchain-agent", + "link": true + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -3349,7 +3411,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4787,6 +4848,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", @@ -5630,6 +5703,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -5865,8 +5950,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isomorphic-ws": { "version": "4.0.1", @@ -6750,7 +6834,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, "engines": { "node": ">=6" } @@ -7096,6 +7179,18 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -7588,6 +7683,131 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -7793,7 +8013,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -7816,8 +8035,7 @@ "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -8034,7 +8252,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -8440,6 +8657,49 @@ "node": ">=10" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -8710,7 +8970,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -8722,7 +8981,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -8825,8 +9083,7 @@ "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" }, "node_modules/slash": { "version": "3.0.0", @@ -8931,6 +9188,18 @@ "node": ">=8" } }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -9948,7 +10217,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -10253,6 +10521,25 @@ "viem": "^2.21.26" } }, + "typescript/create-onchain-agent": { + "version": "0.1.2", + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "cross-spawn": "^7.0.3", + "ora": "^8.1.0", + "picocolors": "^1.1.0", + "prompts": "^2.4.2" + }, + "bin": { + "create-onchain-agent": "dist/esm/cli.js" + }, + "devDependencies": { + "@types/cross-spawn": "^6.0.6", + "@types/node": "^20.12.10", + "@types/prompts": "^2.4.9" + } + }, "typescript/examples/langchain-cdp-chatbot": { "name": "@coinbase/cdp-langchain-chatbot-example", "version": "1.0.0", @@ -11990,6 +12277,15 @@ "@types/node": "*" } }, + "@types/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -12100,6 +12396,16 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "@types/prompts": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", + "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", + "dev": true, + "requires": { + "@types/node": "*", + "kleur": "^3.0.3" + } + }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -12816,6 +13122,11 @@ "node-gyp-build": "^4.3.0" } }, + "cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==" + }, "call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -12957,6 +13268,19 @@ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "dev": true }, + "cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "requires": { + "restore-cursor": "^5.0.0" + } + }, + "cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==" + }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -13065,6 +13389,19 @@ "prompts": "^2.0.1" } }, + "create-onchain-agent": { + "version": "file:typescript/create-onchain-agent", + "requires": { + "@types/cross-spawn": "^6.0.6", + "@types/node": "^20.12.10", + "@types/prompts": "^2.4.9", + "cac": "^6.7.14", + "cross-spawn": "^7.0.3", + "ora": "^8.1.0", + "picocolors": "^1.1.0", + "prompts": "^2.4.2" + } + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -13075,7 +13412,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -14131,6 +14467,11 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==" + }, "get-intrinsic": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", @@ -14702,6 +15043,11 @@ "is-extglob": "^2.1.1" } }, + "is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==" + }, "is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -14844,8 +15190,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "isomorphic-ws": { "version": "4.0.1", @@ -15513,8 +15858,7 @@ "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" }, "langsmith": { "version": "0.3.4", @@ -15781,6 +16125,11 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==" + }, "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -16131,6 +16480,78 @@ "word-wrap": "^1.2.5" } }, + "ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "requires": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" + }, + "chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==" + }, + "emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==" + }, + "is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==" + }, + "log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "requires": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "dependencies": { + "is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==" + } + } + }, + "string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, "own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -16269,8 +16690,7 @@ "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "path-parse": { "version": "1.0.7", @@ -16287,8 +16707,7 @@ "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "picomatch": { "version": "2.3.1", @@ -16445,7 +16864,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, "requires": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -16737,6 +17155,30 @@ "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true }, + "restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "requires": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "dependencies": { + "onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "requires": { + "mimic-function": "^5.0.0" + } + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + } + } + }, "retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -16932,7 +17374,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -16940,8 +17381,7 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, "side-channel": { "version": "1.1.0", @@ -17014,8 +17454,7 @@ "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" }, "slash": { "version": "3.0.0", @@ -17112,6 +17551,11 @@ } } }, + "stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==" + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -17826,7 +18270,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "requires": { "isexe": "^2.0.0" } diff --git a/package.json b/package.json index be9434790..5b29e315e 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "workspaces": [ "typescript/agentkit", + "typescript/create-onchain-agent", "typescript/framework-extensions/langchain", "typescript/examples/langchain-cdp-chatbot", "typescript/examples/langchain-twitter-chatbot", diff --git a/typescript/README.md b/typescript/README.md index a65e3700f..37b381ca7 100644 --- a/typescript/README.md +++ b/typescript/README.md @@ -16,3 +16,8 @@ LangChain extension of AgentKit. Enables agentic workflows to interact with onch See [AgentKit LangChain](./framework-extensions/langchain/README.md) to get started! +### `create-onchain-agent` + +A quickstart CLI tool to scaffold out a fullstack chatbot application using Coinbase AgentKit. Runnable via `npm create onchain-agent@latest`. + +See [Create Onchain Agent](./create-onchain-agent/README.md) to get started! \ No newline at end of file diff --git a/typescript/create-onchain-agent/.eslintrc.json b/typescript/create-onchain-agent/.eslintrc.json new file mode 100644 index 000000000..91571ba7a --- /dev/null +++ b/typescript/create-onchain-agent/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": ["../../.eslintrc.base.json"] +} diff --git a/typescript/create-onchain-agent/.npmignore b/typescript/create-onchain-agent/.npmignore new file mode 100644 index 000000000..1eed77ab4 --- /dev/null +++ b/typescript/create-onchain-agent/.npmignore @@ -0,0 +1,4 @@ +# Source +src/** +./node_modules/ +__tests__/** \ No newline at end of file diff --git a/typescript/create-onchain-agent/.prettierignore b/typescript/create-onchain-agent/.prettierignore new file mode 100644 index 000000000..cc592ae32 --- /dev/null +++ b/typescript/create-onchain-agent/.prettierignore @@ -0,0 +1,7 @@ +docs/ +dist/ +coverage/ +.github/ +src/client +**/**/*.json +*.md \ No newline at end of file diff --git a/typescript/create-onchain-agent/.prettierrc b/typescript/create-onchain-agent/.prettierrc new file mode 100644 index 000000000..ffb416b74 --- /dev/null +++ b/typescript/create-onchain-agent/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/create-onchain-agent/README.md b/typescript/create-onchain-agent/README.md new file mode 100644 index 000000000..4c38e5ee3 --- /dev/null +++ b/typescript/create-onchain-agent/README.md @@ -0,0 +1,73 @@ +# create-onchain-agent + +## Overview + +`create-onchain-agent` is a CLI tool powered by [AgentKit](https://github.com/coinbase/agentkit) that allows developers to quickly scaffold an **onchain agent** project. This tool simplifies the setup process by generating a project with predefined configurations, including blockchain network selection, wallet providers, and framework setup. + +## Prerequisites + +Before using `create-onchain-agent`, ensure you have the following installed: + +- **Node.js** (v18 or later) – [Download here](https://nodejs.org/) +- **npm** (v9 or later) – Comes bundled with Node.js + +## Usage + +To use `create-onchain-agent`, simply run: + +```sh +npm create onchain-agent@latest +``` + +This command will guide you through setting up an onchain agent project by prompting for necessary configuration options. + +## Features + +- **Guided setup**: Interactive prompts help configure the project. +- **Supports multiple blockchain networks**. +- **Select your preferred wallet provider**. +- **Preconfigured with Next.js, React, Tailwind CSS, and ESLint**. +- **Automates environment setup**. + +### **Setup Process** +The CLI will prompt you for the following details: + +1. **Project Name**: The name of your new onchain agent project. +2. **Package Name**: The npm package name (auto-formatted if needed). +3. **Network**: Choose from available blockchain networks. +4. **Chain ID**: Specify if using a custom network. +5. **Wallet Provider**: Select a preferred method for wallet management. + +After answering the prompts, the CLI will: + +- Generate the project structure. +- Copy necessary template files. +- Configure the selected settings. +- Display next steps to get started. + +## Getting Started + +Once your project is created, navigate into the directory and install dependencies: + +```sh +cd my-project +npm install +``` + +Then, configure your environment variables: + +```sh +mv .env.local .env +``` + +Run the development server: + +```sh +npm run dev +``` + +## Documentation & Support + +- **Docs:** [https://docs.cdp.coinbase.com/agentkit/docs/welcome](https://docs.cdp.coinbase.com/agentkit/docs/welcome) +- **GitHub Repo:** [http://github.com/coinbase/agentkit](http://github.com/coinbase/agentkit) +- **Community & Support:** [https://discord.gg/CDP](https://discord.gg/CDP) diff --git a/typescript/create-onchain-agent/package-lock.json b/typescript/create-onchain-agent/package-lock.json new file mode 100644 index 000000000..77bff5836 --- /dev/null +++ b/typescript/create-onchain-agent/package-lock.json @@ -0,0 +1,417 @@ +{ + "name": "create-onchain-agent", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "create-onchain-agent", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "cross-spawn": "^7.0.3", + "ora": "^8.1.0", + "picocolors": "^1.1.0", + "prompts": "^2.4.2" + }, + "bin": { + "create-onchain-agent": "dist/esm/cli.js" + }, + "devDependencies": { + "@types/cross-spawn": "^6.0.6", + "@types/node": "^20.12.10", + "@types/prompts": "^2.4.9" + } + }, + "node_modules/@types/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.17.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz", + "integrity": "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/prompts": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", + "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "kleur": "^3.0.3" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + } + } +} diff --git a/typescript/create-onchain-agent/package.json b/typescript/create-onchain-agent/package.json new file mode 100644 index 000000000..31a85abea --- /dev/null +++ b/typescript/create-onchain-agent/package.json @@ -0,0 +1,45 @@ +{ + "name": "create-onchain-agent", + "description": "Instantly create onchain-agent applications with Coinbase AgentKit.", + "version": "0.1.2", + "license": "MIT", + "scripts": { + "build": "npm run clean && npm run build:esm+types", + "build:esm+types": "tsc --project tsconfig.build.json --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint -c .eslintrc.json \"src/**/*.ts\"", + "lint:fix": "eslint -c .eslintrc.json \"src/**/*.ts\" --fix", + "check:types": "tsc --noEmit", + "clean": "rm -rf dist tsconfig.tsbuildinfo", + "dev": "node --loader ts-node/esm src/cli.ts" + }, + "files": [ + "dist/**", + "!dist/**/*.tsbuildinfo", + "src/**/*.ts", + "!src/**/*.test.ts", + "!src/**/*.test-d.ts", + "templates/**" + ], + "bin": { + "create-onchain-agent": "./dist/esm/cli.js" + }, + "sideEffects": false, + "type": "module", + "exports": { + "./package.json": "./package.json" + }, + "dependencies": { + "cac": "^6.7.14", + "cross-spawn": "^7.0.3", + "ora": "^8.1.0", + "picocolors": "^1.1.0", + "prompts": "^2.4.2" + }, + "devDependencies": { + "@types/cross-spawn": "^6.0.6", + "@types/node": "^20.12.10", + "@types/prompts": "^2.4.9" + } +} diff --git a/typescript/create-onchain-agent/src/cli.ts b/typescript/create-onchain-agent/src/cli.ts new file mode 100644 index 000000000..81e8cc1c3 --- /dev/null +++ b/typescript/create-onchain-agent/src/cli.ts @@ -0,0 +1,178 @@ +#!/usr/bin/env node +import fs from "fs"; +import ora from "ora"; +import path from "path"; +import pc from "picocolors"; +import prompts from "prompts"; +import { Networks, NetworkToWalletProviders } from "./constants.js"; +import { Network, WalletProviderChoice } from "./types.js"; +import { copyTemplate } from "./fileSystem.js"; +import { + handleSelection, + isValidPackageName, + toValidPackageName, + getWalletProviders, +} from "./utils.js"; + +/** + * Initializes the project creation process. + * + * - Prompts the user for project details including project name, package name, network, chain ID, and wallet provider. + * - Validates user input, ensuring directories do not already exist and package names are valid. + * - Copies the selected template to the new project directory. + * - Handles network and wallet provider selection logic. + * - Displays a summary of the created project along with next steps. + */ +async function init() { + console.log( + `${pc.blue(` + █████ ██████ ███████ ███ ██ ████████ ██ ██ ██ ████████ +██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ +███████ ██ ███ █████ ██ ██ ██ ██ █████ ██ ██ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ██ ██████ ███████ ██ ████ ██ ██ ██ ██ ██ + + Giving every AI agent a crypto wallet +`)}`, + ); + + const defaultProjectName = "onchain-agent"; + + let result: prompts.Answers< + "projectName" | "packageName" | "walletProvider" | "network" | "chainId" + >; + + try { + result = await prompts( + [ + { + type: "text", + name: "projectName", + message: pc.reset("Project name:"), + initial: defaultProjectName, + onState: state => { + state.value = state.value.trim(); + }, + validate: value => { + const targetDir = path.join(process.cwd(), value); + if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) { + return "Directory already exists and is not empty. Please choose a different name."; + } + return true; + }, + }, + { + type: (_, { projectName }: { projectName: string }) => + isValidPackageName(projectName) ? null : "text", + name: "packageName", + message: pc.reset("Package name:"), + initial: (_, { projectName }: { projectName: string }) => toValidPackageName(projectName), + validate: dir => isValidPackageName(dir) || "Invalid package.json name", + }, + { + type: "select", + name: "network", + message: pc.reset("Choose a network:"), + choices: Networks.map(network => ({ + title: network === "base-sepolia" ? `${network} (default)` : network, + value: network as Network | null, + })).concat([{ title: "other", value: null }]), + initial: Networks.indexOf("base-sepolia"), + }, + { + type: (prev, { network }) => (network === null ? "text" : null), + name: "chainId", + message: pc.reset("Enter your Ethereum chain ID:"), + validate: value => + value.trim() + ? Number.parseInt(value) + ? true + : "Chain ID must be a number." + : "Chain ID cannot be empty.", + }, + { + type: (prev, { network }) => + !network || NetworkToWalletProviders[network as Network].length > 1 ? "select" : null, + name: "walletProvider", + message: (prev, { network }) => { + const walletDescriptions: Record = { + CDP: "Uses Coinbase Developer Platform (CDP)'s managed wallet.", + Viem: "Client-side Ethereum wallet.", + Privy: "Authentication and wallet infrastructure.", + SolanaKeypair: "Client-side Solana wallet.", + }; + + const providerDescriptions = getWalletProviders(network) + .map(provider => ` - ${provider}: ${walletDescriptions[provider]}`) + .join("\n"); + + return pc.reset(`Choose a wallet provider:\n${providerDescriptions}\n`); + }, + choices: (prev, { network }) => { + const walletProviders = getWalletProviders(network); + return getWalletProviders(network).map(provider => ({ + title: provider === walletProviders[0] ? `${provider} (default)` : provider, + value: provider, + })); + }, + initial: 0, + }, + ], + { + onCancel: () => { + console.log("\nProject creation cancelled."); + process.exit(0); + }, + }, + ); + } catch (cancelled: unknown) { + if (cancelled instanceof Error) { + console.info(cancelled.message); + } else { + console.info("An unknown error occurred"); + } + process.exit(1); + } + const { projectName, packageName, network, chainId, walletProvider } = result; + + const spinner = ora(`Creating ${projectName}...`).start(); + + // Copy template over to new project + const root = await copyTemplate(projectName, packageName); + + // Handle selection-specific logic over copied-template + await handleSelection(root, walletProvider, network, chainId); + + spinner.succeed(); + console.log(pc.blueBright(`\nSuccessfully created your AgentKit project in ${root}`)); + + console.log(`\nFrameworks:`); + console.log(pc.gray("- AgentKit")); + console.log(pc.gray("- React")); + console.log(pc.gray("- Next.js")); + console.log(pc.gray("- Tailwind CSS")); + console.log(pc.gray("- ESLint")); + + console.log(pc.bold("\nWhat's Next?")); + + console.log(`\nTo get started, run the following commands:\n`); + if (root !== process.cwd()) { + console.log(` - cd ${path.relative(process.cwd(), root)}`); + } + console.log(" - npm install"); + console.log(pc.gray(" - # Open .env.local and configure your API keys")); + console.log(" - mv .env.local .env"); + console.log(" - npm run dev"); + + console.log(pc.bold("\nLearn more")); + console.log(" - Checkout the docs"); + console.log(pc.blueBright(" - https://docs.cdp.coinbase.com/agentkit/docs/welcome")); + console.log(" - Visit the repo"); + console.log(pc.blueBright(" - http://github.com/coinbase/agentkit")); + console.log(" - Join the community"); + console.log(pc.blueBright(" - https://discord.gg/CDP\n")); +} + +init().catch(e => { + console.error(e); +}); diff --git a/typescript/create-onchain-agent/src/constants.ts b/typescript/create-onchain-agent/src/constants.ts new file mode 100644 index 000000000..ecabf459e --- /dev/null +++ b/typescript/create-onchain-agent/src/constants.ts @@ -0,0 +1,137 @@ +import { + EVMNetwork, + Network, + SVMNetwork, + WalletProviderChoice, + WalletProviderRouteConfiguration, +} from "./types"; + +export const EVM_NETWORKS: Set = new Set([ + "ethereum-mainnet", + "ethereum-sepolia", + "polygon-mainnet", + "polygon-mumbai", + "base-mainnet", + "base-sepolia", + "arbitrum-mainnet", + "arbitrum-sepolia", + "optimism-mainnet", + "optimism-sepolia", +]); + +export const SVM_NETWORKS: Set = new Set([ + "solana-mainnet", + "solana-devnet", + "solana-testnet", +]); + +const CDP_SUPPORTED_EVM_WALLET_PROVIDERS: WalletProviderChoice[] = ["CDP", "Viem", "Privy"]; +const SVM_WALLET_PROVIDERS: WalletProviderChoice[] = ["SolanaKeypair", "Privy"]; +export const NON_CDP_SUPPORTED_EVM_WALLET_PROVIDERS: WalletProviderChoice[] = ["Viem", "Privy"]; + +export const NetworkToWalletProviders: Record = { + "arbitrum-mainnet": CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + "arbitrum-sepolia": NON_CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + "base-mainnet": CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + "base-sepolia": CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + "ethereum-mainnet": CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + "ethereum-sepolia": NON_CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + "optimism-mainnet": NON_CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + "optimism-sepolia": NON_CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + "polygon-mainnet": CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + "polygon-mumbai": NON_CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + "solana-mainnet": SVM_WALLET_PROVIDERS, + "solana-devnet": SVM_WALLET_PROVIDERS, + "solana-testnet": SVM_WALLET_PROVIDERS, +}; + +export const Networks: Network[] = [...EVM_NETWORKS, ...SVM_NETWORKS]; + +export const WalletProviderChoices: WalletProviderChoice[] = [ + ...new Set([ + ...CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + ...NON_CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + ...SVM_WALLET_PROVIDERS, + ]), +]; + +export const WalletProviderRouteConfigurations: Record< + "EVM" | "SVM", + Partial> +> = { + EVM: { + CDP: { + env: { + topComments: ["Get keys from CDP Portal: https://portal.cdp.coinbase.com/"], + required: ["CDP_API_KEY_NAME=", "CDP_API_KEY_PRIVATE_KEY="], + optional: [], + }, + apiRoute: "evm/cdp/route.ts", + }, + Viem: { + env: { + topComments: [ + "Export private key from your Ethereum wallet and save", + "Get keys from CDP Portal: https://portal.cdp.coinbase.com/", + ], + required: ["PRIVATE_KEY=", "CDP_API_KEY_NAME=", "CDP_API_KEY_PRIVATE_KEY="], + optional: [], + }, + apiRoute: "evm/viem/route.ts", + }, + Privy: { + env: { + topComments: [ + "Get keys from Privy Dashboard: https://dashboard.privy.io/", + "Get keys from CDP Portal: https://portal.cdp.coinbase.com/", + ], + required: [ + "PRIVY_APP_ID=", + "PRIVY_APP_SECRET=", + "CDP_API_KEY_NAME=", + "CDP_API_KEY_PRIVATE_KEY=", + ], + optional: [ + "CHAIN_ID=", + "PRIVY_WALLET_ID=", + "PRIVY_WALLET_AUTHORIZATION_PRIVATE_KEY=", + "PRIVY_WALLET_AUTHORIZATION_KEY_ID=", + ], + }, + apiRoute: "evm/privy/route.ts", + }, + }, + SVM: { + SolanaKeypair: { + env: { + topComments: [ + "Export private key from your Solana wallet and save", + "Get keys from CDP Portal: https://portal.cdp.coinbase.com/", + ], + required: ["SOLANA_PRIVATE_KEY=", "CDP_API_KEY_NAME=", "CDP_API_KEY_PRIVATE_KEY="], + optional: ["SOLANA_RPC_URL="], + }, + apiRoute: "svm/solanaKeypair/route.ts", + }, + Privy: { + env: { + topComments: [ + "Get keys from Privy Dashboard: https://dashboard.privy.io/", + "Get keys from CDP Portal: https://portal.cdp.coinbase.com/", + ], + required: [ + "PRIVY_APP_ID=", + "PRIVY_APP_SECRET=", + "CDP_API_KEY_NAME=", + "CDP_API_KEY_PRIVATE_KEY=", + ], + optional: [ + "PRIVY_WALLET_ID=", + "PRIVY_WALLET_AUTHORIZATION_PRIVATE_KEY=", + "PRIVY_WALLET_AUTHORIZATION_KEY_ID=", + ], + }, + apiRoute: "svm/privy/route.ts", + }, + }, +}; diff --git a/typescript/create-onchain-agent/src/fileSystem.ts b/typescript/create-onchain-agent/src/fileSystem.ts new file mode 100644 index 000000000..94f242676 --- /dev/null +++ b/typescript/create-onchain-agent/src/fileSystem.ts @@ -0,0 +1,78 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { optimizedCopy, toValidPackageName } from "./utils.js"; + +const sourceDir = path.resolve(fileURLToPath(import.meta.url), "../../../templates/next"); + +const renameFiles: Record = { + _gitignore: ".gitignore", + "_env.local": ".env.local", +}; + +const excludeDirs = ["node_modules", ".next"]; +const excludeFiles = [".DS_Store", "Thumbs.db"]; + +/** + * Retrieves the source directory for copying template files. + * + * @returns {string} The source directory path. + */ +function getSourceDir(): string { + return sourceDir; +} + +/** + * Recursively copies a directory and its contents from the source to the destination. + * + * - Creates the destination directory if it does not exist. + * - Copies files, renaming them if specified in `renameFiles`. + * - Skips directories listed in `excludeDirs`. + * - Skips files listed in `excludeFiles`. + * + * @param {string} src - The source directory path. + * @param {string} dest - The destination directory path. + * @returns {Promise} A promise that resolves when the copy operation is complete. + */ +async function copyDir(src: string, dest: string): Promise { + await fs.promises.mkdir(dest, { recursive: true }); + const entries = await fs.promises.readdir(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, renameFiles[entry.name] || entry.name); + + if (entry.isDirectory()) { + if (!excludeDirs.includes(entry.name)) { + await copyDir(srcPath, destPath); + } + } else { + if (!excludeFiles.includes(entry.name)) { + await optimizedCopy(srcPath, destPath); + } + } + } +} + +/** + * Copies a project template to a new directory and updates the package name. + * + * - Copies the template from the source directory. + * - Updates the `package.json` file with the provided package name. + * + * @param {string} projectName - The name of the new project directory. + * @param {string} packageName - The npm package name to use (auto-formatted if empty). + * @returns {Promise} The path of the newly created project directory. + */ +export async function copyTemplate(projectName: string, packageName: string): Promise { + const root = path.join(process.cwd(), projectName); + await copyDir(getSourceDir(), root); + + const pkgPath = path.join(root, "package.json"); + const pkg = JSON.parse(await fs.promises.readFile(pkgPath, "utf-8")); + pkg.name = packageName || toValidPackageName(projectName); + + await fs.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2)); + + return root; +} diff --git a/typescript/create-onchain-agent/src/types.ts b/typescript/create-onchain-agent/src/types.ts new file mode 100644 index 000000000..8fa13ae3f --- /dev/null +++ b/typescript/create-onchain-agent/src/types.ts @@ -0,0 +1,26 @@ +export type EVMNetwork = + | "ethereum-mainnet" + | "ethereum-sepolia" + | "polygon-mainnet" + | "polygon-mumbai" + | "base-mainnet" + | "base-sepolia" + | "arbitrum-mainnet" + | "arbitrum-sepolia" + | "optimism-mainnet" + | "optimism-sepolia"; + +export type SVMNetwork = "solana-mainnet" | "solana-devnet" | "solana-testnet"; + +export type Network = EVMNetwork | SVMNetwork; + +export type WalletProviderChoice = "CDP" | "Viem" | "Privy" | "SolanaKeypair"; + +export type WalletProviderRouteConfiguration = { + env: { + topComments: string[]; + required: string[]; + optional: string[]; + }; + apiRoute: string; +}; diff --git a/typescript/create-onchain-agent/src/utils.ts b/typescript/create-onchain-agent/src/utils.ts new file mode 100644 index 000000000..dd38ae6d2 --- /dev/null +++ b/typescript/create-onchain-agent/src/utils.ts @@ -0,0 +1,238 @@ +import fs from "fs/promises"; +import path from "path"; +import { EVMNetwork, Network, SVMNetwork, WalletProviderChoice } from "./types"; +import { + EVM_NETWORKS, + NetworkToWalletProviders, + NON_CDP_SUPPORTED_EVM_WALLET_PROVIDERS, + SVM_NETWORKS, + WalletProviderRouteConfigurations, +} from "./constants.js"; + +/** + * Determines the network family based on the provided network. + * + * @param {EVMNetwork | SVMNetwork} network - The network to check. + * @returns {"EVM" | "SVM" | undefined} The network family, or `undefined` if not recognized. + */ +export function getNetworkFamily(network: EVMNetwork | SVMNetwork): "EVM" | "SVM" | undefined { + return EVM_NETWORKS.has(network as EVMNetwork) + ? "EVM" + : SVM_NETWORKS.has(network as SVMNetwork) + ? "SVM" + : undefined; +} + +/** + * Copies a file from the source path to the destination path. + * + * @param {string} src - The source file path. + * @param {string} dest - The destination file path. + * @returns {Promise} A promise that resolves when the file is copied. + */ +async function copyFile(src: string, dest: string): Promise { + await fs.copyFile(src, dest); +} + +/** + * Recursively copies a directory from the source path to the destination path. + * + * @param {string} src - The source directory path. + * @param {string} dest - The destination directory path. + * @returns {Promise} A promise that resolves when the directory and its contents are copied. + */ +async function copyDir(src: string, dest: string): Promise { + await fs.mkdir(dest, { recursive: true }); + const entries = await fs.readdir(src, { withFileTypes: true }); + + await Promise.all( + entries.map(async entry => { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + if (entry.isDirectory()) { + await copyDir(srcPath, destPath); + } else { + await copyFile(srcPath, destPath); + } + }), + ); +} +/** + * Recursively copies a file or directory from the source path to the destination path. + * + * @param {string} src - The source file or directory path. + * @param {string} dest - The destination file or directory path. + * @returns {Promise} A promise that resolves when the copy operation is complete. + */ +export async function optimizedCopy(src: string, dest: string): Promise { + const stat = await fs.stat(src); + if (stat.isDirectory()) { + await copyDir(src, dest); + } else { + await copyFile(src, dest); + } +} + +/** + * Generates a clickable terminal hyperlink using ANSI escape codes. + * + * @param {string} text - The display text for the link. + * @param {string} url - The URL the link points to. + * @returns {string} The formatted clickable link string. + */ +export function createClickableLink(text: string, url: string): string { + // OSC 8 ;; URL \a TEXT \a + return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`; +} + +/** + * Validates whether a given string is a valid npm package name. + * + * @param {string} projectName - The package name to validate. + * @returns {boolean} `true` if the package name is valid, otherwise `false`. + */ +export function isValidPackageName(projectName: string): boolean { + return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(projectName); +} + +/** + * Converts a given project name into a valid npm package name. + * + * @param {string} projectName - The input project name. + * @returns {string} A sanitized, valid npm package name. + */ +export function toValidPackageName(projectName: string): string { + return projectName + .trim() + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/^[._]/, "") + .replace(/[^a-z\d\-~]+/g, "-"); +} + +/** + * Detects the package manager currently being used. + * + * Checks the `npm_config_user_agent` environment variable to determine if `npm`, `yarn`, `pnpm`, or `bun` is being used. + * If no package manager is detected, it defaults to `npm`. + * + * @returns {string} The detected package manager (`npm`, `yarn`, `pnpm`, or `bun`). + */ +export function detectPackageManager(): string { + if (process.env.npm_config_user_agent) { + if (process.env.npm_config_user_agent.startsWith("yarn")) { + return "yarn"; + } + if (process.env.npm_config_user_agent.startsWith("pnpm")) { + return "pnpm"; + } + if (process.env.npm_config_user_agent.startsWith("npm")) { + return "npm"; + } + if (process.env.npm_config_user_agent.startsWith("bun")) { + return "bun"; + } + } + return "npm"; // Default to npm if unable to detect +} + +/** + * Retrieves the available wallet providers for a given blockchain network. + * + * - If a `network` is provided, returns the corresponding wallet providers. + * - If no network is specified, returns a default list of EVM wallet providers. + * + * @param {Network} [network] - The optional network to get wallet providers for. + * @returns {WalletProviderChoice[]} An array of wallet providers for the specified network. + */ +export const getWalletProviders = (network?: Network): WalletProviderChoice[] => { + if (network) { + return NetworkToWalletProviders[network]; + } + return NON_CDP_SUPPORTED_EVM_WALLET_PROVIDERS; +}; + +/** + * Handles the selection of a network and wallet provider, updating the project configuration accordingly. + * + * This function: + * - Determines the network family (`EVM` or `SVM`) based on the provided network or chain ID. + * - Retrieves the correct route configuration for the selected wallet provider. + * - Creates or updates the `.env.local` file with required and optional environment variables. + * - Moves the selected API route file to `api/agent/route.ts`. + * - Deletes all unselected API routes and cleans up empty directories. + * + * @param {string} root - The root directory of the project. + * @param {WalletProviderChoice} walletProvider - The selected wallet provider. + * @param {Network} [network] - The optional blockchain network. + * @param {string} [chainId] - The optional chain ID for the network. + * @throws {Error} If neither `network` nor `chainId` are provided, or if the selected combination is invalid. + * @returns {Promise} A promise that resolves when the selection process is complete. + */ +export async function handleSelection( + root: string, + walletProvider: WalletProviderChoice, + network?: Network, + chainId?: string, +) { + const agentDir = path.join(root, "app", "api", "agent"); + + let networkFamily: ReturnType; + if (network) { + networkFamily = getNetworkFamily(network); + } else if (chainId) { + networkFamily = "EVM"; + } else { + throw new Error("Unsupported network and chainId selected"); + } + + const selectedRouteConfig = WalletProviderRouteConfigurations[networkFamily!][walletProvider]; + + if (!selectedRouteConfig) { + throw new Error("Selected invalid network & wallet provider combination"); + } + + // Create .env file + const envPath = path.join(root, ".env.local"); + const envLines = [ + // Start file with notes regarding .env var setup + ...[ + "Get keys from OpenAI Platform: https://platform.openai.com/api-keys", + ...selectedRouteConfig.env.topComments, + ] + .map(comment => `# ${comment}`) + .join("\n"), + // Continue with # Required section + "\n\n# Required\n", + ...["OPENAI_API_KEY=", ...selectedRouteConfig.env.required].join("\n"), + // Finish with # Optional section + "\n\n# Optional\n", + ...[`NETWORK_ID=${network}`, ...selectedRouteConfig.env.optional].join("\n"), + ]; + await fs.writeFile(envPath, envLines); + + // Promote selected route (move `apiRoute` to `api/agent/route.ts`) + const selectedRoutePath = path.join(agentDir, selectedRouteConfig.apiRoute); + const newRoutePath = path.join(agentDir, "route.ts"); + + await fs.rename(selectedRoutePath, newRoutePath); + + // Delete all unselected routes + const allRouteConfigurations = Object.values(WalletProviderRouteConfigurations) + .flatMap(routeConfigurations => Object.values(routeConfigurations)) + .filter(x => x); + const providerRoutes = allRouteConfigurations.map(config => path.join(agentDir, config.apiRoute)); + for (const routePath of providerRoutes) { + // Remove file + await fs.rm(routePath, { recursive: true, force: true }); + + // If directory is empty, remove directory + const parentFolder = path.dirname(routePath); + const files = await fs.readdir(parentFolder); + if (files.length === 0) { + await fs.rm(parentFolder, { recursive: true, force: true }); + } + } + await fs.rm(path.join(agentDir, "evm"), { recursive: true, force: true }); + await fs.rm(path.join(agentDir, "svm"), { recursive: true, force: true }); +} diff --git a/typescript/create-onchain-agent/templates/next/.eslintrc.json b/typescript/create-onchain-agent/templates/next/.eslintrc.json new file mode 100644 index 000000000..372241854 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/typescript/create-onchain-agent/templates/next/.gitignore b/typescript/create-onchain-agent/templates/next/.gitignore new file mode 100644 index 000000000..ba6ecc8ce --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz +package-lock.json + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/typescript/create-onchain-agent/templates/next/.yarnrc.yml b/typescript/create-onchain-agent/templates/next/.yarnrc.yml new file mode 100644 index 000000000..14fd359c8 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/.yarnrc.yml @@ -0,0 +1,2 @@ +# You can remove this file if you don't want to use Yarn package manager. +nodeLinker: node-modules \ No newline at end of file diff --git a/typescript/create-onchain-agent/templates/next/README.md b/typescript/create-onchain-agent/templates/next/README.md new file mode 100644 index 000000000..33be04f06 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/README.md @@ -0,0 +1,67 @@ +# Onchain Agent Powered by AgentKit + +This is a [Next.js](https://nextjs.org) project bootstrapped with `create-onchain-agent`. + +It integrates [AgentKit](https://github.com/coinbase/agentkit) to provide AI-driven interactions with on-chain capabilities. + +## Getting Started + +First, install dependencies: + +```sh +npm install +``` + +Then, configure your environment variables: + +```sh +mv .env.local .env +``` + +Run the development server: + +```sh +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser to see the project. + + +## Configuring Your Agent + +You can [modify your configuration](https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#usage) of the agent. By default, your agent configuration occurs in the `/api/agent/route.ts` file. + +### 1. Select Your LLM +Modify the `ChatOpenAI` instantiation to use the model of your choice. + +### 2. Select Your Wallet Provider +AgentKit requires a **Wallet Provider** to interact with blockchain networks. + +### 3. Select Your Action Providers +Action Providers define what your agent can do. You can use built-in providers or create your own. + +--- + +## Next Steps + +- Explore the AgentKit README: [AgentKit Documentation](https://github.com/coinbase/agentkit) +- Learn more about available Wallet Providers & Action Providers. +- Experiment with custom Action Providers for your specific use case. + +--- + +## Learn More + +- [Learn more about CDP](https://docs.cdp.coinbase.com/) +- [Learn more about AgentKit](https://docs.cdp.coinbase.com/agentkit/docs/welcome) +- [Learn more about Next.js](https://nextjs.org/docs) +- [Learn more about Tailwind CSS](https://tailwindcss.com/docs) + +--- + +## Contributing + +Interested in contributing to AgentKit? Follow the contribution guide: + +- [Contribution Guide](https://github.com/coinbase/agentkit/blob/main/CONTRIBUTING.md) +- Join the discussion on [Discord](https://discord.gg/CDP) diff --git a/typescript/create-onchain-agent/templates/next/app/api/agent/evm/cdp/route.ts b/typescript/create-onchain-agent/templates/next/app/api/agent/evm/cdp/route.ts new file mode 100644 index 000000000..7473039c8 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/app/api/agent/evm/cdp/route.ts @@ -0,0 +1,200 @@ +import { NextResponse } from "next/server"; +import { + AgentKit, + cdpApiActionProvider, + cdpWalletActionProvider, + CdpWalletProvider, + erc20ActionProvider, + pythActionProvider, + walletActionProvider, + wethActionProvider, +} from "@coinbase/agentkit"; +import { getLangChainTools } from "@coinbase/agentkit-langchain"; +import { ChatOpenAI } from "@langchain/openai"; +import { MemorySaver } from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { AgentRequest, AgentResponse } from "@/app/types/api"; +import * as fs from "fs"; + +/** + * AgentKit Integration Route + * + * This file is your gateway to integrating AgentKit with your product. + * It defines the interaction between your system and the AI agent, + * allowing you to configure the agent to suit your needs. + * + * # Key Steps to Customize Your Agent:** + * + * 1. Select your LLM: + * - Modify the `ChatOpenAI` instantiation to choose your preferred LLM. + * + * 2. Set up your WalletProvider: + * - Learn more: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#evm-wallet-providers + * + * 3️. Set up your ActionProviders: + * - ActionProviders define what your agent can do. + * - Choose from built-in providers or create your own: + * - Built-in: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#action-providers + * - Custom: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#creating-an-action-provider + * + * 4. Instantiate your Agent: + * - Pass the LLM, tools, and memory into `createReactAgent()` to bring your agent to life. + * + * # Next Steps: + * - Explore the AgentKit README: https://github.com/coinbase/agentkit + * - Learn more about available WalletProviders & ActionProviders. + * - Experiment with custom ActionProviders for your unique use case. + * + * ## Want to contribute? + * Join us in shaping AgentKit! Check out the contribution guide: + * - https://github.com/coinbase/agentkit/blob/main/CONTRIBUTING.md + * - https://discord.gg/CDP + */ + +// The agent +let agent: ReturnType; + +// Configure a file to persist the agent's CDP MPC Wallet Data +const WALLET_DATA_FILE = "wallet_data.txt"; + +/** + * Initializes and returns an instance of the AI agent. + * If an agent instance already exists, it returns the existing one. + * + * @function getOrInitializeAgent + * @returns {Promise>} The initialized AI agent. + * + * @description Handles agent setup + * + * @throws {Error} If the agent initialization fails. + */ +async function getOrInitializeAgent(): Promise> { + // If agent has already been initialized, return it + if (agent) { + return agent; + } + + try { + // Initialize LLM: https://platform.openai.com/docs/models#gpt-4o + const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); + + let walletDataStr: string | null = null; + + // Read existing wallet data if available + if (fs.existsSync(WALLET_DATA_FILE)) { + try { + walletDataStr = fs.readFileSync(WALLET_DATA_FILE, "utf8"); + } catch (error) { + console.error("Error reading wallet data:", error); + // Continue without wallet data + } + } + + // Initialize WalletProvider: https://docs.cdp.coinbase.com/agentkit/docs/wallet-management + const walletProvider = await CdpWalletProvider.configureWithWallet({ + apiKeyName: process.env.CDP_API_KEY_NAME, + apiKeyPrivateKey: process.env.CDP_API_KEY_PRIVATE_KEY, + networkId: process.env.NETWORK_ID || "base-sepolia", + cdpWalletData: walletDataStr || undefined, + }); + + // Initialize AgentKit: https://docs.cdp.coinbase.com/agentkit/docs/agent-actions + const agentkit = await AgentKit.from({ + walletProvider, + actionProviders: [ + wethActionProvider(), + pythActionProvider(), + walletActionProvider(), + erc20ActionProvider(), + cdpApiActionProvider({ + apiKeyName: process.env.CDP_API_KEY_NAME, + apiKeyPrivateKey: process.env.CDP_API_KEY_PRIVATE_KEY, + }), + cdpWalletActionProvider({ + apiKeyName: process.env.CDP_API_KEY_NAME, + apiKeyPrivateKey: process.env.CDP_API_KEY_PRIVATE_KEY, + }), + ], + }); + const tools = await getLangChainTools(agentkit); + const memory = new MemorySaver(); + + // Initialize Agent + const canUseFaucet = walletProvider.getNetwork().networkId == "base-sepolia"; + const faucetMessage = `If you ever need funds, you can request them from the faucet.`; + const cantUseFaucetMessage = `If you need funds, you can provide your wallet details and request funds from the user.`; + agent = createReactAgent({ + llm, + tools, + checkpointSaver: memory, + messageModifier: ` + You are a helpful agent that can interact onchain using the Coinbase Developer Platform AgentKit. You are + empowered to interact onchain using your tools. ${canUseFaucet ? faucetMessage : cantUseFaucetMessage}. + Before executing your first action, get the wallet details to see what network + you're on. If there is a 5XX (internal) HTTP error code, ask the user to try again later. If someone + asks you to do something you can't do with your currently available tools, you must say so, and + encourage them to implement it themselves using the CDP SDK + Agentkit, recommend they go to + docs.cdp.coinbase.com for more information. Be concise and helpful with your responses. Refrain from + restating your tools' descriptions unless it is explicitly requested. + `, + }); + + // Save wallet data + const exportedWallet = await walletProvider.exportWallet(); + fs.writeFileSync(WALLET_DATA_FILE, JSON.stringify(exportedWallet)); + + return agent; + } catch (error) { + console.error("Error initializing agent:", error); + throw new Error("Failed to initialize agent"); + } +} + +/** + * Handles incoming POST requests to interact with the AgentKit-powered AI agent. + * This function processes user messages and streams responses from the agent. + * + * @function POST + * @param {Request & { json: () => Promise }} req - The incoming request object containing the user message. + * @returns {Promise>} JSON response containing the AI-generated reply or an error message. + * + * @description Sends a single message to the agent and returns the agents' final response. + * + * @example + * const response = await fetch("/api/agent", { + * method: "POST", + * headers: { "Content-Type": "application/json" }, + * body: JSON.stringify({ userMessage: input }), + * }); + */ +export async function POST( + req: Request & { json: () => Promise }, +): Promise> { + try { + // 1️. Extract user message from the request body + const { userMessage } = await req.json(); + + // 2. Get the agent + const agent = await getOrInitializeAgent(); + + // 3.Start streaming the agent's response + const stream = await agent.stream( + { messages: [{ content: userMessage, role: "user" }] }, // The new message to send to the agent + { configurable: { thread_id: "AgentKit Discussion" } }, // Customizable thread ID for tracking conversations + ); + + // 4️. Process the streamed response chunks into a single message + let agentResponse = ""; + for await (const chunk of stream) { + if ("agent" in chunk) { + agentResponse += chunk.agent.messages[0].content; + } + } + + // 5️. Return the final response + return NextResponse.json({ response: agentResponse }); + } catch (error) { + console.error("Error processing request:", error); + return NextResponse.json({ error: "Failed to process message" }); + } +} diff --git a/typescript/create-onchain-agent/templates/next/app/api/agent/evm/privy/route.ts b/typescript/create-onchain-agent/templates/next/app/api/agent/evm/privy/route.ts new file mode 100644 index 000000000..f3baeb47c --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/app/api/agent/evm/privy/route.ts @@ -0,0 +1,178 @@ +import { AgentRequest, AgentResponse } from "@/app/types/api"; +import { + AgentKit, + cdpApiActionProvider, + erc20ActionProvider, + PrivyWalletProvider, + pythActionProvider, + walletActionProvider, + wethActionProvider, +} from "@coinbase/agentkit"; +import { getLangChainTools } from "@coinbase/agentkit-langchain"; +import { MemorySaver } from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { ChatOpenAI } from "@langchain/openai"; +import { NextResponse } from "next/server"; + +/** + * AgentKit Integration Route + * + * This file is your gateway to integrating AgentKit with your product. + * It defines the interaction between your system and the AI agent, + * allowing you to configure the agent to suit your needs. + * + * # Key Steps to Customize Your Agent:** + * + * 1. Select your LLM: + * - Modify the `ChatOpenAI` instantiation to choose your preferred LLM. + * + * 2. Set up your WalletProvider: + * - Learn more: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#evm-wallet-providers + * + * 3️. Set up your ActionProviders: + * - ActionProviders define what your agent can do. + * - Choose from built-in providers or create your own: + * - Built-in: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#action-providers + * - Custom: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#creating-an-action-provider + * + * 4. Instantiate your Agent: + * - Pass the LLM, tools, and memory into `createReactAgent()` to bring your agent to life. + * + * # Next Steps: + * - Explore the AgentKit README: https://github.com/coinbase/agentkit + * - Learn more about available WalletProviders & ActionProviders. + * - Experiment with custom ActionProviders for your unique use case. + * + * ## Want to contribute? + * Join us in shaping AgentKit! Check out the contribution guide: + * - https://github.com/coinbase/agentkit/blob/main/CONTRIBUTING.md + * - https://discord.gg/CDP + */ + +// The agent +let agent: ReturnType; + +/** + * Initializes and returns an instance of the AI agent. + * If an agent instance already exists, it returns the existing one. + * + * @function getOrInitializeAgent + * @returns {Promise>} The initialized AI agent. + * + * @description Handles agent setup + * + * @throws {Error} If the agent initialization fails. + */ +async function getOrInitializeAgent(): Promise> { + // If agent has already been initialized, return it + if (agent) { + return agent; + } + + try { + // Initialize LLM: https://platform.openai.com/docs/models#gpt-4o + const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); + + // Initialize WalletProvider: https://docs.cdp.coinbase.com/agentkit/docs/wallet-management + const walletProvider = await PrivyWalletProvider.configureWithWallet({ + appId: process.env.PRIVY_APP_ID as string, + appSecret: process.env.PRIVY_APP_SECRET as string, + walletId: process.env.PRIVY_WALLET_ID as string, + chainId: process.env.CHAIN_ID, + authorizationPrivateKey: process.env.PRIVY_WALLET_AUTHORIZATION_PRIVATE_KEY, + authorizationKeyId: process.env.PRIVY_WALLET_AUTHORIZATION_KEY_ID, + }); + + // Initialize AgentKit: https://docs.cdp.coinbase.com/agentkit/docs/agent-actions + const agentkit = await AgentKit.from({ + walletProvider, + actionProviders: [ + wethActionProvider(), + pythActionProvider(), + walletActionProvider(), + erc20ActionProvider(), + // The CDP API Action Provider provides faucet functionality on base-sepolia. Can be removed if you do not need this functionality. + cdpApiActionProvider({ + apiKeyName: process.env.CDP_API_KEY_NAME, + apiKeyPrivateKey: process.env.CDP_API_KEY_PRIVATE_KEY, + }), + ], + }); + const tools = await getLangChainTools(agentkit); + const memory = new MemorySaver(); + + // Initialize Agent + const canUseFaucet = walletProvider.getNetwork().networkId == "base-sepolia"; + const faucetMessage = `If you ever need funds, you can request them from the faucet.`; + const cantUseFaucetMessage = `If you need funds, you can provide your wallet details and request funds from the user.`; + agent = createReactAgent({ + llm, + tools, + checkpointSaver: memory, + messageModifier: ` + You are a helpful agent that can interact onchain using the Coinbase Developer Platform AgentKit. You are + empowered to interact onchain using your tools. ${canUseFaucet ? faucetMessage : cantUseFaucetMessage}. + Before executing your first action, get the wallet details to see what network + you're on. If there is a 5XX (internal) HTTP error code, ask the user to try again later. If someone + asks you to do something you can't do with your currently available tools, you must say so, and + encourage them to implement it themselves using the CDP SDK + Agentkit, recommend they go to + docs.cdp.coinbase.com for more information. Be concise and helpful with your responses. Refrain from + restating your tools' descriptions unless it is explicitly requested. + `, + }); + + return agent; + } catch (error) { + console.error("Error initializing agent:", error); + throw new Error("Failed to initialize agent"); + } +} + +/** + * Handles incoming POST requests to interact with the AgentKit-powered AI agent. + * This function processes user messages and streams responses from the agent. + * + * @function POST + * @param {Request & { json: () => Promise }} req - The incoming request object containing the user message. + * @returns {Promise>} JSON response containing the AI-generated reply or an error message. + * + * @description Sends a single message to the agent and returns the agents' final response. + * + * @example + * const response = await fetch("/api/agent", { + * method: "POST", + * headers: { "Content-Type": "application/json" }, + * body: JSON.stringify({ userMessage: input }), + * }); + */ +export async function POST( + req: Request & { json: () => Promise }, +): Promise> { + try { + // 1️. Extract user message from the request body + const { userMessage } = await req.json(); + + // 2. Get the agent + const agent = await getOrInitializeAgent(); + + // 3.Start streaming the agent's response + const stream = await agent.stream( + { messages: [{ content: userMessage, role: "user" }] }, // The new message to send to the agent + { configurable: { thread_id: "AgentKit Discussion" } }, // Customizable thread ID for tracking conversations + ); + + // 4️. Process the streamed response chunks into a single message + let agentResponse = ""; + for await (const chunk of stream) { + if ("agent" in chunk) { + agentResponse += chunk.agent.messages[0].content; + } + } + + // 5️. Return the final response + return NextResponse.json({ response: agentResponse }); + } catch (error) { + console.error("Error processing request:", error); + return NextResponse.json({ error: "Failed to process message" }); + } +} diff --git a/typescript/create-onchain-agent/templates/next/app/api/agent/evm/viem/route.ts b/typescript/create-onchain-agent/templates/next/app/api/agent/evm/viem/route.ts new file mode 100644 index 000000000..08b63350d --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/app/api/agent/evm/viem/route.ts @@ -0,0 +1,183 @@ +import { NextResponse } from "next/server"; +import { + AgentKit, + cdpApiActionProvider, + cdpWalletActionProvider, + CdpWalletProvider, + erc20ActionProvider, + NETWORK_ID_TO_VIEM_CHAIN, + pythActionProvider, + ViemWalletProvider, + walletActionProvider, + wethActionProvider, +} from "@coinbase/agentkit"; +import { getLangChainTools } from "@coinbase/agentkit-langchain"; +import { ChatOpenAI } from "@langchain/openai"; +import { MemorySaver } from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { AgentRequest, AgentResponse } from "@/app/types/api"; +import { createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; + +/** + * AgentKit Integration Route + * + * This file is your gateway to integrating AgentKit with your product. + * It defines the interaction between your system and the AI agent, + * allowing you to configure the agent to suit your needs. + * + * # Key Steps to Customize Your Agent:** + * + * 1. Select your LLM: + * - Modify the `ChatOpenAI` instantiation to choose your preferred LLM. + * + * 2. Set up your WalletProvider: + * - Learn more: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#evm-wallet-providers + * + * 3️. Set up your ActionProviders: + * - ActionProviders define what your agent can do. + * - Choose from built-in providers or create your own: + * - Built-in: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#action-providers + * - Custom: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#creating-an-action-provider + * + * 4. Instantiate your Agent: + * - Pass the LLM, tools, and memory into `createReactAgent()` to bring your agent to life. + * + * # Next Steps: + * - Explore the AgentKit README: https://github.com/coinbase/agentkit + * - Learn more about available WalletProviders & ActionProviders. + * - Experiment with custom ActionProviders for your unique use case. + * + * ## Want to contribute? + * Join us in shaping AgentKit! Check out the contribution guide: + * - https://github.com/coinbase/agentkit/blob/main/CONTRIBUTING.md + * - https://discord.gg/CDP + */ + +// The agent +let agent: ReturnType; + +/** + * Initializes and returns an instance of the AI agent. + * If an agent instance already exists, it returns the existing one. + * + * @function getOrInitializeAgent + * @returns {Promise>} The initialized AI agent. + * + * @description Handles agent setup + * + * @throws {Error} If the agent initialization fails. + */ +async function getOrInitializeAgent(): Promise> { + // If agent has already been initialized, return it + if (agent) { + return agent; + } + + try { + // Initialize LLM: https://platform.openai.com/docs/models#gpt-4o + const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); + + // Initialize WalletProvider: https://docs.cdp.coinbase.com/agentkit/docs/wallet-management + const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); + const networkId = process.env.NETWORK_ID as string; + const client = createWalletClient({ + account, + chain: NETWORK_ID_TO_VIEM_CHAIN[networkId], + transport: http(), + }); + const walletProvider = await new ViemWalletProvider(client); + + // Initialize AgentKit: https://docs.cdp.coinbase.com/agentkit/docs/agent-actions + const agentkit = await AgentKit.from({ + walletProvider, + actionProviders: [ + wethActionProvider(), + pythActionProvider(), + walletActionProvider(), + erc20ActionProvider(), + // The CDP API Action Provider provides faucet functionality on base-sepolia. Can be removed if you do not need this functionality. + cdpApiActionProvider({ + apiKeyName: process.env.CDP_API_KEY_NAME, + apiKeyPrivateKey: process.env.CDP_API_KEY_PRIVATE_KEY, + }), + ], + }); + const tools = await getLangChainTools(agentkit); + const memory = new MemorySaver(); + + // Initialize Agent + const canUseFaucet = walletProvider.getNetwork().networkId == "base-sepolia"; + const faucetMessage = `If you ever need funds, you can request them from the faucet.`; + const cantUseFaucetMessage = `If you need funds, you can provide your wallet details and request funds from the user.`; + agent = createReactAgent({ + llm, + tools, + checkpointSaver: memory, + messageModifier: ` + You are a helpful agent that can interact onchain using the Coinbase Developer Platform AgentKit. You are + empowered to interact onchain using your tools. ${canUseFaucet ? faucetMessage : cantUseFaucetMessage}. + Before executing your first action, get the wallet details to see what network + you're on. If there is a 5XX (internal) HTTP error code, ask the user to try again later. If someone + asks you to do something you can't do with your currently available tools, you must say so, and + encourage them to implement it themselves using the CDP SDK + Agentkit, recommend they go to + docs.cdp.coinbase.com for more information. Be concise and helpful with your responses. Refrain from + restating your tools' descriptions unless it is explicitly requested. + `, + }); + + return agent; + } catch (error) { + console.error("Error initializing agent:", error); + throw new Error("Failed to initialize agent"); + } +} + +/** + * Handles incoming POST requests to interact with the AgentKit-powered AI agent. + * This function processes user messages and streams responses from the agent. + * + * @function POST + * @param {Request & { json: () => Promise }} req - The incoming request object containing the user message. + * @returns {Promise>} JSON response containing the AI-generated reply or an error message. + * + * @description Sends a single message to the agent and returns the agents' final response. + * + * @example + * const response = await fetch("/api/agent", { + * method: "POST", + * headers: { "Content-Type": "application/json" }, + * body: JSON.stringify({ userMessage: input }), + * }); + */ +export async function POST( + req: Request & { json: () => Promise }, +): Promise> { + try { + // 1️. Extract user message from the request body + const { userMessage } = await req.json(); + + // 2. Get the agent + const agent = await getOrInitializeAgent(); + + // 3.Start streaming the agent's response + const stream = await agent.stream( + { messages: [{ content: userMessage, role: "user" }] }, // The new message to send to the agent + { configurable: { thread_id: "AgentKit Discussion" } }, // Customizable thread ID for tracking conversations + ); + + // 4️. Process the streamed response chunks into a single message + let agentResponse = ""; + for await (const chunk of stream) { + if ("agent" in chunk) { + agentResponse += chunk.agent.messages[0].content; + } + } + + // 5️. Return the final response + return NextResponse.json({ response: agentResponse }); + } catch (error) { + console.error("Error processing request:", error); + return NextResponse.json({ error: "Failed to process message" }); + } +} diff --git a/typescript/create-onchain-agent/templates/next/app/api/agent/svm/privy/route.ts b/typescript/create-onchain-agent/templates/next/app/api/agent/svm/privy/route.ts new file mode 100644 index 000000000..ac7170f8a --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/app/api/agent/svm/privy/route.ts @@ -0,0 +1,180 @@ +import { AgentRequest, AgentResponse } from "@/app/types/api"; +import { + AgentKit, + cdpApiActionProvider, + erc20ActionProvider, + jupiterActionProvider, + PrivyWalletProvider, + pythActionProvider, + splActionProvider, + walletActionProvider, + wethActionProvider, +} from "@coinbase/agentkit"; +import { getLangChainTools } from "@coinbase/agentkit-langchain"; +import { MemorySaver } from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { ChatOpenAI } from "@langchain/openai"; +import { NextResponse } from "next/server"; + +/** + * AgentKit Integration Route + * + * This file is your gateway to integrating AgentKit with your product. + * It defines the interaction between your system and the AI agent, + * allowing you to configure the agent to suit your needs. + * + * # Key Steps to Customize Your Agent:** + * + * 1. Select your LLM: + * - Modify the `ChatOpenAI` instantiation to choose your preferred LLM. + * + * 2. Set up your WalletProvider: + * - Learn more: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#evm-wallet-providers + * + * 3️. Set up your ActionProviders: + * - ActionProviders define what your agent can do. + * - Choose from built-in providers or create your own: + * - Built-in: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#action-providers + * - Custom: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#creating-an-action-provider + * + * 4. Instantiate your Agent: + * - Pass the LLM, tools, and memory into `createReactAgent()` to bring your agent to life. + * + * # Next Steps: + * - Explore the AgentKit README: https://github.com/coinbase/agentkit + * - Learn more about available WalletProviders & ActionProviders. + * - Experiment with custom ActionProviders for your unique use case. + * + * ## Want to contribute? + * Join us in shaping AgentKit! Check out the contribution guide: + * - https://github.com/coinbase/agentkit/blob/main/CONTRIBUTING.md + * - https://discord.gg/CDP + */ + +// The agent +let agent: ReturnType; + +/** + * Initializes and returns an instance of the AI agent. + * If an agent instance already exists, it returns the existing one. + * + * @function getOrInitializeAgent + * @returns {Promise>} The initialized AI agent. + * + * @description Handles agent setup + * + * @throws {Error} If the agent initialization fails. + */ +async function getOrInitializeAgent(): Promise> { + // If agent has already been initialized, return it + if (agent) { + return agent; + } + + try { + // Initialize LLM: https://platform.openai.com/docs/models#gpt-4o + const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); + + // Initialize WalletProvider: https://docs.cdp.coinbase.com/agentkit/docs/wallet-management + const walletProvider = await PrivyWalletProvider.configureWithWallet({ + appId: process.env.PRIVY_APP_ID as string, + appSecret: process.env.PRIVY_APP_SECRET as string, + walletId: process.env.PRIVY_WALLET_ID as string, + authorizationPrivateKey: process.env.PRIVY_WALLET_AUTHORIZATION_PRIVATE_KEY, + authorizationKeyId: process.env.PRIVY_WALLET_AUTHORIZATION_KEY_ID, + chainType: "solana", + networkId: process.env.NETWORK_ID, + }); + + // Initialize AgentKit: https://docs.cdp.coinbase.com/agentkit/docs/agent-actions + const agentkit = await AgentKit.from({ + walletProvider, + actionProviders: [ + walletActionProvider(), + splActionProvider(), + jupiterActionProvider(), + // The CDP API Action Provider provides faucet functionality on solana-devnet. Can be removed if you do not need this functionality. + cdpApiActionProvider({ + apiKeyName: process.env.CDP_API_KEY_NAME, + apiKeyPrivateKey: process.env.CDP_API_KEY_PRIVATE_KEY, + }), + ], + }); + const tools = await getLangChainTools(agentkit); + const memory = new MemorySaver(); + + // Initialize Agent + const canUseFaucet = walletProvider.getNetwork().networkId == "solana-devnet"; + const faucetMessage = `If you ever need funds, you can request them from the faucet.`; + const cantUseFaucetMessage = `If you need funds, you can provide your wallet details and request funds from the user.`; + agent = createReactAgent({ + llm, + tools, + checkpointSaver: memory, + messageModifier: ` + You are a helpful agent that can interact onchain using the Coinbase Developer Platform AgentKit. You are + empowered to interact onchain using your tools. ${canUseFaucet ? faucetMessage : cantUseFaucetMessage}. + Before executing your first action, get the wallet details to see what network + you're on. If there is a 5XX (internal) HTTP error code, ask the user to try again later. If someone + asks you to do something you can't do with your currently available tools, you must say so, and + encourage them to implement it themselves using the CDP SDK + Agentkit, recommend they go to + docs.cdp.coinbase.com for more information. Be concise and helpful with your responses. Refrain from + restating your tools' descriptions unless it is explicitly requested. + `, + }); + + return agent; + } catch (error) { + console.error("Error initializing agent:", error); + throw new Error("Failed to initialize agent"); + } +} + +/** + * Handles incoming POST requests to interact with the AgentKit-powered AI agent. + * This function processes user messages and streams responses from the agent. + * + * @function POST + * @param {Request & { json: () => Promise }} req - The incoming request object containing the user message. + * @returns {Promise>} JSON response containing the AI-generated reply or an error message. + * + * @description Sends a single message to the agent and returns the agents' final response. + * + * @example + * const response = await fetch("/api/agent", { + * method: "POST", + * headers: { "Content-Type": "application/json" }, + * body: JSON.stringify({ userMessage: input }), + * }); + */ +export async function POST( + req: Request & { json: () => Promise }, +): Promise> { + try { + // 1️. Extract user message from the request body + const { userMessage } = await req.json(); + + // 2. Get the agent + const agent = await getOrInitializeAgent(); + + // 3.Start streaming the agent's response + const stream = await agent.stream( + { messages: [{ content: userMessage, role: "user" }] }, // The new message to send to the agent + { configurable: { thread_id: "AgentKit Discussion" } }, // Customizable thread ID for tracking conversations + ); + + // 4️. Process the streamed response chunks into a single message + let agentResponse = ""; + for await (const chunk of stream) { + if ("agent" in chunk) { + agentResponse += chunk.agent.messages[0].content; + } + } + + // 5️. Return the final response + return NextResponse.json({ response: agentResponse }); + } catch (error) { + console.error("Error processing request:", error); + return NextResponse.json({ error: "Failed to process message" }); + } +} diff --git a/typescript/create-onchain-agent/templates/next/app/api/agent/svm/solanaKeypair/route.ts b/typescript/create-onchain-agent/templates/next/app/api/agent/svm/solanaKeypair/route.ts new file mode 100644 index 000000000..5077eca06 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/app/api/agent/svm/solanaKeypair/route.ts @@ -0,0 +1,190 @@ +import { AgentRequest, AgentResponse } from "@/app/types/api"; +import { + AgentKit, + SOLANA_NETWORK_ID, + SolanaKeypairWalletProvider, + splActionProvider, + walletActionProvider, + jupiterActionProvider, + cdpApiActionProvider, +} from "@coinbase/agentkit"; +import { getLangChainTools } from "@coinbase/agentkit-langchain"; +import { MemorySaver } from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { ChatOpenAI } from "@langchain/openai"; +import { Keypair } from "@solana/web3.js"; +import bs58 from "bs58"; +import { NextResponse } from "next/server"; + +/** + * AgentKit Integration Route + * + * This file is your gateway to integrating AgentKit with your product. + * It defines the interaction between your system and the AI agent, + * allowing you to configure the agent to suit your needs. + * + * # Key Steps to Customize Your Agent:** + * + * 1. Select your LLM: + * - Modify the `ChatOpenAI` instantiation to choose your preferred LLM. + * + * 2. Set up your WalletProvider: + * - Learn more: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#evm-wallet-providers + * + * 3️. Set up your ActionProviders: + * - ActionProviders define what your agent can do. + * - Choose from built-in providers or create your own: + * - Built-in: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#action-providers + * - Custom: https://github.com/coinbase/agentkit/tree/main/typescript/agentkit#creating-an-action-provider + * + * 4. Instantiate your Agent: + * - Pass the LLM, tools, and memory into `createReactAgent()` to bring your agent to life. + * + * # Next Steps: + * - Explore the AgentKit README: https://github.com/coinbase/agentkit + * - Learn more about available WalletProviders & ActionProviders. + * - Experiment with custom ActionProviders for your unique use case. + * + * ## Want to contribute? + * Join us in shaping AgentKit! Check out the contribution guide: + * - https://github.com/coinbase/agentkit/blob/main/CONTRIBUTING.md + * - https://discord.gg/CDP + */ + +// The agent +let agent: ReturnType; + +/** + * Initializes and returns an instance of the AI agent. + * If an agent instance already exists, it returns the existing one. + * + * @function getOrInitializeAgent + * @returns {Promise>} The initialized AI agent. + * + * @description Handles agent setup + * + * @throws {Error} If the agent initialization fails. + */ +async function getOrInitializeAgent(): Promise> { + // If agent has already been initialized, return it + if (agent) { + return agent; + } + + try { + // Initialize LLM: https://platform.openai.com/docs/models#gpt-4o + const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); + + // Setup Private Key + let solanaPrivateKey = process.env.SOLANA_PRIVATE_KEY as string; + if (!solanaPrivateKey) { + console.log(`No Solana account detected. Generating a wallet...`); + const keypair = Keypair.generate(); + solanaPrivateKey = bs58.encode(keypair.secretKey); + console.log(`Created Solana wallet: ${keypair.publicKey.toBase58()}`); + console.log(`Store the private key in your .env for future reuse: ${solanaPrivateKey}`); + } + + // Initialize WalletProvider: https://docs.cdp.coinbase.com/agentkit/docs/wallet-management + // Configure Solana Keypair Wallet Provider + const rpcUrl = process.env.SOLANA_RPC_URL; + let walletProvider: SolanaKeypairWalletProvider; + if (rpcUrl) { + walletProvider = await SolanaKeypairWalletProvider.fromRpcUrl(rpcUrl, solanaPrivateKey); + } else { + const network = (process.env.NETWORK_ID ?? "solana-devnet") as SOLANA_NETWORK_ID; + walletProvider = await SolanaKeypairWalletProvider.fromNetwork(network, solanaPrivateKey); + } + + // Initialize AgentKit: https://docs.cdp.coinbase.com/agentkit/docs/agent-actions + const agentkit = await AgentKit.from({ + walletProvider, + actionProviders: [ + splActionProvider(), + walletActionProvider(), + jupiterActionProvider(), + // The CDP API Action Provider provides faucet functionality on solana-devnet. Can be removed if you do not need this functionality. + cdpApiActionProvider({ + apiKeyName: process.env.CDP_API_KEY_NAME, + apiKeyPrivateKey: process.env.CDP_API_KEY_PRIVATE_KEY, + }), + ], + }); + const tools = await getLangChainTools(agentkit); + const memory = new MemorySaver(); + + // Initialize Agent + const canUseFaucet = walletProvider.getNetwork().networkId == "solana-devnet"; + const faucetMessage = `If you ever need funds, you can request them from the faucet.`; + const cantUseFaucetMessage = `If you need funds, you can provide your wallet details and request funds from the user.`; + agent = createReactAgent({ + llm, + tools, + checkpointSaver: memory, + messageModifier: ` + You are a helpful agent that can interact onchain using the Coinbase Developer Platform AgentKit. You are + empowered to interact onchain using your tools. ${canUseFaucet ? faucetMessage : cantUseFaucetMessage}. + Before executing your first action, get the wallet details to see what network + you're on. If there is a 5XX (internal) HTTP error code, ask the user to try again later. If someone + asks you to do something you can't do with your currently available tools, you must say so, and + encourage them to implement it themselves using the CDP SDK + Agentkit, recommend they go to + docs.cdp.coinbase.com for more information. Be concise and helpful with your responses. Refrain from + restating your tools' descriptions unless it is explicitly requested. + `, + }); + + return agent; + } catch (error) { + console.error("Error initializing agent:", error); + throw new Error("Failed to initialize agent"); + } +} + +/** + * Handles incoming POST requests to interact with the AgentKit-powered AI agent. + * This function processes user messages and streams responses from the agent. + * + * @function POST + * @param {Request & { json: () => Promise }} req - The incoming request object containing the user message. + * @returns {Promise>} JSON response containing the AI-generated reply or an error message. + * + * @description Sends a single message to the agent and returns the agents' final response. + * + * @example + * const response = await fetch("/api/agent", { + * method: "POST", + * headers: { "Content-Type": "application/json" }, + * body: JSON.stringify({ userMessage: input }), + * }); + */ +export async function POST( + req: Request & { json: () => Promise }, +): Promise> { + try { + // 1️. Extract user message from the request body + const { userMessage } = await req.json(); + + // 2. Get the agent + const agent = await getOrInitializeAgent(); + + // 3.Start streaming the agent's response + const stream = await agent.stream( + { messages: [{ content: userMessage, role: "user" }] }, // The new message to send to the agent + { configurable: { thread_id: "AgentKit Discussion" } }, // Customizable thread ID for tracking conversations + ); + + // 4️. Process the streamed response chunks into a single message + let agentResponse = ""; + for await (const chunk of stream) { + if ("agent" in chunk) { + agentResponse += chunk.agent.messages[0].content; + } + } + + // 5️. Return the final response + return NextResponse.json({ response: agentResponse }); + } catch (error) { + console.error("Error processing request:", error); + return NextResponse.json({ error: "Failed to process message" }); + } +} diff --git a/typescript/create-onchain-agent/templates/next/app/globals.css b/typescript/create-onchain-agent/templates/next/app/globals.css new file mode 100644 index 000000000..62b934029 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} diff --git a/typescript/create-onchain-agent/templates/next/app/hooks/useAgent.ts b/typescript/create-onchain-agent/templates/next/app/hooks/useAgent.ts new file mode 100644 index 000000000..7d3c2a22f --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/app/hooks/useAgent.ts @@ -0,0 +1,74 @@ +import { useState } from "react"; +import { AgentRequest, AgentResponse } from "../types/api"; + +/** + * Sends a user message to the AgentKit backend API and retrieves the agent's response. + * + * @async + * @function callAgentAPI + * @param {string} userMessage - The message sent by the user. + * @returns {Promise} The agent's response message or `null` if an error occurs. + * + * @throws {Error} Logs an error if the request fails. + */ +async function messageAgent(userMessage: string): Promise { + try { + const response = await fetch("/api/agent", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userMessage } as AgentRequest), + }); + + const data = (await response.json()) as AgentResponse; + return data.response ?? data.error ?? null; + } catch (error) { + console.error("Error communicating with agent:", error); + return null; + } +} + +/** + * + * This hook manages interactions with the AI agent by making REST calls to the backend. + * It also stores the local conversation state, tracking messages sent by the user and + * responses from the agent. + * + * #### How It Works + * - `sendMessage(input)` sends a message to `/api/agent` and updates state. + * - `messages` stores the chat history. + * - `isThinking` tracks whether the agent is processing a response. + * + * #### See Also + * - The API logic in `/api/agent.ts` + * + * @returns {Object} An object containing: + * - `messages`: The conversation history. + * - `sendMessage`: A function to send a new message. + * - `isThinking`: Boolean indicating if the agent is processing a response. + */ +export function useAgent() { + const [messages, setMessages] = useState<{ text: string; sender: "user" | "agent" }[]>([]); + const [isThinking, setIsThinking] = useState(false); + + /** + * Sends a user message, updates local state, and retrieves the agent's response. + * + * @param {string} input - The message from the user. + */ + const sendMessage = async (input: string) => { + if (!input.trim()) return; + + setMessages(prev => [...prev, { text: input, sender: "user" }]); + setIsThinking(true); + + const responseMessage = await messageAgent(input); + + if (responseMessage) { + setMessages(prev => [...prev, { text: responseMessage, sender: "agent" }]); + } + + setIsThinking(false); + }; + + return { messages, sendMessage, isThinking }; +} diff --git a/typescript/create-onchain-agent/templates/next/app/layout.tsx b/typescript/create-onchain-agent/templates/next/app/layout.tsx new file mode 100644 index 000000000..fcadbe5f8 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/app/layout.tsx @@ -0,0 +1,88 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "AgentKit Quickstart", + description: "Generated by `create-onchain-agent`, a Next.js template for AgentKit", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {/* Header (Fixed Height) */} +
+ Coinbase + + + AgentKit + +
+ + {/* Main Content (Dynamic, Grows but Doesn't Force Scroll) */} +
+ {children} +
+ + {/* Footer (Fixed Height) */} + + + + ); +} diff --git a/typescript/create-onchain-agent/templates/next/app/page.tsx b/typescript/create-onchain-agent/templates/next/app/page.tsx new file mode 100644 index 000000000..2e7517cc9 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/app/page.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { useAgent } from "./hooks/useAgent"; +import ReactMarkdown from "react-markdown"; + +export default function Home() { + const [input, setInput] = useState(""); + const { messages, sendMessage, isThinking } = useAgent(); + + // Ref for the messages container + const messagesEndRef = useRef(null); + + // Function to scroll to the bottom + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + // Auto-scroll whenever messages change + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const onSendMessage = async () => { + if (!input.trim() || isThinking) return; + const message = input; + setInput(""); + await sendMessage(message); + }; + + return ( +
+ + ); +} diff --git a/typescript/create-onchain-agent/templates/next/app/types/api.ts b/typescript/create-onchain-agent/templates/next/app/types/api.ts new file mode 100644 index 000000000..707406085 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/app/types/api.ts @@ -0,0 +1,3 @@ +export type AgentRequest = { userMessage: string }; + +export type AgentResponse = { response?: string; error?: string }; diff --git a/typescript/create-onchain-agent/templates/next/next.config.js b/typescript/create-onchain-agent/templates/next/next.config.js new file mode 100644 index 000000000..91ef62f0d --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +}; + +module.exports = nextConfig; diff --git a/typescript/create-onchain-agent/templates/next/package.json b/typescript/create-onchain-agent/templates/next/package.json new file mode 100644 index 000000000..adff341d6 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/package.json @@ -0,0 +1,33 @@ +{ + "name": "next", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@coinbase/agentkit": "latest", + "@solana/web3.js": "^1.98.0", + "@tanstack/react-query": "^5", + "bs58": "^6.0.0", + "next": "14.2.15", + "react": "^18", + "react-dom": "^18", + "react-markdown": "^10.0.0", + "viem": "^2.17.4", + "wagmi": "^2.11.0" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.15", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/typescript/create-onchain-agent/templates/next/postcss.config.mjs b/typescript/create-onchain-agent/templates/next/postcss.config.mjs new file mode 100644 index 000000000..1a69fd2a4 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/typescript/create-onchain-agent/templates/next/tailwind.config.ts b/typescript/create-onchain-agent/templates/next/tailwind.config.ts new file mode 100644 index 000000000..2e394cd42 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/tailwind.config.ts @@ -0,0 +1,19 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + fontFamily: { + sans: ["DM Sans", "sans-serif"], + }, + }, + }, + plugins: [], +}; + +export default config; diff --git a/typescript/create-onchain-agent/templates/next/tsconfig.json b/typescript/create-onchain-agent/templates/next/tsconfig.json new file mode 100644 index 000000000..e7ff90fd2 --- /dev/null +++ b/typescript/create-onchain-agent/templates/next/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/typescript/create-onchain-agent/tsconfig.build.json b/typescript/create-onchain-agent/tsconfig.build.json new file mode 100644 index 000000000..f34c137f8 --- /dev/null +++ b/typescript/create-onchain-agent/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts"], + "compilerOptions": { + "sourceMap": true + } +} diff --git a/typescript/create-onchain-agent/tsconfig.json b/typescript/create-onchain-agent/tsconfig.json new file mode 100644 index 000000000..cf5f07f19 --- /dev/null +++ b/typescript/create-onchain-agent/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist/esm", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "declarationDir": "./dist/types" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.test.ts", "**/*.test-d.ts"] +}