create-hybrid 1.4.4 → 2.0.0

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,393 +1,824 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command } from "commander";
5
- import degit from "degit";
6
- import { readFile, readdir, writeFile } from "fs/promises";
7
- import { join } from "path";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
+ import { dirname, join } from "path";
6
+ import { fileURLToPath } from "url";
8
7
  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
- available: true
17
- },
18
- {
19
- name: "miniapp",
20
- description: "Hybrid agent with miniapp integration for onchain interactions",
21
- path: "miniapp",
22
- available: true
23
- },
24
- {
25
- name: "with-ponder",
26
- description: "Agent with Ponder integration for indexing blockchain data",
27
- path: "with-ponder",
28
- available: false,
29
- message: "Coming soon"
30
- },
31
- {
32
- name: "with-foundry",
33
- description: "Agent with Foundry integration for smart contract development",
34
- path: "with-foundry",
35
- available: false,
36
- message: "Coming soon"
37
- }
38
- ];
39
- function replaceTemplateVariables(content, variables) {
40
- return content.replace(
41
- /\{\{(\w+)\}\}/g,
42
- (match, key) => variables[key] || match
43
- );
44
- }
45
- async function updateTemplateFiles(projectDir, projectName) {
46
- const variables = { projectName };
47
- const filesToUpdate = [
48
- join(projectDir, "package.json"),
49
- join(projectDir, "README.md"),
50
- join(projectDir, "src", "agent.ts")
51
- ];
52
- for (const filePath of filesToUpdate) {
53
- try {
54
- let content = await readFile(filePath, "utf-8");
55
- content = replaceTemplateVariables(content, variables);
56
- if (filePath.endsWith("package.json")) {
57
- try {
58
- const packageJson = JSON.parse(content);
59
- let updated = false;
60
- if (packageJson.name === "agent" || packageJson.name === "hybrid-example-basic-agent") {
61
- packageJson.name = projectName;
62
- updated = true;
63
- }
64
- if (!packageJson.scripts) {
65
- packageJson.scripts = {};
66
- }
67
- const requiredScripts = {
68
- clean: "hybrid clean",
69
- dev: "hybrid dev",
70
- build: "hybrid build",
71
- start: "hybrid start",
72
- keys: "hybrid keys --write",
73
- test: "vitest",
74
- "test:watch": "vitest --watch",
75
- "test:coverage": "vitest --coverage",
76
- lint: "biome lint --write",
77
- "lint:check": "biome lint",
78
- format: "biome format --write",
79
- "format:check": "biome format --check",
80
- typecheck: "tsc --noEmit"
81
- };
82
- for (const [scriptName, scriptCommand] of Object.entries(
83
- requiredScripts
84
- )) {
85
- packageJson.scripts[scriptName] = scriptCommand;
86
- updated = true;
87
- }
88
- if (packageJson.dependencies) {
89
- if (packageJson.dependencies.hybrid === "workspace:*") {
90
- packageJson.dependencies.hybrid = "latest";
91
- updated = true;
92
- }
93
- if (packageJson.dependencies["@openrouter/ai-sdk-provider"] === "catalog:ai") {
94
- packageJson.dependencies["@openrouter/ai-sdk-provider"] = "^1.1.2";
95
- updated = true;
96
- }
97
- if (packageJson.dependencies.zod === "catalog:stack") {
98
- packageJson.dependencies.zod = "^3.23.8";
99
- updated = true;
100
- }
101
- if (packageJson.dependencies["@hybrd/xmtp"]) {
102
- packageJson.dependencies["@hybrd/xmtp"] = void 0;
103
- updated = true;
104
- }
105
- }
106
- if (packageJson.devDependencies) {
107
- const independentDevDeps = {
108
- "@biomejs/biome": "^1.9.4",
109
- "@types/node": "^22.0.0",
110
- "@hybrd/cli": "latest",
111
- tsx: "^4.20.5",
112
- typescript: "^5.8.3",
113
- vitest: "^3.2.4"
114
- };
115
- packageJson.devDependencies["@config/biome"] = void 0;
116
- packageJson.devDependencies["@config/tsconfig"] = void 0;
117
- for (const [depName, depVersion] of Object.entries(
118
- independentDevDeps
119
- )) {
120
- packageJson.devDependencies[depName] = depVersion;
121
- }
122
- updated = true;
123
- }
124
- if (updated) {
125
- content = `${JSON.stringify(packageJson, null, " ")}
126
- `;
127
- }
128
- } catch (parseError) {
129
- console.log("\u26A0\uFE0F Could not parse package.json for name update");
130
- }
8
+ var __dirname = dirname(fileURLToPath(import.meta.url));
9
+ var TEMPLATES_DIR = join(__dirname, "..", "templates");
10
+ function parseArgs() {
11
+ const args = process.argv.slice(2);
12
+ const result = {};
13
+ for (let i = 0; i < args.length; i++) {
14
+ const arg = args[i];
15
+ if (arg?.startsWith("--")) {
16
+ const key = arg.slice(2);
17
+ const value = args[i + 1];
18
+ if (value && !value.startsWith("--")) {
19
+ result[key] = value;
20
+ i++;
21
+ } else {
22
+ result[key] = "true";
131
23
  }
132
- if (filePath.endsWith("README.md")) {
133
- content = content.replace(/^# .*$/m, `# ${projectName}`);
134
- }
135
- await writeFile(filePath, content, "utf-8");
136
- } catch (error) {
137
- console.log(
138
- `\u26A0\uFE0F Could not update ${filePath.split("/").pop()}: file not found or error occurred`
139
- );
24
+ } else if (!result.name && arg) {
25
+ result.name = arg;
140
26
  }
141
27
  }
142
- const envPath = join(projectDir, ".env");
143
- try {
144
- await readFile(envPath, "utf-8");
145
- } catch {
146
- const envContent = `# Required
147
- OPENROUTER_API_KEY=your_openrouter_api_key_here
148
- XMTP_WALLET_KEY=your_wallet_key_here
149
- XMTP_DB_ENCRYPTION_KEY=your_encryption_key_here
150
-
151
- # Optional
152
- XMTP_ENV=dev
153
- PORT=8454`;
154
- await writeFile(envPath, envContent, "utf-8");
155
- console.log("\u{1F4C4} Created .env template file");
156
- }
157
- const vitestConfigPath = join(projectDir, "vitest.config.ts");
158
- try {
159
- await readFile(vitestConfigPath, "utf-8");
160
- } catch {
161
- const vitestConfigContent = `import { defineConfig } from "vitest/config"
162
-
163
- export default defineConfig({
164
- test: {
165
- environment: "node",
166
- globals: true,
167
- setupFiles: []
168
- },
169
- resolve: {
170
- alias: {
171
- "@": "./src"
172
- }
173
- }
174
- })`;
175
- await writeFile(vitestConfigPath, vitestConfigContent, "utf-8");
176
- console.log("\u{1F4C4} Created vitest.config.ts file");
177
- }
178
- const agentTestPath = join(projectDir, "src", "agent.test.ts");
179
- try {
180
- await readFile(agentTestPath, "utf-8");
181
- } catch {
182
- const agentTestContent = `import { describe, expect, it } from "vitest"
183
-
184
- // Example test file - replace with actual tests for your agent
185
-
186
- describe("Agent", () => {
187
- it("should be defined", () => {
188
- // This is a placeholder test
189
- // Add real tests for your agent functionality
190
- expect(true).toBe(true)
191
- })
192
- })`;
193
- await writeFile(agentTestPath, agentTestContent, "utf-8");
194
- console.log("\u{1F4C4} Created src/agent.test.ts file");
195
- }
196
- }
197
- async function checkDirectoryEmpty(dirPath) {
198
- try {
199
- const files = await readdir(dirPath);
200
- const significantFiles = files.filter(
201
- (file) => !file.startsWith(".") && file !== "node_modules" && file !== "package-lock.json" && file !== "yarn.lock" && file !== "pnpm-lock.yaml"
202
- );
203
- return significantFiles.length === 0;
204
- } catch {
205
- return true;
206
- }
28
+ return result;
207
29
  }
208
- async function createProject(projectName, exampleName) {
209
- console.log("\u{1F680} Creating a new Hybrid project...");
210
- if (!projectName || projectName.trim() === "") {
211
- console.error("\u274C Project name is required");
212
- process.exit(1);
213
- }
214
- const sanitizedName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
215
- const currentDir = process.cwd();
216
- const projectDir = projectName === "." ? currentDir : join(currentDir, sanitizedName);
217
- const isEmpty = await checkDirectoryEmpty(projectDir);
218
- if (!isEmpty) {
219
- console.error(
220
- `\u274C Directory "${sanitizedName}" already exists and is not empty`
221
- );
222
- console.error(
223
- "Please choose a different name or remove the existing directory"
224
- );
225
- process.exit(1);
226
- }
227
- let selectedExample;
228
- if (exampleName) {
229
- const example = EXAMPLES.find((ex) => ex.name === exampleName);
230
- if (!example) {
231
- console.error(`\u274C Example "${exampleName}" not found`);
232
- console.error(
233
- `Available examples: ${EXAMPLES.map((ex) => ex.name).join(", ")}`
234
- );
235
- process.exit(1);
236
- }
237
- selectedExample = example;
238
- console.log(`\u{1F4CB} Using example: ${selectedExample.name}`);
239
- } else {
240
- if (!process.stdin.isTTY) {
241
- console.error(
242
- "\u274C Example is required in non-interactive mode. Use --example <name>"
243
- );
244
- console.error(
245
- `Available examples: ${EXAMPLES.map((ex) => ex.name).join(", ")}`
246
- );
247
- process.exit(1);
248
- }
249
- const { example } = await prompts({
250
- type: "select",
251
- name: "example",
252
- message: "Which example would you like to use?",
253
- choices: EXAMPLES.map((ex) => ({
254
- title: ex.name,
255
- description: ex.available ? ex.description : `${ex.description} (${ex.message || "Coming soon"})`,
256
- value: ex,
257
- disabled: !ex.available
258
- })),
30
+ async function main() {
31
+ console.log("\n \u{1F916} Create Hybrid Agent\n");
32
+ const cliArgs = parseArgs();
33
+ const response = await prompts([
34
+ {
35
+ type: cliArgs.name ? null : "text",
36
+ name: "name",
37
+ message: "Project name",
38
+ initial: "my-agent"
39
+ },
40
+ {
41
+ type: cliArgs.env ? null : "select",
42
+ name: "env",
43
+ message: "XMTP environment",
44
+ choices: [
45
+ { title: "production", value: "production" },
46
+ { title: "dev", value: "dev" }
47
+ ],
259
48
  initial: 0
260
- });
261
- if (!example) {
262
- console.log("\u274C No example selected. Exiting...");
263
- process.exit(1);
264
- }
265
- if (!example.available) {
266
- console.log(
267
- `\u274C Example "${example.name}" is not yet available. ${example.message || "Coming soon"}`
268
- );
269
- process.exit(1);
49
+ },
50
+ {
51
+ type: cliArgs["agent-name"] ? null : "text",
52
+ name: "agentName",
53
+ message: "Agent display name",
54
+ initial: "Hybrid Agent"
270
55
  }
271
- selectedExample = example;
56
+ ]);
57
+ const name = cliArgs.name || response.name;
58
+ const env = cliArgs.env || response.env;
59
+ const agentName = cliArgs["agent-name"] || response.agentName;
60
+ if (!name) {
61
+ console.log("\n Cancelled.\n");
62
+ process.exit(0);
272
63
  }
273
- console.log(`\u{1F4E6} Cloning ${selectedExample.name} example...`);
274
- try {
275
- let degitSource;
276
- if (REPO.includes("#")) {
277
- const [userRepo, branch] = REPO.split("#");
278
- degitSource = `${userRepo}/examples/${selectedExample.name}#${branch}`;
279
- } else {
280
- degitSource = `${REPO}/examples/${selectedExample.name}`;
281
- }
282
- console.log(`\u{1F50D} Degit source: ${degitSource}`);
283
- const emitter = degit(degitSource);
284
- await emitter.clone(projectDir);
285
- console.log(`\u2705 Template cloned to: ${sanitizedName}`);
286
- } catch (error) {
287
- console.error("\u274C Failed to clone template:", error);
64
+ const projectDir = join(process.cwd(), name);
65
+ if (existsSync(projectDir)) {
66
+ console.log(`
67
+ Error: Directory "${name}" already exists.
68
+ `);
288
69
  process.exit(1);
289
70
  }
290
- console.log("\u{1F527} Updating template variables...");
291
- try {
292
- await updateTemplateFiles(projectDir, sanitizedName);
293
- console.log("\u2705 Template variables updated");
294
- } catch (error) {
295
- console.error("\u274C Failed to update template variables:", error);
296
- }
297
- console.log("\n\u{1F389} Hybrid project created successfully!");
298
- console.log(`
299
- \u{1F4C2} Project created in: ${projectDir}`);
300
- console.log("\n\u{1F4CB} Next steps:");
301
- if (projectName !== ".") {
302
- console.log(`1. cd ${sanitizedName}`);
303
- }
304
- console.log(
305
- `${projectName !== "." ? "2" : "1"}. Install dependencies (npm install, yarn install, or pnpm install)`
306
- );
307
- console.log(
308
- `${projectName !== "." ? "3" : "2"}. Get your OpenRouter API key from https://openrouter.ai/keys`
309
- );
310
- console.log(
311
- `${projectName !== "." ? "4" : "3"}. Add your API key to the OPENROUTER_API_KEY in .env`
312
- );
313
- console.log(
314
- `${projectName !== "." ? "5" : "4"}. Set XMTP_ENV in .env (dev or production)`
71
+ mkdirSync(projectDir, { recursive: true });
72
+ mkdirSync(join(projectDir, "src", "gateway"), { recursive: true });
73
+ mkdirSync(join(projectDir, "src", "server"), { recursive: true });
74
+ mkdirSync(join(projectDir, "users"), { recursive: true });
75
+ const templateData = {
76
+ name,
77
+ agentName: agentName || "Hybrid Agent",
78
+ env: env || "production"
79
+ };
80
+ writeFileSync(join(projectDir, "package.json"), packageJson(templateData));
81
+ writeFileSync(join(projectDir, "tsconfig.json"), tsconfigJson());
82
+ writeFileSync(join(projectDir, "wrangler.jsonc"), wranglerJsonc(templateData));
83
+ writeFileSync(join(projectDir, "Dockerfile"), dockerfile());
84
+ writeFileSync(join(projectDir, "build.mjs"), buildMjs());
85
+ writeFileSync(join(projectDir, "start.sh"), startSh());
86
+ writeFileSync(join(projectDir, "src", "gateway", "index.ts"), gatewayIndex());
87
+ writeFileSync(
88
+ join(projectDir, "src", "server", "index.ts"),
89
+ serverIndex(templateData)
315
90
  );
316
- console.log(
317
- `${projectName !== "." ? "6" : "5"}. Generate keys: npm run keys (or yarn/pnpm equivalent)`
91
+ writeFileSync(join(projectDir, "src", "dev-gateway.ts"), devGateway());
92
+ const agentTemplatesDir = join(
93
+ __dirname,
94
+ "..",
95
+ "..",
96
+ "cli",
97
+ "templates",
98
+ "agent"
318
99
  );
319
- console.log(
320
- `${projectName !== "." ? "7" : "6"}. Start development: npm run dev (or yarn/pnpm equivalent)`
100
+ const templates = [
101
+ "AGENTS.md",
102
+ "SOUL.md",
103
+ "IDENTITY.md",
104
+ "USER.md",
105
+ "TOOLS.md",
106
+ "BOOTSTRAP.md",
107
+ "HEARTBEAT.md"
108
+ ];
109
+ for (const template of templates) {
110
+ const templatePath = join(agentTemplatesDir, template);
111
+ if (existsSync(templatePath)) {
112
+ const content = readFileSync(templatePath, "utf-8");
113
+ writeFileSync(join(projectDir, template), content);
114
+ }
115
+ }
116
+ const aclContent = `## Owners
117
+
118
+ <!-- Add your wallet address here to become the owner -->
119
+ <!-- Example: 0xabc123... -->
120
+ - YOUR_WALLET_ADDRESS_HERE
121
+ `;
122
+ writeFileSync(join(projectDir, "ACL.md"), aclContent);
123
+ writeFileSync(join(projectDir, "SOUL.md"), soulMd(templateData));
124
+ writeFileSync(join(projectDir, ".env.example"), envExample(templateData));
125
+ writeFileSync(join(projectDir, ".gitignore"), gitignore());
126
+ console.log(`
127
+ \u2713 Created ${name}
128
+ `);
129
+ console.log(" Next steps:\n");
130
+ console.log(` cd ${name}`);
131
+ console.log(" pnpm install");
132
+ console.log(" pnpm dev\n");
133
+ console.log(" Deploy:\n");
134
+ console.log(" wrangler secret put ANTHROPIC_AUTH_TOKEN");
135
+ console.log(" pnpm deploy\n");
136
+ }
137
+ function packageJson(data) {
138
+ return JSON.stringify(
139
+ {
140
+ name: data.name,
141
+ private: true,
142
+ type: "module",
143
+ scripts: {
144
+ build: "node build.mjs",
145
+ dev: "tsx src/server/index.ts & tsx src/dev-gateway.ts & wait",
146
+ "dev:container": "tsx src/server/index.ts",
147
+ "dev:gateway": "tsx src/dev-gateway.ts",
148
+ deploy: "wrangler deploy",
149
+ typecheck: "tsc --noEmit"
150
+ },
151
+ dependencies: {
152
+ "@anthropic-ai/claude-agent-sdk": "^0.2.38",
153
+ "@cloudflare/sandbox": "^0.7.1",
154
+ ai: "^6.0.0",
155
+ hono: "^4.10.8"
156
+ },
157
+ devDependencies: {
158
+ "@cloudflare/workers-types": "^4.20250214.0",
159
+ "@types/node": "^22.8.6",
160
+ "bun-types": "^1.2.0",
161
+ tsx: "^4.19.3",
162
+ typescript: "^5.9.2",
163
+ wrangler: "^4.0.0"
164
+ }
165
+ },
166
+ null,
167
+ 2
321
168
  );
322
- console.log(
323
- "\n\u{1F4D6} For more information, see the README.md file in your project"
169
+ }
170
+ function tsconfigJson() {
171
+ return JSON.stringify(
172
+ {
173
+ compilerOptions: {
174
+ lib: ["ES2022"],
175
+ types: ["@cloudflare/workers-types", "node", "bun-types"],
176
+ module: "ESNext",
177
+ moduleResolution: "bundler",
178
+ noEmit: true,
179
+ strict: true,
180
+ esModuleInterop: true,
181
+ skipLibCheck: true
182
+ },
183
+ include: ["src/**/*"],
184
+ exclude: ["node_modules", "dist"]
185
+ },
186
+ null,
187
+ 2
324
188
  );
325
189
  }
326
- async function initializeProject() {
327
- const program = new Command();
328
- program.name("create-hybrid").description("Create a new Hybrid XMTP agent project").version("1.2.3").argument("[project-name]", "Name of the project").option(
329
- "-e, --example <example>",
330
- "Example to use (basic, with-ponder, with-foundry)"
331
- ).action(async (projectName, options) => {
332
- let finalProjectName = projectName;
333
- if (process.env.CI) {
334
- console.log(
335
- `\u{1F50D} Debug: projectName="${projectName}", options.example="${options?.example}"`
336
- );
337
- }
338
- if (!finalProjectName || finalProjectName.trim() === "") {
339
- if (!process.stdin.isTTY) {
340
- console.error("\u274C Project name is required");
341
- process.exit(1);
342
- }
343
- const { name } = await prompts({
344
- type: "text",
345
- name: "name",
346
- message: "What is your project name?",
347
- validate: (value) => {
348
- if (!value || !value.trim()) {
349
- return "Project name is required";
350
- }
351
- return true;
190
+ function wranglerJsonc(data) {
191
+ return JSON.stringify(
192
+ {
193
+ name: data.name,
194
+ main: "src/gateway/index.ts",
195
+ compatibility_date: "2025-05-06",
196
+ compatibility_flags: ["nodejs_compat"],
197
+ containers: [
198
+ {
199
+ class_name: "Sandbox",
200
+ image: "./Dockerfile",
201
+ instance_type: "standard-1",
202
+ max_instances: 50
352
203
  }
353
- });
354
- if (!name) {
355
- console.log("\u274C Project name is required. Exiting...");
356
- process.exit(1);
204
+ ],
205
+ durable_objects: {
206
+ bindings: [{ class_name: "Sandbox", name: "Sandbox" }]
207
+ },
208
+ migrations: [{ tag: "v1", new_sqlite_classes: ["Sandbox"] }],
209
+ vars: {
210
+ AGENT_PORT: "8454"
357
211
  }
358
- finalProjectName = name;
359
- }
360
- await createProject(finalProjectName, options?.example);
361
- });
362
- await program.parseAsync();
212
+ },
213
+ null,
214
+ 2
215
+ );
363
216
  }
364
- async function main() {
365
- const nodeVersion = process.versions.node;
366
- const [major] = nodeVersion.split(".").map(Number);
367
- if (!major || major < 20) {
368
- console.error("Error: Node.js version 20 or higher is required");
369
- process.exit(1);
370
- }
371
- try {
372
- await initializeProject();
373
- } catch (error) {
374
- console.error("Failed to initialize project:", error);
375
- console.error(
376
- "Error details:",
377
- error instanceof Error ? error.stack : String(error)
378
- );
379
- process.exit(1);
380
- }
217
+ function dockerfile() {
218
+ return `FROM docker.io/cloudflare/sandbox:0.7.0
219
+
220
+ WORKDIR /app
221
+
222
+ COPY package.json ./
223
+ RUN npm install --omit=dev --legacy-peer-deps
224
+
225
+ COPY dist/server/ ./dist/server/
226
+ COPY AGENTS.md SOUL.md IDENTITY.md USER.md TOOLS.md BOOT.md BOOTSTRAP.md HEARTBEAT.md ./
227
+ COPY start.sh ./
228
+ RUN chmod +x start.sh
229
+
230
+ ENV AGENT_PORT=8454
231
+ EXPOSE 8454
232
+ `;
381
233
  }
382
- main().catch((error) => {
383
- console.error("CLI error:", error);
384
- console.error(
385
- "Error details:",
386
- error instanceof Error ? error.stack : String(error)
387
- );
234
+ function startSh() {
235
+ return `#!/bin/bash
236
+ exec node dist/server/index.js
237
+ `;
238
+ }
239
+ function gatewayIndex() {
240
+ return `import { Sandbox } from "@cloudflare/sandbox"
241
+ import type { UIMessage } from "ai"
242
+ import { Hono } from "hono"
243
+ import { cors } from "hono/cors"
244
+
245
+ export interface GatewayEnv {
246
+ Sandbox: DurableObjectNamespace
247
+ ANTHROPIC_API_KEY?: string
248
+ ANTHROPIC_BASE_URL?: string
249
+ ANTHROPIC_AUTH_TOKEN?: string
250
+ }
251
+
252
+ type SandboxStub = InstanceType<typeof Sandbox>
253
+
254
+ const app = new Hono<{ Bindings: GatewayEnv }>()
255
+
256
+ app.use("*", cors())
257
+
258
+ app.get("/health", (c) => {
259
+ return c.json({
260
+ status: "healthy",
261
+ timestamp: new Date().toISOString()
262
+ })
263
+ })
264
+
265
+ function extractTextFromParts(parts: UIMessage["parts"]): string {
266
+ return parts
267
+ .filter((p): p is { type: "text"; text: string } => p.type === "text")
268
+ .map((p) => p.text)
269
+ .join("")
270
+ }
271
+
272
+ app.post("/api/chat", async (c) => {
273
+ const env = c.env
274
+ const body = await c.req.json<{
275
+ messages: UIMessage[]
276
+ chatId: string
277
+ teamId?: string
278
+ systemPrompt?: string
279
+ }>()
280
+
281
+ const sandbox = getSandbox(env, body.teamId || "default")
282
+ await ensureAgentServer(sandbox, env)
283
+
284
+ const messages = body.messages.map((m) => ({
285
+ id: m.id,
286
+ role: m.role,
287
+ content: extractTextFromParts(m.parts)
288
+ }))
289
+
290
+ const response = await sandbox.containerFetch(
291
+ "http://container/api/chat",
292
+ {
293
+ method: "POST",
294
+ headers: { "Content-Type": "application/json" },
295
+ body: JSON.stringify({
296
+ messages,
297
+ chatId: body.chatId,
298
+ teamId: body.teamId,
299
+ systemPrompt: body.systemPrompt
300
+ })
301
+ },
302
+ 8454
303
+ )
304
+
305
+ return new Response(response.body, {
306
+ headers: {
307
+ "Content-Type": "text/event-stream",
308
+ "Cache-Control": "no-cache",
309
+ Connection: "keep-alive"
310
+ }
311
+ })
312
+ })
313
+
314
+ function getSandbox(env: GatewayEnv, teamId: string): SandboxStub {
315
+ const id = env.Sandbox.idFromName(teamId)
316
+ return env.Sandbox.get(id) as unknown as SandboxStub
317
+ }
318
+
319
+ async function ensureAgentServer(sandbox: SandboxStub, env: GatewayEnv) {
320
+ const AGENT_PORT = 8454
321
+
322
+ try {
323
+ const health = await sandbox.containerFetch(
324
+ "http://container/health",
325
+ {},
326
+ AGENT_PORT
327
+ )
328
+ if (health.ok) return
329
+ } catch {
330
+ // Server not running, start it
331
+ }
332
+
333
+ const processes = await sandbox.listProcesses()
334
+ for (const p of processes) {
335
+ if (p.command?.includes("node")) {
336
+ await sandbox.killProcess(p.id)
337
+ }
338
+ }
339
+
340
+ await sandbox.startProcess("bash /app/start.sh", {
341
+ env: {
342
+ ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY ?? "",
343
+ ANTHROPIC_BASE_URL: env.ANTHROPIC_BASE_URL ?? "",
344
+ ANTHROPIC_AUTH_TOKEN: env.ANTHROPIC_AUTH_TOKEN ?? "",
345
+ AGENT_PORT: String(AGENT_PORT)
346
+ },
347
+ cwd: "/app"
348
+ })
349
+
350
+ for (let i = 0; i < 30; i++) {
351
+ try {
352
+ const health = await sandbox.containerFetch(
353
+ "http://container/health",
354
+ {},
355
+ AGENT_PORT
356
+ )
357
+ if (health.ok) return
358
+ } catch {
359
+ await new Promise((r) => setTimeout(r, 1000))
360
+ }
361
+ }
362
+
363
+ throw new Error("Agent server failed to start")
364
+ }
365
+
366
+ export default app
367
+ `;
368
+ }
369
+ function serverIndex(data) {
370
+ return `import { readFileSync } from "node:fs"
371
+ import { createRequire } from "node:module"
372
+ import { dirname, join } from "node:path"
373
+ import { fileURLToPath } from "node:url"
374
+ import { type Options, query } from "@anthropic-ai/claude-agent-sdk"
375
+ import { Hono } from "hono"
376
+
377
+ const __dirname = dirname(fileURLToPath(import.meta.url))
378
+ const require = createRequire(import.meta.url)
379
+
380
+ const AGENT_PORT = Number.parseInt(process.env.AGENT_PORT || "8454")
381
+ const AGENT_ENDPOINT = "/api/chat"
382
+ const HEALTH_CHECK_PATH = "/health"
383
+
384
+ function resolveClaudeCodeExecutable(): string {
385
+ if (process.env.CLAUDE_CODE_EXECUTABLE_PATH)
386
+ return process.env.CLAUDE_CODE_EXECUTABLE_PATH
387
+ const sdkDir = dirname(
388
+ require.resolve("@anthropic-ai/claude-agent-sdk/cli.js")
389
+ )
390
+ return join(sdkDir, "cli.js")
391
+ }
392
+
393
+ function resolveProjectRoot(): string {
394
+ if (process.env.AGENT_PROJECT_ROOT) return process.env.AGENT_PROJECT_ROOT
395
+ let dir = __dirname
396
+ for (let i = 0; i < 5; i++) {
397
+ try {
398
+ readFileSync(join(dir, "AGENTS.md"), "utf-8")
399
+ return dir
400
+ } catch {
401
+ dir = dirname(dir)
402
+ }
403
+ }
404
+ return join(__dirname, "..", "..")
405
+ }
406
+
407
+ const PROJECT_ROOT = resolveProjectRoot()
408
+
409
+ function loadMarkdownFile(relativePath: string): string {
410
+ try {
411
+ return readFileSync(join(PROJECT_ROOT, relativePath), "utf-8").trim()
412
+ } catch {
413
+ return ""
414
+ }
415
+ }
416
+
417
+ function loadUserMarkdown(userId?: string): string {
418
+ if (!userId) return loadMarkdownFile("USER.md")
419
+
420
+ const userPath = join("users", userId, "USER.md")
421
+ const userFile = loadMarkdownFile(userPath)
422
+
423
+ return userFile || loadMarkdownFile("USER.md")
424
+ }
425
+
426
+ const IDENTITY_MD = loadMarkdownFile("IDENTITY.md")
427
+ const SOUL_MD = loadMarkdownFile("SOUL.md")
428
+ const AGENTS_MD = loadMarkdownFile("AGENTS.md")
429
+ const TOOLS_MD = loadMarkdownFile("TOOLS.md")
430
+ const BOOT_MD = loadMarkdownFile("BOOT.md")
431
+ const BOOTSTRAP_MD = loadMarkdownFile("BOOTSTRAP.md")
432
+ const HEARTBEAT_MD = loadMarkdownFile("HEARTBEAT.md")
433
+
434
+ interface ContainerRequest {
435
+ messages: Array<{
436
+ id: string
437
+ role: "system" | "user" | "assistant"
438
+ content: string
439
+ }>
440
+ chatId: string
441
+ teamId?: string
442
+ userId?: string
443
+ systemPrompt?: string
444
+ }
445
+
446
+ function encodeSSE(data: string): Uint8Array {
447
+ return new TextEncoder().encode(\`data: \${data}\\n\\n\`)
448
+ }
449
+
450
+ function encodeSSEJson(data: unknown): Uint8Array {
451
+ return encodeSSE(JSON.stringify(data))
452
+ }
453
+
454
+ function encodeDone(): Uint8Array {
455
+ return new TextEncoder().encode("data: [DONE]\\n\\n")
456
+ }
457
+
458
+ const HISTORY_TAIL_SIZE = 20
459
+
460
+ function buildPromptWithHistory(
461
+ messages: ContainerRequest["messages"]
462
+ ): string {
463
+ if (messages.length <= 1) {
464
+ return messages.at(-1)?.content ?? ""
465
+ }
466
+
467
+ const currentMessage = messages.at(-1)
468
+ if (!currentMessage) return ""
469
+
470
+ const priorMessages = messages.slice(0, -1)
471
+
472
+ let historyMessages: ContainerRequest["messages"]
473
+ if (priorMessages.length <= HISTORY_TAIL_SIZE) {
474
+ historyMessages = priorMessages
475
+ } else {
476
+ const tail = priorMessages.slice(-HISTORY_TAIL_SIZE + 1)
477
+ const first = priorMessages.slice(0, 1)
478
+ const omitted: ContainerRequest["messages"] = [
479
+ {
480
+ id: "",
481
+ role: "system",
482
+ content: \`... \${priorMessages.length - HISTORY_TAIL_SIZE} earlier messages omitted ...\`
483
+ }
484
+ ]
485
+ historyMessages = [...first, ...omitted, ...tail]
486
+ }
487
+
488
+ const historyBlock = historyMessages
489
+ .map((m) => \`[\${m.role}]: \${m.content}\`)
490
+ .join("\\n\\n")
491
+
492
+ return \`<conversation_history>
493
+ \${historyBlock}
494
+ </conversation_history>
495
+
496
+ \${currentMessage.content}\`
497
+ }
498
+
499
+ function extractTextDelta(msg: any): string | null {
500
+ if (msg.type === "stream_event") {
501
+ const event = msg.event
502
+ if (
503
+ event?.type === "content_block_delta" &&
504
+ event.delta?.type === "text_delta"
505
+ ) {
506
+ return event.delta.text ?? ""
507
+ }
508
+ } else if (msg.type === "assistant") {
509
+ const content = msg.message?.content
510
+ if (Array.isArray(content)) {
511
+ const textBlock = content.find((b: any) => b.type === "text")
512
+ return textBlock?.text ?? null
513
+ }
514
+ }
515
+ return null
516
+ }
517
+
518
+ function runAgent(req: ContainerRequest): ReadableStream<Uint8Array> {
519
+ const USER_MD = loadUserMarkdown(req.userId)
520
+
521
+ const systemPrompt = [
522
+ IDENTITY_MD,
523
+ SOUL_MD,
524
+ req.systemPrompt,
525
+ AGENTS_MD,
526
+ TOOLS_MD,
527
+ USER_MD
528
+ ]
529
+ .filter(Boolean)
530
+ .join("\\n\\n")
531
+ const prompt = buildPromptWithHistory(req.messages)
532
+
533
+ const abortController = new AbortController()
534
+
535
+ const options: Options = {
536
+ abortController,
537
+ systemPrompt,
538
+ model: "claude-sonnet-4-20250514",
539
+ cwd: PROJECT_ROOT,
540
+ pathToClaudeCodeExecutable: resolveClaudeCodeExecutable(),
541
+ settingSources: [],
542
+ permissionMode: "bypassPermissions",
543
+ allowDangerouslySkipPermissions: true,
544
+ tools: [],
545
+ maxTurns: 25,
546
+ includePartialMessages: true,
547
+ env: {
548
+ ...process.env,
549
+ ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
550
+ ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN
551
+ }
552
+ }
553
+
554
+ console.log(\`[agent] calling query() with prompt="\${prompt.slice(0, 200)}"\`)
555
+
556
+ const conversation = query({ prompt, options })
557
+
558
+ let messageCount = 0
559
+
560
+ return new ReadableStream<Uint8Array>({
561
+ cancel() {
562
+ console.log("[agent] stream cancelled, aborting agent")
563
+ abortController.abort()
564
+ },
565
+ async pull(controller) {
566
+ try {
567
+ const { value: msg, done } = await conversation.next()
568
+ if (done) {
569
+ console.log(\`[agent] conversation done after \${messageCount} messages\`)
570
+ controller.enqueue(encodeDone())
571
+ controller.close()
572
+ return
573
+ }
574
+
575
+ messageCount++
576
+
577
+ if (msg.type === "stream_event") {
578
+ const event = msg.event as { type: string }
579
+ if (event.type !== "content_block_delta") {
580
+ console.log(\`[agent] msg #\${messageCount} event.type="\${event.type}"\`)
581
+ }
582
+
583
+ const text = extractTextDelta(msg)
584
+ if (text) {
585
+ controller.enqueue(encodeSSEJson({ type: "text", content: text }))
586
+ return
587
+ }
588
+
589
+ if (event.type === "content_block_start") {
590
+ const block = (event as any).content_block
591
+ if (block?.type === "tool_use") {
592
+ controller.enqueue(
593
+ encodeSSEJson({
594
+ type: "tool-call-start",
595
+ toolCallId: block.id,
596
+ toolName: block.name
597
+ })
598
+ )
599
+ }
600
+ return
601
+ }
602
+
603
+ if (event.type === "content_block_delta") {
604
+ const delta = (event as any).delta
605
+ if (delta?.type === "input_json_delta") {
606
+ controller.enqueue(
607
+ encodeSSEJson({
608
+ type: "tool-call-delta",
609
+ toolCallId: (event as any).index?.toString(),
610
+ argsTextDelta: delta.partial_json ?? ""
611
+ })
612
+ )
613
+ }
614
+ return
615
+ }
616
+
617
+ if (event.type === "content_block_stop") {
618
+ controller.enqueue(
619
+ encodeSSEJson({
620
+ type: "tool-call-end",
621
+ toolCallId: (event as any).index?.toString()
622
+ })
623
+ )
624
+ return
625
+ }
626
+ } else if (msg.type === "result") {
627
+ const usage = msg.usage
628
+ controller.enqueue(
629
+ encodeSSEJson({
630
+ type: "usage",
631
+ inputTokens: usage?.input_tokens ?? 0,
632
+ outputTokens: usage?.output_tokens ?? 0,
633
+ totalCostUsd: msg.total_cost_usd ?? 0,
634
+ numTurns: msg.num_turns ?? 1
635
+ })
636
+ )
637
+ } else if (msg.type === "assistant") {
638
+ const text = extractTextDelta(msg)
639
+ if (text) {
640
+ controller.enqueue(encodeSSEJson({ type: "text", content: text }))
641
+ }
642
+ }
643
+ } catch (err) {
644
+ const errorMessage = err instanceof Error ? err.message : "Agent error"
645
+ console.error("[agent] error:", err)
646
+ try {
647
+ controller.enqueue(encodeSSEJson({ type: "error", content: errorMessage }))
648
+ controller.enqueue(encodeDone())
649
+ controller.close()
650
+ } catch {
651
+ // Stream already cancelled
652
+ }
653
+ }
654
+ }
655
+ })
656
+ }
657
+
658
+ const app = new Hono()
659
+
660
+ app.get(HEALTH_CHECK_PATH, (c) => {
661
+ return c.json({ ok: true, service: "${data.agentName}" })
662
+ })
663
+
664
+ app.post(AGENT_ENDPOINT, async (c) => {
665
+ const req = await c.req.json<ContainerRequest>()
666
+ const stream = runAgent(req)
667
+
668
+ return new Response(stream, {
669
+ headers: {
670
+ "Content-Type": "text/event-stream",
671
+ "Cache-Control": "no-cache",
672
+ Connection: "keep-alive"
673
+ }
674
+ })
675
+ })
676
+
677
+ process.on("uncaughtException", (err) => {
678
+ console.error("[agent] uncaughtException:", err)
679
+ process.exit(1)
680
+ })
681
+
682
+ process.on("unhandledRejection", (reason) => {
683
+ console.error("[agent] unhandledRejection:", reason)
684
+ process.exit(1)
685
+ })
686
+
687
+ console.log(\`${data.agentName} listening on http://localhost:\${AGENT_PORT}\`)
688
+ console.log(\` Templates loaded:\`)
689
+ console.log(\` IDENTITY.md \${IDENTITY_MD ? "\u2713" : "\u2717"}\`)
690
+ console.log(\` SOUL.md \${SOUL_MD ? "\u2713" : "\u2717"}\`)
691
+ console.log(\` AGENTS.md \${AGENTS_MD ? "\u2713" : "\u2717"}\`)
692
+ console.log(\` TOOLS.md \${TOOLS_MD ? "\u2713" : "\u2717"}\`)
693
+ console.log(\` USER.md \${loadMarkdownFile("USER.md") ? "\u2713" : "\u2717"}\`)
694
+ console.log(\` BOOT.md \${BOOT_MD ? "\u2713" : "\u2717"}\`)
695
+ console.log(\` BOOTSTRAP.md \${BOOTSTRAP_MD ? "\u2713" : "\u2717"}\`)
696
+ console.log(\` HEARTBEAT.md \${HEARTBEAT_MD ? "\u2713" : "\u2717"}\`)
697
+
698
+ Bun.serve({
699
+ port: AGENT_PORT,
700
+ fetch: app.fetch
701
+ })
702
+
703
+ export default app
704
+ `;
705
+ }
706
+ function soulMd(data) {
707
+ return `## Identity
708
+
709
+ You are ${data.agentName}. Be accurate, concise, and practical.
710
+
711
+ ## Principles
712
+
713
+ - Verify before asserting. If unsure, say so.
714
+ - Use available tools to find information.
715
+ - Never claim actions you haven't completed.
716
+ - Ask for clarification when needed.
717
+
718
+ ## Style
719
+
720
+ - Be direct and brief.
721
+ - Use bullet points over numbered lists.
722
+ - Anticipate follow-up questions.
723
+ `;
724
+ }
725
+ function envExample(data) {
726
+ return `# Anthropic API (or use OpenRouter below)
727
+ ANTHROPIC_API_KEY=your_api_key_here
728
+
729
+ # OpenRouter proxy (optional)
730
+ # ANTHROPIC_BASE_URL=https://openrouter.ai/api
731
+ # ANTHROPIC_AUTH_TOKEN=your_openrouter_key
732
+
733
+ # Agent configuration
734
+ AGENT_WALLET_KEY=your_private_key_here
735
+ XMTP_ENV=${data.env}
736
+ `;
737
+ }
738
+ function gitignore() {
739
+ return `node_modules/
740
+ dist/
741
+ .wrangler/
742
+ .dev.vars
743
+ .env
744
+ *.log
745
+ `;
746
+ }
747
+ function buildMjs() {
748
+ return `import { build } from "esbuild"
749
+
750
+ await build({
751
+ entryPoints: ["src/server/index.ts"],
752
+ bundle: true,
753
+ platform: "node",
754
+ target: "node22",
755
+ format: "esm",
756
+ outfile: "dist/server/index.js",
757
+ minify: true
758
+ })
759
+
760
+ console.log("Build complete")
761
+ `;
762
+ }
763
+ function devGateway() {
764
+ return `import type { UIMessage } from "ai"
765
+ import { Hono } from "hono"
766
+ import { cors } from "hono/cors"
767
+
768
+ const app = new Hono()
769
+
770
+ app.use("*", cors())
771
+
772
+ app.get("/health", (c) => {
773
+ return c.json({ status: "healthy", mode: "dev-gateway" })
774
+ })
775
+
776
+ function extractTextFromParts(parts: UIMessage["parts"]): string {
777
+ return parts
778
+ .filter((p): p is { type: "text"; text: string } => p.type === "text")
779
+ .map((p) => p.text)
780
+ .join("")
781
+ }
782
+
783
+ app.post("/api/chat", async (c) => {
784
+ const body = await c.req.json<{
785
+ messages: UIMessage[]
786
+ chatId: string
787
+ teamId?: string
788
+ systemPrompt?: string
789
+ }>()
790
+
791
+ const messages = body.messages.map((m) => ({
792
+ id: m.id,
793
+ role: m.role,
794
+ content: extractTextFromParts(m.parts)
795
+ }))
796
+
797
+ const containerRes = await fetch("http://localhost:8454/api/chat", {
798
+ method: "POST",
799
+ headers: { "Content-Type": "application/json" },
800
+ body: JSON.stringify({
801
+ messages,
802
+ chatId: body.chatId,
803
+ teamId: body.teamId,
804
+ systemPrompt: body.systemPrompt
805
+ })
806
+ })
807
+
808
+ return new Response(containerRes.body, {
809
+ headers: {
810
+ "Content-Type": "text/event-stream",
811
+ "Cache-Control": "no-cache",
812
+ Connection: "keep-alive"
813
+ }
814
+ })
815
+ })
816
+
817
+ console.log("Dev gateway running on http://localhost:8787")
818
+ Bun.serve({ port: 8787, fetch: app.fetch })
819
+ `;
820
+ }
821
+ main().catch((err) => {
822
+ console.error(err);
388
823
  process.exit(1);
389
824
  });
390
- export {
391
- initializeProject
392
- };
393
- //# sourceMappingURL=index.js.map