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/README.md +145 -30
- package/dist/index.js +800 -369
- package/package.json +7 -35
- package/dist/index.d.ts +0 -3
- package/dist/index.js.map +0 -1
- package/src/index.ts +0 -513
- package/src/types.d.ts +0 -8
package/dist/index.js
CHANGED
|
@@ -1,393 +1,824 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
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
|
|
10
|
-
var
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
|
209
|
-
console.log("\u{
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
await program.parseAsync();
|
|
212
|
+
},
|
|
213
|
+
null,
|
|
214
|
+
2
|
|
215
|
+
);
|
|
363
216
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|