@clanker-chain/clanker-cli 2026.9.8 → 2026.9.12-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 CHANGED
@@ -3,7 +3,14 @@
3
3
  */
4
4
 
5
5
  import { stdin as input } from "node:process";
6
- import { existsSync } from "node:fs";
6
+ import {
7
+ chmodSync,
8
+ copyFileSync,
9
+ existsSync,
10
+ mkdirSync,
11
+ } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { basename, join } from "node:path";
7
14
  import { getAddress } from "viem";
8
15
  import * as clack from "@clack/prompts";
9
16
  import {
@@ -33,8 +40,12 @@ import {
33
40
  exportFoundryKey,
34
41
  resolveFoundryAddress,
35
42
  } from "./foundry.mjs";
43
+ import {
44
+ consumerFundHints,
45
+ generateOperatorKeyFile,
46
+ } from "./operator-key.mjs";
47
+ import { wireOpenClawMqtt } from "./openclaw-wire.mjs";
36
48
  import { c, nextHint } from "./ui.mjs";
37
- import { join } from "node:path";
38
49
 
39
50
  /**
40
51
  * @param {string[]} argv
@@ -52,6 +63,8 @@ export function parseSetupFlags(argv) {
52
63
  let skipChainCheck = false;
53
64
  let foundryAccount = null;
54
65
  let exportKey = false;
66
+ let generateKey = false;
67
+ let botKey = null;
55
68
 
56
69
  for (let i = 0; i < argv.length; i += 1) {
57
70
  const a = argv[i];
@@ -62,7 +75,9 @@ export function parseSetupFlags(argv) {
62
75
  else if (a === "--key-env" && argv[i + 1]) keyEnv = argv[++i];
63
76
  else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
64
77
  else if (a === "--foundry-account" && argv[i + 1]) foundryAccount = argv[++i];
78
+ else if (a === "--bot-key" && argv[i + 1]) botKey = argv[++i];
65
79
  else if (a === "--export-key") exportKey = true;
80
+ else if (a === "--generate-key") generateKey = true;
66
81
  else if (a === "--force") force = true;
67
82
  else if (a === "--yes" || a === "-y") yes = true;
68
83
  else if (a === "--skip-key") skipKey = true;
@@ -82,6 +97,8 @@ export function parseSetupFlags(argv) {
82
97
  skipChainCheck,
83
98
  foundryAccount,
84
99
  exportKey,
100
+ generateKey,
101
+ botKey,
85
102
  };
86
103
  }
87
104
 
@@ -155,6 +172,39 @@ export function buildKeyPointer({ keyFile, keyEnv, skipKey, env = process.env })
155
172
  return null;
156
173
  }
157
174
 
175
+ /**
176
+ * Copy a downloaded bot key into ~/.openclaw/keys and wire channels.mqtt.
177
+ * @param {string} botKeyPath
178
+ * @param {{
179
+ * operatorLabel: string,
180
+ * network: { rpc: string, registry: string|null, brokerUrl?: string, mqttAuthServiceUrl?: string },
181
+ * openclawHome?: string,
182
+ * }} opts
183
+ */
184
+ export function attachBotKeyFile(botKeyPath, opts) {
185
+ if (!existsSync(botKeyPath)) {
186
+ throw new Error(`Bot key file not found: ${botKeyPath}`);
187
+ }
188
+ const botId = basename(botKeyPath).replace(/\.key$/i, "");
189
+ if (!botId || botId.includes("/") || botId.includes("..")) {
190
+ throw new Error(`Invalid bot key basename: ${botKeyPath}`);
191
+ }
192
+ const openclawHome = opts.openclawHome ?? join(homedir(), ".openclaw");
193
+ const destDir = join(openclawHome, "keys");
194
+ mkdirSync(destDir, { recursive: true });
195
+ const dest = join(destDir, `${botId}.key`);
196
+ copyFileSync(botKeyPath, dest);
197
+ chmodSync(dest, 0o600);
198
+ const wired = wireOpenClawMqtt({
199
+ botId,
200
+ operatorId: opts.operatorLabel,
201
+ network: opts.network,
202
+ keyPath: dest,
203
+ openclawHome,
204
+ });
205
+ return { botId, keyPath: dest, openclawPath: wired.path };
206
+ }
207
+
158
208
  /**
159
209
  * Apply setup choices to disk.
160
210
  */
@@ -203,7 +253,7 @@ export async function runSetupNonInteractive(argv, opts = {}) {
203
253
  if (!flags.preset || !PRESETS[flags.preset]) {
204
254
  throw new Error(
205
255
  "Non-interactive setup requires --preset sepolia|local (stdin is not a TTY). " +
206
- "Also pass --operator <label> and --address 0x…",
256
+ "Also pass --operator <label> and --generate-key (or --address / --key-file)",
207
257
  );
208
258
  }
209
259
  if (!flags.operator) {
@@ -218,6 +268,23 @@ export async function runSetupNonInteractive(argv, opts = {}) {
218
268
  inheritStdio: false,
219
269
  };
220
270
 
271
+ if (flags.generateKey) {
272
+ if (flags.skipKey) {
273
+ throw new Error("Cannot combine --generate-key with --skip-key");
274
+ }
275
+ const dest = keyFile || defaultOperatorKeyPath(home);
276
+ const created = generateOperatorKeyFile(dest, {
277
+ force: flags.force,
278
+ });
279
+ keyFile = created.path;
280
+ if (address && getAddress(address) !== getAddress(created.address)) {
281
+ throw new Error(
282
+ `Generated key address ${created.address} does not match --address ${getAddress(address)}`,
283
+ );
284
+ }
285
+ address = created.address;
286
+ }
287
+
221
288
  if (flags.foundryAccount) {
222
289
  if (!address) {
223
290
  address = resolveFoundryAddress(flags.foundryAccount, castOpts);
@@ -238,7 +305,7 @@ export async function runSetupNonInteractive(argv, opts = {}) {
238
305
  if (!address && env.OPERATOR_PRIVATE_KEY) address = addressFromEnv(env);
239
306
  if (!address) {
240
307
  throw new Error(
241
- "Non-interactive setup requires --address 0x…, --key-file, OPERATOR_PRIVATE_KEY, or --foundry-account",
308
+ "Non-interactive setup requires --generate-key, --address 0x…, --key-file, OPERATOR_PRIVATE_KEY, or --foundry-account",
242
309
  );
243
310
  }
244
311
 
@@ -275,7 +342,7 @@ export async function runSetupNonInteractive(argv, opts = {}) {
275
342
  }
276
343
  }
277
344
 
278
- return applySetup(
345
+ const result = applySetup(
279
346
  {
280
347
  preset: flags.preset,
281
348
  force: flags.force || flags.yes,
@@ -287,6 +354,34 @@ export async function runSetupNonInteractive(argv, opts = {}) {
287
354
  },
288
355
  { home, env },
289
356
  );
357
+ return finishSetupResult(result, flags, { ...opts, quiet: true });
358
+ }
359
+
360
+ /**
361
+ * @param {object} result
362
+ * @param {{ botKey?: string|null, operator?: string|null }} flags
363
+ * @param {{ openclawHome?: string, quiet?: boolean }} [opts]
364
+ */
365
+ async function finishSetupResult(result, flags, opts = {}) {
366
+ if (flags.botKey) {
367
+ const attached = attachBotKeyFile(flags.botKey, {
368
+ operatorLabel: result.operator?.label ?? flags.operator,
369
+ network: {
370
+ rpc: result.config.chainRpcUrl,
371
+ registry: result.config.registryAddress,
372
+ brokerUrl: result.config.brokerUrl,
373
+ mqttAuthServiceUrl: result.config.mqttAuthServiceUrl,
374
+ },
375
+ openclawHome: opts.openclawHome,
376
+ });
377
+ result.botKey = attached;
378
+ if (!opts.quiet) {
379
+ clack.log.success(
380
+ `Bot key → ${attached.keyPath}; wired ${attached.openclawPath}`,
381
+ );
382
+ }
383
+ }
384
+ return result;
290
385
  }
291
386
 
292
387
  /**
@@ -305,9 +400,13 @@ export async function runSetupInteractive(argv, opts = {}) {
305
400
  });
306
401
 
307
402
  clack.intro(c.bold("clanker setup"));
308
- clack.log.step("Creates ~/.clanker/config.json (network) and operator.json (identity).");
403
+ clack.log.step(
404
+ "Sets up your operator identity (org account) and network for OpenClaw bots.",
405
+ );
309
406
  clack.log.message(
310
- c.dim("whoami works from owner without a key; mint/revoke need a key pointer later."),
407
+ c.dim(
408
+ "Attach an existing owner address (/join or any 0x), or create ~/.clanker/op.key. Mint needs test ETH later.",
409
+ ),
311
410
  );
312
411
  clack.log.message(c.dim(`Profile: ${home}`));
313
412
  console.log("");
@@ -366,6 +465,28 @@ export async function runSetupInteractive(argv, opts = {}) {
366
465
 
367
466
  let address = flags.address ?? null;
368
467
  let foundryAccountUsed = null;
468
+ let generatedKeyFile = null;
469
+ if (!address && flags.generateKey) {
470
+ const dest = flags.keyFile || defaultOperatorKeyPath(home);
471
+ let forceGen = flags.force;
472
+ if (existsSync(dest) && !forceGen) {
473
+ forceGen = cancelIf(
474
+ await clack.confirm({
475
+ message: `Overwrite existing ${dest}?`,
476
+ initialValue: false,
477
+ }),
478
+ );
479
+ if (!forceGen) {
480
+ clack.cancel("Aborted.");
481
+ process.exit(0);
482
+ }
483
+ }
484
+ const created = generateOperatorKeyFile(dest, { force: true });
485
+ generatedKeyFile = created.path;
486
+ address = created.address;
487
+ clack.log.success(`Created operator key at ${created.path}`);
488
+ clack.log.info(`Your operator address: ${created.address}`);
489
+ }
369
490
  if (!address && flags.keyFile) {
370
491
  address = addressFromKeyFile(flags.keyFile);
371
492
  clack.log.info(`Address from --key-file: ${address}`);
@@ -388,23 +509,87 @@ export async function runSetupInteractive(argv, opts = {}) {
388
509
  );
389
510
  if (useOp) address = hints.operator.owner;
390
511
  }
391
- if (!address && hints.foundryAccounts.length) {
512
+ if (!address) {
392
513
  const options = [
393
- ...hints.foundryAccounts.map((n) => ({
394
- value: n,
395
- label: n,
396
- hint: "resolve via cast wallet address",
397
- })),
398
- { value: "__paste__", label: "Paste an address…", hint: "0x…" },
514
+ {
515
+ value: "__paste__",
516
+ label: "I already have an owner address",
517
+ hint: "/join or any 0x — usually read-only",
518
+ },
519
+ {
520
+ value: "__generate__",
521
+ label: "Create a new operator key for me",
522
+ hint: "writes ~/.clanker/op.key",
523
+ },
524
+ {
525
+ value: "__keyfile__",
526
+ label: "Use an existing key file…",
527
+ hint: "path to a 0x private key file",
528
+ },
399
529
  ];
530
+ if (hints.foundryAccounts.length) {
531
+ for (const n of hints.foundryAccounts) {
532
+ options.push({
533
+ value: n,
534
+ label: `Foundry: ${n}`,
535
+ hint: "advanced — cast wallet",
536
+ });
537
+ }
538
+ } else {
539
+ options.push({
540
+ value: "__foundry_missing__",
541
+ label: "Foundry account…",
542
+ hint: "advanced — install Foundry first",
543
+ });
544
+ }
545
+
400
546
  const pick = cancelIf(
401
547
  await clack.select({
402
- message: "Operator owner source",
548
+ message: "How do you want to set your operator identity?",
403
549
  options,
404
- initialValue: hints.foundryAccounts[0],
550
+ initialValue: "__paste__",
405
551
  }),
406
552
  );
407
- if (pick === "__paste__") {
553
+
554
+ let attachReadOnly = false;
555
+ if (pick === "__generate__") {
556
+ const dest = defaultOperatorKeyPath(home);
557
+ let forceGen = flags.force;
558
+ if (existsSync(dest) && !forceGen) {
559
+ forceGen = cancelIf(
560
+ await clack.confirm({
561
+ message: `Overwrite existing ${dest}?`,
562
+ initialValue: false,
563
+ }),
564
+ );
565
+ if (!forceGen) {
566
+ clack.cancel("Aborted.");
567
+ process.exit(0);
568
+ }
569
+ }
570
+ const created = generateOperatorKeyFile(dest, { force: true });
571
+ generatedKeyFile = created.path;
572
+ address = created.address;
573
+ clack.log.success(`Created operator key at ${created.path}`);
574
+ clack.log.info(`Your operator address: ${created.address}`);
575
+ } else if (pick === "__keyfile__") {
576
+ const path = cancelIf(
577
+ await clack.text({
578
+ message: "Path to operator key file",
579
+ placeholder: join(home, "op.key"),
580
+ validate: (v) =>
581
+ v && String(v).trim() && existsSync(String(v).trim())
582
+ ? undefined
583
+ : "File not found",
584
+ }),
585
+ );
586
+ generatedKeyFile = String(path).trim();
587
+ address = addressFromKeyFile(generatedKeyFile);
588
+ clack.log.info(`Address from key file: ${address}`);
589
+ } else if (pick === "__foundry_missing__") {
590
+ clack.log.warn(
591
+ "Foundry (cast) is not available. Install https://book.getfoundry.sh/ or choose Create a new operator key.",
592
+ );
408
593
  address = cancelIf(
409
594
  await clack.text({
410
595
  message: "Operator owner address",
@@ -413,6 +598,17 @@ export async function runSetupInteractive(argv, opts = {}) {
413
598
  /^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
414
599
  }),
415
600
  );
601
+ attachReadOnly = true;
602
+ } else if (pick === "__paste__") {
603
+ address = cancelIf(
604
+ await clack.text({
605
+ message: "Owner address (from /join or any EOA)",
606
+ placeholder: "0x…",
607
+ validate: (v) =>
608
+ /^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
609
+ }),
610
+ );
611
+ attachReadOnly = true;
416
612
  } else {
417
613
  foundryAccountUsed = pick;
418
614
  try {
@@ -435,16 +631,9 @@ export async function runSetupInteractive(argv, opts = {}) {
435
631
  );
436
632
  }
437
633
  }
438
- }
439
- if (!address) {
440
- address = cancelIf(
441
- await clack.text({
442
- message: "Operator owner address",
443
- placeholder: "0x…",
444
- validate: (v) =>
445
- /^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
446
- }),
447
- );
634
+ if (attachReadOnly) {
635
+ flags.skipKey = true;
636
+ }
448
637
  }
449
638
  address = getAddress(address);
450
639
 
@@ -483,7 +672,7 @@ export async function runSetupInteractive(argv, opts = {}) {
483
672
  throw err;
484
673
  }
485
674
 
486
- let keyFile = flags.keyFile;
675
+ let keyFile = flags.keyFile || generatedKeyFile;
487
676
  let keyEnv = flags.keyEnv;
488
677
  let skipKey = flags.skipKey;
489
678
  if (!skipKey && !keyFile && !keyEnv) {
@@ -575,11 +764,23 @@ export async function runSetupInteractive(argv, opts = {}) {
575
764
  { home, env },
576
765
  );
577
766
 
767
+ await finishSetupResult(result, { ...flags, operator: label }, opts);
768
+
578
769
  clack.outro(c.green(`Wrote ${result.configPath}\nWrote ${result.operatorPath}`));
579
770
  if (!key) {
580
771
  nextHint([
772
+ "clanker fund",
581
773
  "clanker whoami",
582
- "clanker setup --key-file ~/.clanker/op.key --force # when you need mint",
774
+ "mint/pair/rotate need a signing key or stay on /join",
775
+ ]);
776
+ } else if (preset === "sepolia" && generatedKeyFile) {
777
+ nextHint(consumerFundHints({ address, label }));
778
+ } else if (preset === "sepolia") {
779
+ nextHint([
780
+ "clanker fund",
781
+ "clanker doctor",
782
+ "clanker whoami",
783
+ `clanker bot mint <label>`,
583
784
  ]);
584
785
  } else {
585
786
  nextHint(["clanker whoami", `clanker bot mint <label>`]);
@@ -593,23 +794,20 @@ export async function runSetupInteractive(argv, opts = {}) {
593
794
  export async function runSetup(argv, opts = {}) {
594
795
  const flags = parseSetupFlags(argv);
595
796
  const isTTY = opts.isTTY ?? Boolean(input.isTTY);
797
+ const hasIdentitySource = Boolean(
798
+ flags.address ||
799
+ flags.keyFile ||
800
+ flags.generateKey ||
801
+ flags.foundryAccount ||
802
+ opts.env?.OPERATOR_PRIVATE_KEY ||
803
+ process.env.OPERATOR_PRIVATE_KEY,
804
+ );
596
805
 
597
- if (
598
- !isTTY ||
599
- (flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile))
600
- ) {
806
+ if (!isTTY || (flags.yes && flags.preset && flags.operator && hasIdentitySource)) {
601
807
  if (!isTTY && !(flags.preset && flags.operator)) {
602
808
  return runSetupNonInteractive(argv, opts);
603
809
  }
604
- if (
605
- flags.yes &&
606
- flags.preset &&
607
- flags.operator &&
608
- (flags.address ||
609
- flags.keyFile ||
610
- opts.env?.OPERATOR_PRIVATE_KEY ||
611
- process.env.OPERATOR_PRIVATE_KEY)
612
- ) {
810
+ if (flags.yes && flags.preset && flags.operator && hasIdentitySource) {
613
811
  return runSetupNonInteractive(argv, opts);
614
812
  }
615
813
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clanker-chain/clanker-cli",
3
- "version": "2026.9.8",
3
+ "version": "2026.9.12-1",
4
4
  "description": "CLI for wiring clanker-chain identity and MQTT into OpenClaw and other agentic stacks.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,6 +25,9 @@
25
25
  "dependencies": {
26
26
  "@clack/prompts": "^1.8.0",
27
27
  "picocolors": "^1.1.1",
28
- "viem": "^2.21.0"
28
+ "viem": "^2.56.3"
29
+ },
30
+ "overrides": {
31
+ "ws": "^8.21.0"
29
32
  }
30
33
  }