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 +98 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
- package/templates/agent/README.md +164 -0
- package/templates/agent/package.json +37 -0
- package/templates/agent/src/agent.test.ts +11 -0
- package/templates/agent/src/agent.ts +52 -0
- package/templates/agent/tsconfig.json +22 -0
- package/templates/agent/vitest.config.ts +8 -0
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
|
package/dist/index.d.ts
ADDED
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
|
+
}
|