@wiimdy/openfunderse 0.1.0 → 0.1.2

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.
@@ -1,26 +1,86 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { spawn } from "node:child_process";
3
4
  import { existsSync } from "node:fs";
4
- import { cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
5
+ import { chmod, cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
5
6
  import os from "node:os";
6
7
  import path from "node:path";
8
+ import { createInterface } from "node:readline/promises";
7
9
  import { fileURLToPath } from "node:url";
8
10
 
9
11
  const THIS_FILE = fileURLToPath(import.meta.url);
10
12
  const PACKAGE_ROOT = path.resolve(path.dirname(THIS_FILE), "..");
11
13
  const PACKS_ROOT = path.join(PACKAGE_ROOT, "packs");
14
+ const DEFAULT_RUNTIME_PACKAGE = "@wiimdy/openfunderse-agents";
15
+ const SUPPORTED_ENV_PROFILES = new Set(["strategy", "participant", "all"]);
16
+ const SUPPORTED_BOT_INIT_ROLES = new Set(["strategy", "participant"]);
17
+ const DEFAULT_MONAD_CHAIN_ID = "10143";
18
+ const TEMP_PRIVATE_KEY = "0x1111111111111111111111111111111111111111111111111111111111111111";
19
+
20
+ const STRATEGY_ENV_TEMPLATE = `# OpenFunderse strategy env scaffold
21
+ # Copy values from your relayer + deployed contracts.
22
+
23
+ RELAYER_URL=https://your-relayer.example.com
24
+ BOT_ID=bot-strategy-1
25
+ BOT_API_KEY=replace_me
26
+ BOT_ADDRESS=0x0000000000000000000000000000000000000000
27
+ CHAIN_ID=10143
28
+ RPC_URL=https://testnet-rpc.monad.xyz
29
+
30
+ # NadFun / protocol addresses
31
+ INTENT_BOOK_ADDRESS=0x0000000000000000000000000000000000000000
32
+ NADFUN_EXECUTION_ADAPTER_ADDRESS=0x0000000000000000000000000000000000000000
33
+ VAULT_ADDRESS=0x0000000000000000000000000000000000000000
34
+ NADFUN_LENS_ADDRESS=0x0000000000000000000000000000000000000000
35
+ NADFUN_BONDING_CURVE_ROUTER=0x0000000000000000000000000000000000000000
36
+ NADFUN_DEX_ROUTER=0x0000000000000000000000000000000000000000
37
+ NADFUN_WMON_ADDRESS=0x0000000000000000000000000000000000000000
38
+
39
+ # Strategy signer (EOA)
40
+ # Temporary bootstrap key (public and unsafe). Replace via bot-init before real usage.
41
+ STRATEGY_PRIVATE_KEY=${TEMP_PRIVATE_KEY}
42
+ # STRATEGY_CREATE_MIN_SIGNER_BALANCE_WEI=10000000000000000
43
+
44
+ # Safety defaults
45
+ STRATEGY_REQUIRE_EXPLICIT_SUBMIT=true
46
+ STRATEGY_AUTO_SUBMIT=false
47
+ # STRATEGY_TRUSTED_RELAYER_HOSTS=openfunderse-relayer.example.com
48
+ # STRATEGY_ALLOW_HTTP_RELAYER=true
49
+ `;
50
+
51
+ const PARTICIPANT_ENV_TEMPLATE = `# OpenFunderse participant env scaffold
52
+ RELAYER_URL=https://your-relayer.example.com
53
+ BOT_ID=bot-participant-1
54
+ BOT_API_KEY=replace_me
55
+ BOT_ADDRESS=0x0000000000000000000000000000000000000000
56
+ CHAIN_ID=10143
57
+ # Temporary bootstrap key (public and unsafe). Replace via bot-init before real usage.
58
+ PARTICIPANT_PRIVATE_KEY=${TEMP_PRIVATE_KEY}
59
+ CLAIM_ATTESTATION_VERIFIER_ADDRESS=0x0000000000000000000000000000000000000000
60
+ PARTICIPANT_REQUIRE_EXPLICIT_SUBMIT=true
61
+ PARTICIPANT_AUTO_SUBMIT=false
62
+ # PARTICIPANT_TRUSTED_RELAYER_HOSTS=openfunderse-relayer.example.com
63
+ # PARTICIPANT_ALLOW_HTTP_RELAYER=true
64
+ `;
12
65
 
13
66
  function printUsage() {
14
67
  console.log(`openfunderse
15
68
 
16
69
  Usage:
17
70
  openfunderse list
18
- openfunderse install <pack-name> [--dest <skills-dir>] [--codex-home <dir>] [--force]
71
+ openfunderse bot-init [--role <strategy|participant>] [--skill-name <name>] [--env-path <path>] [--wallet-dir <dir>] [--wallet-name <name>] [--force] [--yes]
72
+ openfunderse install <pack-name> [--dest <skills-dir>] [--codex-home <dir>] [--force] [--with-runtime]
73
+ [--no-init-env] [--env-path <path>] [--env-profile <strategy|participant|all>]
74
+ [--runtime-package <name>] [--runtime-dir <dir>] [--runtime-manager <npm|pnpm|yarn|bun>]
19
75
 
20
76
  Examples:
21
77
  openfunderse list
22
78
  openfunderse install openfunderse
79
+ openfunderse install openfunderse --with-runtime
80
+ openfunderse install openfunderse-strategy --with-runtime
23
81
  openfunderse install openfunderse --codex-home /tmp/codex-home
82
+ openfunderse bot-init --env-path .env.participant --wallet-name participant-bot --yes
83
+ openfunderse bot-init --skill-name strategy --env-path .env.strategy --force
24
84
  `);
25
85
  }
26
86
 
@@ -30,7 +90,21 @@ function parseArgs(argv) {
30
90
  const options = {
31
91
  force: false,
32
92
  dest: "",
33
- codexHome: ""
93
+ codexHome: "",
94
+ withRuntime: false,
95
+ initEnv: true,
96
+ initEnvExplicit: false,
97
+ envFile: "",
98
+ envProfile: "",
99
+ envProfileExplicit: false,
100
+ runtimePackage: "",
101
+ runtimeDir: "",
102
+ runtimeManager: "",
103
+ role: "",
104
+ skillName: "",
105
+ walletDir: "",
106
+ walletName: "",
107
+ yes: false
34
108
  };
35
109
  const positionals = [];
36
110
 
@@ -44,6 +118,10 @@ function parseArgs(argv) {
44
118
  options.force = true;
45
119
  continue;
46
120
  }
121
+ if (token === "--yes") {
122
+ options.yes = true;
123
+ continue;
124
+ }
47
125
  if (token === "--dest") {
48
126
  options.dest = args[i + 1] ?? "";
49
127
  i += 1;
@@ -54,6 +132,66 @@ function parseArgs(argv) {
54
132
  i += 1;
55
133
  continue;
56
134
  }
135
+ if (token === "--with-runtime") {
136
+ options.withRuntime = true;
137
+ continue;
138
+ }
139
+ if (token === "--init-env") {
140
+ options.initEnv = true;
141
+ options.initEnvExplicit = true;
142
+ continue;
143
+ }
144
+ if (token === "--no-init-env") {
145
+ options.initEnv = false;
146
+ options.initEnvExplicit = true;
147
+ continue;
148
+ }
149
+ if (token === "--env-file" || token === "--env-path") {
150
+ options.envFile = args[i + 1] ?? "";
151
+ i += 1;
152
+ continue;
153
+ }
154
+ if (token === "--env-profile") {
155
+ options.envProfile = args[i + 1] ?? "";
156
+ options.envProfileExplicit = true;
157
+ i += 1;
158
+ continue;
159
+ }
160
+ if (token === "--runtime-package") {
161
+ options.runtimePackage = args[i + 1] ?? "";
162
+ i += 1;
163
+ continue;
164
+ }
165
+ if (token === "--runtime-dir") {
166
+ options.runtimeDir = args[i + 1] ?? "";
167
+ i += 1;
168
+ continue;
169
+ }
170
+ if (token === "--runtime-manager") {
171
+ options.runtimeManager = args[i + 1] ?? "";
172
+ i += 1;
173
+ continue;
174
+ }
175
+ if (token === "--role") {
176
+ options.role = args[i + 1] ?? "";
177
+ i += 1;
178
+ continue;
179
+ }
180
+ if (token === "--skill-name") {
181
+ options.skillName = args[i + 1] ?? "";
182
+ i += 1;
183
+ continue;
184
+ }
185
+ if (token === "--wallet-dir") {
186
+ options.walletDir = args[i + 1] ?? "";
187
+ i += 1;
188
+ continue;
189
+ }
190
+ if (token === "--wallet-name") {
191
+ options.walletName = args[i + 1] ?? "";
192
+ i += 1;
193
+ continue;
194
+ }
57
195
  if (token.startsWith("--")) {
58
196
  throw new Error(`unknown option: ${token}`);
59
197
  }
@@ -93,6 +231,403 @@ async function listPacks() {
93
231
  }
94
232
  }
95
233
 
234
+ function normalizeEnvProfile(rawProfile) {
235
+ const profile = (rawProfile || "").trim().toLowerCase();
236
+ if (SUPPORTED_ENV_PROFILES.has(profile)) {
237
+ return profile;
238
+ }
239
+ throw new Error(
240
+ `invalid --env-profile value: ${rawProfile} (expected strategy|participant|all)`
241
+ );
242
+ }
243
+
244
+ function defaultEnvProfileForPack(packName) {
245
+ const normalized = String(packName || "").trim().toLowerCase();
246
+ if (normalized.includes("participant")) {
247
+ return "participant";
248
+ }
249
+ if (normalized.includes("strategy")) {
250
+ return "strategy";
251
+ }
252
+ return "all";
253
+ }
254
+
255
+ function normalizeBotInitRole(rawRole) {
256
+ const role = (rawRole || "").trim().toLowerCase();
257
+ if (SUPPORTED_BOT_INIT_ROLES.has(role)) {
258
+ return role;
259
+ }
260
+ throw new Error(
261
+ `invalid --role value: ${rawRole} (expected strategy|participant)`
262
+ );
263
+ }
264
+
265
+ function inferRoleFromHint(hint) {
266
+ const normalized = (hint || "").trim().toLowerCase();
267
+ if (!normalized) return "";
268
+ if (normalized.includes("participant")) return "participant";
269
+ if (normalized.includes("strategy")) return "strategy";
270
+ return "";
271
+ }
272
+
273
+ function resolveBotInitRole(options) {
274
+ if (options.role && options.role.trim().length > 0) {
275
+ return normalizeBotInitRole(options.role);
276
+ }
277
+
278
+ const envSkillHints = [
279
+ process.env.OPENCLAW_SKILL_KEY,
280
+ process.env.OPENCLAW_ACTIVE_SKILL,
281
+ process.env.SKILL_KEY,
282
+ process.env.SKILL_NAME
283
+ ];
284
+
285
+ const hints = [
286
+ options.skillName,
287
+ options.envFile ? path.basename(options.envFile) : "",
288
+ options.walletName,
289
+ ...envSkillHints
290
+ ].filter((entry) => Boolean(entry && entry.trim().length > 0));
291
+
292
+ const inferredRoles = new Set();
293
+ for (const hint of hints) {
294
+ const inferred = inferRoleFromHint(hint);
295
+ if (inferred) {
296
+ inferredRoles.add(inferred);
297
+ }
298
+ }
299
+
300
+ if (inferredRoles.size === 1) {
301
+ return [...inferredRoles][0];
302
+ }
303
+
304
+ if (inferredRoles.size > 1) {
305
+ throw new Error(
306
+ "conflicting role hints found. pass explicit --role <strategy|participant>."
307
+ );
308
+ }
309
+
310
+ throw new Error(
311
+ "cannot infer bot role. pass --role or include strategy/participant in --skill-name, --env-path, --wallet-name, or OPENCLAW_SKILL_KEY."
312
+ );
313
+ }
314
+
315
+ function runtimeEnvExamplePath(runtimeDir, runtimePackage) {
316
+ return path.join(runtimeDir, "node_modules", ...runtimePackage.split("/"), ".env.example");
317
+ }
318
+
319
+ async function buildEnvScaffold(profile, runtimeDir, runtimePackage) {
320
+ if (profile === "strategy") {
321
+ return STRATEGY_ENV_TEMPLATE;
322
+ }
323
+ if (profile === "participant") {
324
+ return PARTICIPANT_ENV_TEMPLATE;
325
+ }
326
+
327
+ const runtimeTemplate = runtimeEnvExamplePath(runtimeDir, runtimePackage);
328
+ if (existsSync(runtimeTemplate)) {
329
+ return readFile(runtimeTemplate, "utf8");
330
+ }
331
+
332
+ return `${STRATEGY_ENV_TEMPLATE}\n\n${PARTICIPANT_ENV_TEMPLATE}`;
333
+ }
334
+
335
+ async function writeEnvScaffold(options) {
336
+ const runtimeDir = options.runtimeDir ? path.resolve(options.runtimeDir) : process.cwd();
337
+ const runtimePackage = options.runtimePackage || DEFAULT_RUNTIME_PACKAGE;
338
+ const rawProfile =
339
+ typeof options.envProfile === "string" && options.envProfile.trim().length > 0
340
+ ? options.envProfile
341
+ : "all";
342
+ const profile = normalizeEnvProfile(rawProfile);
343
+ const envTarget = options.envFile
344
+ ? path.resolve(options.envFile)
345
+ : path.join(runtimeDir, ".env.openfunderse");
346
+
347
+ const alreadyExists = existsSync(envTarget);
348
+ if (alreadyExists && !options.force) {
349
+ return {
350
+ written: false,
351
+ envFile: envTarget,
352
+ profile
353
+ };
354
+ }
355
+
356
+ if (alreadyExists && options.force) {
357
+ await rm(envTarget, { force: true });
358
+ }
359
+
360
+ const scaffold = await buildEnvScaffold(profile, runtimeDir, runtimePackage);
361
+ await mkdir(path.dirname(envTarget), { recursive: true });
362
+ await writeFile(envTarget, scaffold.endsWith("\n") ? scaffold : `${scaffold}\n`);
363
+
364
+ return {
365
+ written: true,
366
+ envFile: envTarget,
367
+ profile
368
+ };
369
+ }
370
+
371
+ function defaultEnvPathForRole(role) {
372
+ return path.join(process.cwd(), `.env.${role}`);
373
+ }
374
+
375
+ function readAssignedEnvValue(content, key) {
376
+ const lines = content.split(/\r?\n/);
377
+ for (const line of lines) {
378
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
379
+ if (!match) continue;
380
+ if (match[1] !== key) continue;
381
+ return match[2];
382
+ }
383
+ return "";
384
+ }
385
+
386
+ function isPlaceholderEnvValue(value) {
387
+ const normalized = (value || "").trim();
388
+ if (!normalized) return true;
389
+ if (normalized === "replace_me") return true;
390
+ if (normalized.toLowerCase() === TEMP_PRIVATE_KEY.toLowerCase()) return true;
391
+ if (/^0xYOUR_/i.test(normalized)) return true;
392
+ if (/^YOUR_/i.test(normalized)) return true;
393
+ if (/^0x0+$/i.test(normalized)) return true;
394
+ if (normalized === "0x0000000000000000000000000000000000000000") return true;
395
+ return false;
396
+ }
397
+
398
+ function upsertEnvValues(content, updates) {
399
+ const lines = content.split(/\r?\n/);
400
+ const applied = new Set();
401
+
402
+ const patched = lines.map((line) => {
403
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
404
+ if (!match) return line;
405
+ const key = match[1];
406
+ if (!(key in updates)) return line;
407
+ applied.add(key);
408
+ return `${key}=${updates[key]}`;
409
+ });
410
+
411
+ for (const [key, value] of Object.entries(updates)) {
412
+ if (applied.has(key)) continue;
413
+ patched.push(`${key}=${value}`);
414
+ }
415
+
416
+ return `${patched.join("\n").replace(/\n+$/g, "")}\n`;
417
+ }
418
+
419
+ function shellQuote(input) {
420
+ return `'${input.replace(/'/g, `'\\''`)}'`;
421
+ }
422
+
423
+ async function runCommandCapture(cmd, args, cwd = process.cwd()) {
424
+ return await new Promise((resolve, reject) => {
425
+ const child = spawn(cmd, args, {
426
+ cwd,
427
+ stdio: ["ignore", "pipe", "pipe"]
428
+ });
429
+
430
+ const stdout = [];
431
+ const stderr = [];
432
+
433
+ child.stdout.on("data", (chunk) => {
434
+ stdout.push(chunk);
435
+ });
436
+ child.stderr.on("data", (chunk) => {
437
+ stderr.push(chunk);
438
+ });
439
+ child.on("error", (error) => {
440
+ reject(error);
441
+ });
442
+ child.on("exit", (code) => {
443
+ if (code === 0) {
444
+ resolve({
445
+ stdout: Buffer.concat(stdout).toString("utf8"),
446
+ stderr: Buffer.concat(stderr).toString("utf8")
447
+ });
448
+ return;
449
+ }
450
+ const stderrText = Buffer.concat(stderr).toString("utf8").trim();
451
+ reject(
452
+ new Error(
453
+ `command failed: ${cmd} ${args.join(" ")} (exit ${code})${stderrText ? ` - ${stderrText}` : ""}`
454
+ )
455
+ );
456
+ });
457
+ });
458
+ }
459
+
460
+ async function confirmBotInit({ role, envFile, privateKeyKey, isRotation, assumeYes }) {
461
+ const mode = isRotation ? "wallet rotation" : "new wallet bootstrap";
462
+ console.log("WARNING: bot-init will generate a new wallet and update your env private key.");
463
+ console.log(`- Mode: ${mode}`);
464
+ console.log(`- Role: ${role}`);
465
+ console.log(`- Env file: ${envFile}`);
466
+ console.log(`- Key field: ${privateKeyKey}`);
467
+ console.log("- Existing funds tied to old key are not moved automatically.");
468
+
469
+ if (assumeYes) {
470
+ return;
471
+ }
472
+
473
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
474
+ throw new Error("bot-init requires confirmation in TTY. Re-run with --yes to confirm non-interactively.");
475
+ }
476
+
477
+ const rl = createInterface({
478
+ input: process.stdin,
479
+ output: process.stdout
480
+ });
481
+ try {
482
+ const answer = await rl.question("Type YES to continue: ");
483
+ if (answer.trim().toUpperCase() !== "YES") {
484
+ throw new Error("bot-init cancelled by user.");
485
+ }
486
+ } finally {
487
+ rl.close();
488
+ }
489
+ }
490
+
491
+ async function generateMonadWalletWithCast() {
492
+ let output;
493
+ try {
494
+ output = await runCommandCapture("cast", ["wallet", "new", "--json"]);
495
+ } catch (error) {
496
+ const message = error instanceof Error ? error.message : String(error);
497
+ if (message.includes("ENOENT")) {
498
+ throw new Error(
499
+ "cast is not installed. Install Foundry first (https://book.getfoundry.sh/getting-started/installation)."
500
+ );
501
+ }
502
+ throw error;
503
+ }
504
+
505
+ let parsed;
506
+ try {
507
+ parsed = JSON.parse(output.stdout);
508
+ } catch {
509
+ throw new Error(`failed to parse cast output as JSON: ${output.stdout}`);
510
+ }
511
+
512
+ const first = Array.isArray(parsed) ? parsed[0] : parsed;
513
+ const address = String(first?.address ?? "");
514
+ const privateKey = String(first?.private_key ?? first?.privateKey ?? "");
515
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
516
+ throw new Error(`invalid wallet address from cast: ${address}`);
517
+ }
518
+ if (!/^0x[0-9a-fA-F]{64}$/.test(privateKey)) {
519
+ throw new Error("invalid private key from cast output");
520
+ }
521
+
522
+ return {
523
+ address,
524
+ privateKey
525
+ };
526
+ }
527
+
528
+ function roleEnvUpdates(role, wallet) {
529
+ if (role === "strategy") {
530
+ return {
531
+ CHAIN_ID: DEFAULT_MONAD_CHAIN_ID,
532
+ STRATEGY_PRIVATE_KEY: wallet.privateKey,
533
+ BOT_ADDRESS: wallet.address
534
+ };
535
+ }
536
+ return {
537
+ CHAIN_ID: DEFAULT_MONAD_CHAIN_ID,
538
+ PARTICIPANT_PRIVATE_KEY: wallet.privateKey,
539
+ PARTICIPANT_BOT_ADDRESS: wallet.address,
540
+ BOT_ADDRESS: wallet.address
541
+ };
542
+ }
543
+
544
+ async function persistWallet(role, wallet, options) {
545
+ const walletDir = options.walletDir
546
+ ? path.resolve(options.walletDir)
547
+ : path.join(defaultCodexHome(), "openfunderse", "wallets");
548
+ const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "");
549
+ const rawName = (options.walletName || `${role}-${timestamp}`).trim();
550
+ if (!rawName) {
551
+ throw new Error("--wallet-name must not be empty");
552
+ }
553
+ if (rawName.includes("/") || rawName.includes("\\")) {
554
+ throw new Error("--wallet-name must be a file name, not a path");
555
+ }
556
+ const fileName = rawName.endsWith(".json") ? rawName : `${rawName}.json`;
557
+ const walletPath = path.join(walletDir, fileName);
558
+ if (existsSync(walletPath) && !options.force) {
559
+ throw new Error(`wallet file already exists: ${walletPath} (use --force to overwrite)`);
560
+ }
561
+
562
+ const payload = {
563
+ createdAt: new Date().toISOString(),
564
+ role,
565
+ chainId: DEFAULT_MONAD_CHAIN_ID,
566
+ address: wallet.address,
567
+ privateKey: wallet.privateKey
568
+ };
569
+ await mkdir(walletDir, { recursive: true, mode: 0o700 });
570
+ await writeFile(walletPath, `${JSON.stringify(payload, null, 2)}\n`, {
571
+ mode: 0o600
572
+ });
573
+ await chmod(walletPath, 0o600);
574
+ return walletPath;
575
+ }
576
+
577
+ function assertPrivateKeyRotationAllowed(envContent, role, force) {
578
+ const privateKeyKey = role === "strategy" ? "STRATEGY_PRIVATE_KEY" : "PARTICIPANT_PRIVATE_KEY";
579
+ const existing = readAssignedEnvValue(envContent, privateKeyKey);
580
+ if (!existing) return;
581
+ if (isPlaceholderEnvValue(existing)) return;
582
+ if (force) return;
583
+ throw new Error(
584
+ `${privateKeyKey} already has a value. bot-init creates a new wallet and will replace it; rerun with --force to rotate.`
585
+ );
586
+ }
587
+
588
+ async function runBotInit(options) {
589
+ const role = resolveBotInitRole(options);
590
+ const envFile = options.envFile ? path.resolve(options.envFile) : defaultEnvPathForRole(role);
591
+ const runtimeDir = options.runtimeDir ? path.resolve(options.runtimeDir) : process.cwd();
592
+ const runtimePackage = options.runtimePackage || DEFAULT_RUNTIME_PACKAGE;
593
+
594
+ let envContent;
595
+ if (existsSync(envFile)) {
596
+ envContent = await readFile(envFile, "utf8");
597
+ } else {
598
+ envContent = await buildEnvScaffold(role, runtimeDir, runtimePackage);
599
+ }
600
+
601
+ const privateKeyKey = role === "strategy" ? "STRATEGY_PRIVATE_KEY" : "PARTICIPANT_PRIVATE_KEY";
602
+ const existingPrivateKey = readAssignedEnvValue(envContent, privateKeyKey);
603
+ const isRotation = Boolean(existingPrivateKey) && !isPlaceholderEnvValue(existingPrivateKey);
604
+
605
+ assertPrivateKeyRotationAllowed(envContent, role, options.force);
606
+ await confirmBotInit({
607
+ role,
608
+ envFile,
609
+ privateKeyKey,
610
+ isRotation,
611
+ assumeYes: options.yes
612
+ });
613
+
614
+ const wallet = await generateMonadWalletWithCast();
615
+ const walletPath = await persistWallet(role, wallet, options);
616
+ const updates = roleEnvUpdates(role, wallet);
617
+ const nextEnvContent = upsertEnvValues(envContent, updates);
618
+
619
+ await mkdir(path.dirname(envFile), { recursive: true });
620
+ await writeFile(envFile, nextEnvContent);
621
+ await chmod(envFile, 0o600);
622
+ const sourceCommand = `set -a; source ${shellQuote(envFile)}; set +a`;
623
+
624
+ console.log(`Initialized ${role} bot wallet for Monad testnet (${DEFAULT_MONAD_CHAIN_ID}).`);
625
+ console.log(`Address: ${wallet.address}`);
626
+ console.log(`Env file updated: ${envFile}`);
627
+ console.log(`Wallet backup (keep secret): ${walletPath}`);
628
+ console.log(`Load env now: ${sourceCommand}`);
629
+ }
630
+
96
631
  async function loadManifest(packDir) {
97
632
  const candidates = [
98
633
  path.join(packDir, "manifest.json"),
@@ -122,6 +657,65 @@ async function copySkillDir(sourceDir, destinationDir, force) {
122
657
  await cp(sourceDir, destinationDir, { recursive: true });
123
658
  }
124
659
 
660
+ function detectRuntimeManager() {
661
+ const userAgent = process.env.npm_config_user_agent || "";
662
+ if (userAgent.startsWith("pnpm/")) return "pnpm";
663
+ if (userAgent.startsWith("yarn/")) return "yarn";
664
+ if (userAgent.startsWith("bun/")) return "bun";
665
+ return "npm";
666
+ }
667
+
668
+ function commandForRuntimeInstall(manager, runtimePackage) {
669
+ if (manager === "pnpm") {
670
+ return { cmd: "pnpm", args: ["add", runtimePackage] };
671
+ }
672
+ if (manager === "yarn") {
673
+ return { cmd: "yarn", args: ["add", runtimePackage] };
674
+ }
675
+ if (manager === "bun") {
676
+ return { cmd: "bun", args: ["add", runtimePackage] };
677
+ }
678
+ return { cmd: "npm", args: ["install", runtimePackage] };
679
+ }
680
+
681
+ async function installRuntimePackage(options) {
682
+ const runtimePackage = options.runtimePackage || DEFAULT_RUNTIME_PACKAGE;
683
+ const runtimeDir = options.runtimeDir ? path.resolve(options.runtimeDir) : process.cwd();
684
+ const runtimeManager = options.runtimeManager || detectRuntimeManager();
685
+ const packageJsonPath = path.join(runtimeDir, "package.json");
686
+
687
+ if (!existsSync(packageJsonPath)) {
688
+ throw new Error(
689
+ `runtime install target has no package.json: ${runtimeDir} (use --runtime-dir <project-root>)`
690
+ );
691
+ }
692
+
693
+ const { cmd, args } = commandForRuntimeInstall(runtimeManager, runtimePackage);
694
+ await new Promise((resolve, reject) => {
695
+ const child = spawn(cmd, args, {
696
+ cwd: runtimeDir,
697
+ stdio: "inherit"
698
+ });
699
+
700
+ child.on("error", (error) => {
701
+ reject(error);
702
+ });
703
+ child.on("exit", (code) => {
704
+ if (code === 0) {
705
+ resolve(undefined);
706
+ return;
707
+ }
708
+ reject(new Error(`runtime install failed with exit code ${code}`));
709
+ });
710
+ });
711
+
712
+ return {
713
+ runtimePackage,
714
+ runtimeDir,
715
+ runtimeManager
716
+ };
717
+ }
718
+
125
719
  async function installPack(packName, options) {
126
720
  const packDir = path.join(PACKS_ROOT, packName);
127
721
  if (!existsSync(packDir)) {
@@ -180,10 +774,44 @@ async function installPack(packName, options) {
180
774
  };
181
775
  await writeFile(path.join(packMetaRoot, "install.json"), `${JSON.stringify(installedMeta, null, 2)}\n`);
182
776
 
777
+ let runtimeInstallMeta = null;
778
+ if (options.withRuntime) {
779
+ runtimeInstallMeta = await installRuntimePackage(options);
780
+ }
781
+
782
+ let envScaffoldMeta = null;
783
+ if (options.initEnv) {
784
+ const resolvedEnvProfile = options.envProfileExplicit
785
+ ? options.envProfile
786
+ : defaultEnvProfileForPack(packName);
787
+ const envOptions = {
788
+ ...options,
789
+ envProfile: resolvedEnvProfile,
790
+ runtimeDir: runtimeInstallMeta?.runtimeDir ?? options.runtimeDir,
791
+ runtimePackage: runtimeInstallMeta?.runtimePackage ?? options.runtimePackage
792
+ };
793
+ envScaffoldMeta = await writeEnvScaffold(envOptions);
794
+ }
795
+
183
796
  console.log(`Installed pack: ${packName}`);
184
797
  console.log(`Skills root: ${skillsRoot}`);
185
798
  console.log(`Installed skills: ${installed.join(", ")}`);
186
799
  console.log(`Pack metadata: ${packMetaRoot}`);
800
+ if (runtimeInstallMeta) {
801
+ console.log(
802
+ `Installed runtime package: ${runtimeInstallMeta.runtimePackage} (${runtimeInstallMeta.runtimeManager})`
803
+ );
804
+ console.log(`Runtime install dir: ${runtimeInstallMeta.runtimeDir}`);
805
+ }
806
+ if (envScaffoldMeta) {
807
+ if (envScaffoldMeta.written) {
808
+ console.log(`Generated env scaffold (${envScaffoldMeta.profile}): ${envScaffoldMeta.envFile}`);
809
+ } else {
810
+ console.log(
811
+ `Env scaffold already exists (use --force to overwrite): ${envScaffoldMeta.envFile}`
812
+ );
813
+ }
814
+ }
187
815
  console.log("Restart Codex to pick up new skills.");
188
816
  }
189
817
 
@@ -209,6 +837,11 @@ async function main() {
209
837
  return;
210
838
  }
211
839
 
840
+ if (command === "bot-init") {
841
+ await runBotInit(options);
842
+ return;
843
+ }
844
+
212
845
  throw new Error(`unknown command: ${command}`);
213
846
  }
214
847