Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local Playground #33

Merged
merged 58 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
e30f9ea
initial work commit
microchipgnu Jan 14, 2025
1ade0f4
add packages (#34)
rubenmarcus Jan 17, 2025
a6e6286
workign
microchipgnu Jan 17, 2025
60b6e07
up
microchipgnu Jan 17, 2025
a0f3cf9
update
microchipgnu Jan 20, 2025
a024cf0
up
microchipgnu Jan 20, 2025
4eb3d1c
up
microchipgnu Jan 20, 2025
2ca4822
update
microchipgnu Jan 22, 2025
f2eb50a
fmt
microchipgnu Jan 22, 2025
ef03da8
fix format
microchipgnu Jan 22, 2025
648f9c3
remove dep ajv
microchipgnu Jan 22, 2025
48100cb
undo dumb
microchipgnu Jan 22, 2025
d7ed5ea
up
microchipgnu Jan 22, 2025
2b45336
update package.json
microchipgnu Jan 22, 2025
6eb086d
up
microchipgnu Jan 22, 2025
e8c782e
prep for release
microchipgnu Jan 22, 2025
a8a615c
comment workflow
microchipgnu Jan 22, 2025
a3a9242
update workflow
microchipgnu Jan 22, 2025
db1a202
try 1
microchipgnu Jan 22, 2025
055ecab
try 2
microchipgnu Jan 22, 2025
a96ef0a
try 3
microchipgnu Jan 22, 2025
c5aba60
try 4
microchipgnu Jan 22, 2025
2c414b1
update publish workflow
microchipgnu Jan 22, 2025
63316c1
pr requests
microchipgnu Jan 22, 2025
f8a8bd5
0.0.22-0
microchipgnu Jan 22, 2025
1592a3d
resolve paths
microchipgnu Jan 22, 2025
c7e324b
0.0.22-1
microchipgnu Jan 22, 2025
17e46fb
update server
microchipgnu Jan 22, 2025
1ab354b
0.0.22-2
microchipgnu Jan 22, 2025
a087f6f
update port
microchipgnu Jan 22, 2025
7f615ad
0.0.22-3
microchipgnu Jan 22, 2025
a0a5603
up
microchipgnu Jan 22, 2025
d822397
0.0.22-4
microchipgnu Jan 22, 2025
21c3623
update app
microchipgnu Jan 22, 2025
9895c6a
0.0.22-5
microchipgnu Jan 22, 2025
9098291
fmt
microchipgnu Jan 22, 2025
1beb9a8
up
microchipgnu Jan 23, 2025
01104b3
0.0.22-6
microchipgnu Jan 23, 2025
6a9b5dd
up
microchipgnu Jan 23, 2025
562c5e0
use pnpm + bun
Markeljan Jan 27, 2025
d1c4b53
update chatUrl, add env.example
Markeljan Jan 27, 2025
6496d40
update server, apiConfig
Markeljan Jan 27, 2025
bac96e2
remove unused cors (#43)
Markeljan Jan 28, 2025
25251c8
Working local playground
SurgeCode Jan 30, 2025
bc1ec36
Change build script
SurgeCode Jan 31, 2025
70a75b9
Merge branch 'main' into local-playground
microchipgnu Feb 3, 2025
34fae23
Remove default port value from dev command
GAllen98 Feb 3, 2025
804f847
linting
GAllen98 Feb 3, 2025
8a43b52
Improve type safety for startUIServer function
microchipgnu Feb 3, 2025
edd46ee
Add evm connection to local playground (#47)
sainthiago Feb 4, 2025
4c9c9e2
lock file
rubenmarcus Feb 4, 2025
4a0c009
frozen it again
rubenmarcus Feb 4, 2025
43a10b8
lint
rubenmarcus Feb 4, 2025
55ad837
fix bun lint
sainthiago Feb 4, 2025
8ae2571
fix return
sainthiago Feb 4, 2025
aa0449f
null
rubenmarcus Feb 4, 2025
d8b1a44
branch release
rubenmarcus Feb 4, 2025
08982f8
remove branch release
rubenmarcus Feb 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
20 changes: 13 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "bun run ./src/index.ts dev --port 3000",
"build": "bun run typecheck && bun build ./src/index.ts --outdir ./dist --target node",
"build": "bun run typecheck && bun build --minify ./src/index.ts --outdir ./dist --target node && bun run build:ui",
"build:ui": "bun run typecheck && cd ./src/playground && bun run build && cp -r ./dist ../../dist/playground",
"prepublishOnly": "bun run build",
"build-binary": "bun run typecheck && bun build ./src/index.ts --compile --outfile make-agent",
"lint": "prettier --check '{src,tests}/**/*.{js,ts}' && eslint . --ignore-pattern dist/",
Expand All @@ -34,16 +35,18 @@
"license": "MIT",
"devDependencies": {
"@types/bun": "latest",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/inquirer": "^9.0.7",
"@types/localtunnel": "^2.0.4",
"@types/node": "^22.7.2",
"@types/open": "^6.2.1",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"ajv": "^8.17.1",
"ajv-draft-04": "^1.0.0",
"eslint": "^9.15.0",
"eslint-plugin-import": "^2.31.0",
"prettier": "^3.3.3"
"prettier": "^3.3.3",
"ajv": "^8.17.1",
"ajv-draft-04": "^1.0.0"
},
"peerDependencies": {
"typescript": "^5.0.0"
Expand All @@ -52,13 +55,16 @@
"@apidevtools/swagger-parser": "^10.1.0",
"borsh": "^0.7.0",
"commander": "^12.1.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"hono": "^4.5.11",
"express": "^4.21.2",
"inquirer": "^12.0.0",
"is-port-reachable": "^4.0.0",
"js-sha256": "^0.11.0",
"localtunnel": "^2.0.2",
"near-api-js": "^4.0.3",
"open": "^10.1.0",
"vercel-url": "^0.2.1"
"near-ca": "^0.8.1",
"near-safe": "^0.9.6",
Comment on lines +64 to +65
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe that either of these dependencies are used in this project.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

closed by #54

"open": "^10.1.0"
}
}
30 changes: 30 additions & 0 deletions src/commands/dev-deprecated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Command } from "commander";

import { TunnelService } from "../services/tunnel";
import { detectPort } from "../utils/port-detector";

export const devDeprecatedCommand = new Command()
.name("dev-deprecated")
.description("Make your AI agent discoverable and register the plugin")
.option("-p, --port <number>", "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}`);
}
const tunnelService = new TunnelService({
port,
useServeo: options.serveo,
useTestnet: options.testnet,
});
await tunnelService.start();
});
134 changes: 116 additions & 18 deletions src/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,128 @@
import { Command } from "commander";
import dotenv from "dotenv";
import isPortReachable from "is-port-reachable";

import { TunnelService } from "../services/tunnel";
import { startApiServer } from "../services/proxy";
import { getDeployedUrl } from "../utils/deployed-url";
import { validateEnv } from "../utils/env";
import { validateAndParseOpenApiSpec } from "../utils/openapi";
import { detectPort } from "../utils/port-detector";
import { getHostname, getSpecUrl } from "../utils/url-utils";

dotenv.config();
validateEnv();

interface ApiConfig {
key: string;
url: string;
serverPort: number;
}

interface ValidationResult {
pluginId: string;
accountId: string;
spec: unknown;
}

const DEFAULT_PORTS = {
SERVER: 3010,
} as const;

async function findAvailablePort(startPort: number): Promise<number> {
let port = startPort;
while (await isPortReachable(port, { host: "localhost" })) {
port++;
}
return port;
}

const API_CONFIG: ApiConfig = {
key: process.env.BITTE_API_KEY!,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely the culprit of the following dirty error which I will paste in entirety to demonstrate how gross it is:

yarn run v1.22.22
$ concurrently "next dev" "make-agent dev"
[1] file:///Users/bh2smith/Projects/other/agent-next-boilerplate/node_modules/make-agent/dist/index.js:493
[1] Status: ${V.status}`)}catch(V){let J=V instanceof Error?V.message:JSON.stringify(V);console.error(`Failed to request plugin verification: ${J}`)}}}var{VERCEL_ENV:Lo,VERCEL_URL:hi1,VERCEL_BRANCH_URL:ni1,VERCEL_PROJECT_PRODUCTION_URL:oi1}=process.env,si1=(()=>{switch(Lo){case"production":return`https://${oi1}`;case"preview":return`https://${ni1||hi1}`;default:return"http://localhost:3000/"}})(),JU=(p)=>{if(Lo)return si1;if(process.env.URL)return process.env.URL;if(process.env.HEROKU_APP_NAME)return`https://${process.env.HEROKU_APP_NAME}.herokuapp.com`;if(process.env.EB_ENVIRONMENT_URL)return process.env.EB_ENVIRONMENT_URL;if(process.env.K_SERVICE&&process.env.K_REVISION)return`https://${process.env.K_SERVICE}-${process.env.K_REVISION}.a.run.app`;if(process.env.WEBSITE_HOSTNAME)return`https://${process.env.WEBSITE_HOSTNAME}`;if(process.env.DIGITALOCEAN_APP_URL)return process.env.DIGITALOCEAN_APP_URL;if(process.env.RENDER_EXTERNAL_URL)return process.env.RENDER_EXTERNAL_URL;if(process.env.BITTE_AGENT_URL)return process.env.BITTE_AGENT_URL;return`http://localhost/:${p||3000}`},bd=JU();var F21=D0(Q21(),1),t90=3,e90=1000;async function u8(p){try{let d=p.toString(),D=await W21(d),f;try{f=JSON.parse(D)}catch(v){return console.error("Failed to parse OpenAPI spec JSON:",v instanceof Error?v.message:"Unknown error"),{isValid:!1}}try{await F21.default.validate(f),console.log("OpenAPI specification is valid.")}catch(v){if(v instanceof Error){if(console.error("OpenAPI validation failed:",v.message),"details"in v){let Z=v.details;console.error("Validation details:",Z.map((V)=>({path:V.instancePath,error:V.message,params:V.params})))}}return{isValid:!1}}return{isValid:!0,accountId:f["x-mb"]?.["account-id"]}}catch(d){return console.error("Unexpected error:",d instanceof Error?d.message:"Unknown error"),{isValid:!1}}}async function W21(p,d=t90){try{let D=await fetch(p);if(!D.ok)throw new Error(`HTTP error! status: ${D.status}`);let f=await D.text();return JSON.parse(f),f}catch(D){if(d>0)return console.log("Retrying..."),await new Promise((f)=>setTimeout(f,e90)),W21(p,d-1);throw D}}function t5(p){return new URL(p).hostname}function k8(p){return new URL(`${p}/${ku}`)}var K21=new S4().name("delete").description("Delete your AI agent plugin").option("-u, --url <url>","Specify the deployment URL").action(async(p)=>{let d=p.url||bd;if(!d){console.error("Deployed URL could not be determined.");return}let D=t5(d),f=k8(d),{isValid:$,accountId:v}=await u8(f);if(!$){console.error("OpenAPI specification validation failed.");return}if(!v){console.error("Failed to parse account ID from OpenAPI specification.");return}let Z=new F9;if(!await Z.auth.getAuthentication()){console.error("Authentication failed. Unable to delete the plugin.");return}try{await Z.delete(D),console.log(`Plugin ${D} deleted successfully.`)}catch(J){console.error("Failed to delete the plugin:",J)}});var q21=new S4().name("deploy").description("Deploy your AI agent, making it discoverable and registering it as a plugin").option("-u, --url <url>","Specify the deployment URL").action(async(p)=>{let d=p.url||bd;if(!d){console.error("Deployed URL could not be determined.");return}let D=t5(d),f=k8(d),{isValid:$,accountId:v}=await u8(f);if(!$){console.error("OpenAPI specification validation failed.");return}if(!v){console.error("Failed to parse account ID from OpenAPI specification.");return}let Z=new F9;try{if(!await Z.update(D))console.log("Attempting to register plugin..."),await Z.register({pluginId:D})}catch(V){console.error(`Failed to deploy plugin ${D}. Error: ${V}`)}});var Fd1=D0(df(),1);import p50 from"node:net";async function PM(p,{host:d,timeout:D=1000}={}){if(typeof d!=="string")throw new TypeError("Specify a `host`");let f=new Promise(($,v)=>{let Z=new p50.Socket,V=()=>{Z.destroy(),v()};Z.setTimeout(D),Z.once("error",V),Z.once("timeout",V),Z.connect(p,d,()=>{Z.end(),$()})});try{return await f,!0}catch{return!1}}var cX=D0(Xd1(),1);import{promises as VT}from"fs";import ev from"path";async function Gd1(p,d){let D=cX.default();D.use(cX.default.json({limit:"2mb"}));let f=[ev.resolve(process.cwd(),"dist","playground"),ev.resolve(process.cwd(),"node_modules","make-agent","dist","playground"),ev.resolve(process.cwd(),"..","make-agent","dist","playground")],$;for(let v of f)try{if(await VT.access(v),await VT.access(ev.join(v,"index.html")).then(()=>!0).catch(()=>!1)){$=v;break}}catch{continue}if(!$)throw new Error("Could not find static files directory with index.html");return D.use(cX.default.static($,{setHeaders:(v,Z)=>{if(Z.endsWith(".css"))v.setHeader("Content-Type","text/css")}})),D.get("/api/config",async(v,Z)=>{try{let V={serverStartTime:new Date().toISOString(),environment:"make-agent",localAgent:{pluginId:v.hostname,accountId:"anon",spec:d},bitteApiKey:p.key,bitteApiUrl:p.url};Z.json(V)}catch(V){Z.status(500).json({error:"Failed to fetch AI plugin spec"})}}),D.get("*",async(v,Z)=>{console.log(v.path);let V=ev.join($,"index.html");try{let J=await VT.readFile(V,"utf8");Z.setHeader("Content-Type","text/html"),Z.send(J)}catch(J){Z.status(404).send("Not found")}}),new Promise((v,Z)=>{try{let V=D.listen(p.serverPort,()=>{console.log(`[Server] UI server listening http://localhost/:${p.serverPort}`),console.log("[Server] Ready to handle requests"),v(V)})}catch(V){Z(V)}})}function zd1(){let d=["BITTE_API_KEY"].filter((D)=>!process.env[D]);if(d.length>0)throw new Error(`Missing required environment variables: ${d.join(", ")}
[1]                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
[1] 
[1] Error: Missing required environment variables: BITTE_API_KEY
[1] Please ensure these are set in your .env file
[1]     at zd1 (file:///Users/bh2smith/Projects/other/agent-next-boilerplate/node_modules/make-agent/dist/index.js:493:5392)
[1]     at file:///Users/bh2smith/Projects/other/agent-next-boilerplate/node_modules/make-agent/dist/index.js:496:680
[1]     at ModuleJob.run (node:internal/modules/esm/module_job:262:25)
[1]     at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:483:26)
[1]     at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5)
[1]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically the "dev" command is the only one no longer subject to the check with the warning.

Search the project for BITTE_API_KEY and find the other commands still have it.

url: process.env.BITTE_API_URL!,
serverPort: DEFAULT_PORTS.SERVER,
};

async function fetchAndValidateSpec(url: string): Promise<ValidationResult> {
const pluginId = getHostname(url);
const specUrl = getSpecUrl(url);

const validation = await validateAndParseOpenApiSpec(specUrl);
const { isValid, accountId } = validation;

const specContent = await fetch(specUrl).then((res) => res.text());
let spec = JSON.parse(specContent);

if (!isValid) {
spec = {
...spec,
servers: [{ url }],
"x-mb": {
...spec["x-mb"],
"account-id": accountId || "anon",
},
};
}

return {
pluginId,
accountId: accountId || "anon",
spec,
};
}

async function setupPorts(options: {
port?: string;
}): Promise<{ port: number; serverPort: number }> {
let port = parseInt(options.port || "") || 0;

if (port === 0) {
const detectedPort = await detectPort();
if (detectedPort) {
port = detectedPort;
} else {
port = await findAvailablePort(3000);
}
}

const serverPort = await findAvailablePort(DEFAULT_PORTS.SERVER);

return { port, serverPort };
}

export const devCommand = new Command()
.name("dev")
.description("Make your AI agent discoverable and register the plugin")
.option("-p, --port <number>", "Local port to expose", parseInt)
.option("-s, --serveo", "Use Serveo instead of Localtunnel", false)
.description("Start a local playground for your AI agent")
.option("-p, --port <port>", "Port to run playground on", "3000")
.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.",
try {
const { port, serverPort } = await setupPorts(options);

API_CONFIG.serverPort = serverPort;
const server = await startApiServer(API_CONFIG);

const deployedUrl = getDeployedUrl(port);
if (!deployedUrl) {
throw new Error("Deployed URL could not be determined.");
}

try {
console.log(
"[Dev] Fetching and validating OpenAPI spec from:",
deployedUrl,
);
process.exit(1);
await fetchAndValidateSpec(deployedUrl);
console.log("[Dev] OpenAPI spec validation successful");
} catch (error) {
console.error("[Dev] Error validating OpenAPI spec:", error);
throw error;
}
console.log(`Detected port: ${port}`);

process.on("SIGINT", async () => {
server.close();
process.exit(0);
});
} catch (error) {
process.exit(1);
}
const tunnelService = new TunnelService({
port,
useServeo: options.serveo,
useTestnet: options.testnet,
});
await tunnelService.start();
});
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { contractCommand } from "./commands/contract";
import { deleteCommand } from "./commands/delete";
import { deployCommand } from "./commands/deploy";
import { devCommand } from "./commands/dev";
import { devDeprecatedCommand } from "./commands/dev-deprecated";
import { registerCommand } from "./commands/register";
import { updateCommand } from "./commands/update";
import { verifyCommand } from "./commands/verify";
Expand All @@ -25,6 +26,7 @@ program
.addCommand(registerCommand)
.addCommand(updateCommand)
.addCommand(deleteCommand)
.addCommand(verifyCommand);
.addCommand(verifyCommand)
.addCommand(devDeprecatedCommand);

program.parse();
24 changes: 24 additions & 0 deletions src/playground/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
50 changes: 50 additions & 0 deletions src/playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:

```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```

- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:

```js
// eslint.config.js
import react from 'eslint-plugin-react'

export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```
Binary file added src/playground/bun.lockb
Binary file not shown.
28 changes: 28 additions & 0 deletions src/playground/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";

export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
},
);
13 changes: 13 additions & 0 deletions src/playground/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading
Loading