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