@wiimdy/openfunderse 0.1.1 → 0.1.3

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 CHANGED
@@ -15,9 +15,20 @@ npx @wiimdy/openfunderse@latest list
15
15
  # install pack into ~/.codex/skills
16
16
  npx @wiimdy/openfunderse@latest install openfunderse
17
17
 
18
+ # install strategy-only pack
19
+ npx @wiimdy/openfunderse@latest install openfunderse-strategy
20
+
21
+ # install participant-only pack
22
+ npx @wiimdy/openfunderse@latest install openfunderse-participant
23
+
18
24
  # install pack + runtime package in one command (recommended)
19
25
  npx @wiimdy/openfunderse@latest install openfunderse --with-runtime
20
26
 
27
+ # install pack + runtime + strategy env scaffold in one command
28
+ npx @wiimdy/openfunderse@latest install openfunderse \
29
+ --with-runtime \
30
+ --env-profile strategy
31
+
21
32
  # install into custom codex home
22
33
  npx @wiimdy/openfunderse@latest install openfunderse --codex-home /custom/.codex
23
34
 
@@ -25,6 +36,16 @@ npx @wiimdy/openfunderse@latest install openfunderse --codex-home /custom/.codex
25
36
  npx @wiimdy/openfunderse@latest install openfunderse \
26
37
  --with-runtime \
27
38
  --runtime-dir /path/to/project
39
+
40
+ # initialize bot env + fresh Monad wallet (strategy)
41
+ npx @wiimdy/openfunderse@latest bot-init \
42
+ --skill-name strategy \
43
+ --yes
44
+
45
+ # initialize participant bot env + wallet
46
+ npx @wiimdy/openfunderse@latest bot-init \
47
+ --skill-name participant \
48
+ --yes
28
49
  ```
29
50
 
30
51
  ## Notes
@@ -33,5 +54,23 @@ npx @wiimdy/openfunderse@latest install openfunderse \
33
54
  - Pack metadata/prompts are copied into `$CODEX_HOME/packs/<pack-name>`.
34
55
  - Use `--force` to overwrite existing installed skills.
35
56
  - `--with-runtime` installs `@wiimdy/openfunderse-agents` into the current project (`package.json` required).
57
+ - Env scaffold generation is enabled by default (default path: `.env`).
58
+ - `--env-profile` controls scaffold scope: `strategy` | `participant` | `all` (auto-selected by pack when omitted).
59
+ - Use `--no-init-env` to skip env scaffold generation.
60
+ - `--env-path` sets a custom env scaffold path.
36
61
  - Optional: `--runtime-package`, `--runtime-dir`, `--runtime-manager`.
37
- - Default unified bundle is `clawbot-core` (strategy + participant role actions).
62
+ - Available packs: `openfunderse` (unified), `openfunderse-strategy`, `openfunderse-participant`.
63
+ - Split packs (`openfunderse-strategy`, `openfunderse-participant`) are intentionally minimal and centered on the skill payload.
64
+ - Prefer `--env-path` (Node 20+ reserves `--env-file` as a runtime flag).
65
+ - `bot-init` uses `cast wallet new --json` (Foundry) to generate a new wallet for Monad testnet.
66
+ - `bot-init` infers role from `--skill-name`, `--env-path`, or `--wallet-name` when `--role` is omitted.
67
+ - `bot-init` writes to `.env` by default. Use `--env-path` to split strategy/participant env files.
68
+ - It also infers from active skill env hints (`OPENCLAW_SKILL_KEY`, `OPENCLAW_ACTIVE_SKILL`, `SKILL_KEY`, `SKILL_NAME`).
69
+ - `bot-init` writes role-specific key fields:
70
+ - `strategy`: `STRATEGY_PRIVATE_KEY`, `BOT_ADDRESS`
71
+ - `participant`: `PARTICIPANT_PRIVATE_KEY`, `PARTICIPANT_BOT_ADDRESS`, `BOT_ADDRESS`
72
+ - `bot-init` stores a wallet backup JSON under `$CODEX_HOME/openfunderse/wallets`.
73
+ - Generated scaffolds include a temporary bootstrap key value. It is public/unsafe and must be rotated via `bot-init` before funding.
74
+ - `bot-init` shows a warning and requires confirmation (`Type YES`) unless `--yes` is passed.
75
+ - If private key already exists in the target env file, `bot-init` requires `--force` to rotate.
76
+ - CLI cannot mutate your parent shell env directly; run the printed `set -a; source ...; set +a` command.
@@ -2,29 +2,85 @@
2
2
 
3
3
  import { spawn } from "node:child_process";
4
4
  import { existsSync } from "node:fs";
5
- import { cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
5
+ import { chmod, cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
8
+ import { createInterface } from "node:readline/promises";
8
9
  import { fileURLToPath } from "node:url";
9
10
 
10
11
  const THIS_FILE = fileURLToPath(import.meta.url);
11
12
  const PACKAGE_ROOT = path.resolve(path.dirname(THIS_FILE), "..");
12
13
  const PACKS_ROOT = path.join(PACKAGE_ROOT, "packs");
13
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
+ `;
14
65
 
15
66
  function printUsage() {
16
67
  console.log(`openfunderse
17
68
 
18
69
  Usage:
19
70
  openfunderse list
71
+ openfunderse bot-init [--role <strategy|participant>] [--skill-name <name>] [--env-path <path>] [--wallet-dir <dir>] [--wallet-name <name>] [--force] [--yes]
20
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>]
21
74
  [--runtime-package <name>] [--runtime-dir <dir>] [--runtime-manager <npm|pnpm|yarn|bun>]
22
75
 
23
76
  Examples:
24
77
  openfunderse list
25
78
  openfunderse install openfunderse
26
79
  openfunderse install openfunderse --with-runtime
80
+ openfunderse install openfunderse-strategy --with-runtime
27
81
  openfunderse install openfunderse --codex-home /tmp/codex-home
82
+ openfunderse bot-init --skill-name participant --wallet-name participant-bot --yes
83
+ openfunderse bot-init --skill-name strategy --force
28
84
  `);
29
85
  }
30
86
 
@@ -36,9 +92,19 @@ function parseArgs(argv) {
36
92
  dest: "",
37
93
  codexHome: "",
38
94
  withRuntime: false,
95
+ initEnv: true,
96
+ initEnvExplicit: false,
97
+ envFile: "",
98
+ envProfile: "",
99
+ envProfileExplicit: false,
39
100
  runtimePackage: "",
40
101
  runtimeDir: "",
41
- runtimeManager: ""
102
+ runtimeManager: "",
103
+ role: "",
104
+ skillName: "",
105
+ walletDir: "",
106
+ walletName: "",
107
+ yes: false
42
108
  };
43
109
  const positionals = [];
44
110
 
@@ -52,6 +118,10 @@ function parseArgs(argv) {
52
118
  options.force = true;
53
119
  continue;
54
120
  }
121
+ if (token === "--yes") {
122
+ options.yes = true;
123
+ continue;
124
+ }
55
125
  if (token === "--dest") {
56
126
  options.dest = args[i + 1] ?? "";
57
127
  i += 1;
@@ -66,6 +136,27 @@ function parseArgs(argv) {
66
136
  options.withRuntime = true;
67
137
  continue;
68
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
+ }
69
160
  if (token === "--runtime-package") {
70
161
  options.runtimePackage = args[i + 1] ?? "";
71
162
  i += 1;
@@ -81,6 +172,26 @@ function parseArgs(argv) {
81
172
  i += 1;
82
173
  continue;
83
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
+ }
84
195
  if (token.startsWith("--")) {
85
196
  throw new Error(`unknown option: ${token}`);
86
197
  }
@@ -120,6 +231,403 @@ async function listPacks() {
120
231
  }
121
232
  }
122
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");
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");
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
+
123
631
  async function loadManifest(packDir) {
124
632
  const candidates = [
125
633
  path.join(packDir, "manifest.json"),
@@ -271,6 +779,20 @@ async function installPack(packName, options) {
271
779
  runtimeInstallMeta = await installRuntimePackage(options);
272
780
  }
273
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
+
274
796
  console.log(`Installed pack: ${packName}`);
275
797
  console.log(`Skills root: ${skillsRoot}`);
276
798
  console.log(`Installed skills: ${installed.join(", ")}`);
@@ -281,6 +803,15 @@ async function installPack(packName, options) {
281
803
  );
282
804
  console.log(`Runtime install dir: ${runtimeInstallMeta.runtimeDir}`);
283
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
+ }
284
815
  console.log("Restart Codex to pick up new skills.");
285
816
  }
286
817
 
@@ -306,6 +837,11 @@ async function main() {
306
837
  return;
307
838
  }
308
839
 
840
+ if (command === "bot-init") {
841
+ await runBotInit(options);
842
+ return;
843
+ }
844
+
309
845
  throw new Error(`unknown command: ${command}`);
310
846
  }
311
847
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wiimdy/openfunderse",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Install OpenFunderse skill packs into Codex",
5
5
  "type": "module",
6
6
  "bin": {