create-hybrid 1.2.3 → 1.2.4

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/dist/index.js CHANGED
@@ -1,127 +1,274 @@
1
1
  #!/usr/bin/env node
2
2
 
3
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
- }
4
+ import { Command } from "commander";
5
+ import degit from "degit";
6
+ import { readFile, readdir, writeFile } from "fs/promises";
7
+ import { join } from "path";
8
+ import prompts from "prompts";
9
+ var DEFAULT_REPO = "ian/hybrid";
10
+ var REPO = process.env.REPO || DEFAULT_REPO;
11
+ var EXAMPLES = [
12
+ {
13
+ name: "basic",
14
+ description: "Basic XMTP agent with message filtering and AI responses",
15
+ path: "basic"
16
+ // Path is now handled separately in degit options
17
+ },
18
+ {
19
+ name: "crypto-agent",
20
+ description: "Advanced agent with blockchain integration and crypto tools",
21
+ path: "crypto-agent"
22
+ // Path is now handled separately in degit options
23
+ }
24
+ ];
21
25
  function replaceTemplateVariables(content, variables) {
22
26
  return content.replace(
23
27
  /\{\{(\w+)\}\}/g,
24
28
  (match, key) => variables[key] || match
25
29
  );
26
30
  }
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);
31
+ async function updateTemplateFiles(projectDir, projectName) {
32
+ const variables = { projectName };
33
+ const filesToUpdate = [
34
+ join(projectDir, "package.json"),
35
+ join(projectDir, "README.md"),
36
+ join(projectDir, "src", "agent.ts")
37
+ ];
38
+ for (const filePath of filesToUpdate) {
39
+ try {
40
+ let content = await readFile(filePath, "utf-8");
41
+ content = replaceTemplateVariables(content, variables);
42
+ if (filePath.endsWith("package.json")) {
43
+ try {
44
+ const packageJson = JSON.parse(content);
45
+ let updated = false;
46
+ if (packageJson.name === "agent" || packageJson.name === "hybrid-example-basic-agent") {
47
+ packageJson.name = projectName;
48
+ updated = true;
49
+ }
50
+ if (!packageJson.scripts) {
51
+ packageJson.scripts = {};
52
+ }
53
+ const requiredScripts = {
54
+ clean: "hybrid clean",
55
+ dev: "hybrid dev",
56
+ build: "hybrid build",
57
+ start: "hybrid start",
58
+ keys: "hybrid gen:keys --write",
59
+ test: "vitest",
60
+ "test:watch": "vitest --watch",
61
+ "test:coverage": "vitest --coverage",
62
+ lint: "biome lint --write",
63
+ "lint:check": "biome lint",
64
+ format: "biome format --write",
65
+ "format:check": "biome format --check",
66
+ typecheck: "tsc --noEmit"
67
+ };
68
+ for (const [scriptName, scriptCommand] of Object.entries(
69
+ requiredScripts
70
+ )) {
71
+ packageJson.scripts[scriptName] = scriptCommand;
72
+ updated = true;
73
+ }
74
+ if (packageJson.dependencies) {
75
+ if (packageJson.dependencies.hybrid === "workspace:*") {
76
+ packageJson.dependencies.hybrid = "latest";
77
+ updated = true;
78
+ }
79
+ if (packageJson.dependencies["@openrouter/ai-sdk-provider"] === "catalog:ai") {
80
+ packageJson.dependencies["@openrouter/ai-sdk-provider"] = "^1.1.2";
81
+ updated = true;
82
+ }
83
+ if (packageJson.dependencies.zod === "catalog:stack") {
84
+ packageJson.dependencies.zod = "^3.23.8";
85
+ updated = true;
86
+ }
87
+ if (packageJson.dependencies["@hybrd/xmtp"]) {
88
+ packageJson.dependencies["@hybrd/xmtp"] = void 0;
89
+ updated = true;
90
+ }
91
+ }
92
+ if (packageJson.devDependencies) {
93
+ const independentDevDeps = {
94
+ "@biomejs/biome": "^1.9.4",
95
+ "@types/node": "^22.0.0",
96
+ "@hybrd/cli": "latest",
97
+ tsx: "^4.20.5",
98
+ typescript: "^5.8.3",
99
+ vitest: "^3.2.4"
100
+ };
101
+ packageJson.devDependencies["@config/biome"] = void 0;
102
+ packageJson.devDependencies["@config/tsconfig"] = void 0;
103
+ for (const [depName, depVersion] of Object.entries(
104
+ independentDevDeps
105
+ )) {
106
+ packageJson.devDependencies[depName] = depVersion;
107
+ }
108
+ updated = true;
109
+ }
110
+ if (updated) {
111
+ content = `${JSON.stringify(packageJson, null, " ")}
112
+ `;
113
+ }
114
+ } catch (parseError) {
115
+ console.log("\u26A0\uFE0F Could not parse package.json for name update");
116
+ }
117
+ }
118
+ if (filePath.endsWith("README.md")) {
119
+ content = content.replace(/^# .*$/m, `# ${projectName}`);
120
+ }
121
+ await writeFile(filePath, content, "utf-8");
122
+ } catch (error) {
123
+ console.log(
124
+ `\u26A0\uFE0F Could not update ${filePath.split("/").pop()}: file not found or error occurred`
125
+ );
37
126
  }
38
127
  }
128
+ const envPath = join(projectDir, ".env");
129
+ try {
130
+ await readFile(envPath, "utf-8");
131
+ } catch {
132
+ const envContent = `# Required
133
+ OPENROUTER_API_KEY=your_openrouter_api_key_here
134
+ XMTP_WALLET_KEY=your_wallet_key_here
135
+ XMTP_ENCRYPTION_KEY=your_encryption_key_here
136
+
137
+ # Optional
138
+ XMTP_ENV=dev
139
+ PORT=8454`;
140
+ await writeFile(envPath, envContent, "utf-8");
141
+ console.log("\u{1F4C4} Created .env template file");
142
+ }
143
+ const vitestConfigPath = join(projectDir, "vitest.config.ts");
144
+ try {
145
+ await readFile(vitestConfigPath, "utf-8");
146
+ } catch {
147
+ const vitestConfigContent = `import { defineConfig } from "vitest/config"
148
+
149
+ export default defineConfig({
150
+ test: {
151
+ environment: "node",
152
+ globals: true,
153
+ setupFiles: []
154
+ },
155
+ resolve: {
156
+ alias: {
157
+ "@": "./src"
158
+ }
159
+ }
160
+ })`;
161
+ await writeFile(vitestConfigPath, vitestConfigContent, "utf-8");
162
+ console.log("\u{1F4C4} Created vitest.config.ts file");
163
+ }
164
+ const agentTestPath = join(projectDir, "src", "agent.test.ts");
165
+ try {
166
+ await readFile(agentTestPath, "utf-8");
167
+ } catch {
168
+ const agentTestContent = `import { describe, expect, it } from "vitest"
169
+
170
+ // Example test file - replace with actual tests for your agent
171
+
172
+ describe("Agent", () => {
173
+ it("should be defined", () => {
174
+ // This is a placeholder test
175
+ // Add real tests for your agent functionality
176
+ expect(true).toBe(true)
177
+ })
178
+ })`;
179
+ await writeFile(agentTestPath, agentTestContent, "utf-8");
180
+ console.log("\u{1F4C4} Created src/agent.test.ts file");
181
+ }
39
182
  }
40
- async function initializeProject() {
183
+ async function checkDirectoryEmpty(dirPath) {
184
+ try {
185
+ const files = await readdir(dirPath);
186
+ const significantFiles = files.filter(
187
+ (file) => !file.startsWith(".") && file !== "node_modules" && file !== "package-lock.json" && file !== "yarn.lock" && file !== "pnpm-lock.yaml"
188
+ );
189
+ return significantFiles.length === 0;
190
+ } catch {
191
+ return true;
192
+ }
193
+ }
194
+ async function createProject(projectName, exampleName) {
41
195
  console.log("\u{1F680} Creating a new Hybrid project...");
42
- const projectNameArg = process.argv[2];
43
- let projectName = projectNameArg;
44
- if (projectNameArg === "") {
196
+ if (!projectName || projectName.trim() === "") {
45
197
  console.error("\u274C Project name is required");
46
198
  process.exit(1);
47
199
  }
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
200
  const sanitizedName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
57
201
  const currentDir = process.cwd();
58
202
  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 {
203
+ const isEmpty = await checkDirectoryEmpty(projectDir);
204
+ if (!isEmpty) {
205
+ console.error(
206
+ `\u274C Directory "${sanitizedName}" already exists and is not empty`
207
+ );
208
+ console.error(
209
+ "Please choose a different name or remove the existing directory"
210
+ );
211
+ process.exit(1);
212
+ }
213
+ let selectedExample;
214
+ if (exampleName) {
215
+ const example = EXAMPLES.find((ex) => ex.name === exampleName);
216
+ if (!example) {
217
+ console.error(`\u274C Example "${exampleName}" not found`);
218
+ console.error(
219
+ `Available examples: ${EXAMPLES.map((ex) => ex.name).join(", ")}`
220
+ );
221
+ process.exit(1);
72
222
  }
223
+ selectedExample = example;
224
+ console.log(`\u{1F4CB} Using example: ${selectedExample.name}`);
73
225
  } 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"
226
+ if (!process.stdin.isTTY) {
227
+ console.error(
228
+ "\u274C Example is required in non-interactive mode. Use --example <name>"
78
229
  );
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 {
230
+ console.error(
231
+ `Available examples: ${EXAMPLES.map((ex) => ex.name).join(", ")}`
232
+ );
233
+ process.exit(1);
87
234
  }
235
+ const { example } = await prompts({
236
+ type: "select",
237
+ name: "example",
238
+ message: "Which example would you like to use?",
239
+ choices: EXAMPLES.map((ex) => ({
240
+ title: ex.name,
241
+ description: ex.description,
242
+ value: ex
243
+ })),
244
+ initial: 0
245
+ });
246
+ if (!example) {
247
+ console.log("\u274C No example selected. Exiting...");
248
+ process.exit(1);
249
+ }
250
+ selectedExample = example;
88
251
  }
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
- }
252
+ console.log(`\u{1F4E6} Cloning ${selectedExample.name} example...`);
97
253
  try {
98
- await mkdir(projectDir, { recursive: true });
99
- await copyTemplate(templateDir, projectDir);
100
- console.log(`\u2705 Template files copied to: ${sanitizedName}`);
254
+ let degitSource;
255
+ if (REPO.includes("#")) {
256
+ const [repoWithBranch] = REPO.split("/examples/");
257
+ degitSource = `${repoWithBranch}/examples/${selectedExample.name}`;
258
+ } else {
259
+ degitSource = `${REPO}/examples/${selectedExample.name}`;
260
+ }
261
+ console.log(`\u{1F50D} Degit source: ${degitSource}`);
262
+ const emitter = degit(degitSource);
263
+ await emitter.clone(projectDir);
264
+ console.log(`\u2705 Template cloned to: ${sanitizedName}`);
101
265
  } catch (error) {
102
- console.error("\u274C Failed to copy template files:", error);
266
+ console.error("\u274C Failed to clone template:", error);
103
267
  process.exit(1);
104
268
  }
105
- const variables = {
106
- projectName: sanitizedName
107
- };
269
+ console.log("\u{1F527} Updating template variables...");
108
270
  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
- }
271
+ await updateTemplateFiles(projectDir, sanitizedName);
125
272
  console.log("\u2705 Template variables updated");
126
273
  } catch (error) {
127
274
  console.error("\u274C Failed to update template variables:", error);
@@ -130,19 +277,66 @@ async function initializeProject() {
130
277
  console.log(`
131
278
  \u{1F4C2} Project created in: ${projectDir}`);
132
279
  console.log("\n\u{1F4CB} Next steps:");
133
- console.log(`1. cd ${sanitizedName}`);
280
+ if (projectName !== ".") {
281
+ console.log(`1. cd ${sanitizedName}`);
282
+ }
134
283
  console.log(
135
- "2. Install dependencies (npm install, yarn install, or pnpm install)"
284
+ `${projectName !== "." ? "2" : "1"}. Install dependencies (npm install, yarn install, or pnpm install)`
285
+ );
286
+ console.log(
287
+ `${projectName !== "." ? "3" : "2"}. Get your OpenRouter API key from https://openrouter.ai/keys`
288
+ );
289
+ console.log(
290
+ `${projectName !== "." ? "4" : "3"}. Add your API key to the OPENROUTER_API_KEY in .env`
291
+ );
292
+ console.log(
293
+ `${projectName !== "." ? "5" : "4"}. Set XMTP_ENV in .env (dev or production)`
294
+ );
295
+ console.log(
296
+ `${projectName !== "." ? "6" : "5"}. Generate keys: npm run keys (or yarn/pnpm equivalent)`
297
+ );
298
+ console.log(
299
+ `${projectName !== "." ? "7" : "6"}. Start development: npm run dev (or yarn/pnpm equivalent)`
136
300
  );
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
301
  console.log(
143
302
  "\n\u{1F4D6} For more information, see the README.md file in your project"
144
303
  );
145
304
  }
305
+ async function initializeProject() {
306
+ const program = new Command();
307
+ program.name("create-hybrid").description("Create a new Hybrid XMTP agent project").version("1.2.3").argument("[project-name]", "Name of the project").option("-e, --example <example>", "Example to use (basic, crypto-agent)").action(async (projectName, options) => {
308
+ let finalProjectName = projectName;
309
+ if (process.env.CI) {
310
+ console.log(
311
+ `\u{1F50D} Debug: projectName="${projectName}", options.example="${options?.example}"`
312
+ );
313
+ }
314
+ if (!finalProjectName || finalProjectName.trim() === "") {
315
+ if (!process.stdin.isTTY) {
316
+ console.error("\u274C Project name is required");
317
+ process.exit(1);
318
+ }
319
+ const { name } = await prompts({
320
+ type: "text",
321
+ name: "name",
322
+ message: "What is your project name?",
323
+ validate: (value) => {
324
+ if (!value || !value.trim()) {
325
+ return "Project name is required";
326
+ }
327
+ return true;
328
+ }
329
+ });
330
+ if (!name) {
331
+ console.log("\u274C Project name is required. Exiting...");
332
+ process.exit(1);
333
+ }
334
+ finalProjectName = name;
335
+ }
336
+ await createProject(finalProjectName, options?.example);
337
+ });
338
+ await program.parseAsync();
339
+ }
146
340
  async function main() {
147
341
  const nodeVersion = process.versions.node;
148
342
  const [major] = nodeVersion.split(".").map(Number);
package/dist/index.js.map CHANGED
@@ -1 +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":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { Command } from \"commander\"\nimport degit from \"degit\"\nimport { readFile, readdir, writeFile } from \"node:fs/promises\"\nimport { join } from \"node:path\"\nimport prompts from \"prompts\"\n\ninterface Example {\n\tname: string\n\tdescription: string\n\tpath: string\n}\n\n// Default to main branch, but allow override via REPO env var for CI/testing\nconst DEFAULT_REPO = \"ian/hybrid\"\nconst REPO = process.env.REPO || DEFAULT_REPO\n\nconst EXAMPLES: Example[] = [\n\t{\n\t\tname: \"basic\",\n\t\tdescription: \"Basic XMTP agent with message filtering and AI responses\",\n\t\tpath: \"basic\" // Path is now handled separately in degit options\n\t},\n\t{\n\t\tname: \"crypto-agent\",\n\t\tdescription: \"Advanced agent with blockchain integration and crypto tools\",\n\t\tpath: \"crypto-agent\" // Path is now handled separately in degit options\n\t}\n]\n\nfunction isInMonorepo(): boolean {\n\t// Check if we're running from within the monorepo (for development/testing)\n\tconst currentDir = process.cwd()\n\tconst packageJsonPath = join(currentDir, \"package.json\")\n\ttry {\n\t\tconst packageJson = require(packageJsonPath)\n\t\treturn (\n\t\t\tpackageJson.name === \"hybrid\" || currentDir.includes(\"/hybrid/packages/\")\n\t\t)\n\t} catch {\n\t\treturn false\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 updateTemplateFiles(\n\tprojectDir: string,\n\tprojectName: string\n): Promise<void> {\n\tconst variables = { projectName }\n\n\tconst filesToUpdate = [\n\t\tjoin(projectDir, \"package.json\"),\n\t\tjoin(projectDir, \"README.md\"),\n\t\tjoin(projectDir, \"src\", \"agent.ts\")\n\t]\n\n\tfor (const filePath of filesToUpdate) {\n\t\ttry {\n\t\t\tlet content = await readFile(filePath, \"utf-8\")\n\n\t\t\t// First try template variable replacement\n\t\t\tcontent = replaceTemplateVariables(content, variables)\n\n\t\t\t// Special handling for package.json if template variables weren't found\n\t\t\tif (filePath.endsWith(\"package.json\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst packageJson = JSON.parse(content)\n\t\t\t\t\tlet updated = false\n\n\t\t\t\t\t// If name is still a generic name, replace it\n\t\t\t\t\tif (\n\t\t\t\t\t\tpackageJson.name === \"agent\" ||\n\t\t\t\t\t\tpackageJson.name === \"hybrid-example-basic-agent\"\n\t\t\t\t\t) {\n\t\t\t\t\t\tpackageJson.name = projectName\n\t\t\t\t\t\tupdated = true\n\t\t\t\t\t}\n\n\t\t\t\t\t// Ensure required scripts exist\n\t\t\t\t\tif (!packageJson.scripts) {\n\t\t\t\t\t\tpackageJson.scripts = {}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Add missing scripts and update old scripts to use hybrid CLI\n\t\t\t\t\tconst requiredScripts = {\n\t\t\t\t\t\tclean: \"hybrid clean\",\n\t\t\t\t\t\tdev: \"hybrid dev\",\n\t\t\t\t\t\tbuild: \"hybrid build\",\n\t\t\t\t\t\tstart: \"hybrid start\",\n\t\t\t\t\t\tkeys: \"hybrid gen:keys --write\",\n\t\t\t\t\t\ttest: \"vitest\",\n\t\t\t\t\t\t\"test:watch\": \"vitest --watch\",\n\t\t\t\t\t\t\"test:coverage\": \"vitest --coverage\",\n\t\t\t\t\t\tlint: \"biome lint --write\",\n\t\t\t\t\t\t\"lint:check\": \"biome lint\",\n\t\t\t\t\t\tformat: \"biome format --write\",\n\t\t\t\t\t\t\"format:check\": \"biome format --check\",\n\t\t\t\t\t\ttypecheck: \"tsc --noEmit\"\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const [scriptName, scriptCommand] of Object.entries(\n\t\t\t\t\t\trequiredScripts\n\t\t\t\t\t)) {\n\t\t\t\t\t\t// Always update scripts to use the correct hybrid CLI commands\n\t\t\t\t\t\tpackageJson.scripts[scriptName] = scriptCommand\n\t\t\t\t\t\tupdated = true\n\t\t\t\t\t}\n\n\t\t\t\t\t// Update dependencies to use independent packages\n\t\t\t\t\tif (packageJson.dependencies) {\n\t\t\t\t\t\tif (packageJson.dependencies.hybrid === \"workspace:*\") {\n\t\t\t\t\t\t\tpackageJson.dependencies.hybrid = \"latest\"\n\t\t\t\t\t\t\tupdated = true\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tpackageJson.dependencies[\"@openrouter/ai-sdk-provider\"] ===\n\t\t\t\t\t\t\t\"catalog:ai\"\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tpackageJson.dependencies[\"@openrouter/ai-sdk-provider\"] = \"^1.1.2\"\n\t\t\t\t\t\t\tupdated = true\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (packageJson.dependencies.zod === \"catalog:stack\") {\n\t\t\t\t\t\t\tpackageJson.dependencies.zod = \"^3.23.8\"\n\t\t\t\t\t\t\tupdated = true\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Remove workspace dependencies\n\t\t\t\t\t\tif (packageJson.dependencies[\"@hybrd/xmtp\"]) {\n\t\t\t\t\t\t\tpackageJson.dependencies[\"@hybrd/xmtp\"] = undefined\n\t\t\t\t\t\t\tupdated = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Update devDependencies to use independent packages\n\t\t\t\t\tif (packageJson.devDependencies) {\n\t\t\t\t\t\tconst independentDevDeps = {\n\t\t\t\t\t\t\t\"@biomejs/biome\": \"^1.9.4\",\n\t\t\t\t\t\t\t\"@types/node\": \"^22.0.0\",\n\t\t\t\t\t\t\t\"@hybrd/cli\": \"latest\",\n\t\t\t\t\t\t\ttsx: \"^4.20.5\",\n\t\t\t\t\t\t\ttypescript: \"^5.8.3\",\n\t\t\t\t\t\t\tvitest: \"^3.2.4\"\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Remove workspace dependencies\n\t\t\t\t\t\tpackageJson.devDependencies[\"@config/biome\"] = undefined\n\t\t\t\t\t\tpackageJson.devDependencies[\"@config/tsconfig\"] = undefined\n\n\t\t\t\t\t\t// Add independent dependencies\n\t\t\t\t\t\tfor (const [depName, depVersion] of Object.entries(\n\t\t\t\t\t\t\tindependentDevDeps\n\t\t\t\t\t\t)) {\n\t\t\t\t\t\t\tpackageJson.devDependencies[depName] = depVersion\n\t\t\t\t\t\t}\n\t\t\t\t\t\tupdated = true\n\t\t\t\t\t}\n\n\t\t\t\t\tif (updated) {\n\t\t\t\t\t\tcontent = `${JSON.stringify(packageJson, null, \"\\t\")}\\n`\n\t\t\t\t\t}\n\t\t\t\t} catch (parseError) {\n\t\t\t\t\tconsole.log(\"⚠️ Could not parse package.json for name update\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Special handling for README.md if template variables weren't found\n\t\t\tif (filePath.endsWith(\"README.md\")) {\n\t\t\t\t// Replace common README title patterns with project name\n\t\t\t\tcontent = content.replace(/^# .*$/m, `# ${projectName}`)\n\t\t\t}\n\n\t\t\tawait writeFile(filePath, content, \"utf-8\")\n\t\t} catch (error) {\n\t\t\tconsole.log(\n\t\t\t\t`⚠️ Could not update ${filePath.split(\"/\").pop()}: file not found or error occurred`\n\t\t\t)\n\t\t}\n\t}\n\n\t// Ensure .env file exists - create it if missing\n\tconst envPath = join(projectDir, \".env\")\n\ttry {\n\t\tawait readFile(envPath, \"utf-8\")\n\t} catch {\n\t\t// .env file doesn't exist, create it\n\t\tconst envContent = `# Required\nOPENROUTER_API_KEY=your_openrouter_api_key_here\nXMTP_WALLET_KEY=your_wallet_key_here\nXMTP_ENCRYPTION_KEY=your_encryption_key_here\n\n# Optional\nXMTP_ENV=dev\nPORT=8454`\n\t\tawait writeFile(envPath, envContent, \"utf-8\")\n\t\tconsole.log(\"📄 Created .env template file\")\n\t}\n\n\t// Ensure vitest.config.ts exists - create it if missing\n\tconst vitestConfigPath = join(projectDir, \"vitest.config.ts\")\n\ttry {\n\t\tawait readFile(vitestConfigPath, \"utf-8\")\n\t} catch {\n\t\t// vitest.config.ts doesn't exist, create it\n\t\tconst vitestConfigContent = `import { defineConfig } from \"vitest/config\"\n\nexport default defineConfig({\n\ttest: {\n\t\tenvironment: \"node\",\n\t\tglobals: true,\n\t\tsetupFiles: []\n\t},\n\tresolve: {\n\t\talias: {\n\t\t\t\"@\": \"./src\"\n\t\t}\n\t}\n})`\n\t\tawait writeFile(vitestConfigPath, vitestConfigContent, \"utf-8\")\n\t\tconsole.log(\"📄 Created vitest.config.ts file\")\n\t}\n\n\t// Ensure src/agent.test.ts exists - create it if missing\n\tconst agentTestPath = join(projectDir, \"src\", \"agent.test.ts\")\n\ttry {\n\t\tawait readFile(agentTestPath, \"utf-8\")\n\t} catch {\n\t\t// agent.test.ts doesn't exist, create it\n\t\tconst agentTestContent = `import { describe, expect, it } from \"vitest\"\n\n// Example test file - replace with actual tests for your agent\n\ndescribe(\"Agent\", () => {\n\tit(\"should be defined\", () => {\n\t\t// This is a placeholder test\n\t\t// Add real tests for your agent functionality\n\t\texpect(true).toBe(true)\n\t})\n})`\n\t\tawait writeFile(agentTestPath, agentTestContent, \"utf-8\")\n\t\tconsole.log(\"📄 Created src/agent.test.ts file\")\n\t}\n}\n\nasync function checkDirectoryEmpty(dirPath: string): Promise<boolean> {\n\ttry {\n\t\tconst files = await readdir(dirPath)\n\t\tconst significantFiles = files.filter(\n\t\t\t(file) =>\n\t\t\t\t!file.startsWith(\".\") &&\n\t\t\t\tfile !== \"node_modules\" &&\n\t\t\t\tfile !== \"package-lock.json\" &&\n\t\t\t\tfile !== \"yarn.lock\" &&\n\t\t\t\tfile !== \"pnpm-lock.yaml\"\n\t\t)\n\t\treturn significantFiles.length === 0\n\t} catch {\n\t\t// Directory doesn't exist, so it's \"empty\"\n\t\treturn true\n\t}\n}\n\nasync function createProject(\n\tprojectName: string,\n\texampleName?: string\n): Promise<void> {\n\tconsole.log(\"🚀 Creating a new Hybrid project...\")\n\n\t// Validate project name\n\tif (!projectName || projectName.trim() === \"\") {\n\t\tconsole.error(\"❌ Project name is required\")\n\t\tprocess.exit(1)\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\t// Check if directory is empty\n\tconst isEmpty = await checkDirectoryEmpty(projectDir)\n\tif (!isEmpty) {\n\t\tconsole.error(\n\t\t\t`❌ Directory \"${sanitizedName}\" already exists and is not empty`\n\t\t)\n\t\tconsole.error(\n\t\t\t\"Please choose a different name or remove the existing directory\"\n\t\t)\n\t\tprocess.exit(1)\n\t}\n\n\t// Select example if not provided\n\tlet selectedExample: Example\n\tif (exampleName) {\n\t\tconst example = EXAMPLES.find((ex) => ex.name === exampleName)\n\t\tif (!example) {\n\t\t\tconsole.error(`❌ Example \"${exampleName}\" not found`)\n\t\t\tconsole.error(\n\t\t\t\t`Available examples: ${EXAMPLES.map((ex) => ex.name).join(\", \")}`\n\t\t\t)\n\t\t\tprocess.exit(1)\n\t\t}\n\t\tselectedExample = example\n\t\tconsole.log(`📋 Using example: ${selectedExample.name}`)\n\t} else {\n\t\t// Check if we're running in a non-interactive environment (like CI)\n\t\tif (!process.stdin.isTTY) {\n\t\t\tconsole.error(\n\t\t\t\t\"❌ Example is required in non-interactive mode. Use --example <name>\"\n\t\t\t)\n\t\t\tconsole.error(\n\t\t\t\t`Available examples: ${EXAMPLES.map((ex) => ex.name).join(\", \")}`\n\t\t\t)\n\t\t\tprocess.exit(1)\n\t\t}\n\n\t\tconst { example } = await prompts({\n\t\t\ttype: \"select\",\n\t\t\tname: \"example\",\n\t\t\tmessage: \"Which example would you like to use?\",\n\t\t\tchoices: EXAMPLES.map((ex) => ({\n\t\t\t\ttitle: ex.name,\n\t\t\t\tdescription: ex.description,\n\t\t\t\tvalue: ex\n\t\t\t})),\n\t\t\tinitial: 0\n\t\t})\n\n\t\tif (!example) {\n\t\t\tconsole.log(\"❌ No example selected. Exiting...\")\n\t\t\tprocess.exit(1)\n\t\t}\n\n\t\tselectedExample = example\n\t}\n\n\tconsole.log(`📦 Cloning ${selectedExample.name} example...`)\n\n\ttry {\n\t\t// For degit, the correct syntax is: repo#branch/subdirectory\n\t\t// But we need to be careful about the path construction\n\t\tlet degitSource: string\n\n\t\tif (REPO.includes(\"#\")) {\n\t\t\t// REPO is in format \"user/repo#branch\"\n\t\t\t// We need to construct: user/repo#branch/examples/basic\n\t\t\tconst [repoWithBranch] = REPO.split(\"/examples/\") // Remove any existing path\n\t\t\tdegitSource = `${repoWithBranch}/examples/${selectedExample.name}`\n\t\t} else {\n\t\t\t// No branch specified, use default format\n\t\t\tdegitSource = `${REPO}/examples/${selectedExample.name}`\n\t\t}\n\n\t\tconsole.log(`🔍 Degit source: ${degitSource}`)\n\t\tconst emitter = degit(degitSource)\n\t\tawait emitter.clone(projectDir)\n\t\tconsole.log(`✅ Template cloned to: ${sanitizedName}`)\n\t} catch (error) {\n\t\tconsole.error(\"❌ Failed to clone template:\", error)\n\t\tprocess.exit(1)\n\t}\n\n\t// Update template variables\n\tconsole.log(\"🔧 Updating template variables...\")\n\ttry {\n\t\tawait updateTemplateFiles(projectDir, sanitizedName)\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\tif (projectName !== \".\") {\n\t\tconsole.log(`1. cd ${sanitizedName}`)\n\t}\n\tconsole.log(\n\t\t`${projectName !== \".\" ? \"2\" : \"1\"}. Install dependencies (npm install, yarn install, or pnpm install)`\n\t)\n\tconsole.log(\n\t\t`${projectName !== \".\" ? \"3\" : \"2\"}. Get your OpenRouter API key from https://openrouter.ai/keys`\n\t)\n\tconsole.log(\n\t\t`${projectName !== \".\" ? \"4\" : \"3\"}. Add your API key to the OPENROUTER_API_KEY in .env`\n\t)\n\tconsole.log(\n\t\t`${projectName !== \".\" ? \"5\" : \"4\"}. Set XMTP_ENV in .env (dev or production)`\n\t)\n\tconsole.log(\n\t\t`${projectName !== \".\" ? \"6\" : \"5\"}. Generate keys: npm run keys (or yarn/pnpm equivalent)`\n\t)\n\tconsole.log(\n\t\t`${projectName !== \".\" ? \"7\" : \"6\"}. Start development: npm run dev (or yarn/pnpm equivalent)`\n\t)\n\n\tconsole.log(\n\t\t\"\\n📖 For more information, see the README.md file in your project\"\n\t)\n}\n\nexport async function initializeProject(): Promise<void> {\n\tconst program = new Command()\n\n\tprogram\n\t\t.name(\"create-hybrid\")\n\t\t.description(\"Create a new Hybrid XMTP agent project\")\n\t\t.version(\"1.2.3\")\n\t\t.argument(\"[project-name]\", \"Name of the project\")\n\t\t.option(\"-e, --example <example>\", \"Example to use (basic, crypto-agent)\")\n\t\t.action(async (projectName?: string, options?: { example?: string }) => {\n\t\t\tlet finalProjectName = projectName\n\n\t\t\t// Debug logging for CI troubleshooting\n\t\t\tif (process.env.CI) {\n\t\t\t\tconsole.log(\n\t\t\t\t\t`🔍 Debug: projectName=\"${projectName}\", options.example=\"${options?.example}\"`\n\t\t\t\t)\n\t\t\t}\n\n\t\t\t// If no project name provided or empty string, prompt for it\n\t\t\tif (!finalProjectName || finalProjectName.trim() === \"\") {\n\t\t\t\t// Check if we're running in a non-interactive environment (like tests)\n\t\t\t\tif (!process.stdin.isTTY) {\n\t\t\t\t\tconsole.error(\"❌ Project name is required\")\n\t\t\t\t\tprocess.exit(1)\n\t\t\t\t}\n\n\t\t\t\tconst { name } = await prompts({\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\tname: \"name\",\n\t\t\t\t\tmessage: \"What is your project name?\",\n\t\t\t\t\tvalidate: (value: string) => {\n\t\t\t\t\t\tif (!value || !value.trim()) {\n\t\t\t\t\t\t\treturn \"Project name is required\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\t\tif (!name) {\n\t\t\t\t\tconsole.log(\"❌ Project name is required. Exiting...\")\n\t\t\t\t\tprocess.exit(1)\n\t\t\t\t}\n\n\t\t\t\tfinalProjectName = name\n\t\t\t}\n\n\t\t\tawait createProject(finalProjectName as string, options?.example)\n\t\t})\n\n\tawait program.parseAsync()\n}\n\nasync function main(): Promise<void> {\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 CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "create-hybrid",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Create a new Hybrid XMTP agent project",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-hybrid": "./dist/index.js"
8
8
  },
9
9
  "files": [
10
- "dist",
11
- "templates"
10
+ "dist"
12
11
  ],
13
12
  "exports": {
14
13
  ".": {
@@ -16,12 +15,17 @@
16
15
  "import": "./dist/index.js"
17
16
  }
18
17
  },
19
- "dependencies": {},
18
+ "dependencies": {
19
+ "degit": "^2.8.4",
20
+ "commander": "^12.1.0",
21
+ "prompts": "^2.4.2"
22
+ },
20
23
  "devDependencies": {
21
24
  "@types/node": "22.8.6",
25
+ "@types/prompts": "^2.4.9",
22
26
  "tsup": "^8.5.0",
23
- "@config/biome": "0.0.0",
24
- "@config/tsconfig": "0.0.0"
27
+ "@config/tsconfig": "0.0.0",
28
+ "@config/biome": "0.0.0"
25
29
  },
26
30
  "engines": {
27
31
  "node": ">=20"
@@ -1,164 +0,0 @@
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
@@ -1,37 +0,0 @@
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
- }
@@ -1,11 +0,0 @@
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
- })
@@ -1,52 +0,0 @@
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
@@ -1,22 +0,0 @@
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
- }
@@ -1,8 +0,0 @@
1
- import { defineConfig } from "vitest/config"
2
-
3
- export default defineConfig({
4
- test: {
5
- globals: true,
6
- environment: "node"
7
- }
8
- })