create-hybrid 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # create-hybrid
2
+
3
+ Create a new Hybrid XMTP agent project with a single command.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ # Create a new project
9
+ npx create hybrid my-agent
10
+
11
+ # Create in current directory
12
+ npx create hybrid .
13
+
14
+ # Interactive mode (will prompt for name)
15
+ npx create hybrid
16
+ ```
17
+
18
+ ## What it creates
19
+
20
+ This package creates a new Hybrid XMTP agent project with:
21
+
22
+ - **TypeScript configuration** - Ready-to-use TypeScript setup
23
+ - **Agent template** - Pre-configured agent with OpenRouter integration
24
+ - **Development scripts** - Build, dev, test, and lint commands
25
+ - **Environment setup** - Template `.env` file with required variables:
26
+
27
+ ```env
28
+ # OpenRouter Configuration
29
+ # Get your OpenRouter API key from https://openrouter.ai/keys
30
+ OPENROUTER_API_KEY=your_openrouter_api_key_here
31
+
32
+ # XMTP Wallet and Encryption Keys
33
+ # Run 'npx hybrid gen:keys' to generate these values
34
+ XMTP_WALLET_KEY=your_wallet_key_here
35
+ XMTP_ENCRYPTION_KEY=your_encryption_key_here
36
+
37
+ # XMTP Environment (dev, production)
38
+ XMTP_ENV=production
39
+
40
+ # Server Configuration
41
+ # PORT=8454
42
+ ```
43
+ - **Testing framework** - Vitest configuration for unit tests
44
+
45
+ ## Project structure
46
+
47
+ ```
48
+ my-agent/
49
+ ├── src/
50
+ │ ├── agent.ts # Main agent implementation
51
+ │ └── agent.test.ts # Example tests
52
+ ├── .env # Environment variables
53
+ ├── .gitignore # Git ignore rules
54
+ ├── package.json # Dependencies and scripts
55
+ ├── tsconfig.json # TypeScript configuration
56
+ ├── vitest.config.ts # Test configuration
57
+ └── README.md # Project documentation
58
+ ```
59
+
60
+ ## Next steps
61
+
62
+ After creating your project:
63
+
64
+ 1. **Install dependencies**
65
+ ```bash
66
+ cd my-agent
67
+ npm install
68
+ ```
69
+
70
+ 2. **Get your OpenRouter API key**
71
+ - Visit [OpenRouter](https://openrouter.ai/keys)
72
+ - Create an account and generate an API key
73
+ - Add it to your `.env` file
74
+
75
+ 3. **Generate XMTP keys**
76
+ ```bash
77
+ npm run keys
78
+ ```
79
+
80
+ 4. **Start development**
81
+ ```bash
82
+ npm run dev
83
+ ```
84
+
85
+ ## Requirements
86
+
87
+ - Node.js 20 or higher
88
+ - npm, yarn, or pnpm
89
+
90
+ ## Related packages
91
+
92
+ - [`hybrid`](../core) - The main Hybrid framework
93
+ - [`@hybrd/cli`](../cli) - CLI tools for Hybrid development
94
+ - [`@hybrd/xmtp`](../xmtp) - XMTP client integration
95
+
96
+ ## License
97
+
98
+ MIT
@@ -0,0 +1,3 @@
1
+ declare function initializeProject(): Promise<void>;
2
+
3
+ export { initializeProject };
package/dist/index.js ADDED
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFile, readdir, writeFile, mkdir, copyFile, stat } from "fs/promises";
5
+ import { dirname, join } from "path";
6
+ import { createInterface } from "readline";
7
+ import { fileURLToPath } from "url";
8
+ var __dirname = dirname(fileURLToPath(import.meta.url));
9
+ function prompt(question) {
10
+ const rl = createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout
13
+ });
14
+ return new Promise((resolve) => {
15
+ rl.question(question, (answer) => {
16
+ rl.close();
17
+ resolve(answer);
18
+ });
19
+ });
20
+ }
21
+ function replaceTemplateVariables(content, variables) {
22
+ return content.replace(
23
+ /\{\{(\w+)\}\}/g,
24
+ (match, key) => variables[key] || match
25
+ );
26
+ }
27
+ async function copyTemplate(sourceDir, targetDir) {
28
+ const entries = await readdir(sourceDir, { withFileTypes: true });
29
+ for (const entry of entries) {
30
+ const sourcePath = join(sourceDir, entry.name);
31
+ const targetPath = join(targetDir, entry.name);
32
+ if (entry.isDirectory()) {
33
+ await mkdir(targetPath, { recursive: true });
34
+ await copyTemplate(sourcePath, targetPath);
35
+ } else {
36
+ await copyFile(sourcePath, targetPath);
37
+ }
38
+ }
39
+ }
40
+ async function initializeProject() {
41
+ console.log("\u{1F680} Creating a new Hybrid project...");
42
+ const projectNameArg = process.argv[2];
43
+ let projectName = projectNameArg;
44
+ if (projectNameArg === "") {
45
+ console.error("\u274C Project name is required");
46
+ process.exit(1);
47
+ }
48
+ if (!projectName || !projectName.trim()) {
49
+ while (!projectName || !projectName.trim()) {
50
+ projectName = await prompt("Enter project name: ");
51
+ if (!projectName || !projectName.trim()) {
52
+ console.log("\u274C Project name is required. Please enter a valid name.");
53
+ }
54
+ }
55
+ }
56
+ const sanitizedName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
57
+ const currentDir = process.cwd();
58
+ const projectDir = projectName === "." ? currentDir : join(currentDir, sanitizedName);
59
+ if (projectName !== ".") {
60
+ try {
61
+ const existingFiles = await readdir(projectDir);
62
+ if (existingFiles.length > 0) {
63
+ console.error(
64
+ `\u274C Directory "${sanitizedName}" already exists and is not empty`
65
+ );
66
+ console.error(
67
+ "Please choose a different name or remove the existing directory"
68
+ );
69
+ process.exit(1);
70
+ }
71
+ } catch {
72
+ }
73
+ } else {
74
+ try {
75
+ const existingFiles = await readdir(currentDir);
76
+ const significantFiles = existingFiles.filter(
77
+ (file) => !file.startsWith(".") && file !== "node_modules" && file !== "package-lock.json" && file !== "yarn.lock" && file !== "pnpm-lock.yaml"
78
+ );
79
+ if (significantFiles.length > 0) {
80
+ console.error(`\u274C Current directory already exists and is not empty`);
81
+ console.error(
82
+ "Please choose a different directory or remove existing files"
83
+ );
84
+ process.exit(1);
85
+ }
86
+ } catch {
87
+ }
88
+ }
89
+ console.log("\u{1F4E6} Copying template files...");
90
+ const templateDir = join(__dirname, "..", "templates", "agent");
91
+ try {
92
+ await stat(templateDir);
93
+ } catch {
94
+ console.error("\u274C Template directory not found");
95
+ process.exit(1);
96
+ }
97
+ try {
98
+ await mkdir(projectDir, { recursive: true });
99
+ await copyTemplate(templateDir, projectDir);
100
+ console.log(`\u2705 Template files copied to: ${sanitizedName}`);
101
+ } catch (error) {
102
+ console.error("\u274C Failed to copy template files:", error);
103
+ process.exit(1);
104
+ }
105
+ const variables = {
106
+ projectName: sanitizedName
107
+ };
108
+ try {
109
+ const filesToUpdate = [
110
+ join(projectDir, "package.json"),
111
+ join(projectDir, "README.md"),
112
+ join(projectDir, "src", "agent.ts")
113
+ ];
114
+ for (const filePath of filesToUpdate) {
115
+ try {
116
+ let content = await readFile(filePath, "utf-8");
117
+ content = replaceTemplateVariables(content, variables);
118
+ await writeFile(filePath, content, "utf-8");
119
+ } catch (error) {
120
+ console.log(
121
+ `\u26A0\uFE0F Could not update ${filePath.split("/").pop()}: file not found`
122
+ );
123
+ }
124
+ }
125
+ console.log("\u2705 Template variables updated");
126
+ } catch (error) {
127
+ console.error("\u274C Failed to update template variables:", error);
128
+ }
129
+ console.log("\n\u{1F389} Hybrid project created successfully!");
130
+ console.log(`
131
+ \u{1F4C2} Project created in: ${projectDir}`);
132
+ console.log("\n\u{1F4CB} Next steps:");
133
+ console.log(`1. cd ${sanitizedName}`);
134
+ console.log(
135
+ "2. Install dependencies (npm install, yarn install, or pnpm install)"
136
+ );
137
+ console.log("3. Get your OpenRouter API key from https://openrouter.ai/keys");
138
+ console.log("4. Add your API key to the OPENROUTER_API_KEY in .env");
139
+ console.log("5. Set XMTP_ENV in .env (dev or production)");
140
+ console.log("6. Generate keys: npm run keys (or yarn/pnpm equivalent)");
141
+ console.log("7. Start development: npm run dev (or yarn/pnpm equivalent)");
142
+ console.log(
143
+ "\n\u{1F4D6} For more information, see the README.md file in your project"
144
+ );
145
+ }
146
+ async function main() {
147
+ const nodeVersion = process.versions.node;
148
+ const [major] = nodeVersion.split(".").map(Number);
149
+ if (!major || major < 20) {
150
+ console.error("Error: Node.js version 20 or higher is required");
151
+ process.exit(1);
152
+ }
153
+ try {
154
+ await initializeProject();
155
+ } catch (error) {
156
+ console.error("Failed to initialize project:", error);
157
+ console.error(
158
+ "Error details:",
159
+ error instanceof Error ? error.stack : String(error)
160
+ );
161
+ process.exit(1);
162
+ }
163
+ }
164
+ main().catch((error) => {
165
+ console.error("CLI error:", error);
166
+ console.error(
167
+ "Error details:",
168
+ error instanceof Error ? error.stack : String(error)
169
+ );
170
+ process.exit(1);
171
+ });
172
+ export {
173
+ initializeProject
174
+ };
175
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { readFile, readdir, writeFile, mkdir, copyFile, stat } from \"node:fs/promises\"\nimport { dirname, join } from \"node:path\"\nimport { createInterface } from \"node:readline\"\nimport { fileURLToPath } from \"node:url\"\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nfunction prompt(question: string): Promise<string> {\n\tconst rl = createInterface({\n\t\tinput: process.stdin,\n\t\toutput: process.stdout\n\t})\n\n\treturn new Promise((resolve) => {\n\t\trl.question(question, (answer) => {\n\t\t\trl.close()\n\t\t\tresolve(answer)\n\t\t})\n\t})\n}\n\nfunction replaceTemplateVariables(\n\tcontent: string,\n\tvariables: Record<string, string>\n): string {\n\treturn content.replace(\n\t\t/\\{\\{(\\w+)\\}\\}/g,\n\t\t(match, key) => variables[key] || match\n\t)\n}\n\nasync function copyTemplate(sourceDir: string, targetDir: string): Promise<void> {\n\tconst entries = await readdir(sourceDir, { withFileTypes: true })\n\t\n\tfor (const entry of entries) {\n\t\tconst sourcePath = join(sourceDir, entry.name)\n\t\tconst targetPath = join(targetDir, entry.name)\n\t\t\n\t\tif (entry.isDirectory()) {\n\t\t\tawait mkdir(targetPath, { recursive: true })\n\t\t\tawait copyTemplate(sourcePath, targetPath)\n\t\t} else {\n\t\t\tawait copyFile(sourcePath, targetPath)\n\t\t}\n\t}\n}\n\nexport async function initializeProject() {\n\tconsole.log(\"🚀 Creating a new Hybrid project...\")\n\n\tconst projectNameArg = process.argv[2]\n\tlet projectName = projectNameArg\n\n\tif (projectNameArg === \"\") {\n\t\tconsole.error(\"❌ Project name is required\")\n\t\tprocess.exit(1)\n\t}\n\n\tif (!projectName || !projectName.trim()) {\n\t\twhile (!projectName || !projectName.trim()) {\n\t\t\tprojectName = await prompt(\"Enter project name: \")\n\t\t\tif (!projectName || !projectName.trim()) {\n\t\t\t\tconsole.log(\"❌ Project name is required. Please enter a valid name.\")\n\t\t\t}\n\t\t}\n\t}\n\n\tconst sanitizedName = projectName\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9-]/g, \"-\")\n\t\t.replace(/-+/g, \"-\")\n\t\t.replace(/^-|-$/g, \"\")\n\n\tconst currentDir = process.cwd()\n\tconst projectDir =\n\t\tprojectName === \".\" ? currentDir : join(currentDir, sanitizedName)\n\n\tif (projectName !== \".\") {\n\t\ttry {\n\t\t\tconst existingFiles = await readdir(projectDir)\n\t\t\tif (existingFiles.length > 0) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`❌ Directory \"${sanitizedName}\" already exists and is not empty`\n\t\t\t\t)\n\t\t\t\tconsole.error(\n\t\t\t\t\t\"Please choose a different name or remove the existing directory\"\n\t\t\t\t)\n\t\t\t\tprocess.exit(1)\n\t\t\t}\n\t\t} catch {\n\t\t}\n\t} else {\n\t\ttry {\n\t\t\tconst existingFiles = await readdir(currentDir)\n\t\t\tconst significantFiles = existingFiles.filter(\n\t\t\t\t(file) =>\n\t\t\t\t\t!file.startsWith(\".\") &&\n\t\t\t\t\tfile !== \"node_modules\" &&\n\t\t\t\t\tfile !== \"package-lock.json\" &&\n\t\t\t\t\tfile !== \"yarn.lock\" &&\n\t\t\t\t\tfile !== \"pnpm-lock.yaml\"\n\t\t\t)\n\t\t\tif (significantFiles.length > 0) {\n\t\t\t\tconsole.error(`❌ Current directory already exists and is not empty`)\n\t\t\t\tconsole.error(\n\t\t\t\t\t\"Please choose a different directory or remove existing files\"\n\t\t\t\t)\n\t\t\t\tprocess.exit(1)\n\t\t\t}\n\t\t} catch {\n\t\t}\n\t}\n\n\tconsole.log(\"📦 Copying template files...\")\n\t\n\tconst templateDir = join(__dirname, \"..\", \"templates\", \"agent\")\n\t\n\ttry {\n\t\tawait stat(templateDir)\n\t} catch {\n\t\tconsole.error(\"❌ Template directory not found\")\n\t\tprocess.exit(1)\n\t}\n\n\ttry {\n\t\tawait mkdir(projectDir, { recursive: true })\n\t\tawait copyTemplate(templateDir, projectDir)\n\t\tconsole.log(`✅ Template files copied to: ${sanitizedName}`)\n\t} catch (error) {\n\t\tconsole.error(\"❌ Failed to copy template files:\", error)\n\t\tprocess.exit(1)\n\t}\n\n\tconst variables = {\n\t\tprojectName: sanitizedName\n\t}\n\n\ttry {\n\t\tconst filesToUpdate = [\n\t\t\tjoin(projectDir, \"package.json\"),\n\t\t\tjoin(projectDir, \"README.md\"),\n\t\t\tjoin(projectDir, \"src\", \"agent.ts\")\n\t\t]\n\n\t\tfor (const filePath of filesToUpdate) {\n\t\t\ttry {\n\t\t\t\tlet content = await readFile(filePath, \"utf-8\")\n\t\t\t\tcontent = replaceTemplateVariables(content, variables)\n\t\t\t\tawait writeFile(filePath, content, \"utf-8\")\n\t\t\t} catch (error) {\n\t\t\t\tconsole.log(\n\t\t\t\t\t`⚠️ Could not update ${filePath.split(\"/\").pop()}: file not found`\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tconsole.log(\"✅ Template variables updated\")\n\t} catch (error) {\n\t\tconsole.error(\"❌ Failed to update template variables:\", error)\n\t}\n\n\tconsole.log(\"\\n🎉 Hybrid project created successfully!\")\n\tconsole.log(`\\n📂 Project created in: ${projectDir}`)\n\tconsole.log(\"\\n📋 Next steps:\")\n\tconsole.log(`1. cd ${sanitizedName}`)\n\tconsole.log(\n\t\t\"2. Install dependencies (npm install, yarn install, or pnpm install)\"\n\t)\n\tconsole.log(\"3. Get your OpenRouter API key from https://openrouter.ai/keys\")\n\tconsole.log(\"4. Add your API key to the OPENROUTER_API_KEY in .env\")\n\tconsole.log(\"5. Set XMTP_ENV in .env (dev or production)\")\n\tconsole.log(\"6. Generate keys: npm run keys (or yarn/pnpm equivalent)\")\n\tconsole.log(\"7. Start development: npm run dev (or yarn/pnpm equivalent)\")\n\n\tconsole.log(\n\t\t\"\\n📖 For more information, see the README.md file in your project\"\n\t)\n}\n\nasync function main() {\n\tconst nodeVersion = process.versions.node\n\tconst [major] = nodeVersion.split(\".\").map(Number)\n\tif (!major || major < 20) {\n\t\tconsole.error(\"Error: Node.js version 20 or higher is required\")\n\t\tprocess.exit(1)\n\t}\n\n\ttry {\n\t\tawait initializeProject()\n\t} catch (error) {\n\t\tconsole.error(\"Failed to initialize project:\", error)\n\t\tconsole.error(\n\t\t\t\"Error details:\",\n\t\t\terror instanceof Error ? error.stack : String(error)\n\t\t)\n\t\tprocess.exit(1)\n\t}\n}\n\nmain().catch((error) => {\n\tconsole.error(\"CLI error:\", error)\n\tconsole.error(\n\t\t\"Error details:\",\n\t\terror instanceof Error ? error.stack : String(error)\n\t)\n\tprocess.exit(1)\n})\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "create-hybrid",
3
+ "version": "1.2.3",
4
+ "description": "Create a new Hybrid XMTP agent project",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-hybrid": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "dependencies": {},
20
+ "devDependencies": {
21
+ "@types/node": "22.8.6",
22
+ "tsup": "^8.5.0",
23
+ "@config/biome": "0.0.0",
24
+ "@config/tsconfig": "0.0.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "keywords": [
30
+ "hybrid",
31
+ "xmtp",
32
+ "agent",
33
+ "cli",
34
+ "create"
35
+ ],
36
+ "author": "Ian Hunter",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/ian/hybrid.git",
41
+ "directory": "packages/create-hybrid"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "build:watch": "tsup --watch",
46
+ "clean": "rm -rf .turbo dist",
47
+ "typecheck": "tsc --noEmit",
48
+ "lint": "biome lint --unsafe",
49
+ "lint:fix": "biome lint --write --unsafe",
50
+ "format": "biome format --write"
51
+ }
52
+ }
@@ -0,0 +1,164 @@
1
+ # {{projectName}}
2
+
3
+ A Hybrid XMTP agent built with TypeScript and AI capabilities.
4
+
5
+ ## 🚀 Quick Start
6
+
7
+ ### Prerequisites
8
+
9
+ - Node.js 20 or higher
10
+ - npm or yarn
11
+
12
+ ### Installation
13
+
14
+ ```bash
15
+ # Install dependencies
16
+ npm install
17
+ ```
18
+
19
+ ### Setup
20
+
21
+ 1. **Get your OpenRouter API key**
22
+ - Visit [OpenRouter](https://openrouter.ai/keys) and create an account
23
+ - Generate an API key
24
+ - Add it to your `.env` file
25
+
26
+ 2. **Generate XMTP keys**
27
+ ```bash
28
+ npm run keys
29
+ # or
30
+ npx hybrid gen:keys
31
+ ```
32
+
33
+ 3. **Update environment variables**
34
+ Edit the `.env` file with your API key and generated keys.
35
+
36
+ ### Development
37
+
38
+ ```bash
39
+ # Start development server with auto-reload
40
+ npm run dev
41
+
42
+ # Build for production
43
+ npm run build
44
+
45
+ # Start production server
46
+ npm start
47
+ ```
48
+
49
+ ## 📁 Project Structure
50
+
51
+ ```
52
+ {{projectName}}/
53
+ ├── src/
54
+ │ └── agent.ts # Main agent implementation
55
+ ├── dist/ # Compiled JavaScript (after build)
56
+ ├── .env # Environment variables
57
+ ├── package.json # Dependencies and scripts
58
+ ├── tsconfig.json # TypeScript configuration
59
+ ├── vitest.config.ts # Test configuration
60
+ └── README.md # This file
61
+ ```
62
+
63
+ ## 🛠️ Available Scripts
64
+
65
+ - `npm run dev` - Start development server with hot reload
66
+ - `npm run build` - Build the project for production
67
+ - `npm run start` - Start the production server
68
+ - `npm run test` - Run tests
69
+ - `npm run test:watch` - Run tests in watch mode
70
+ - `npm run test:coverage` - Run tests with coverage report
71
+ - `npm run lint` - Lint and fix code
72
+ - `npm run format` - Format code
73
+ - `npm run typecheck` - Check TypeScript types
74
+
75
+ ## 🤖 Agent Configuration
76
+
77
+ The agent is configured in `src/agent.ts`. You can customize:
78
+
79
+ - **AI Model**: Change the model in the `openrouter()` call
80
+ - **Instructions**: Modify the agent's system prompt
81
+ - **Message Filtering**: Adjust which messages the agent responds to
82
+ - **Port**: Change the server port in `.env`
83
+
84
+ ### Example Customizations
85
+
86
+ ```typescript
87
+ const agent = new Agent({
88
+ name: "My Custom Agent",
89
+ model: openrouter("anthropic/claude-3-haiku"), // Different model
90
+ instructions: "You are a helpful assistant that specializes in..."
91
+ })
92
+ ```
93
+
94
+ ## 🔧 Environment Variables
95
+
96
+ Create a `.env` file with:
97
+
98
+ ```env
99
+ # Required
100
+ OPENROUTER_API_KEY=your_openrouter_api_key_here
101
+ XMTP_WALLET_KEY=your_generated_wallet_key
102
+ XMTP_ENCRYPTION_KEY=your_generated_encryption_key
103
+
104
+ # Optional
105
+ XMTP_ENV=dev
106
+ PORT=8454
107
+ ```
108
+
109
+ ## 🧪 Testing
110
+
111
+ ```bash
112
+ # Run all tests
113
+ npm test
114
+
115
+ # Run tests in watch mode
116
+ npm run test:watch
117
+
118
+ # Run with coverage
119
+ npm run test:coverage
120
+ ```
121
+
122
+ ## 📚 Key Concepts
123
+
124
+ ### Message Filtering
125
+
126
+ The agent uses a filter function to determine which messages to respond to:
127
+
128
+ ```typescript
129
+ const filter = async ({ message }) => {
130
+ // Return true to respond, false to ignore
131
+ const content = message.content?.toString()
132
+
133
+ // Example: Only respond to messages mentioning the bot
134
+ return content?.toLowerCase().includes('@bot')
135
+ }
136
+ ```
137
+
138
+ ### Agent Instructions
139
+
140
+ The system prompt tells the AI how to behave:
141
+
142
+ ```typescript
143
+ const agent = new Agent({
144
+ instructions: "You are a helpful XMTP agent that..."
145
+ })
146
+ ```
147
+
148
+ ## 🔗 Useful Links
149
+
150
+ - [Hybrid Documentation](https://github.com/your-org/hybrid)
151
+ - [XMTP Documentation](https://docs.xmtp.org/)
152
+ - [OpenRouter Models](https://openrouter.ai/docs#models)
153
+
154
+ ## 🤝 Contributing
155
+
156
+ 1. Fork the repository
157
+ 2. Create a feature branch
158
+ 3. Make your changes
159
+ 4. Add tests
160
+ 5. Submit a pull request
161
+
162
+ ## 📄 License
163
+
164
+ MIT License - see LICENSE file for details
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "A Hybrid XMTP agent",
6
+ "type": "module",
7
+ "scripts": {
8
+ "clean": "hybrid clean",
9
+ "dev": "hybrid dev",
10
+ "build": "hybrid build",
11
+ "start": "hybrid start",
12
+ "keys": "hybrid gen:keys --write",
13
+ "test": "vitest",
14
+ "test:watch": "vitest --watch",
15
+ "lint": "biome lint --write",
16
+ "lint:check": "biome lint",
17
+ "format": "biome format --write",
18
+ "format:check": "biome format --check",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "dependencies": {
22
+ "@openrouter/ai-sdk-provider": "^1.1.2",
23
+ "hybrid": "^1.2.2"
24
+ },
25
+ "devDependencies": {
26
+ "@biomejs/biome": "^1.9.4",
27
+ "@types/node": "^22.0.0",
28
+ "@hybrd/cli": "^1.2.2",
29
+ "typescript": "^5.8.3",
30
+ "vitest": "^3.2.4"
31
+ },
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "author": "",
36
+ "license": "MIT"
37
+ }
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { agent } from "./agent.js"
3
+
4
+ describe("Agent", () => {
5
+ it("should generate a response", async () => {
6
+ const response = await agent.generate("Hello, how are you?")
7
+ expect(response).toBeDefined()
8
+ expect(typeof response).toBe("string")
9
+ expect(response.length).toBeGreaterThan(0)
10
+ })
11
+ })
@@ -0,0 +1,52 @@
1
+ import { createOpenRouter } from "@openrouter/ai-sdk-provider"
2
+ import { Agent, type MessageListenerConfig, type Reaction } from "hybrid"
3
+
4
+ export const openrouter = createOpenRouter({
5
+ apiKey: process.env.OPENROUTER_API_KEY
6
+ })
7
+
8
+ const agent = new Agent({
9
+ name: "{{projectName}}",
10
+ model: openrouter("x-ai/grok-4"),
11
+ instructions:
12
+ "You are a XMTP agent that responds to messages and reactions. Try and be as conversational as possible."
13
+ })
14
+
15
+ const filter: MessageListenerConfig["filter"] = async ({ message }) => {
16
+ const messageContent = message.content?.toString()
17
+ const contentTypeId = message.contentType?.typeId
18
+ const isMessage = contentTypeId === "text"
19
+ const isReaction = contentTypeId === "reaction"
20
+ const isReply = contentTypeId === "reply"
21
+
22
+ if (isReply) {
23
+ return true
24
+ }
25
+
26
+ if (isReaction) {
27
+ const { content, action } = message.content as Reaction
28
+
29
+ if (action === "added") {
30
+ if (content.toLowerCase().includes("👍")) {
31
+ return true
32
+ }
33
+ }
34
+ }
35
+
36
+ if (isMessage) {
37
+ const lowerContent = messageContent?.toLowerCase()
38
+ const mentionPatterns = ["@bot"]
39
+
40
+ return mentionPatterns.some((pattern) => lowerContent?.includes(pattern))
41
+ }
42
+
43
+ return false
44
+ }
45
+
46
+ agent.listen({
47
+ port: process.env.PORT || "8454",
48
+ filter
49
+ })
50
+
51
+ export { agent }
52
+ export default agent
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "lib": ["ES2020"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "outDir": "dist",
8
+ "rootDir": ".",
9
+ "strict": true,
10
+ "noEmit": false,
11
+ "declaration": true,
12
+ "skipLibCheck": true,
13
+ "isolatedModules": false,
14
+ "esModuleInterop": true,
15
+ "allowSyntheticDefaultImports": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "resolveJsonModule": true,
18
+ "types": ["node"]
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
22
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config"
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "node"
7
+ }
8
+ })