@clanker-chain/clanker-cli 2026.9.12 → 2026.9.13

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/resolve.mjs CHANGED
@@ -78,6 +78,11 @@ export function keyPointerFromSource(opts) {
78
78
  return { type: "env", value: "OPERATOR_PRIVATE_KEY" };
79
79
  }
80
80
 
81
+ /** Honest error when mint/pair/rotate/transfer have no owner signer. */
82
+ export const OWNER_SIGNER_REQUIRED =
83
+ "Owner actions need clanker login (email vault), or a signing key (--key-file / OPERATOR_PRIVATE_KEY). " +
84
+ "A read-only profile can still run whoami, bots, fund, and doctor.";
85
+
81
86
  /**
82
87
  * Resolve the operator signing key with Anvil guard.
83
88
  *
@@ -133,11 +138,7 @@ export function resolveOperatorKey(argv = [], opts = {}) {
133
138
  key = normalizePrivateKey(ANVIL_DEFAULT_PRIVATE_KEY);
134
139
  source = "anvil-default (local RPC)";
135
140
  } else {
136
- throw new Error(
137
- "Operator private key required for non-local RPC. Set OPERATOR_PRIVATE_KEY, " +
138
- "pass --key / --key-file, or configure ~/.clanker/operator.json key pointer. " +
139
- "Anvil account #0 is not used on public networks.",
140
- );
141
+ throw new Error(OWNER_SIGNER_REQUIRED);
141
142
  }
142
143
  }
143
144
 
@@ -216,7 +217,11 @@ export function resolveReadIdentity(argv = [], opts = {}) {
216
217
  };
217
218
  } catch (err) {
218
219
  const msg = err?.message ?? String(err);
219
- if (!/private key required|Anvil account #0|Key file not found|Profile keyFile/i.test(msg)) {
220
+ if (
221
+ !/private key required|Owner actions need|clanker login|Anvil account #0|Key file not found|Profile keyFile/i.test(
222
+ msg,
223
+ )
224
+ ) {
220
225
  throw err;
221
226
  }
222
227
  }
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 {
@@ -37,8 +44,9 @@ import {
37
44
  consumerFundHints,
38
45
  generateOperatorKeyFile,
39
46
  } from "./operator-key.mjs";
47
+ import { wireOpenClawMqtt } from "./openclaw-wire.mjs";
48
+ import { runLogin } from "./login.mjs";
40
49
  import { c, nextHint } from "./ui.mjs";
41
- import { join } from "node:path";
42
50
 
43
51
  /**
44
52
  * @param {string[]} argv
@@ -57,6 +65,7 @@ export function parseSetupFlags(argv) {
57
65
  let foundryAccount = null;
58
66
  let exportKey = false;
59
67
  let generateKey = false;
68
+ let botKey = null;
60
69
 
61
70
  for (let i = 0; i < argv.length; i += 1) {
62
71
  const a = argv[i];
@@ -67,6 +76,7 @@ export function parseSetupFlags(argv) {
67
76
  else if (a === "--key-env" && argv[i + 1]) keyEnv = argv[++i];
68
77
  else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
69
78
  else if (a === "--foundry-account" && argv[i + 1]) foundryAccount = argv[++i];
79
+ else if (a === "--bot-key" && argv[i + 1]) botKey = argv[++i];
70
80
  else if (a === "--export-key") exportKey = true;
71
81
  else if (a === "--generate-key") generateKey = true;
72
82
  else if (a === "--force") force = true;
@@ -89,6 +99,7 @@ export function parseSetupFlags(argv) {
89
99
  foundryAccount,
90
100
  exportKey,
91
101
  generateKey,
102
+ botKey,
92
103
  };
93
104
  }
94
105
 
@@ -162,6 +173,39 @@ export function buildKeyPointer({ keyFile, keyEnv, skipKey, env = process.env })
162
173
  return null;
163
174
  }
164
175
 
176
+ /**
177
+ * Copy a downloaded bot key into ~/.openclaw/keys and wire channels.mqtt.
178
+ * @param {string} botKeyPath
179
+ * @param {{
180
+ * operatorLabel: string,
181
+ * network: { rpc: string, registry: string|null, brokerUrl?: string, mqttAuthServiceUrl?: string },
182
+ * openclawHome?: string,
183
+ * }} opts
184
+ */
185
+ export function attachBotKeyFile(botKeyPath, opts) {
186
+ if (!existsSync(botKeyPath)) {
187
+ throw new Error(`Bot key file not found: ${botKeyPath}`);
188
+ }
189
+ const botId = basename(botKeyPath).replace(/\.key$/i, "");
190
+ if (!botId || botId.includes("/") || botId.includes("..")) {
191
+ throw new Error(`Invalid bot key basename: ${botKeyPath}`);
192
+ }
193
+ const openclawHome = opts.openclawHome ?? join(homedir(), ".openclaw");
194
+ const destDir = join(openclawHome, "keys");
195
+ mkdirSync(destDir, { recursive: true });
196
+ const dest = join(destDir, `${botId}.key`);
197
+ copyFileSync(botKeyPath, dest);
198
+ chmodSync(dest, 0o600);
199
+ const wired = wireOpenClawMqtt({
200
+ botId,
201
+ operatorId: opts.operatorLabel,
202
+ network: opts.network,
203
+ keyPath: dest,
204
+ openclawHome,
205
+ });
206
+ return { botId, keyPath: dest, openclawPath: wired.path };
207
+ }
208
+
165
209
  /**
166
210
  * Apply setup choices to disk.
167
211
  */
@@ -299,7 +343,7 @@ export async function runSetupNonInteractive(argv, opts = {}) {
299
343
  }
300
344
  }
301
345
 
302
- return applySetup(
346
+ const result = applySetup(
303
347
  {
304
348
  preset: flags.preset,
305
349
  force: flags.force || flags.yes,
@@ -311,6 +355,34 @@ export async function runSetupNonInteractive(argv, opts = {}) {
311
355
  },
312
356
  { home, env },
313
357
  );
358
+ return finishSetupResult(result, flags, { ...opts, quiet: true });
359
+ }
360
+
361
+ /**
362
+ * @param {object} result
363
+ * @param {{ botKey?: string|null, operator?: string|null }} flags
364
+ * @param {{ openclawHome?: string, quiet?: boolean }} [opts]
365
+ */
366
+ async function finishSetupResult(result, flags, opts = {}) {
367
+ if (flags.botKey) {
368
+ const attached = attachBotKeyFile(flags.botKey, {
369
+ operatorLabel: result.operator?.label ?? flags.operator,
370
+ network: {
371
+ rpc: result.config.chainRpcUrl,
372
+ registry: result.config.registryAddress,
373
+ brokerUrl: result.config.brokerUrl,
374
+ mqttAuthServiceUrl: result.config.mqttAuthServiceUrl,
375
+ },
376
+ openclawHome: opts.openclawHome,
377
+ });
378
+ result.botKey = attached;
379
+ if (!opts.quiet) {
380
+ clack.log.success(
381
+ `Bot key → ${attached.keyPath}; wired ${attached.openclawPath}`,
382
+ );
383
+ }
384
+ }
385
+ return result;
314
386
  }
315
387
 
316
388
  /**
@@ -334,7 +406,7 @@ export async function runSetupInteractive(argv, opts = {}) {
334
406
  );
335
407
  clack.log.message(
336
408
  c.dim(
337
- "New here? Create a key file no wallet app or Foundry required. Mint needs a little test ETH later.",
409
+ "Sign in with email (recommended), attach an existing 0x, or create ~/.clanker/op.key.",
338
410
  ),
339
411
  );
340
412
  clack.log.message(c.dim(`Profile: ${home}`));
@@ -395,6 +467,7 @@ export async function runSetupInteractive(argv, opts = {}) {
395
467
  let address = flags.address ?? null;
396
468
  let foundryAccountUsed = null;
397
469
  let generatedKeyFile = null;
470
+ let privyKeyPointer = null;
398
471
  if (!address && flags.generateKey) {
399
472
  const dest = flags.keyFile || defaultOperatorKeyPath(home);
400
473
  let forceGen = flags.force;
@@ -440,10 +513,20 @@ export async function runSetupInteractive(argv, opts = {}) {
440
513
  }
441
514
  if (!address) {
442
515
  const options = [
516
+ {
517
+ value: "__login__",
518
+ label: "Sign in (email — recommended)",
519
+ hint: "Privy vault — no op.key on disk",
520
+ },
521
+ {
522
+ value: "__paste__",
523
+ label: "I already have an owner address",
524
+ hint: "/join or any 0x — usually read-only until login",
525
+ },
443
526
  {
444
527
  value: "__generate__",
445
528
  label: "Create a new operator key for me",
446
- hint: "writes ~/.clanker/op.key (recommended)",
529
+ hint: "writes ~/.clanker/op.key (self-custody)",
447
530
  },
448
531
  {
449
532
  value: "__keyfile__",
@@ -466,21 +549,27 @@ export async function runSetupInteractive(argv, opts = {}) {
466
549
  hint: "advanced — install Foundry first",
467
550
  });
468
551
  }
469
- options.push({
470
- value: "__paste__",
471
- label: "Paste an address…",
472
- hint: "read-only unless you add a key later",
473
- });
474
552
 
475
553
  const pick = cancelIf(
476
554
  await clack.select({
477
555
  message: "How do you want to set your operator identity?",
478
556
  options,
479
- initialValue: "__generate__",
557
+ initialValue: "__login__",
480
558
  }),
481
559
  );
482
560
 
483
- if (pick === "__generate__") {
561
+ let attachReadOnly = false;
562
+ if (pick === "__login__") {
563
+ clack.log.step("Opening browser for clanker login…");
564
+ const logged = await runLogin([], {
565
+ home,
566
+ env,
567
+ });
568
+ address = logged.address;
569
+ privyKeyPointer = { type: "privy", value: logged.walletId };
570
+ flags.skipKey = true;
571
+ clack.log.success(`Signed in as ${address}`);
572
+ } else if (pick === "__generate__") {
484
573
  const dest = defaultOperatorKeyPath(home);
485
574
  let forceGen = flags.force;
486
575
  if (existsSync(dest) && !forceGen) {
@@ -526,15 +615,17 @@ export async function runSetupInteractive(argv, opts = {}) {
526
615
  /^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
527
616
  }),
528
617
  );
618
+ attachReadOnly = true;
529
619
  } else if (pick === "__paste__") {
530
620
  address = cancelIf(
531
621
  await clack.text({
532
- message: "Operator owner address",
622
+ message: "Owner address (from /join or any EOA)",
533
623
  placeholder: "0x…",
534
624
  validate: (v) =>
535
625
  /^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
536
626
  }),
537
627
  );
628
+ attachReadOnly = true;
538
629
  } else {
539
630
  foundryAccountUsed = pick;
540
631
  try {
@@ -557,6 +648,9 @@ export async function runSetupInteractive(argv, opts = {}) {
557
648
  );
558
649
  }
559
650
  }
651
+ if (attachReadOnly) {
652
+ flags.skipKey = true;
653
+ }
560
654
  }
561
655
  address = getAddress(address);
562
656
 
@@ -645,7 +739,9 @@ export async function runSetupInteractive(argv, opts = {}) {
645
739
  }
646
740
  }
647
741
 
648
- const key = buildKeyPointer({ keyFile, keyEnv, skipKey, env });
742
+ const key =
743
+ privyKeyPointer ||
744
+ buildKeyPointer({ keyFile, keyEnv, skipKey, env });
649
745
  if (key?.type === "keyFile") {
650
746
  const fromKey = getAddress(addressFromKeyFile(key.value));
651
747
  if (fromKey !== address) {
@@ -687,11 +783,21 @@ export async function runSetupInteractive(argv, opts = {}) {
687
783
  { home, env },
688
784
  );
689
785
 
786
+ await finishSetupResult(result, { ...flags, operator: label }, opts);
787
+
690
788
  clack.outro(c.green(`Wrote ${result.configPath}\nWrote ${result.operatorPath}`));
691
- if (!key) {
789
+ if (privyKeyPointer) {
692
790
  nextHint([
791
+ "clanker fund",
792
+ "clanker whoami",
793
+ "clanker pair add <peer> --yes",
794
+ "clanker operator mint <label> --yes # if not minted yet",
795
+ ]);
796
+ } else if (!key) {
797
+ nextHint([
798
+ "clanker login # to enable mint/pair with the email vault",
799
+ "clanker fund",
693
800
  "clanker whoami",
694
- "clanker setup — choose Create a new operator key when you need mint",
695
801
  ]);
696
802
  } else if (preset === "sepolia" && generatedKeyFile) {
697
803
  nextHint(consumerFundHints({ address, label }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clanker-chain/clanker-cli",
3
- "version": "2026.9.12",
3
+ "version": "2026.9.13",
4
4
  "description": "CLI for wiring clanker-chain identity and MQTT into OpenClaw and other agentic stacks.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,11 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@clack/prompts": "^1.8.0",
27
+ "@hpke/chacha20poly1305": "^1.8.0",
28
+ "@hpke/core": "^1.9.0",
29
+ "@noble/curves": "^1.9.7",
30
+ "@noble/hashes": "^1.8.0",
31
+ "canonicalize": "^2.1.0",
27
32
  "picocolors": "^1.1.1",
28
33
  "viem": "^2.56.3"
29
34
  },