From 5931ca6000f5062a56591b0b6e42ae318f0c9773 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 19 Nov 2024 09:17:05 +0100 Subject: [PATCH] Add CI File with Basic Lint (i.e. Prettier) (#20) Introducing some moderate CI. This requires linting with prettier. In a follow up I will, introduce eslint. --- .github/workflows/pull-request.yaml | 40 +++ bun.lockb | Bin 58662 -> 59030 bytes package.json | 7 +- src/commands/contract.ts | 88 +++--- src/commands/delete.ts | 64 ++--- src/commands/deploy.ts | 74 ++--- src/commands/dev.ts | 36 +-- src/commands/register.ts | 58 ++-- src/commands/update.ts | 66 ++--- src/config/constants.ts | 26 +- src/index.ts | 34 +-- src/services/api-service.ts | 21 +- src/services/openapi-service.ts | 53 ++-- src/services/plugin-service.ts | 135 +++++---- src/services/signer-service.ts | 33 ++- src/services/tunnel-service.ts | 427 ++++++++++++++++------------ src/utils/deployed-url.ts | 100 +++---- src/utils/file-utils.ts | 89 +++--- src/utils/port-detector.ts | 47 +-- src/utils/url-utils.ts | 8 +- src/utils/verify-msg-utils.ts | 4 +- 21 files changed, 793 insertions(+), 617 deletions(-) create mode 100644 .github/workflows/pull-request.yaml diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml new file mode 100644 index 0000000..1ddd2e7 --- /dev/null +++ b/.github/workflows/pull-request.yaml @@ -0,0 +1,40 @@ +name: Node.js CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + types: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install Bun + run: | + curl -fsSL https://bun.sh/install | bash + export PATH="$HOME/.bun/bin:$PATH" + + - name: Cache node modules + uses: actions/cache@v4 + with: + path: | + node_modules + ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install, Lint & Build + run: | + export PATH="$HOME/.bun/bin:$PATH" + bun install --frozen-lockfile + bun lint + bun run build diff --git a/bun.lockb b/bun.lockb index 3aa74fd5c34d6fd278b37a70d1da33611234cf6b..c02c10305884e5b0103d46a7092b4aea37b9312e 100755 GIT binary patch delta 3407 zcmeH~YfzL`9LCSP%Wcsm2$4HjA&SIu6%^v8(FKNV`XDhmSTsZg5;2Y=P%dN*^`Q^iPRLpc(@f6PNY+$j5;F~`%~TYXlY&A$&%5t7%M2eZzw$6Yo^yZC`#*;;Tm#z$myhFOw(nqk@co5r9s+F*~*}t#_wslK0aHQ z9xLnP)6IT(08s!Ypo;DWmIIatY90iqTwRBJh+67!w#Qsm(K+_rfbC40C%`>Gst|Xo z4@yUdTF4a{zBba#+DyRVvw$At0?O4rk$*rfUD(;L0B|~mTDyfDu@{>Gr@2K4v--fT zS_e8S$^Zu{*SrEwxw<>@)oSU@ITl;g0ruOa_1mRBU1p{R7PExGU>q7XZ-P^<&S~ui zT*g-b<8lB{3%Op-CI-fE)Hb!vd}q|!0o@7|Ihz`&2w9jE8O%Q2fO2(DS(wH)hm!); z)jNWEwmqu(J8+7Q9WRapzH~pJT%D^i2>ASu0p;p^%QJxWpK5*<&M17&eaXZZTE$nI zkI-dda-f;h8U>WAv)35lw8jCokn^#Z)Y|#ZaW4Ze@T*qu>a71wh%lOo$aVwJQ7OP2i(G$uy`cQKPncF1joKyJQB1x z+{_U%??_n7j{o^cum!Kbl9;o%a$}0o=i;8T8K)cCXU1|Z-4~LFjm48i4-Q`6cSlRw zNJUa_!g;^RrB_lso6k*u`EE({k8>}^r=FXbE{z!5;V%m-gQT&tK#{VzvPgPW8RaSD z+NGhYNDd&kt*SujwArirmdFHeH>2~5=va&pzOt#hug%*_w)m~Kt*Gk`Yx7=d=yXIO z$0|Q*&*s%K_qRhVKL~h^`7i@H^E~r-22vKlHVjcDpjrXTc$3@(DBgrDwhIsueR5;LSC|~0LAN)4SDvj22=;&M0lOy`%aa5og&36kcTsM2(T~z@NWS2=>jan zN1$i{)a(3~VWAQ51m_^UgIUH1WC7|h;2^hwjezO}Y*)h>KvP{Zzw;?~UnFPb2c40* zlSuKh9tRWPBH-X0tOsy_H^7_VEwCHB1YQQsAXEleGxB1Q;`gLKSOx+B-;;Y7cb}L7 zzk@$OI^eZ$2Ooluz$tKAwp&;E9YgAE&cEJQ6Q}5O57JoFm{{ za2{*`jQPFbK5#!U11|Cf@Er&PH$VkQ09(MbU@O=Ria;UY8qWgmOnNRunukOISizt` zVZB($Xm=fw(iYq`p^xDH>;&AC9hyJfiv*NvmmxjTdZc7_&!!;fU!U_I3@@c5*@Pe4 zzMi=trB?1e5v=sfBPYte8|`X;@%Tx(d>}_YI`D(H<)ou%?vibTeI&-Gw4yrRIiTrD e`NVj5fHNB(_is&HvWz$rC9B6gZ0QrfDE|QHn#mXd delta 3318 zcmeH}ZA_JA7{{M;fD9B9U*b(-`hoHh<%MUHExh$#BJ%y1|!os_&iqT8aaChUoi98YXnns^}3ajNL?o0R00 zzC1HrP~j^viG`KEr_Fu*9yGfOEBzPq&2V8^a30GDAEbax8V`#={u`~PqOz&#r=9M4 zJ=ORO+m>pr<)Cr5SS>`agWDXZ*u7CH4OuMJf@L5r2gmGRksXZVBA^x)aDP3W#q~Y5 z9ZNNDEoeLU+i|fpk-losHrIpZZ2%b>bUQZE&_cCV>rvTMO`zT6djFlyW^4;+UYniw zcMW>+*lq_%6(8KLwxhCk_t#TxbO-3g?gT}u17xvOPu%JDPFLNhYcgbAc7Rk9UbPxR zHL%xe7?s6RpXn&hE>iDa(6;u0W)FZYmio+@;?$r<4Z34IDO4Le4UXw3N!9CcJ_Akv0<^8OAd971 zZOrYRu3GgxTo2#d{;8_|A3)o`X!}up=+hk6Y-R>9vUHn& z%qfx9(qD?q(S~&<+K_Cr8rS)Z+0Kl{I2(50D-c)1Knvx@JO`+MZY<)X&yfwNw zvB!IiK*$7!7MjtXdvZ6oZDQRte&c7ij8ctlInbL1vDoN zT5k@#=r|7}j`JGGsmD`N)*>pb^38=+AbTCO$PI8O$ofFzwrU61g|J!MTbrP$oHvJh zvsdru>MVQ(=RgZ9r^BW!}r&;X544tKx`D1t5U z6r@5L=$xzS_ZyUf-ndQ+pMbs+eHFD(2MS{aR6+~1!ZWZrDaeE?po3%~tcPaU0M+ma=m>TdCP1H|GyrQVecOe)k(cIwm*e*p z6%-W|-_B;tv1nb=qfu|($|3$}0Y8VH_MtD6>W4h input.length > 0 || 'Contract name is required' + type: "input", + name: "contract", + message: "Enter the Near Protocol contract name:", + validate: (input: string) => + input.length > 0 || "Contract name is required", }, { - type: 'input', - name: 'description', - message: 'Enter the contract description (agent instructions):', - validate: (input: string) => input.length > 0 || 'Contract description is required' + type: "input", + name: "description", + message: "Enter the contract description (agent instructions):", + validate: (input: string) => + input.length > 0 || "Contract description is required", }, { - type: 'input', - name: 'output', - message: 'Enter the output directory (press Enter for current directory):', - default: '.', + type: "input", + name: "output", + message: + "Enter the output directory (press Enter for current directory):", + default: ".", }, { - type: 'input', - name: 'accountId', - message: 'Enter your near account ID to generate an API key:', - validate: (input: string) => input.length > 0 || 'Near account ID is required' - } + type: "input", + name: "accountId", + message: "Enter your near account ID to generate an API key:", + validate: (input: string) => + input.length > 0 || "Near account ID is required", + }, ]; return await inquirer.prompt<{ @@ -75,38 +78,46 @@ async function generateTypes(outputDir: string, contract: string) { await execAsync(`npx near2ts ${contract}`); } -async function generateAIAgent(answers: { - contract: string; - description: string; - accountId: string; -}, outputDir: string) { +async function generateAIAgent( + answers: { + contract: string; + description: string; + accountId: string; + }, + outputDir: string, +) { const apiUrl = "https://contract-to-agent.vercel.app/api/generate"; showLoadingMessage("Generating AI agent"); - const typesContent = await fs.readFile(path.join(outputDir, `contract_types.ts`), 'utf-8'); + const typesContent = await fs.readFile( + path.join(outputDir, `contract_types.ts`), + "utf-8", + ); const postData = { contract: answers.contract, contractDescription: answers.description, accountId: answers.accountId, - types: typesContent + types: typesContent, }; const response = await fetch(apiUrl, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify(postData), }); if (response.status === 429) { - throw new Error('You have reached the daily prompt limit. Please try again tomorrow.'); + throw new Error( + "You have reached the daily prompt limit. Please try again tomorrow.", + ); } if (!response.ok) { - console.error('Failed to generate AI agent'); + console.error("Failed to generate AI agent"); throw new Error(`HTTP error! status: ${response.status}`); } @@ -117,7 +128,7 @@ async function generateAIAgent(answers: { async function writeFiles(outputDir: string, code: string, contract: string) { await fs.mkdir(outputDir, { recursive: true }); - await fs.writeFile(path.join(outputDir, 'index.ts'), code); + await fs.writeFile(path.join(outputDir, "index.ts"), code); const tsConfig = { compilerOptions: { @@ -134,7 +145,7 @@ async function writeFiles(outputDir: string, code: string, contract: string) { }; await fs.writeFile( path.join(outputDir, "tsconfig.json"), - JSON.stringify(tsConfig, null, 2) + JSON.stringify(tsConfig, null, 2), ); const packageJson = { @@ -150,7 +161,7 @@ async function writeFiles(outputDir: string, code: string, contract: string) { express: "^4.17.1", "@types/express": "^4.17.13", "make-agent": "latest", - "dotenv": "^10.0.0", + dotenv: "^10.0.0", }, devDependencies: { typescript: "^4.5.4", @@ -158,7 +169,7 @@ async function writeFiles(outputDir: string, code: string, contract: string) { }; await fs.writeFile( path.join(outputDir, "package.json"), - JSON.stringify(packageJson, null, 2) + JSON.stringify(packageJson, null, 2), ); } @@ -169,7 +180,7 @@ async function setupAndRunAgent(outputDir: string) { await execAsync("npm install --legacy-peer-deps"); showLoadingMessage("Running server"); - const serverProcess = spawn('npx', ['tsx', './index.ts']); + const serverProcess = spawn("npx", ["tsx", "./index.ts"]); serverProcess.stdout.on("data", (data) => { console.log(`Server: ${data}`); @@ -181,15 +192,15 @@ async function setupAndRunAgent(outputDir: string) { // Wait for the server to start await new Promise((resolve) => { - serverProcess.stdout.on('data', (data) => { - if (data.toString().includes('Server is running')) { + serverProcess.stdout.on("data", (data) => { + if (data.toString().includes("Server is running")) { resolve(true); } }); }); showLoadingMessage("Running agent"); - const agentProcess = spawn('npx', ['make-agent', 'dev', '-p', '8080']); + const agentProcess = spawn("npx", ["make-agent", "dev", "-p", "8080"]); agentProcess.stdout.on("data", (data) => { console.log(`Agent: ${data}`); @@ -201,10 +212,9 @@ async function setupAndRunAgent(outputDir: string) { agentProcess.on("close", (code) => { console.log(`make-agent process exited with code ${code}`); - serverProcess.kill(); + serverProcess.kill(); }); // Keep the main process running await new Promise((resolve) => {}); } - diff --git a/src/commands/delete.ts b/src/commands/delete.ts index 6d8d991..f4ca1a4 100644 --- a/src/commands/delete.ts +++ b/src/commands/delete.ts @@ -6,41 +6,41 @@ import { deployedUrl } from "../utils/deployed-url"; import { getAuthentication } from "../services/signer-service"; export const deleteCommand = new Command() - .name('delete') - .description('Delete your AI agent plugin') - .option('-u, --url ', 'Specify the deployment URL') - .action(async (options) => { - const url = options.url || deployedUrl; + .name("delete") + .description("Delete your AI agent plugin") + .option("-u, --url ", "Specify the deployment URL") + .action(async (options) => { + const url = options.url || deployedUrl; - if (!url) { - console.error('Deployed URL could not be determined.'); - return; - } + if (!url) { + console.error("Deployed URL could not be determined."); + return; + } - const pluginId = getHostname(url); - const specUrl = getSpecUrl(url); - const { isValid, accountId } = await validateAndParseOpenApiSpec(specUrl); + const pluginId = getHostname(url); + const specUrl = getSpecUrl(url); + const { isValid, accountId } = await validateAndParseOpenApiSpec(specUrl); - if (!isValid) { - console.error('OpenAPI specification validation failed.'); - return; - } + if (!isValid) { + console.error("OpenAPI specification validation failed."); + return; + } - if (!accountId) { - console.error('Failed to parse account ID from OpenAPI specification.'); - return; - } + if (!accountId) { + console.error("Failed to parse account ID from OpenAPI specification."); + return; + } - const authentication = await getAuthentication(accountId); - if (!authentication) { - console.error('Authentication failed. Unable to delete the plugin.'); - return; - } + const authentication = await getAuthentication(accountId); + if (!authentication) { + console.error("Authentication failed. Unable to delete the plugin."); + return; + } - try { - await deletePlugin(pluginId); - console.log(`Plugin ${pluginId} deleted successfully.`); - } catch (error) { - console.error('Failed to delete the plugin:', error); - } - }); \ No newline at end of file + try { + await deletePlugin(pluginId); + console.log(`Plugin ${pluginId} deleted successfully.`); + } catch (error) { + console.error("Failed to delete the plugin:", error); + } + }); diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 4e6dd54..93e0f9f 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -6,41 +6,47 @@ import { deployedUrl } from "../utils/deployed-url"; import { getBitteUrls } from "../config/constants"; export const deployCommand = new Command() - .name('deploy') - .description('Deploy your AI agent, making it discoverable and registering it as a plugin') - .option('-u, --url ', 'Specify the deployment URL') - .action(async (options) => { - const url = options.url || deployedUrl; + .name("deploy") + .description( + "Deploy your AI agent, making it discoverable and registering it as a plugin", + ) + .option("-u, --url ", "Specify the deployment URL") + .action(async (options) => { + const url = options.url || deployedUrl; - if (!url) { - console.error('Deployed URL could not be determined.'); - return; - } + if (!url) { + console.error("Deployed URL could not be determined."); + return; + } - const id = getHostname(url); - const specUrl = getSpecUrl(url); - const { isValid, accountId } = await validateAndParseOpenApiSpec(specUrl); + const id = getHostname(url); + const specUrl = getSpecUrl(url); + const { isValid, accountId } = await validateAndParseOpenApiSpec(specUrl); - if (!isValid) { - console.error('OpenAPI specification validation failed.'); - return; - } + if (!isValid) { + console.error("OpenAPI specification validation failed."); + return; + } - if (!accountId) { - console.error('Failed to parse account ID from OpenAPI specification.'); - return; - } - const bitteUrls = getBitteUrls(); - try { - await updatePlugin(id, accountId, bitteUrls.BASE_URL); - console.log(`Plugin ${id} updated successfully.`); - } catch (error) { - console.log('Plugin not found. Attempting to register...'); - const result = await registerPlugin({ pluginId: id, accountId, bitteUrls }); - if (result) { - console.log(`Plugin ${id} registered successfully.`); - } else { - console.error('Plugin registration failed.'); - } - } - }); \ No newline at end of file + if (!accountId) { + console.error("Failed to parse account ID from OpenAPI specification."); + return; + } + const bitteUrls = getBitteUrls(); + try { + await updatePlugin(id, accountId, bitteUrls.BASE_URL); + console.log(`Plugin ${id} updated successfully.`); + } catch (error) { + console.log("Plugin not found. Attempting to register..."); + const result = await registerPlugin({ + pluginId: id, + accountId, + bitteUrls, + }); + if (result) { + console.log(`Plugin ${id} registered successfully.`); + } else { + console.error("Plugin registration failed."); + } + } + }); diff --git a/src/commands/dev.ts b/src/commands/dev.ts index e5d74a0..497cac7 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -3,20 +3,22 @@ import { startLocalTunnelAndRegister } from "../services/tunnel-service"; import { detectPort } from "../utils/port-detector"; export const devCommand = new Command() - .name('dev') - .description('Make your AI agent discoverable and register the plugin') - .option('-p, --port ', 'Local port to expose', parseInt) - .option('-s, --serveo', 'Use Serveo instead of Localtunnel', false) - .option('-t, --testnet', 'Use Testnet instead of Mainnet', false) - .action(async (options) => { - let port = options.port; - if (!port) { - port = await detectPort(); - if (!port) { - console.error('Unable to detect the port automatically. Please specify a port using the -p or --port option.'); - process.exit(1); - } - console.log(`Detected port: ${port}`); - } - await startLocalTunnelAndRegister(port, options.serveo, options.testnet); - }); + .name("dev") + .description("Make your AI agent discoverable and register the plugin") + .option("-p, --port ", "Local port to expose", parseInt) + .option("-s, --serveo", "Use Serveo instead of Localtunnel", false) + .option("-t, --testnet", "Use Testnet instead of Mainnet", false) + .action(async (options) => { + let port = options.port; + if (!port) { + port = await detectPort(); + if (!port) { + console.error( + "Unable to detect the port automatically. Please specify a port using the -p or --port option.", + ); + process.exit(1); + } + console.log(`Detected port: ${port}`); + } + await startLocalTunnelAndRegister(port, options.serveo, options.testnet); + }); diff --git a/src/commands/register.ts b/src/commands/register.ts index 0fafb05..e259729 100644 --- a/src/commands/register.ts +++ b/src/commands/register.ts @@ -6,35 +6,39 @@ import { getHostname, getSpecUrl } from "../utils/url-utils"; import { getBitteUrls } from "../config/constants"; export const registerCommand = new Command() - .name('register') - .description('Register a new plugin with a URL') - .option('-u, --url ', 'Specify the deployment URL') - .action(async (options) => { - const url = options.url || deployedUrl; + .name("register") + .description("Register a new plugin with a URL") + .option("-u, --url ", "Specify the deployment URL") + .action(async (options) => { + const url = options.url || deployedUrl; - if (!url) { - console.error('Deployed URL could not be determined.'); - return; - } + if (!url) { + console.error("Deployed URL could not be determined."); + return; + } - const pluginId = getHostname(url); - const specUrl = getSpecUrl(url); - const { isValid, accountId } = await validateAndParseOpenApiSpec(specUrl); + const pluginId = getHostname(url); + const specUrl = getSpecUrl(url); + const { isValid, accountId } = await validateAndParseOpenApiSpec(specUrl); - if (!isValid) { - console.error('OpenAPI specification validation failed.'); - return; - } + if (!isValid) { + console.error("OpenAPI specification validation failed."); + return; + } - if (!accountId) { - console.error('Failed to parse account ID from OpenAPI specification.'); - return; - } + if (!accountId) { + console.error("Failed to parse account ID from OpenAPI specification."); + return; + } - const result = await registerPlugin({ pluginId, accountId, bitteUrls: getBitteUrls() }); - if (result) { - console.log(`Plugin ${pluginId} registered successfully.`); - } else { - console.error('Plugin registration failed.'); - } - }); \ No newline at end of file + const result = await registerPlugin({ + pluginId, + accountId, + bitteUrls: getBitteUrls(), + }); + if (result) { + console.log(`Plugin ${pluginId} registered successfully.`); + } else { + console.error("Plugin registration failed."); + } + }); diff --git a/src/commands/update.ts b/src/commands/update.ts index 6aaa357..0330dcc 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -7,41 +7,41 @@ import { getAuthentication } from "../services/signer-service"; import { getBitteUrls } from "../config/constants"; export const updateCommand = new Command() - .name('update') - .description('Update an existing AI agent plugin') - .option('-u, --url ', 'Specify the deployment URL') - .action(async (options) => { - const url = options.url || deployedUrl; + .name("update") + .description("Update an existing AI agent plugin") + .option("-u, --url ", "Specify the deployment URL") + .action(async (options) => { + const url = options.url || deployedUrl; - if (!url) { - console.error('Deployed URL could not be determined.'); - return; - } + if (!url) { + console.error("Deployed URL could not be determined."); + return; + } - const pluginId = getHostname(url); - const specUrl = getSpecUrl(url); - const { isValid, accountId } = await validateAndParseOpenApiSpec(specUrl); + const pluginId = getHostname(url); + const specUrl = getSpecUrl(url); + const { isValid, accountId } = await validateAndParseOpenApiSpec(specUrl); - if (!isValid) { - console.error('OpenAPI specification validation failed.'); - return; - } + if (!isValid) { + console.error("OpenAPI specification validation failed."); + return; + } - if (!accountId) { - console.error('Failed to parse account ID from OpenAPI specification.'); - return; - } + if (!accountId) { + console.error("Failed to parse account ID from OpenAPI specification."); + return; + } - const authentication = await getAuthentication(accountId); - if (!authentication) { - console.error('Authentication failed. Unable to update the plugin.'); - return; - } - const bitteUrls = getBitteUrls(); - try { - await updatePlugin(pluginId, accountId, bitteUrls.BASE_URL); - console.log(`Plugin ${pluginId} updated successfully.`); - } catch (error) { - console.error('Failed to update the plugin:', error); - } - }); \ No newline at end of file + const authentication = await getAuthentication(accountId); + if (!authentication) { + console.error("Authentication failed. Unable to update the plugin."); + return; + } + const bitteUrls = getBitteUrls(); + try { + await updatePlugin(pluginId, accountId, bitteUrls.BASE_URL); + console.log(`Plugin ${pluginId} updated successfully.`); + } catch (error) { + console.error("Failed to update the plugin:", error); + } + }); diff --git a/src/config/constants.ts b/src/config/constants.ts index afa9cf3..09472ba 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -1,7 +1,7 @@ -import os from 'os'; -import { join } from 'path'; +import os from "os"; +import { join } from "path"; -const getWalletUrl = (isTestnet: boolean = false) => +const getWalletUrl = (isTestnet: boolean = false) => isTestnet ? "https://testnet.wallet.bitte.ai" : "https://wallet.bitte.ai"; export interface BitteUrls { @@ -15,18 +15,18 @@ export interface BitteUrls { export const getBitteUrls = (isTestnet: boolean = false): BitteUrls => { const BITTE_WALLET_URL = getWalletUrl(isTestnet); return { - BITTE_WALLET_URL, - BASE_URL: `${BITTE_WALLET_URL}/api/ai-plugins`, - PLAYGROUND_URL: `${BITTE_WALLET_URL}/smart-actions/prompt/what%20can%20you%20help%20me%20with%3F?mode=debug&agentId=`, - SIGN_MESSAGE_URL: `${BITTE_WALLET_URL}/sign-message`, - SIGN_MESSAGE_SUCCESS_URL: `${BITTE_WALLET_URL}/success`, + BITTE_WALLET_URL, + BASE_URL: `${BITTE_WALLET_URL}/api/ai-plugins`, + PLAYGROUND_URL: `${BITTE_WALLET_URL}/smart-actions/prompt/what%20can%20you%20help%20me%20with%3F?mode=debug&agentId=`, + SIGN_MESSAGE_URL: `${BITTE_WALLET_URL}/sign-message`, + SIGN_MESSAGE_SUCCESS_URL: `${BITTE_WALLET_URL}/success`, }; }; -export const CONFIG_DIR = join(os.homedir(), '.ai-agent-cli'); -export const CONFIG_FILE = join(CONFIG_DIR, 'config.json'); +export const CONFIG_DIR = join(os.homedir(), ".ai-agent-cli"); +export const CONFIG_FILE = join(CONFIG_DIR, "config.json"); export const AI_PLUGIN_PATH = ".well-known/ai-plugin.json"; export const SIGN_MESSAGE_PORT = 6969; -export const SIGN_MESSAGE = "Register Bitte Agent!" -export const BITTE_CONFIG_ENV_KEY = "BITTE_CONFIG" -export const BITTE_KEY_ENV_KEY = "BITTE_KEY" \ No newline at end of file +export const SIGN_MESSAGE = "Register Bitte Agent!"; +export const BITTE_CONFIG_ENV_KEY = "BITTE_CONFIG"; +export const BITTE_KEY_ENV_KEY = "BITTE_KEY"; diff --git a/src/index.ts b/src/index.ts index 8b91c8e..2711f7f 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,36 +1,32 @@ #!/usr/bin/env node -import { program } from 'commander'; +import { program } from "commander"; import packageJson from "../package.json"; -import { deployCommand } from './commands/deploy'; -import { devCommand } from './commands/dev'; +import { deployCommand } from "./commands/deploy"; +import { devCommand } from "./commands/dev"; import dotenv from "dotenv"; -import { registerCommand } from './commands/register'; -import { updateCommand } from './commands/update'; -import { deleteCommand } from './commands/delete'; -import { contractCommand } from './commands/contract'; +import { registerCommand } from "./commands/register"; +import { updateCommand } from "./commands/update"; +import { deleteCommand } from "./commands/delete"; +import { contractCommand } from "./commands/contract"; dotenv.config(); program - .name('make-agent') - .description('CLI tool for managing AI agents') - .version(packageJson.version); + .name("make-agent") + .description("CLI tool for managing AI agents") + .version(packageJson.version); program.addCommand(devCommand); program.addCommand(deployCommand); -program - .addCommand(contractCommand); +program.addCommand(contractCommand); -program - .addCommand(registerCommand); +program.addCommand(registerCommand); -program - .addCommand(updateCommand); +program.addCommand(updateCommand); -program - .addCommand(deleteCommand); +program.addCommand(deleteCommand); -program.parse(); \ No newline at end of file +program.parse(); diff --git a/src/services/api-service.ts b/src/services/api-service.ts index 69b6bfa..79c8ad0 100644 --- a/src/services/api-service.ts +++ b/src/services/api-service.ts @@ -1,10 +1,13 @@ -import { fetch } from 'bun'; +import { fetch } from "bun"; -export async function fetchData(url: string, options?: RequestInit): Promise { - const response = await fetch(url, options); - if (response.ok) { - return await response.json(); - } else { - throw new Error(`Error fetching data: ${await response.text()}`); - } -} \ No newline at end of file +export async function fetchData( + url: string, + options?: RequestInit, +): Promise { + const response = await fetch(url, options); + if (response.ok) { + return await response.json(); + } else { + throw new Error(`Error fetching data: ${await response.text()}`); + } +} diff --git a/src/services/openapi-service.ts b/src/services/openapi-service.ts index 3fc7cf6..06a3d7f 100644 --- a/src/services/openapi-service.ts +++ b/src/services/openapi-service.ts @@ -1,49 +1,56 @@ import SwaggerParser from "@apidevtools/swagger-parser"; const MAX_RETRIES = 3; -const RETRY_DELAY = 1000; +const RETRY_DELAY = 1000; interface ApiResponse { - 'x-mb'?: { - 'account-id': string; + "x-mb"?: { + "account-id": string; }; } //parsing and validation done together to avoid fetching spec twice -export async function validateAndParseOpenApiSpec(url: string | URL): Promise<{ isValid: boolean; accountId?: string }> { - try { - const specUrl = url.toString(); - const specContent = await fetchWithRetry(specUrl); - - const apiResponse = JSON.parse(specContent); - await SwaggerParser.validate(apiResponse); - console.log("OpenAPI specification is valid."); - - const accountId = apiResponse['x-mb']?.['account-id']; - - return { isValid: true, accountId: accountId }; - } catch (error) { - console.error("Error in OpenAPI specification fetch, validation, or parsing:", error); - return { isValid: false }; - } +export async function validateAndParseOpenApiSpec( + url: string | URL, +): Promise<{ isValid: boolean; accountId?: string }> { + try { + const specUrl = url.toString(); + const specContent = await fetchWithRetry(specUrl); + + const apiResponse = JSON.parse(specContent); + await SwaggerParser.validate(apiResponse); + console.log("OpenAPI specification is valid."); + + const accountId = apiResponse["x-mb"]?.["account-id"]; + + return { isValid: true, accountId: accountId }; + } catch (error) { + console.error( + "Error in OpenAPI specification fetch, validation, or parsing:", + error, + ); + return { isValid: false }; } +} -async function fetchWithRetry(url: string, retries = MAX_RETRIES): Promise { +async function fetchWithRetry( + url: string, + retries = MAX_RETRIES, +): Promise { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const text = await response.text(); - JSON.parse(text); + JSON.parse(text); return text; } catch (error) { if (retries > 0) { console.log(`Retrying...`); - await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); + await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); return fetchWithRetry(url, retries - 1); } throw error; } } - diff --git a/src/services/plugin-service.ts b/src/services/plugin-service.ts index 45620ea..bc0efc4 100644 --- a/src/services/plugin-service.ts +++ b/src/services/plugin-service.ts @@ -1,70 +1,89 @@ -import type { BitteUrls } from '../config/constants'; -import { getAuthentication, getSignedMessage } from './signer-service'; +import type { BitteUrls } from "../config/constants"; +import { getAuthentication, getSignedMessage } from "./signer-service"; -export async function registerPlugin({pluginId, accountId, bitteUrls}: {pluginId: string, accountId?: string, bitteUrls: BitteUrls}): Promise { +export async function registerPlugin({ + pluginId, + accountId, + bitteUrls, +}: { + pluginId: string; + accountId?: string; + bitteUrls: BitteUrls; +}): Promise { + let message = await getAuthentication(accountId); - let message = await getAuthentication(accountId) + if (!message || !accountId) { + const signedMessage = await getSignedMessage(bitteUrls); + message = JSON.stringify(signedMessage); + } - if (!message || !accountId) { - const signedMessage = await getSignedMessage(bitteUrls); - message = JSON.stringify(signedMessage); - } - - try { - const response = await fetch(`${bitteUrls.BASE_URL}/${pluginId}`, { method: 'POST', headers: { 'bitte-api-key': message } }); - if (response.ok) { - await response.json(); - console.log(`Plugin registered successfully`); - return pluginId; - } else { - const errorData = await response.json(); - console.error(`Error registering plugin: ${JSON.stringify(errorData)}`); - if (errorData.debugUrl) { - console.log(`Debug URL: ${errorData.debugUrl}`); - } - return null; - } - } catch (error) { - console.error(`Network error during plugin registration: ${error}`); - return null; + try { + const response = await fetch(`${bitteUrls.BASE_URL}/${pluginId}`, { + method: "POST", + headers: { "bitte-api-key": message }, + }); + if (response.ok) { + await response.json(); + console.log(`Plugin registered successfully`); + return pluginId; + } else { + const errorData = await response.json(); + console.error(`Error registering plugin: ${JSON.stringify(errorData)}`); + if (errorData.debugUrl) { + console.log(`Debug URL: ${errorData.debugUrl}`); + } + return null; } + } catch (error) { + console.error(`Network error during plugin registration: ${error}`); + return null; + } } -export async function updatePlugin(pluginId: string, accountId: string | undefined, bitteUrl: string): Promise { - const message = await getAuthentication(accountId) +export async function updatePlugin( + pluginId: string, + accountId: string | undefined, + bitteUrl: string, +): Promise { + const message = await getAuthentication(accountId); - if (!message) { - console.error(`No API key found for plugin ${pluginId}. Please register the plugin first.`); - return; - } + if (!message) { + console.error( + `No API key found for plugin ${pluginId}. Please register the plugin first.`, + ); + return; + } - const response = await fetch(`${bitteUrl}/${pluginId}`, { - method: 'PUT', - headers: { 'bitte-api-key': message }, - }); - if (response.ok) { - console.log("Plugin updated successfully."); - } else { - console.error(`Error updating plugin: ${await response.text()}`); - } + const response = await fetch(`${bitteUrl}/${pluginId}`, { + method: "PUT", + headers: { "bitte-api-key": message }, + }); + if (response.ok) { + console.log("Plugin updated successfully."); + } else { + console.error(`Error updating plugin: ${await response.text()}`); + } } -export async function deletePlugin(pluginId: string, bitteUrl: string): Promise { - const bitteKeyString = process.env.BITTE_KEY; +export async function deletePlugin( + pluginId: string, + bitteUrl: string, +): Promise { + const bitteKeyString = process.env.BITTE_KEY; - if (!bitteKeyString) { - console.error("No API key found. Unable to delete plugin."); - return; - } + if (!bitteKeyString) { + console.error("No API key found. Unable to delete plugin."); + return; + } - const response = await fetch(`${bitteUrl}/${pluginId}`, { - method: 'DELETE', - headers: { 'bitte-api-key': bitteKeyString }, - }); - - if (response.ok) { - console.log("Plugin deleted successfully") - } else { - console.error(`Error deleting plugin: ${await response.text()}`); - } -} \ No newline at end of file + const response = await fetch(`${bitteUrl}/${pluginId}`, { + method: "DELETE", + headers: { "bitte-api-key": bitteKeyString }, + }); + + if (response.ok) { + console.log("Plugin deleted successfully"); + } else { + console.error(`Error deleting plugin: ${await response.text()}`); + } +} diff --git a/src/services/signer-service.ts b/src/services/signer-service.ts index 3b4cae1..3b68dea 100644 --- a/src/services/signer-service.ts +++ b/src/services/signer-service.ts @@ -23,13 +23,20 @@ dotenv.config({ path: `.env.local`, override: true }); * @returns {Promise} A promise that resolves to the signed message if authenticated, null otherwise. */ export async function getAuthentication( - accountId?: string + accountId?: string, ): Promise { const bitteKeyString = process.env.BITTE_KEY; if (!bitteKeyString) return null; const parsedKey = JSON.parse(bitteKeyString) as KeySignMessageParams; - if (accountId && (await verifyMessage({ params: parsedKey, accountIdToVerify: accountId })) || !accountId) { + if ( + (accountId && + (await verifyMessage({ + params: parsedKey, + accountIdToVerify: accountId, + }))) || + !accountId + ) { return bitteKeyString; } @@ -40,7 +47,9 @@ export async function getAuthentication( * for message signing and stores the signed message as an environment variable. * @returns {Promise} A promise that resolves to the signed message if authenticated or key created, null otherwise. */ -export async function authenticateOrCreateKey(bitteUrls: BitteUrls): Promise { +export async function authenticateOrCreateKey( + bitteUrls: BitteUrls, +): Promise { const authentication = await getAuthentication(); if (authentication) { console.log("Already authenticated."); @@ -58,7 +67,9 @@ export async function authenticateOrCreateKey(bitteUrls: BitteUrls): Promise { +async function createAndStoreKey( + bitteUrls: BitteUrls, +): Promise { try { const signedMessage = await getSignedMessage(bitteUrls); if (!signedMessage) { @@ -79,19 +90,21 @@ async function createAndStoreKey(bitteUrls: BitteUrls): Promise { +export function getSignedMessage( + bitteUrls: BitteUrls, +): Promise { return new Promise((resolve, reject) => { const server = createServer(handleRequest); - server.listen(SIGN_MESSAGE_PORT, () => { + server.listen(SIGN_MESSAGE_PORT, () => { const postEndpoint = `http://localhost:${SIGN_MESSAGE_PORT}`; const nonce = crypto.randomBytes(16).toString("hex"); const signUrl = `${bitteUrls.SIGN_MESSAGE_URL}?message=${encodeURIComponent( - SIGN_MESSAGE + SIGN_MESSAGE, )}&callbackUrl=${encodeURIComponent( - bitteUrls.SIGN_MESSAGE_SUCCESS_URL + bitteUrls.SIGN_MESSAGE_SUCCESS_URL, )}&nonce=${encodeURIComponent(nonce)}&postEndpoint=${encodeURIComponent( - postEndpoint + postEndpoint, )}`; open(signUrl).catch((error) => { console.error("Failed to open the browser:", error); @@ -147,7 +160,7 @@ function handlePreflight(res: ServerResponse): void { function handleInvalidMethod( res: ServerResponse, - reject: (reason: any) => void + reject: (reason: any) => void, ): void { res.writeHead(405, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Method Not Allowed" })); diff --git a/src/services/tunnel-service.ts b/src/services/tunnel-service.ts index 4367ea2..66f7b56 100644 --- a/src/services/tunnel-service.ts +++ b/src/services/tunnel-service.ts @@ -1,221 +1,278 @@ -import { watch } from 'fs/promises'; -import localtunnel from 'localtunnel'; -import { spawn } from 'child_process'; -import open from 'open'; -import { relative } from 'path'; -import { BITTE_CONFIG_ENV_KEY, getBitteUrls, type BitteUrls } from '../config/constants'; -import { validateAndParseOpenApiSpec } from './openapi-service'; -import { deletePlugin, registerPlugin, updatePlugin } from './plugin-service'; -import { authenticateOrCreateKey, getAuthentication } from './signer-service'; -import { getSpecUrl } from '../utils/url-utils'; -import { appendToEnv, removeFromEnv } from '../utils/file-utils'; -import { promises as fs } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; +import { watch } from "fs/promises"; +import localtunnel from "localtunnel"; +import { spawn } from "child_process"; +import open from "open"; +import { relative } from "path"; +import { + BITTE_CONFIG_ENV_KEY, + getBitteUrls, + type BitteUrls, +} from "../config/constants"; +import { validateAndParseOpenApiSpec } from "./openapi-service"; +import { deletePlugin, registerPlugin, updatePlugin } from "./plugin-service"; +import { authenticateOrCreateKey, getAuthentication } from "./signer-service"; +import { getSpecUrl } from "../utils/url-utils"; +import { appendToEnv, removeFromEnv } from "../utils/file-utils"; +import { promises as fs } from "fs"; +import { homedir } from "os"; +import { join } from "path"; async function updateBitteConfig(data: any) { - let existingConfig = {}; - try { - const existingData = process?.env?.BITTE_CONFIG; - if (existingData) { - existingConfig = JSON.parse(existingData); - removeFromEnv(BITTE_CONFIG_ENV_KEY); - } - } catch (error) { - // Env var doesn't exist or couldn't be read, we'll create a new one + let existingConfig = {}; + try { + const existingData = process?.env?.BITTE_CONFIG; + if (existingData) { + existingConfig = JSON.parse(existingData); + removeFromEnv(BITTE_CONFIG_ENV_KEY); } + } catch (error) { + // Env var doesn't exist or couldn't be read, we'll create a new one + } + + const updatedConfig = { ...existingConfig, ...data }; - const updatedConfig = { ...existingConfig, ...data }; - - await appendToEnv(BITTE_CONFIG_ENV_KEY, JSON.stringify(updatedConfig, null, 2)); - console.log('BITTE_CONFIG updated successfully.'); + await appendToEnv( + BITTE_CONFIG_ENV_KEY, + JSON.stringify(updatedConfig, null, 2), + ); + console.log("BITTE_CONFIG updated successfully."); } -export async function watchForChanges(pluginId: string, tunnelUrl: string, bitteUrls: BitteUrls): Promise { - const projectDir = process.cwd(); - console.log(`Watching for changes in ${projectDir}`); - console.log('Any file changes will trigger a plugin update attempt.'); - - const watcher = watch(projectDir, { recursive: true }); - - for await (const event of watcher) { - const relativePath = relative(projectDir, event.filename || ''); - // Ignore hidden files and directories - if (!relativePath.startsWith('.') && !relativePath.includes('node_modules') && !relativePath.includes('bitte.dev.json')) { - console.log(`Change detected in ${relativePath}. Attempting to update or register the plugin...`); - const { accountId } = await validateAndParseOpenApiSpec(getSpecUrl(tunnelUrl)); - const authentication = await getAuthentication(accountId); - const result = authentication - ? await updatePlugin(pluginId, accountId, bitteUrls.BASE_URL) - : await registerPlugin({ pluginId, accountId, bitteUrls }); - - if (result && !authentication) { - await openPlayground(result, bitteUrls.PLAYGROUND_URL); - } else if (!result && !authentication) { - console.log('Registration failed. Waiting for next file change to retry...'); - } - } +export async function watchForChanges( + pluginId: string, + tunnelUrl: string, + bitteUrls: BitteUrls, +): Promise { + const projectDir = process.cwd(); + console.log(`Watching for changes in ${projectDir}`); + console.log("Any file changes will trigger a plugin update attempt."); + + const watcher = watch(projectDir, { recursive: true }); + + for await (const event of watcher) { + const relativePath = relative(projectDir, event.filename || ""); + // Ignore hidden files and directories + if ( + !relativePath.startsWith(".") && + !relativePath.includes("node_modules") && + !relativePath.includes("bitte.dev.json") + ) { + console.log( + `Change detected in ${relativePath}. Attempting to update or register the plugin...`, + ); + const { accountId } = await validateAndParseOpenApiSpec( + getSpecUrl(tunnelUrl), + ); + const authentication = await getAuthentication(accountId); + const result = authentication + ? await updatePlugin(pluginId, accountId, bitteUrls.BASE_URL) + : await registerPlugin({ pluginId, accountId, bitteUrls }); + + if (result && !authentication) { + await openPlayground(result, bitteUrls.PLAYGROUND_URL); + } else if (!result && !authentication) { + console.log( + "Registration failed. Waiting for next file change to retry...", + ); + } } + } } -export async function openPlayground(agentId: string, playgroundUrl: string): Promise { - const url = `${playgroundUrl}${agentId}`; - console.log(`Opening playground: ${url}`); - await open(url); +export async function openPlayground( + agentId: string, + playgroundUrl: string, +): Promise { + const url = `${playgroundUrl}${agentId}`; + console.log(`Opening playground: ${url}`); + await open(url); - console.log('Waiting for the ID from the playground...'); - return ""; + console.log("Waiting for the ID from the playground..."); + return ""; } -async function setupAndValidate(tunnelUrl: string, pluginId: string, bitteUrls: BitteUrls): Promise { - await updateBitteConfig({ url: tunnelUrl }); - - await new Promise(resolve => setTimeout(resolve, 1000)); +async function setupAndValidate( + tunnelUrl: string, + pluginId: string, + bitteUrls: BitteUrls, +): Promise { + await updateBitteConfig({ url: tunnelUrl }); - const signedMessage = await authenticateOrCreateKey(bitteUrls); - if (!signedMessage) { - console.log("Failed to authenticate or create a key."); - return; - } + await new Promise((resolve) => setTimeout(resolve, 1000)); - const specUrl = getSpecUrl(tunnelUrl); + const signedMessage = await authenticateOrCreateKey(bitteUrls); + if (!signedMessage) { + console.log("Failed to authenticate or create a key."); + return; + } - console.log("Validating OpenAPI spec..."); - const { isValid, accountId } = await validateAndParseOpenApiSpec(specUrl); + const specUrl = getSpecUrl(tunnelUrl); - if (!isValid) { - console.log('OpenAPI specification validation failed.'); - return; - } + console.log("Validating OpenAPI spec..."); + const { isValid, accountId } = await validateAndParseOpenApiSpec(specUrl); - if (!accountId) { - console.log('Failed to parse account ID from OpenAPI specification.'); - return; - } + if (!isValid) { + console.log("OpenAPI specification validation failed."); + return; + } - const result = await registerPlugin({ pluginId, accountId, bitteUrls }); + if (!accountId) { + console.log("Failed to parse account ID from OpenAPI specification."); + return; + } - if (!result) { - console.log('Initial registration failed. Waiting for file changes to retry...'); - return; - } + const result = await registerPlugin({ pluginId, accountId, bitteUrls }); - const receivedId = await openPlayground(result, bitteUrls.PLAYGROUND_URL); - console.log(`Received ID from playground: ${receivedId}`); + if (!result) { + console.log( + "Initial registration failed. Waiting for file changes to retry...", + ); + return; + } - // Update bitte.dev.json with additional info - await updateBitteConfig({ - pluginId, - receivedId, - }); + const receivedId = await openPlayground(result, bitteUrls.PLAYGROUND_URL); + console.log(`Received ID from playground: ${receivedId}`); + + // Update bitte.dev.json with additional info + await updateBitteConfig({ + pluginId, + receivedId, + }); } -async function setupLocaltunnel(port: number): Promise<{ tunnelUrl: string; cleanup: () => Promise }> { - try { - const tunnel = await localtunnel({ port }); - console.log(`Localtunnel URL: ${tunnel.url}`); - return { - tunnelUrl: tunnel.url, - cleanup: async () => { - tunnel.close(); - } - }; - } catch (error) { - throw new Error("Failed to set up localtunnel."); - } +async function setupLocaltunnel( + port: number, +): Promise<{ tunnelUrl: string; cleanup: () => Promise }> { + try { + const tunnel = await localtunnel({ port }); + console.log(`Localtunnel URL: ${tunnel.url}`); + return { + tunnelUrl: tunnel.url, + cleanup: async () => { + tunnel.close(); + }, + }; + } catch (error) { + throw new Error("Failed to set up localtunnel."); + } } -async function setupServeo(port: number): Promise<{ tunnelUrl: string; cleanup: () => Promise }> { - const sshKeyPath = join(homedir(), '.ssh', 'serveo_key'); - - // Check if SSH key exists, if not, create it - try { - await fs.access(sshKeyPath); - } catch (error) { - console.log('Generating SSH key for Serveo...'); - await new Promise((resolve, reject) => { - const sshKeygen = spawn('ssh-keygen', ['-t', 'rsa', '-b', '4096', '-f', sshKeyPath, '-N', '']); - sshKeygen.on('close', (code) => { - if (code === 0) resolve(null); - else reject(new Error(`ssh-keygen process exited with code ${code}`)); - }); - }); - console.log('SSH key generated successfully.'); - } +async function setupServeo( + port: number, +): Promise<{ tunnelUrl: string; cleanup: () => Promise }> { + const sshKeyPath = join(homedir(), ".ssh", "serveo_key"); - return new Promise((resolve, reject) => { - const tunnel = spawn('ssh', ['-R', `80:localhost:${port}`, 'serveo.net', '-i', sshKeyPath]); - - let tunnelUrl = ''; - - tunnel.stdout.on('data', (data) => { - const output = data.toString(); - console.log(output); - if (output.includes('Forwarding HTTP traffic from')) { - tunnelUrl = output.match(/https?:\/\/[^\s]+/)[0]; - resolve({ - tunnelUrl, - cleanup: async () => { - tunnel.kill(); - } - }); - } - }); + // Check if SSH key exists, if not, create it + try { + await fs.access(sshKeyPath); + } catch (error) { + console.log("Generating SSH key for Serveo..."); + await new Promise((resolve, reject) => { + const sshKeygen = spawn("ssh-keygen", [ + "-t", + "rsa", + "-b", + "4096", + "-f", + sshKeyPath, + "-N", + "", + ]); + sshKeygen.on("close", (code) => { + if (code === 0) resolve(null); + else reject(new Error(`ssh-keygen process exited with code ${code}`)); + }); + }); + console.log("SSH key generated successfully."); + } - tunnel.stderr.on('data', (data) => { - console.error(`Tunnel error: ${data}`); - }); + return new Promise((resolve, reject) => { + const tunnel = spawn("ssh", [ + "-R", + `80:localhost:${port}`, + "serveo.net", + "-i", + sshKeyPath, + ]); + + let tunnelUrl = ""; - tunnel.on('close', (code) => { - if (code !== 0) { - reject(new Error(`Tunnel process exited with code ${code}`)); - } + tunnel.stdout.on("data", (data) => { + const output = data.toString(); + console.log(output); + if (output.includes("Forwarding HTTP traffic from")) { + tunnelUrl = output.match(/https?:\/\/[^\s]+/)[0]; + resolve({ + tunnelUrl, + cleanup: async () => { + tunnel.kill(); + }, }); + } }); -} -export async function startLocalTunnelAndRegister(port: number, useServeo: boolean = false, useTestnet: boolean = false): Promise { - console.log(`Setting up ${useServeo ? 'Serveo' : 'Localtunnel'} tunnel on port ${port}...`); - const { tunnelUrl, cleanup } = useServeo ? await setupServeo(port) : await setupLocaltunnel(port); - const bitteUrls = getBitteUrls(useTestnet); - const pluginId = new URL(tunnelUrl).hostname; - await setupAndValidate(tunnelUrl, pluginId, bitteUrls); - - let isCleaningUp = false; - - const fullCleanup = async () => { - if (isCleaningUp) return; - isCleaningUp = true; - console.log('Terminating. Cleaning up...'); - await removeFromEnv(BITTE_CONFIG_ENV_KEY).catch(() => {}); - console.log('bitte.dev.json file deleted successfully.'); - - try { - await deletePlugin(pluginId, bitteUrls.BASE_URL); - } catch (error) { - console.error('Error deleting plugin:', error); - } - - await cleanup(); - console.log('Cleanup completed. Exiting...'); - process.exit(0) - }; - - process.on('SIGINT', async () => { - await fullCleanup(); - }); - - process.on('SIGTERM', async () => { - await fullCleanup(); + tunnel.stderr.on("data", (data) => { + console.error(`Tunnel error: ${data}`); }); - process.on('unhandledRejection', (reason, promise) => { - console.error('Unhandled Rejection at:', promise, 'reason:', reason); - process.exit(1); + tunnel.on("close", (code) => { + if (code !== 0) { + reject(new Error(`Tunnel process exited with code ${code}`)); + } }); + }); +} + +export async function startLocalTunnelAndRegister( + port: number, + useServeo: boolean = false, + useTestnet: boolean = false, +): Promise { + console.log( + `Setting up ${useServeo ? "Serveo" : "Localtunnel"} tunnel on port ${port}...`, + ); + const { tunnelUrl, cleanup } = useServeo + ? await setupServeo(port) + : await setupLocaltunnel(port); + const bitteUrls = getBitteUrls(useTestnet); + const pluginId = new URL(tunnelUrl).hostname; + await setupAndValidate(tunnelUrl, pluginId, bitteUrls); - console.log('Tunnel is running. Watching for changes. Press Ctrl+C to stop.'); + let isCleaningUp = false; - // Start watching for changes - await watchForChanges(pluginId, tunnelUrl, bitteUrls); -} \ No newline at end of file + const fullCleanup = async () => { + if (isCleaningUp) return; + isCleaningUp = true; + console.log("Terminating. Cleaning up..."); + await removeFromEnv(BITTE_CONFIG_ENV_KEY).catch(() => {}); + console.log("bitte.dev.json file deleted successfully."); + + try { + await deletePlugin(pluginId, bitteUrls.BASE_URL); + } catch (error) { + console.error("Error deleting plugin:", error); + } + + await cleanup(); + console.log("Cleanup completed. Exiting..."); + process.exit(0); + }; + + process.on("SIGINT", async () => { + await fullCleanup(); + }); + + process.on("SIGTERM", async () => { + await fullCleanup(); + }); + + process.on("unhandledRejection", (reason, promise) => { + console.error("Unhandled Rejection at:", promise, "reason:", reason); + process.exit(1); + }); + + console.log("Tunnel is running. Watching for changes. Press Ctrl+C to stop."); + + // Start watching for changes + await watchForChanges(pluginId, tunnelUrl, bitteUrls); +} diff --git a/src/utils/deployed-url.ts b/src/utils/deployed-url.ts index c1cccc2..4c155c7 100644 --- a/src/utils/deployed-url.ts +++ b/src/utils/deployed-url.ts @@ -1,69 +1,69 @@ export const { - VERCEL_ENV, - VERCEL_URL, - VERCEL_BRANCH_URL, - VERCEL_PROJECT_PRODUCTION_URL, + VERCEL_ENV, + VERCEL_URL, + VERCEL_BRANCH_URL, + VERCEL_PROJECT_PRODUCTION_URL, } = process.env; export const VERCEL_DEPLOYMENT_URL = (() => { - switch (VERCEL_ENV) { - case "production": - return `https://${VERCEL_PROJECT_PRODUCTION_URL}`; - case "preview": - return `https://${VERCEL_BRANCH_URL || VERCEL_URL}`; - default: - return "http://localhost:3000"; - } + switch (VERCEL_ENV) { + case "production": + return `https://${VERCEL_PROJECT_PRODUCTION_URL}`; + case "preview": + return `https://${VERCEL_BRANCH_URL || VERCEL_URL}`; + default: + return "http://localhost:3000"; + } })(); const getDeployedUrl = (): string => { - // Vercel - if (VERCEL_ENV) { - return VERCEL_DEPLOYMENT_URL; - } + // Vercel + if (VERCEL_ENV) { + return VERCEL_DEPLOYMENT_URL; + } - // Netlify - if (process.env.URL) { - return process.env.URL; - } + // Netlify + if (process.env.URL) { + return process.env.URL; + } - // Heroku - if (process.env.HEROKU_APP_NAME) { - return `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`; - } + // Heroku + if (process.env.HEROKU_APP_NAME) { + return `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`; + } - // AWS Elastic Beanstalk - if (process.env.EB_ENVIRONMENT_URL) { - return process.env.EB_ENVIRONMENT_URL; - } + // AWS Elastic Beanstalk + if (process.env.EB_ENVIRONMENT_URL) { + return process.env.EB_ENVIRONMENT_URL; + } - // Google Cloud Run - if (process.env.K_SERVICE && process.env.K_REVISION) { - return `https://${process.env.K_SERVICE}-${process.env.K_REVISION}.a.run.app`; - } + // Google Cloud Run + if (process.env.K_SERVICE && process.env.K_REVISION) { + return `https://${process.env.K_SERVICE}-${process.env.K_REVISION}.a.run.app`; + } - // Azure App Service - if (process.env.WEBSITE_HOSTNAME) { - return `https://${process.env.WEBSITE_HOSTNAME}`; - } + // Azure App Service + if (process.env.WEBSITE_HOSTNAME) { + return `https://${process.env.WEBSITE_HOSTNAME}`; + } - // DigitalOcean App Platform - if (process.env.DIGITALOCEAN_APP_URL) { - return process.env.DIGITALOCEAN_APP_URL; - } + // DigitalOcean App Platform + if (process.env.DIGITALOCEAN_APP_URL) { + return process.env.DIGITALOCEAN_APP_URL; + } - // Render - if (process.env.RENDER_EXTERNAL_URL) { - return process.env.RENDER_EXTERNAL_URL; - } + // Render + if (process.env.RENDER_EXTERNAL_URL) { + return process.env.RENDER_EXTERNAL_URL; + } - // Bitte Env - if(process.env.BITTE_AGENT_URL) { - return process.env.BITTE_AGENT_URL - } + // Bitte Env + if (process.env.BITTE_AGENT_URL) { + return process.env.BITTE_AGENT_URL; + } - // Fallback to localhost if no deployment URL is found - return 'http://localhost:3000'; + // Fallback to localhost if no deployment URL is found + return "http://localhost:3000"; }; export const deployedUrl = getDeployedUrl(); diff --git a/src/utils/file-utils.ts b/src/utils/file-utils.ts index ca9182b..f0e8399 100644 --- a/src/utils/file-utils.ts +++ b/src/utils/file-utils.ts @@ -1,63 +1,66 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'fs'; -import dotenv from 'dotenv'; +import { + existsSync, + mkdirSync, + readFileSync, + writeFileSync, + appendFileSync, +} from "fs"; +import dotenv from "dotenv"; export function readFile(filePath: string): string { - return readFileSync(filePath, 'utf-8'); + return readFileSync(filePath, "utf-8"); } export function writeFile(filePath: string, content: string): void { - const dirPath = filePath.split('/').slice(0, -1).join('/'); - if (!existsSync(dirPath)) { - mkdirSync(dirPath, { recursive: true }); - } - writeFileSync(filePath, content); + const dirPath = filePath.split("/").slice(0, -1).join("/"); + if (!existsSync(dirPath)) { + mkdirSync(dirPath, { recursive: true }); + } + writeFileSync(filePath, content); } -const ENV_FILES = ['.env', '.env.local', '.env.development', '.env.production']; +const ENV_FILES = [".env", ".env.local", ".env.development", ".env.production"]; export async function appendToEnv(key: string, value: string): Promise { - // clean up any previous insertion - removeFromEnv(key); + // clean up any previous insertion + removeFromEnv(key); - // make sure the value is in a single line (easier to remove) - const formattedValue = value.replace(/\n/g, ''); + // make sure the value is in a single line (easier to remove) + const formattedValue = value.replace(/\n/g, ""); - const envEntry = `${key}=${formattedValue}`; + const envEntry = `${key}=${formattedValue}`; - let envPath = ENV_FILES.find(file => existsSync(file)); + let envPath = ENV_FILES.find((file) => existsSync(file)); - if (envPath) { - appendFileSync(envPath, `\n${envEntry}`); - } else { - envPath = '.env'; - writeFileSync(envPath, envEntry); - } + if (envPath) { + appendFileSync(envPath, `\n${envEntry}`); + } else { + envPath = ".env"; + writeFileSync(envPath, envEntry); + } - dotenv.config(); - dotenv.config({ path: `.env.local`, override: true }); + dotenv.config(); + dotenv.config({ path: `.env.local`, override: true }); } +export async function removeFromEnv(key: string): Promise { + for (const envPath of ENV_FILES) { + if (existsSync(envPath)) { + let envContent = readFileSync(envPath, "utf-8"); + const regex = new RegExp(`^${key}=.*\n?`, "gm"); + let updatedContent = envContent.replace(regex, ""); + // Remove empty lines + updatedContent = updatedContent.replace(/^\s*[\r\n]|[\r\n]+$/gm, ""); -export async function removeFromEnv(key: string): Promise { - for (const envPath of ENV_FILES) { - if (existsSync(envPath)) { - let envContent = readFileSync(envPath, 'utf-8'); - const regex = new RegExp(`^${key}=.*\n?`, 'gm'); - - let updatedContent = envContent.replace(regex, ''); - - // Remove empty lines - updatedContent = updatedContent.replace(/^\s*[\r\n]|[\r\n]+$/gm, ''); - - if (updatedContent !== envContent) { - writeFileSync(envPath, updatedContent); - console.log(`Removed ${key} from ${envPath}`); - } - } + if (updatedContent !== envContent) { + writeFileSync(envPath, updatedContent); + console.log(`Removed ${key} from ${envPath}`); + } } + } - // Reload environment variables - dotenv.config(); - dotenv.config({ path: `.env.local`, override: true }); -} \ No newline at end of file + // Reload environment variables + dotenv.config(); + dotenv.config({ path: `.env.local`, override: true }); +} diff --git a/src/utils/port-detector.ts b/src/utils/port-detector.ts index b4ea0b1..c2e6165 100644 --- a/src/utils/port-detector.ts +++ b/src/utils/port-detector.ts @@ -1,22 +1,26 @@ -import { exec } from 'child_process'; -import { promisify } from 'util'; +import { exec } from "child_process"; +import { promisify } from "util"; const execAsync = promisify(exec); - export async function detectPort(): Promise { - const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); const maxAttempts = 5; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { // For Unix-like systems (Linux, macOS) // Get PIDs of Node processes running in the current directory - const { stdout: pidOutput } = await execAsync(`lsof -n | grep '${process.cwd()}' | grep node | awk '{print $2}' | uniq`); - const pids = pidOutput.trim().split('\n'); + const { stdout: pidOutput } = await execAsync( + `lsof -n | grep '${process.cwd()}' | grep node | awk '{print $2}' | uniq`, + ); + const pids = pidOutput.trim().split("\n"); if (pids.length === 0) { - console.log(`Attempt ${attempt}/${maxAttempts}: No Node.js processes found running in the current directory.`); + console.log( + `Attempt ${attempt}/${maxAttempts}: No Node.js processes found running in the current directory.`, + ); if (attempt < maxAttempts) { await sleep(1000); continue; @@ -25,20 +29,24 @@ export async function detectPort(): Promise { } // Get ports for all node processes - const { stdout: portOutput } = await execAsync(`lsof -n -i -P | grep LISTEN | grep node`); - const portLines = portOutput.trim().split('\n'); + const { stdout: portOutput } = await execAsync( + `lsof -n -i -P | grep LISTEN | grep node`, + ); + const portLines = portOutput.trim().split("\n"); // Filter port lines by pid and then extract ports const ports = portLines - .filter(line => pids.some(pid => line.includes(pid))) - .map(line => { + .filter((line) => pids.some((pid) => line.includes(pid))) + .map((line) => { const match = line.match(/:(\d+)/); return match ? parseInt(match[1], 10) : null; }) .filter((port): port is number => port !== null); if (ports.length === 0) { - console.log(`Attempt ${attempt}/${maxAttempts}: No ports found for Node.js processes in the current directory.`); + console.log( + `Attempt ${attempt}/${maxAttempts}: No ports found for Node.js processes in the current directory.`, + ); if (attempt < maxAttempts) { await sleep(1000); continue; @@ -47,7 +55,9 @@ export async function detectPort(): Promise { } if (ports.length > 1) { - console.log(`Multiple ports found: ${ports.join(', ')}. Using the first one.`); + console.log( + `Multiple ports found: ${ports.join(", ")}. Using the first one.`, + ); } return ports[0]; @@ -55,13 +65,18 @@ export async function detectPort(): Promise { // If lsof fails, it might be a Windows system or lsof is not installed try { // For Windows - const { stdout } = await execAsync("netstat -ano | findstr :LISTENING | findstr node.exe"); + const { stdout } = await execAsync( + "netstat -ano | findstr :LISTENING | findstr node.exe", + ); const match = stdout.match(/:(\d+)/); if (match && match[1]) { return parseInt(match[1], 10); } } catch (winError) { - console.error(`Attempt ${attempt}/${maxAttempts}: Error detecting port:`, winError); + console.error( + `Attempt ${attempt}/${maxAttempts}: Error detecting port:`, + winError, + ); if (attempt < maxAttempts) { await sleep(1000); continue; @@ -70,4 +85,4 @@ export async function detectPort(): Promise { } } return null; -} \ No newline at end of file +} diff --git a/src/utils/url-utils.ts b/src/utils/url-utils.ts index a13fb15..8aa7d25 100644 --- a/src/utils/url-utils.ts +++ b/src/utils/url-utils.ts @@ -1,11 +1,9 @@ import { AI_PLUGIN_PATH } from "../config/constants"; - export function getHostname(url: string): string { - return new URL(url).hostname; + return new URL(url).hostname; } export function getSpecUrl(baseUrl: string): URL { - return new URL(`${baseUrl}/${AI_PLUGIN_PATH}`); - } - \ No newline at end of file + return new URL(`${baseUrl}/${AI_PLUGIN_PATH}`); +} diff --git a/src/utils/verify-msg-utils.ts b/src/utils/verify-msg-utils.ts index e6c0780..2b841b6 100644 --- a/src/utils/verify-msg-utils.ts +++ b/src/utils/verify-msg-utils.ts @@ -31,7 +31,7 @@ export const verifyMessage = async ({ if (accountIdToVerify && accountIdToVerify !== accountId) { console.error( - `Account mismatch: signed message has account ${accountId}, but provided account was ${accountIdToVerify}` + `Account mismatch: signed message has account ${accountId}, but provided account was ${accountIdToVerify}`, ); return false; } @@ -46,7 +46,7 @@ export const verifyMessage = async ({ return utils.PublicKey.from(publicKey).verify( hashedPayload, - Buffer.from(signature, "base64") + Buffer.from(signature, "base64"), ); };