@clanker-chain/clanker-cli 2026.9.7-1

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/lib/setup.mjs ADDED
@@ -0,0 +1,470 @@
1
+ /**
2
+ * Interactive / flag-driven `clanker setup`.
3
+ */
4
+
5
+ import { createInterface } from "node:readline/promises";
6
+ import { stdin as input, stdout as output } from "node:process";
7
+ import { existsSync } from "node:fs";
8
+ import { getAddress } from "viem";
9
+ import {
10
+ ANVIL_DEFAULT_ADDRESS,
11
+ PRESETS,
12
+ clankerHome,
13
+ configPath,
14
+ initProfile,
15
+ isLocalRpc,
16
+ operatorPath,
17
+ writeOperator,
18
+ } from "./profile.mjs";
19
+ import {
20
+ publicClientFromRpc,
21
+ readOperator,
22
+ resolvePreferredOperator,
23
+ } from "./identity-query.mjs";
24
+ import {
25
+ SEPOLIA_FAST_FROM_BLOCK,
26
+ addressFromEnv,
27
+ addressFromKeyFile,
28
+ detectSetupHints,
29
+ } from "./setup-detect.mjs";
30
+
31
+ /**
32
+ * @param {string[]} argv
33
+ */
34
+ export function parseSetupFlags(argv) {
35
+ let preset = null;
36
+ let operator = null;
37
+ let address = null;
38
+ let keyFile = null;
39
+ let keyEnv = null;
40
+ let fromBlock = null;
41
+ let force = false;
42
+ let yes = false;
43
+ let skipKey = false;
44
+ let skipChainCheck = false;
45
+
46
+ for (let i = 0; i < argv.length; i += 1) {
47
+ const a = argv[i];
48
+ if (a === "--preset" && argv[i + 1]) preset = argv[++i];
49
+ else if (a === "--operator" && argv[i + 1]) operator = argv[++i];
50
+ else if (a === "--address" && argv[i + 1]) address = argv[++i];
51
+ else if (a === "--key-file" && argv[i + 1]) keyFile = argv[++i];
52
+ else if (a === "--key-env" && argv[i + 1]) keyEnv = argv[++i];
53
+ else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
54
+ else if (a === "--force") force = true;
55
+ else if (a === "--yes" || a === "-y") yes = true;
56
+ else if (a === "--skip-key") skipKey = true;
57
+ else if (a === "--skip-chain-check") skipChainCheck = true;
58
+ }
59
+
60
+ return {
61
+ preset,
62
+ operator,
63
+ address,
64
+ keyFile,
65
+ keyEnv,
66
+ fromBlock,
67
+ force,
68
+ yes,
69
+ skipKey,
70
+ skipChainCheck,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Validate address + label against registry (and Anvil-on-public).
76
+ * @param {{ rpc: string, registry: string, label: string, address: string, skipChainCheck?: boolean }} opts
77
+ */
78
+ export async function assertSetupIdentity(opts) {
79
+ const address = getAddress(opts.address);
80
+ const local = isLocalRpc(opts.rpc);
81
+
82
+ if (!local && address.toLowerCase() === ANVIL_DEFAULT_ADDRESS.toLowerCase()) {
83
+ throw new Error(
84
+ "Refusing Anvil account #0 address on a non-local RPC. Use your real operator owner address.",
85
+ );
86
+ }
87
+
88
+ if (opts.skipChainCheck) {
89
+ return { address, operator: null, skipped: true };
90
+ }
91
+
92
+ if (!opts.registry || !/^0x[0-9a-fA-F]{40}$/.test(opts.registry)) {
93
+ if (local) {
94
+ return { address, operator: null, skipped: true };
95
+ }
96
+ throw new Error("Registry address required for chain check on public RPC.");
97
+ }
98
+
99
+ const pub = await publicClientFromRpc(opts.rpc);
100
+ const op = await readOperator(pub, opts.registry, opts.label);
101
+
102
+ if (op.registeredAt === 0n) {
103
+ // New operator — fine; mint later
104
+ return { address, operator: op, skipped: false, unregistered: true };
105
+ }
106
+
107
+ const resolved = await resolvePreferredOperator(pub, {
108
+ registry: opts.registry,
109
+ label: opts.label,
110
+ owner: address,
111
+ });
112
+ if (resolved.error) {
113
+ throw new Error(resolved.error);
114
+ }
115
+ return { address, operator: resolved.operator, skipped: false, unregistered: false };
116
+ }
117
+
118
+ /**
119
+ * Build key pointer from flags / choices.
120
+ * @returns {{ type: 'env'|'keyFile', value: string }|null}
121
+ */
122
+ export function buildKeyPointer({ keyFile, keyEnv, skipKey, env = process.env }) {
123
+ if (skipKey) return null;
124
+ if (keyFile) {
125
+ if (!existsSync(keyFile)) throw new Error(`Key file not found: ${keyFile}`);
126
+ // validate readable
127
+ addressFromKeyFile(keyFile);
128
+ return { type: "keyFile", value: keyFile };
129
+ }
130
+ if (keyEnv) {
131
+ return { type: "env", value: keyEnv };
132
+ }
133
+ if (env.OPERATOR_PRIVATE_KEY) {
134
+ return { type: "env", value: "OPERATOR_PRIVATE_KEY" };
135
+ }
136
+ return null;
137
+ }
138
+
139
+ /**
140
+ * Apply setup choices to disk.
141
+ */
142
+ export function applySetup(choices, opts = {}) {
143
+ const home = opts.home ?? clankerHome(opts.env);
144
+ const force = Boolean(choices.force);
145
+
146
+ if (existsSync(configPath(home)) && !force) {
147
+ throw new Error(
148
+ `Profile already exists at ${configPath(home)}. Pass --force to overwrite.`,
149
+ );
150
+ }
151
+ if (existsSync(operatorPath(home)) && !force) {
152
+ throw new Error(
153
+ `Operator profile already exists at ${operatorPath(home)}. Pass --force to overwrite.`,
154
+ );
155
+ }
156
+
157
+ const { path: configFile, config } = initProfile(choices.preset, {
158
+ force: true,
159
+ home,
160
+ registryAddress: choices.registryAddress,
161
+ fromBlock: choices.fromBlock,
162
+ });
163
+
164
+ const { path: opFile, operator } = writeOperator(
165
+ {
166
+ label: choices.label,
167
+ owner: getAddress(choices.address),
168
+ key: choices.key,
169
+ },
170
+ home,
171
+ );
172
+
173
+ return { configPath: configFile, config, operatorPath: opFile, operator };
174
+ }
175
+
176
+ /**
177
+ * Non-interactive setup (agents / CI).
178
+ * @param {string[]} argv
179
+ * @param {{ home?: string, env?: NodeJS.ProcessEnv, skipChainCheck?: boolean }} [opts]
180
+ */
181
+ export async function runSetupNonInteractive(argv, opts = {}) {
182
+ const flags = parseSetupFlags(argv);
183
+ const env = opts.env ?? process.env;
184
+ const home = opts.home ?? clankerHome(env);
185
+
186
+ if (!flags.preset || !PRESETS[flags.preset]) {
187
+ throw new Error(
188
+ "Non-interactive setup requires --preset sepolia|local (stdin is not a TTY). " +
189
+ "Also pass --operator <label> and --address 0x…",
190
+ );
191
+ }
192
+ if (!flags.operator) {
193
+ throw new Error("Non-interactive setup requires --operator <label>");
194
+ }
195
+ if (!flags.address && !flags.keyFile && !env.OPERATOR_PRIVATE_KEY) {
196
+ throw new Error(
197
+ "Non-interactive setup requires --address 0x…, --key-file, or OPERATOR_PRIVATE_KEY",
198
+ );
199
+ }
200
+
201
+ let address = flags.address;
202
+ if (!address && flags.keyFile) address = addressFromKeyFile(flags.keyFile);
203
+ if (!address && env.OPERATOR_PRIVATE_KEY) address = addressFromEnv(env);
204
+
205
+ const preset = PRESETS[flags.preset];
206
+ let fromBlock = flags.fromBlock;
207
+ if (fromBlock == null && flags.preset === "sepolia") {
208
+ fromBlock = SEPOLIA_FAST_FROM_BLOCK;
209
+ }
210
+
211
+ const rpc = preset.chainRpcUrl;
212
+ const registry = preset.registryAddress;
213
+
214
+ await assertSetupIdentity({
215
+ rpc,
216
+ registry,
217
+ label: flags.operator,
218
+ address,
219
+ skipChainCheck: flags.skipChainCheck || opts.skipChainCheck || !registry,
220
+ });
221
+
222
+ const key = buildKeyPointer({
223
+ keyFile: flags.keyFile,
224
+ keyEnv: flags.keyEnv,
225
+ skipKey: flags.skipKey,
226
+ env,
227
+ });
228
+
229
+ // Address from key must match --address when both set
230
+ if (flags.keyFile && flags.address) {
231
+ const fromKey = getAddress(addressFromKeyFile(flags.keyFile));
232
+ if (fromKey !== getAddress(flags.address)) {
233
+ throw new Error(
234
+ `Key file address ${fromKey} does not match --address ${getAddress(flags.address)}`,
235
+ );
236
+ }
237
+ }
238
+
239
+ return applySetup(
240
+ {
241
+ preset: flags.preset,
242
+ force: flags.force || flags.yes,
243
+ fromBlock,
244
+ registryAddress: registry,
245
+ label: flags.operator,
246
+ address,
247
+ key,
248
+ },
249
+ { home, env },
250
+ );
251
+ }
252
+
253
+ /**
254
+ * Interactive wizard when stdin is a TTY.
255
+ */
256
+ export async function runSetupInteractive(argv, opts = {}) {
257
+ const flags = parseSetupFlags(argv);
258
+ const env = opts.env ?? process.env;
259
+ const home = opts.home ?? clankerHome(env);
260
+ const hints = detectSetupHints({
261
+ home,
262
+ env,
263
+ castBin: opts.castBin,
264
+ spawn: opts.spawn,
265
+ openclawDir: opts.openclawDir,
266
+ });
267
+
268
+ const rl =
269
+ opts.rl ??
270
+ createInterface({ input, output });
271
+ const ask = async (q, def) => {
272
+ const suffix = def != null && def !== "" ? ` [${def}]` : "";
273
+ const ans = (await rl.question(`${q}${suffix}: `)).trim();
274
+ return ans || def || "";
275
+ };
276
+ const askYesNo = async (q, defaultYes = true) => {
277
+ const def = defaultYes ? "Y/n" : "y/N";
278
+ const ans = (await rl.question(`${q} (${def}): `)).trim().toLowerCase();
279
+ if (!ans) return defaultYes;
280
+ return ans === "y" || ans === "yes";
281
+ };
282
+
283
+ try {
284
+ console.log("clanker setup — local profile wizard");
285
+ console.log(`home: ${home}`);
286
+ if (hints.hasConfig) console.log(`existing config: ${hints.configPath}`);
287
+ if (hints.hasOperator) console.log(`existing operator: ${hints.operatorPath}`);
288
+ if (hints.foundryAvailable && hints.foundryAccounts.length) {
289
+ console.log(`Foundry accounts: ${hints.foundryAccounts.join(", ")}`);
290
+ } else if (!hints.foundryAvailable) {
291
+ console.log("Foundry cast: not found (optional)");
292
+ }
293
+ if (hints.openclawBots.length) {
294
+ console.log(
295
+ `OpenClaw bot keys: ${hints.openclawBots.join(", ")} (bot keys ≠ operator owner)`,
296
+ );
297
+ }
298
+ if (hints.hasOperatorPrivateKeyEnv) {
299
+ console.log(`OPERATOR_PRIVATE_KEY: set → ${hints.envAddress ?? "(invalid)"}`);
300
+ }
301
+ console.log("");
302
+
303
+ if ((hints.hasConfig || hints.hasOperator) && !flags.force) {
304
+ const overwrite = await askYesNo("Overwrite existing ~/.clanker profile?", false);
305
+ if (!overwrite) {
306
+ throw new Error("Aborted (profile exists). Re-run with --force to overwrite.");
307
+ }
308
+ flags.force = true;
309
+ }
310
+
311
+ let preset =
312
+ flags.preset ||
313
+ hints.config?.preset ||
314
+ (await ask("Preset (sepolia|local)", "sepolia"));
315
+ preset = String(preset).toLowerCase();
316
+ if (!PRESETS[preset]) throw new Error(`Unknown preset "${preset}"`);
317
+
318
+ let fromBlock = flags.fromBlock;
319
+ if (preset === "sepolia") {
320
+ const useFast =
321
+ fromBlock != null
322
+ ? false
323
+ : await askYesNo(
324
+ `Use faster fromBlock ${SEPOLIA_FAST_FROM_BLOCK} for public RPC scans?`,
325
+ true,
326
+ );
327
+ if (fromBlock == null) {
328
+ fromBlock = useFast ? SEPOLIA_FAST_FROM_BLOCK : PRESETS.sepolia.fromBlock;
329
+ }
330
+ }
331
+
332
+ let address = flags.address ?? null;
333
+ if (!address && flags.keyFile) {
334
+ address = addressFromKeyFile(flags.keyFile);
335
+ }
336
+ if (!address && hints.envAddress) {
337
+ const useEnv = await askYesNo(`Use address from OPERATOR_PRIVATE_KEY (${hints.envAddress})?`, true);
338
+ if (useEnv) address = hints.envAddress;
339
+ }
340
+ if (!address && hints.operator?.owner) {
341
+ const useOp = await askYesNo(`Use existing operator.json owner (${hints.operator.owner})?`, true);
342
+ if (useOp) address = hints.operator.owner;
343
+ }
344
+ if (!address && hints.foundryAccounts.length) {
345
+ console.log("Foundry accounts (signing still needs --key-file or OPERATOR_PRIVATE_KEY):");
346
+ hints.foundryAccounts.forEach((n, i) => console.log(` ${i + 1}. ${n}`));
347
+ const pick = await ask("Foundry account name to associate (or leave blank)", "");
348
+ if (pick) {
349
+ address = await ask(
350
+ `Address for Foundry account "${pick}" (paste 0x…; unlock via cast if needed)`,
351
+ "",
352
+ );
353
+ }
354
+ }
355
+ if (!address) {
356
+ address = await ask("Operator owner address (0x…)", "");
357
+ }
358
+ if (!address || !/^0x[0-9a-fA-F]{40}$/.test(address)) {
359
+ throw new Error("A valid 0x operator address is required");
360
+ }
361
+ address = getAddress(address);
362
+
363
+ let label =
364
+ flags.operator ||
365
+ hints.operator?.label ||
366
+ (await ask("Operator label (e.g. org.you)", ""));
367
+ if (!label) throw new Error("Operator label is required");
368
+
369
+ const presetCfg = PRESETS[preset];
370
+ console.log("Checking chain…");
371
+ const check = await assertSetupIdentity({
372
+ rpc: presetCfg.chainRpcUrl,
373
+ registry: presetCfg.registryAddress,
374
+ label,
375
+ address,
376
+ skipChainCheck: flags.skipChainCheck || !presetCfg.registryAddress,
377
+ });
378
+ if (check.unregistered) {
379
+ console.log(`Note: "${label}" is not registered yet — run clanker operator mint after setup.`);
380
+ } else if (check.operator) {
381
+ console.log(`On-chain: ${label} active, owner matches.`);
382
+ }
383
+
384
+ let keyFile = flags.keyFile;
385
+ let keyEnv = flags.keyEnv;
386
+ let skipKey = flags.skipKey;
387
+ if (!skipKey && !keyFile && !keyEnv) {
388
+ if (hints.hasOperatorPrivateKeyEnv) {
389
+ const use = await askYesNo("Store env pointer OPERATOR_PRIVATE_KEY for signing?", true);
390
+ if (use) keyEnv = "OPERATOR_PRIVATE_KEY";
391
+ else skipKey = true;
392
+ } else {
393
+ const path = await ask(
394
+ "Path to operator key file for signing (leave blank for read-only whoami)",
395
+ "",
396
+ );
397
+ if (path) keyFile = path;
398
+ else skipKey = true;
399
+ }
400
+ }
401
+
402
+ const key = buildKeyPointer({ keyFile, keyEnv, skipKey, env });
403
+ if (key?.type === "keyFile") {
404
+ const fromKey = getAddress(addressFromKeyFile(key.value));
405
+ if (fromKey !== address) {
406
+ throw new Error(`Key file address ${fromKey} does not match chosen owner ${address}`);
407
+ }
408
+ }
409
+
410
+ console.log("\nWill write:");
411
+ console.log(` preset: ${preset}`);
412
+ console.log(` fromBlock:${fromBlock ?? presetCfg.fromBlock}`);
413
+ console.log(` label: ${label}`);
414
+ console.log(` owner: ${address}`);
415
+ console.log(` key: ${key ? `${key.type}=${key.value}` : "(none — read-only)"}`);
416
+ const ok = flags.yes || (await askYesNo("Write profile?", true));
417
+ if (!ok) throw new Error("Aborted");
418
+
419
+ const result = applySetup(
420
+ {
421
+ preset,
422
+ force: true,
423
+ fromBlock: fromBlock ?? undefined,
424
+ registryAddress: presetCfg.registryAddress,
425
+ label,
426
+ address,
427
+ key,
428
+ },
429
+ { home, env },
430
+ );
431
+
432
+ console.log(`\nWrote ${result.configPath}`);
433
+ console.log(`Wrote ${result.operatorPath}`);
434
+ if (!key) {
435
+ console.log("Read-only profile: `clanker whoami` works; mint/revoke need --key-file or OPERATOR_PRIVATE_KEY.");
436
+ } else {
437
+ console.log("Try: clanker whoami");
438
+ }
439
+ return result;
440
+ } finally {
441
+ if (!opts.rl) rl.close();
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Entry: interactive if TTY (unless --yes with full flags), else non-interactive.
447
+ */
448
+ export async function runSetup(argv, opts = {}) {
449
+ const flags = parseSetupFlags(argv);
450
+ const isTTY = opts.isTTY ?? Boolean(input.isTTY);
451
+
452
+ // Fully flagged non-interactive path
453
+ if (
454
+ !isTTY ||
455
+ (flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile))
456
+ ) {
457
+ if (!isTTY && !(flags.preset && flags.operator)) {
458
+ return runSetupNonInteractive(argv, opts);
459
+ }
460
+ if (flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile || opts.env?.OPERATOR_PRIVATE_KEY || process.env.OPERATOR_PRIVATE_KEY)) {
461
+ return runSetupNonInteractive(argv, opts);
462
+ }
463
+ }
464
+
465
+ if (!isTTY) {
466
+ return runSetupNonInteractive(argv, opts);
467
+ }
468
+
469
+ return runSetupInteractive(argv, opts);
470
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@clanker-chain/clanker-cli",
3
+ "version": "2026.9.7-1",
4
+ "description": "CLI for wiring clanker-chain identity and MQTT into OpenClaw and other agentic stacks.",
5
+ "type": "module",
6
+ "bin": {
7
+ "clanker": "./bin/clanker.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "lib",
12
+ ".env.example"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "scripts": {
18
+ "lint": "node ./bin/clanker.mjs --help",
19
+ "test": "node --test test/*.test.mjs",
20
+ "mint-operator": "node ./bin/clanker.mjs chain mint-operator",
21
+ "mint-bot": "node ./bin/clanker.mjs chain mint-bot",
22
+ "mint-operator:sepolia": "bash ./scripts/with-sepolia-env.sh mint-operator",
23
+ "mint-bot:sepolia": "bash ./scripts/with-sepolia-env.sh mint-bot"
24
+ },
25
+ "dependencies": {
26
+ "viem": "^2.21.0"
27
+ }
28
+ }