@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 +40 -1
- package/bin/openfunderse.mjs +538 -2
- package/package.json +1 -1
- package/packs/openfunderse/config/setup-manifest.json +3 -13
- package/packs/openfunderse/skills/openfunderse-participant/SKILL.md +201 -0
- package/packs/openfunderse/skills/openfunderse-strategy/SKILL.md +260 -0
- package/packs/openfunderse-participant/config/setup-manifest.json +12 -0
- package/packs/{openfunderse/skills/participant → openfunderse-participant/openfunderse-participant}/SKILL.md +68 -7
- package/packs/openfunderse-strategy/config/setup-manifest.json +12 -0
- package/packs/{openfunderse/skills/strategy → openfunderse-strategy/openfunderse-strategy}/SKILL.md +93 -6
- package/packs/openfunderse/prompts/core/system.md +0 -25
- package/packs/openfunderse/prompts/relayer/system.md +0 -14
- package/packs/openfunderse/skills/clawbot-core/SKILL.md +0 -152
- package/packs/openfunderse/skills/relayer/SKILL.md +0 -10
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
|
-
-
|
|
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.
|
package/bin/openfunderse.mjs
CHANGED
|
@@ -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
|
|