@clanker-chain/clanker-cli 2026.9.7-4 → 2026.9.8-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/bin/clanker.mjs CHANGED
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawnSync } from "node:child_process";
4
- import { existsSync, writeFileSync, mkdirSync } from "node:fs";
4
+ import { existsSync, mkdirSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import os from "node:os";
7
7
  import process from "node:process";
8
8
  import { getAddress } from "viem";
9
9
  import {
10
10
  ANVIL_DEFAULT_PRIVATE_KEY,
11
- harnessSnippet,
12
11
  initProfile,
13
12
  isLocalRpc,
14
13
  loadConfig,
@@ -26,6 +25,10 @@ import {
26
25
  import { resolveForRead, resolveOperatorKey, resolveReadIdentity } from "../lib/resolve.mjs";
27
26
  import { runSetup } from "../lib/setup.mjs";
28
27
  import { runDoctor } from "../lib/doctor.mjs";
28
+ import {
29
+ hubConnectChecklist,
30
+ wireOpenClawMqtt,
31
+ } from "../lib/openclaw-wire.mjs";
29
32
  import {
30
33
  c,
31
34
  confirmPlan,
@@ -599,19 +602,38 @@ async function main() {
599
602
  );
600
603
  if (!ok) process.exit(0);
601
604
  const result = await chainMintBot(botLabel, operatorLabel, flags);
602
- if (hasFlag(flags, "--json")) printJson(result);
603
- else {
605
+ const mqtt = result.channels_mqtt ?? {};
606
+ const wire = wireOpenClawMqtt({
607
+ botId: botLabel,
608
+ operatorId: operatorLabel,
609
+ network: {
610
+ rpc: mqtt.chainRpcUrl,
611
+ registry: mqtt.registryAddress,
612
+ brokerUrl: mqtt.brokerUrl,
613
+ mqttAuthServiceUrl: mqtt.mqttAuthServiceUrl,
614
+ },
615
+ keyPath: result.key_path,
616
+ });
617
+ if (hasFlag(flags, "--json")) {
618
+ printJson({ ...result, openclaw: wire });
619
+ } else {
604
620
  console.log(c.green(`Minted bot ${botLabel} under ${operatorLabel}`));
605
621
  console.log(`botKey: ${result.bot_key}`);
606
622
  console.log(`key file: ${result.key_path}`);
607
623
  console.log(`also: ${result.clanker_key_path}`);
608
624
  console.log(`tx: ${result.tx}`);
609
- console.log("\nchannels.mqtt stub:");
610
- console.log(JSON.stringify(result.channels_mqtt, null, 2));
611
- nextHint([
612
- "Paste channels.mqtt into openclaw.json",
613
- "openclaw plugins install @clanker-chain/mqtt-channel-plugin@…",
614
- ]);
625
+ console.log(
626
+ wire.created
627
+ ? c.green(`Wrote ${wire.path}`)
628
+ : c.green(`Updated channels.mqtt in ${wire.path}`),
629
+ );
630
+ nextHint(
631
+ hubConnectChecklist({
632
+ botId: botLabel,
633
+ channelsMqtt: wire.channelsMqtt,
634
+ pluginPin: wire.pluginPin,
635
+ }),
636
+ );
615
637
  }
616
638
  return;
617
639
  }
@@ -869,22 +891,6 @@ async function main() {
869
891
  }
870
892
 
871
893
  if (cmd === "init-openclaw") {
872
- const home = os.homedir();
873
- const openclawDir = join(home, ".openclaw");
874
- const cfgPath = join(openclawDir, "openclaw.json");
875
- if (!existsSync(openclawDir)) {
876
- mkdirSync(openclawDir, { recursive: true });
877
- }
878
-
879
- if (existsSync(cfgPath)) {
880
- console.log(`${cfgPath} already exists.`);
881
- console.log(
882
- 'Ensure plugins.enabled includes "mqtt" and "mqtt-tools", and channels.mqtt has botId, operatorId, brokerUrl, chainRpcUrl, registryAddress.',
883
- );
884
- console.log("See SETUP.md and docs/operator-cli.md.");
885
- process.exit(0);
886
- }
887
-
888
894
  const profile = loadConfig();
889
895
  const operator = loadOperator();
890
896
  // No profile: local defaults only — do not mix Sepolia registry with localhost MQTT.
@@ -902,27 +908,21 @@ async function main() {
902
908
  mqttAuthServiceUrl: "http://localhost:9090",
903
909
  };
904
910
 
905
- const mqtt = harnessSnippet({
911
+ const wire = wireOpenClawMqtt({
906
912
  botId: "openclaw.your-bot.local",
907
913
  operatorId: operator?.label ?? "org.openclaw.your-operator",
908
914
  network,
909
915
  });
910
- if (!mqtt.registryAddress) {
911
- mqtt.registryAddress = "0x0000000000000000000000000000000000000000";
912
- }
913
-
914
- const cfg = {
915
- plugins: {
916
- enabled: ["mqtt", "mqtt-tools"],
917
- },
918
- channels: {
919
- mqtt,
920
- },
921
- };
922
- writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
923
- console.log(`Created ${cfgPath} with mqtt + mqtt-tools from ${profile ? "active" : "default"} preset.`);
924
- console.log("Next: clanker bot mint <label>, then set channels.mqtt.botId / operatorId.");
925
- console.log("See docs/operator-cli.md and SETUP.md.");
916
+ console.log(
917
+ wire.created
918
+ ? `Created ${wire.path} with mqtt + mqtt-tools from ${profile ? "active" : "default"} preset.`
919
+ : `Updated channels.mqtt in ${wire.path} (plugins mqtt + mqtt-tools ensured).`,
920
+ );
921
+ nextHint([
922
+ "clanker bot mint <label>",
923
+ "Then channels.mqtt.botId / operatorId / privateKeyFile update automatically",
924
+ "See docs/operator-cli.md and docs/closed-beta-invite.md",
925
+ ]);
926
926
  process.exit(0);
927
927
  }
928
928
 
package/lib/doctor.mjs CHANGED
@@ -14,9 +14,9 @@ import { detectSetupHints, formatSetupDetectTable } from "./setup-detect.mjs";
14
14
  import { c, nextHint } from "./ui.mjs";
15
15
 
16
16
  /**
17
- * @param {{ home?: string, env?: NodeJS.ProcessEnv, openclawDir?: string, castBin?: string, spawn?: Function }} [opts]
17
+ * @param {{ home?: string, env?: NodeJS.ProcessEnv, openclawDir?: string, castBin?: string, spawn?: Function, fetchImpl?: typeof fetch }} [opts]
18
18
  */
19
- export function runDoctorChecks(opts = {}) {
19
+ export async function runDoctorChecks(opts = {}) {
20
20
  const env = opts.env ?? process.env;
21
21
  const home = opts.home ?? clankerHome(env);
22
22
  const hints = detectSetupHints({
@@ -28,6 +28,7 @@ export function runDoctorChecks(opts = {}) {
28
28
  });
29
29
  const config = hints.config ?? loadConfig(home);
30
30
  const operator = hints.operator ?? loadOperator(home);
31
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
31
32
 
32
33
  /** @type {{ id: string, ok: boolean, level: 'pass'|'warn'|'fail', message: string }[]} */
33
34
  const checks = [];
@@ -102,6 +103,46 @@ export function runDoctorChecks(opts = {}) {
102
103
  : "Foundry cast not on PATH (optional)",
103
104
  });
104
105
 
106
+ const authUrl = config?.mqttAuthServiceUrl;
107
+ if (authUrl && typeof fetchImpl === "function") {
108
+ const healthUrl = `${String(authUrl).replace(/\/$/, "")}/health`;
109
+ try {
110
+ const ac = new AbortController();
111
+ const t = setTimeout(() => ac.abort(), 5000);
112
+ const res = await fetchImpl(healthUrl, { signal: ac.signal });
113
+ clearTimeout(t);
114
+ if (res.ok) {
115
+ checks.push({
116
+ id: "mqtt_auth_health",
117
+ ok: true,
118
+ level: "pass",
119
+ message: `mqtt-auth health OK (${healthUrl})`,
120
+ });
121
+ } else {
122
+ checks.push({
123
+ id: "mqtt_auth_health",
124
+ ok: false,
125
+ level: "fail",
126
+ message: `mqtt-auth health HTTP ${res.status} (${healthUrl})`,
127
+ });
128
+ }
129
+ } catch (err) {
130
+ checks.push({
131
+ id: "mqtt_auth_health",
132
+ ok: false,
133
+ level: "warn",
134
+ message: `mqtt-auth health unreachable: ${err.message ?? err}`,
135
+ });
136
+ }
137
+ } else if (config) {
138
+ checks.push({
139
+ id: "mqtt_auth_health",
140
+ ok: true,
141
+ level: "warn",
142
+ message: "mqttAuthServiceUrl not set — skip hub health",
143
+ });
144
+ }
145
+
105
146
  const readyWhoami = checks
106
147
  .filter((ch) => ch.id === "config" || ch.id === "registry" || ch.id === "operator")
107
148
  .every((ch) => ch.ok);
@@ -125,7 +166,7 @@ export function runDoctorChecks(opts = {}) {
125
166
  */
126
167
  export async function runDoctor(argv = [], opts = {}) {
127
168
  const json = argv.includes("--json");
128
- const report = runDoctorChecks(opts);
169
+ const report = await runDoctorChecks(opts);
129
170
 
130
171
  if (json) {
131
172
  console.log(
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Foundry cast helpers for setup (address resolve + key export).
3
+ * Never logs private keys.
4
+ */
5
+
6
+ import { mkdirSync, writeFileSync } from "node:fs";
7
+ import { dirname } from "node:path";
8
+ import { spawnSync } from "node:child_process";
9
+ import { getAddress } from "viem";
10
+ import { privateKeyToAccount } from "viem/accounts";
11
+ import { normalizePrivateKey } from "./resolve.mjs";
12
+ import { parseCastWalletList } from "./setup-detect.mjs";
13
+
14
+ /**
15
+ * @param {{ castBin?: string, spawn?: typeof spawnSync }} [opts]
16
+ */
17
+ export function listFoundryAccounts(opts = {}) {
18
+ const spawn = opts.spawn ?? spawnSync;
19
+ const castBin = opts.castBin ?? "cast";
20
+ const result = spawn(castBin, ["wallet", "list"], {
21
+ encoding: "utf8",
22
+ shell: false,
23
+ });
24
+ if (result.error || result.status !== 0) {
25
+ return { available: false, accounts: [] };
26
+ }
27
+ return { available: true, accounts: parseCastWalletList(result.stdout ?? "") };
28
+ }
29
+
30
+ /**
31
+ * Resolve 0x address for a Foundry account name.
32
+ * Uses inherit stdio so unlock prompts work when interactive.
33
+ *
34
+ * @param {string} account
35
+ * @param {{ castBin?: string, spawn?: typeof spawnSync, inheritStdio?: boolean }} [opts]
36
+ * @returns {string} checksummed address
37
+ */
38
+ export function resolveFoundryAddress(account, opts = {}) {
39
+ const spawn = opts.spawn ?? spawnSync;
40
+ const castBin = opts.castBin ?? "cast";
41
+ const inherit = opts.inheritStdio !== false;
42
+ const result = spawn(castBin, ["wallet", "address", account], {
43
+ encoding: "utf8",
44
+ shell: false,
45
+ stdio: inherit ? ["inherit", "pipe", "inherit"] : "pipe",
46
+ });
47
+ if (result.error) {
48
+ throw new Error(`cast failed: ${result.error.message}`);
49
+ }
50
+ if (result.status !== 0) {
51
+ const err = (result.stderr || result.stdout || "").trim();
52
+ throw new Error(
53
+ err || `cast wallet address ${account} failed (status ${result.status})`,
54
+ );
55
+ }
56
+ const line = String(result.stdout ?? "")
57
+ .split(/\r?\n/)
58
+ .map((l) => l.trim())
59
+ .find((l) => /^0x[0-9a-fA-F]{40}$/.test(l));
60
+ if (!line) {
61
+ throw new Error(`Could not parse address from cast wallet address ${account}`);
62
+ }
63
+ return getAddress(line);
64
+ }
65
+
66
+ /**
67
+ * Export Foundry account private key to destPath (mode 0o600).
68
+ * Does not print the key. Unlock prompts via inherit stdio when interactive.
69
+ *
70
+ * @param {string} account
71
+ * @param {string} destPath
72
+ * @param {{ castBin?: string, spawn?: typeof spawnSync, inheritStdio?: boolean }} [opts]
73
+ * @returns {{ path: string, address: string }}
74
+ */
75
+ export function exportFoundryKey(account, destPath, opts = {}) {
76
+ const spawn = opts.spawn ?? spawnSync;
77
+ const castBin = opts.castBin ?? "cast";
78
+ const inherit = opts.inheritStdio !== false;
79
+ const result = spawn(castBin, ["wallet", "private-key", account], {
80
+ encoding: "utf8",
81
+ shell: false,
82
+ stdio: inherit ? ["inherit", "pipe", "inherit"] : "pipe",
83
+ });
84
+ if (result.error) {
85
+ throw new Error(`cast failed: ${result.error.message}`);
86
+ }
87
+ if (result.status !== 0) {
88
+ const err = (result.stderr || "").trim();
89
+ throw new Error(
90
+ err || `cast wallet private-key ${account} failed (status ${result.status})`,
91
+ );
92
+ }
93
+ const raw = String(result.stdout ?? "")
94
+ .split(/\r?\n/)
95
+ .map((l) => l.trim())
96
+ .find((l) => l.length > 0);
97
+ if (!raw) {
98
+ throw new Error("cast returned empty private key");
99
+ }
100
+ const key = normalizePrivateKey(raw);
101
+ const address = privateKeyToAccount(key).address;
102
+
103
+ mkdirSync(dirname(destPath), { recursive: true });
104
+ writeFileSync(destPath, `${key}\n`, { mode: 0o600 });
105
+ return { path: destPath, address };
106
+ }
107
+
108
+ /**
109
+ * Default operator key path under clanker home.
110
+ * @param {string} home
111
+ */
112
+ export function defaultOperatorKeyPath(home) {
113
+ return `${home.replace(/\/$/, "")}/op.key`;
114
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Merge / create ~/.openclaw/openclaw.json channels.mqtt from clanker profile.
3
+ */
4
+
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { harnessSnippet } from "./profile.mjs";
9
+
10
+ /** Current published OpenClaw plugin pins for closed-beta invite. */
11
+ export const OPENCLAW_PLUGIN_PIN = "2026.7.29";
12
+
13
+ /**
14
+ * @param {string} [openclawHome]
15
+ */
16
+ export function openclawConfigPath(openclawHome = join(homedir(), ".openclaw")) {
17
+ return join(openclawHome, "openclaw.json");
18
+ }
19
+
20
+ /**
21
+ * Ensure mqtt + mqtt-tools are in plugins.enabled.
22
+ * @param {object} cfg
23
+ */
24
+ export function ensureMqttPlugins(cfg) {
25
+ const out = { ...cfg };
26
+ const plugins = { ...(out.plugins ?? {}) };
27
+ const enabled = Array.isArray(plugins.enabled) ? [...plugins.enabled] : [];
28
+ for (const id of ["mqtt", "mqtt-tools"]) {
29
+ if (!enabled.includes(id)) enabled.push(id);
30
+ }
31
+ plugins.enabled = enabled;
32
+ out.plugins = plugins;
33
+ return out;
34
+ }
35
+
36
+ /**
37
+ * Wire channels.mqtt into openclaw.json.
38
+ *
39
+ * @param {{
40
+ * botId: string,
41
+ * operatorId: string,
42
+ * network: { rpc: string, registry: string|null, brokerUrl?: string, mqttAuthServiceUrl?: string },
43
+ * keyPath?: string|null,
44
+ * openclawHome?: string,
45
+ * }} opts
46
+ */
47
+ export function wireOpenClawMqtt(opts) {
48
+ const openclawHome = opts.openclawHome ?? join(homedir(), ".openclaw");
49
+ const cfgPath = openclawConfigPath(openclawHome);
50
+ mkdirSync(openclawHome, { recursive: true });
51
+
52
+ const mqtt = harnessSnippet({
53
+ botId: opts.botId,
54
+ operatorId: opts.operatorId,
55
+ network: opts.network,
56
+ });
57
+ if (!mqtt.registryAddress) {
58
+ mqtt.registryAddress = "0x0000000000000000000000000000000000000000";
59
+ }
60
+ if (opts.keyPath) {
61
+ mqtt.privateKeyFile = opts.keyPath;
62
+ }
63
+
64
+ let created = false;
65
+ let cfg;
66
+ if (!existsSync(cfgPath)) {
67
+ created = true;
68
+ cfg = {
69
+ plugins: { enabled: ["mqtt", "mqtt-tools"] },
70
+ channels: { mqtt },
71
+ };
72
+ } else {
73
+ cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
74
+ cfg = ensureMqttPlugins(cfg);
75
+ const channels = { ...(cfg.channels ?? {}) };
76
+ const prev = channels.mqtt && typeof channels.mqtt === "object" ? channels.mqtt : {};
77
+ channels.mqtt = { ...prev, ...mqtt };
78
+ cfg.channels = channels;
79
+ }
80
+
81
+ writeFileSync(cfgPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
82
+ return {
83
+ created,
84
+ path: cfgPath,
85
+ channelsMqtt: cfg.channels.mqtt,
86
+ pluginPin: OPENCLAW_PLUGIN_PIN,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Human checklist lines after wiring.
92
+ * @param {{ botId: string, channelsMqtt: object, pluginPin: string }} opts
93
+ */
94
+ export function hubConnectChecklist(opts) {
95
+ const pin = opts.pluginPin ?? OPENCLAW_PLUGIN_PIN;
96
+ return [
97
+ `openclaw plugins install @clanker-chain/mqtt-channel-plugin@${pin}`,
98
+ `openclaw plugins install @clanker-chain/mqtt-tools@${pin}`,
99
+ "Enable plugin ids mqtt + mqtt-tools (already set in openclaw.json if we wired it)",
100
+ `CONNECT broker: ${opts.channelsMqtt?.brokerUrl ?? "mqtts://mqtt.clanker-chain.com:8883"}`,
101
+ `Ask hub operator to allow bot_id "${opts.botId}" in france dmPolicy / allowFrom`,
102
+ "DM openclaw.france.prod-1 to smoke the mesh",
103
+ ];
104
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Consumer-path operator key helpers (generate local op.key).
3
+ * Never logs private keys.
4
+ */
5
+
6
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
7
+ import { dirname } from "node:path";
8
+ import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
9
+ import { defaultOperatorKeyPath } from "./foundry.mjs";
10
+
11
+ export { defaultOperatorKeyPath };
12
+
13
+ /** Coinbase Developer Platform faucet UI (Base Sepolia). */
14
+ export const BASE_SEPOLIA_FAUCET_URL =
15
+ "https://portal.cdp.coinbase.com/products/faucet";
16
+
17
+ /**
18
+ * Generate a new secp256k1 key and write it to destPath (mode 0o600).
19
+ * Does not print the key.
20
+ *
21
+ * @param {string} destPath
22
+ * @param {{ force?: boolean }} [opts]
23
+ * @returns {{ path: string, address: string }}
24
+ */
25
+ export function generateOperatorKeyFile(destPath, opts = {}) {
26
+ if (existsSync(destPath) && !opts.force) {
27
+ throw new Error(
28
+ `Key file already exists: ${destPath} (pass --force to overwrite)`,
29
+ );
30
+ }
31
+ const key = generatePrivateKey();
32
+ const address = privateKeyToAccount(key).address;
33
+ mkdirSync(dirname(destPath), { recursive: true });
34
+ writeFileSync(destPath, `${key}\n`, { mode: 0o600 });
35
+ return { path: destPath, address };
36
+ }
37
+
38
+ /**
39
+ * Plain-language next steps after creating an operator key on Sepolia.
40
+ * @param {{ address: string, label?: string }} opts
41
+ */
42
+ export function consumerFundHints(opts) {
43
+ const mintLabel = opts.label ? ` ${opts.label}` : "";
44
+ return [
45
+ `Your operator address is ${opts.address} — fund it with Base Sepolia ETH: ${BASE_SEPOLIA_FAUCET_URL}`,
46
+ "clanker doctor",
47
+ `clanker operator mint${mintLabel} --yes`,
48
+ "clanker bot mint <bot_label> --yes",
49
+ ];
50
+ }
package/lib/setup.mjs CHANGED
@@ -28,7 +28,18 @@ import {
28
28
  detectSetupHints,
29
29
  formatSetupDetectTable,
30
30
  } from "./setup-detect.mjs";
31
+ import {
32
+ defaultOperatorKeyPath,
33
+ exportFoundryKey,
34
+ resolveFoundryAddress,
35
+ } from "./foundry.mjs";
36
+ import {
37
+ BASE_SEPOLIA_FAUCET_URL,
38
+ consumerFundHints,
39
+ generateOperatorKeyFile,
40
+ } from "./operator-key.mjs";
31
41
  import { c, nextHint } from "./ui.mjs";
42
+ import { join } from "node:path";
32
43
 
33
44
  /**
34
45
  * @param {string[]} argv
@@ -44,6 +55,9 @@ export function parseSetupFlags(argv) {
44
55
  let yes = false;
45
56
  let skipKey = false;
46
57
  let skipChainCheck = false;
58
+ let foundryAccount = null;
59
+ let exportKey = false;
60
+ let generateKey = false;
47
61
 
48
62
  for (let i = 0; i < argv.length; i += 1) {
49
63
  const a = argv[i];
@@ -53,6 +67,9 @@ export function parseSetupFlags(argv) {
53
67
  else if (a === "--key-file" && argv[i + 1]) keyFile = argv[++i];
54
68
  else if (a === "--key-env" && argv[i + 1]) keyEnv = argv[++i];
55
69
  else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
70
+ else if (a === "--foundry-account" && argv[i + 1]) foundryAccount = argv[++i];
71
+ else if (a === "--export-key") exportKey = true;
72
+ else if (a === "--generate-key") generateKey = true;
56
73
  else if (a === "--force") force = true;
57
74
  else if (a === "--yes" || a === "-y") yes = true;
58
75
  else if (a === "--skip-key") skipKey = true;
@@ -70,6 +87,9 @@ export function parseSetupFlags(argv) {
70
87
  yes,
71
88
  skipKey,
72
89
  skipChainCheck,
90
+ foundryAccount,
91
+ exportKey,
92
+ generateKey,
73
93
  };
74
94
  }
75
95
 
@@ -191,21 +211,61 @@ export async function runSetupNonInteractive(argv, opts = {}) {
191
211
  if (!flags.preset || !PRESETS[flags.preset]) {
192
212
  throw new Error(
193
213
  "Non-interactive setup requires --preset sepolia|local (stdin is not a TTY). " +
194
- "Also pass --operator <label> and --address 0x…",
214
+ "Also pass --operator <label> and --generate-key (or --address / --key-file)",
195
215
  );
196
216
  }
197
217
  if (!flags.operator) {
198
218
  throw new Error("Non-interactive setup requires --operator <label>");
199
219
  }
200
- if (!flags.address && !flags.keyFile && !env.OPERATOR_PRIVATE_KEY) {
201
- throw new Error(
202
- "Non-interactive setup requires --address 0x…, --key-file, or OPERATOR_PRIVATE_KEY",
203
- );
204
- }
205
220
 
206
221
  let address = flags.address;
207
- if (!address && flags.keyFile) address = addressFromKeyFile(flags.keyFile);
222
+ let keyFile = flags.keyFile;
223
+ const castOpts = {
224
+ castBin: opts.castBin,
225
+ spawn: opts.spawn,
226
+ inheritStdio: false,
227
+ };
228
+
229
+ if (flags.generateKey) {
230
+ if (flags.skipKey) {
231
+ throw new Error("Cannot combine --generate-key with --skip-key");
232
+ }
233
+ const dest = keyFile || defaultOperatorKeyPath(home);
234
+ const created = generateOperatorKeyFile(dest, {
235
+ force: flags.force,
236
+ });
237
+ keyFile = created.path;
238
+ if (address && getAddress(address) !== getAddress(created.address)) {
239
+ throw new Error(
240
+ `Generated key address ${created.address} does not match --address ${getAddress(address)}`,
241
+ );
242
+ }
243
+ address = created.address;
244
+ }
245
+
246
+ if (flags.foundryAccount) {
247
+ if (!address) {
248
+ address = resolveFoundryAddress(flags.foundryAccount, castOpts);
249
+ }
250
+ if (flags.exportKey && !keyFile && !flags.skipKey) {
251
+ const dest = defaultOperatorKeyPath(home);
252
+ const exported = exportFoundryKey(flags.foundryAccount, dest, castOpts);
253
+ keyFile = exported.path;
254
+ if (getAddress(exported.address) !== getAddress(address)) {
255
+ throw new Error(
256
+ `Exported Foundry key address ${exported.address} does not match ${address}`,
257
+ );
258
+ }
259
+ }
260
+ }
261
+
262
+ if (!address && keyFile) address = addressFromKeyFile(keyFile);
208
263
  if (!address && env.OPERATOR_PRIVATE_KEY) address = addressFromEnv(env);
264
+ if (!address) {
265
+ throw new Error(
266
+ "Non-interactive setup requires --generate-key, --address 0x…, --key-file, OPERATOR_PRIVATE_KEY, or --foundry-account",
267
+ );
268
+ }
209
269
 
210
270
  const preset = PRESETS[flags.preset];
211
271
  let fromBlock = flags.fromBlock;
@@ -225,14 +285,14 @@ export async function runSetupNonInteractive(argv, opts = {}) {
225
285
  });
226
286
 
227
287
  const key = buildKeyPointer({
228
- keyFile: flags.keyFile,
288
+ keyFile,
229
289
  keyEnv: flags.keyEnv,
230
290
  skipKey: flags.skipKey,
231
291
  env,
232
292
  });
233
293
 
234
- if (flags.keyFile && flags.address) {
235
- const fromKey = getAddress(addressFromKeyFile(flags.keyFile));
294
+ if (keyFile && flags.address) {
295
+ const fromKey = getAddress(addressFromKeyFile(keyFile));
236
296
  if (fromKey !== getAddress(flags.address)) {
237
297
  throw new Error(
238
298
  `Key file address ${fromKey} does not match --address ${getAddress(flags.address)}`,
@@ -270,9 +330,13 @@ export async function runSetupInteractive(argv, opts = {}) {
270
330
  });
271
331
 
272
332
  clack.intro(c.bold("clanker setup"));
273
- clack.log.step("Creates ~/.clanker/config.json (network) and operator.json (identity).");
333
+ clack.log.step(
334
+ "Sets up your operator identity (org account) and network for OpenClaw bots.",
335
+ );
274
336
  clack.log.message(
275
- c.dim("whoami works from owner without a key; mint/revoke need a key pointer later."),
337
+ c.dim(
338
+ "New here? Create a key file — no wallet app or Foundry required. Mint needs a little test ETH later.",
339
+ ),
276
340
  );
277
341
  clack.log.message(c.dim(`Profile: ${home}`));
278
342
  console.log("");
@@ -330,6 +394,29 @@ export async function runSetupInteractive(argv, opts = {}) {
330
394
  }
331
395
 
332
396
  let address = flags.address ?? null;
397
+ let foundryAccountUsed = null;
398
+ let generatedKeyFile = null;
399
+ if (!address && flags.generateKey) {
400
+ const dest = flags.keyFile || defaultOperatorKeyPath(home);
401
+ let forceGen = flags.force;
402
+ if (existsSync(dest) && !forceGen) {
403
+ forceGen = cancelIf(
404
+ await clack.confirm({
405
+ message: `Overwrite existing ${dest}?`,
406
+ initialValue: false,
407
+ }),
408
+ );
409
+ if (!forceGen) {
410
+ clack.cancel("Aborted.");
411
+ process.exit(0);
412
+ }
413
+ }
414
+ const created = generateOperatorKeyFile(dest, { force: true });
415
+ generatedKeyFile = created.path;
416
+ address = created.address;
417
+ clack.log.success(`Created operator key at ${created.path}`);
418
+ clack.log.info(`Your operator address: ${created.address}`);
419
+ }
333
420
  if (!address && flags.keyFile) {
334
421
  address = addressFromKeyFile(flags.keyFile);
335
422
  clack.log.info(`Address from --key-file: ${address}`);
@@ -352,23 +439,86 @@ export async function runSetupInteractive(argv, opts = {}) {
352
439
  );
353
440
  if (useOp) address = hints.operator.owner;
354
441
  }
355
- if (!address && hints.foundryAccounts.length) {
442
+ if (!address) {
356
443
  const options = [
357
- ...hints.foundryAccounts.map((n) => ({
358
- value: n,
359
- label: n,
360
- hint: "Foundry keystore — paste 0x next",
361
- })),
362
- { value: "__paste__", label: "Paste an address…", hint: "0x…" },
444
+ {
445
+ value: "__generate__",
446
+ label: "Create a new operator key for me",
447
+ hint: "writes ~/.clanker/op.key (recommended)",
448
+ },
449
+ {
450
+ value: "__keyfile__",
451
+ label: "Use an existing key file…",
452
+ hint: "path to a 0x private key file",
453
+ },
363
454
  ];
455
+ if (hints.foundryAccounts.length) {
456
+ for (const n of hints.foundryAccounts) {
457
+ options.push({
458
+ value: n,
459
+ label: `Foundry: ${n}`,
460
+ hint: "advanced — cast wallet",
461
+ });
462
+ }
463
+ } else {
464
+ options.push({
465
+ value: "__foundry_missing__",
466
+ label: "Foundry account…",
467
+ hint: "advanced — install Foundry first",
468
+ });
469
+ }
470
+ options.push({
471
+ value: "__paste__",
472
+ label: "Paste an address…",
473
+ hint: "read-only unless you add a key later",
474
+ });
475
+
364
476
  const pick = cancelIf(
365
477
  await clack.select({
366
- message: "Operator owner source",
478
+ message: "How do you want to set your operator identity?",
367
479
  options,
368
- initialValue: hints.foundryAccounts[0],
480
+ initialValue: "__generate__",
369
481
  }),
370
482
  );
371
- if (pick === "__paste__") {
483
+
484
+ if (pick === "__generate__") {
485
+ const dest = defaultOperatorKeyPath(home);
486
+ let forceGen = flags.force;
487
+ if (existsSync(dest) && !forceGen) {
488
+ forceGen = cancelIf(
489
+ await clack.confirm({
490
+ message: `Overwrite existing ${dest}?`,
491
+ initialValue: false,
492
+ }),
493
+ );
494
+ if (!forceGen) {
495
+ clack.cancel("Aborted.");
496
+ process.exit(0);
497
+ }
498
+ }
499
+ const created = generateOperatorKeyFile(dest, { force: true });
500
+ generatedKeyFile = created.path;
501
+ address = created.address;
502
+ clack.log.success(`Created operator key at ${created.path}`);
503
+ clack.log.info(`Your operator address: ${created.address}`);
504
+ } else if (pick === "__keyfile__") {
505
+ const path = cancelIf(
506
+ await clack.text({
507
+ message: "Path to operator key file",
508
+ placeholder: join(home, "op.key"),
509
+ validate: (v) =>
510
+ v && String(v).trim() && existsSync(String(v).trim())
511
+ ? undefined
512
+ : "File not found",
513
+ }),
514
+ );
515
+ generatedKeyFile = String(path).trim();
516
+ address = addressFromKeyFile(generatedKeyFile);
517
+ clack.log.info(`Address from key file: ${address}`);
518
+ } else if (pick === "__foundry_missing__") {
519
+ clack.log.warn(
520
+ "Foundry (cast) is not available. Install https://book.getfoundry.sh/ or choose Create a new operator key.",
521
+ );
372
522
  address = cancelIf(
373
523
  await clack.text({
374
524
  message: "Operator owner address",
@@ -377,27 +527,38 @@ export async function runSetupInteractive(argv, opts = {}) {
377
527
  /^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
378
528
  }),
379
529
  );
380
- } else {
530
+ } else if (pick === "__paste__") {
381
531
  address = cancelIf(
382
532
  await clack.text({
383
- message: `Paste 0x address for Foundry account "${pick}"`,
533
+ message: "Operator owner address",
384
534
  placeholder: "0x…",
385
535
  validate: (v) =>
386
536
  /^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
387
537
  }),
388
538
  );
539
+ } else {
540
+ foundryAccountUsed = pick;
541
+ try {
542
+ clack.log.step(`Resolving address for Foundry account "${pick}" (unlock if prompted)…`);
543
+ address = resolveFoundryAddress(pick, {
544
+ castBin: opts.castBin,
545
+ spawn: opts.spawn,
546
+ inheritStdio: true,
547
+ });
548
+ clack.log.success(`Foundry address: ${address}`);
549
+ } catch (err) {
550
+ clack.log.warn(err.message);
551
+ address = cancelIf(
552
+ await clack.text({
553
+ message: `Paste 0x address for "${pick}"`,
554
+ placeholder: "0x…",
555
+ validate: (v) =>
556
+ /^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
557
+ }),
558
+ );
559
+ }
389
560
  }
390
561
  }
391
- if (!address) {
392
- address = cancelIf(
393
- await clack.text({
394
- message: "Operator owner address",
395
- placeholder: "0x…",
396
- validate: (v) =>
397
- /^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
398
- }),
399
- );
400
- }
401
562
  address = getAddress(address);
402
563
 
403
564
  let label = flags.operator || hints.operator?.label || null;
@@ -435,11 +596,36 @@ export async function runSetupInteractive(argv, opts = {}) {
435
596
  throw err;
436
597
  }
437
598
 
438
- let keyFile = flags.keyFile;
599
+ let keyFile = flags.keyFile || generatedKeyFile;
439
600
  let keyEnv = flags.keyEnv;
440
601
  let skipKey = flags.skipKey;
441
602
  if (!skipKey && !keyFile && !keyEnv) {
442
- if (hints.hasOperatorPrivateKeyEnv) {
603
+ if (foundryAccountUsed) {
604
+ const doExport = cancelIf(
605
+ await clack.confirm({
606
+ message: `Export Foundry key for "${foundryAccountUsed}" to ~/.clanker/op.key for minting?`,
607
+ initialValue: true,
608
+ }),
609
+ );
610
+ if (doExport) {
611
+ const dest = defaultOperatorKeyPath(home);
612
+ clack.log.step("Exporting key via cast (unlock if prompted; key is not printed)…");
613
+ const exported = exportFoundryKey(foundryAccountUsed, dest, {
614
+ castBin: opts.castBin,
615
+ spawn: opts.spawn,
616
+ inheritStdio: true,
617
+ });
618
+ if (getAddress(exported.address) !== address) {
619
+ throw new Error(
620
+ `Exported key address ${exported.address} does not match owner ${address}`,
621
+ );
622
+ }
623
+ keyFile = exported.path;
624
+ clack.log.success(`Wrote ${keyFile} (mode 600)`);
625
+ } else {
626
+ skipKey = true;
627
+ }
628
+ } else if (hints.hasOperatorPrivateKeyEnv) {
443
629
  const use = cancelIf(
444
630
  await clack.confirm({
445
631
  message: "Store OPERATOR_PRIVATE_KEY pointer for signing?",
@@ -452,7 +638,7 @@ export async function runSetupInteractive(argv, opts = {}) {
452
638
  const path = cancelIf(
453
639
  await clack.text({
454
640
  message: "Operator key file path (Enter = read-only)",
455
- placeholder: "~/.clanker/op.key",
641
+ placeholder: join(home, "op.key"),
456
642
  }),
457
643
  );
458
644
  if (path && String(path).trim()) keyFile = String(path).trim();
@@ -506,7 +692,16 @@ export async function runSetupInteractive(argv, opts = {}) {
506
692
  if (!key) {
507
693
  nextHint([
508
694
  "clanker whoami",
509
- "clanker setup --key-file ~/.clanker/op.key --force # when you need mint",
695
+ "clanker setup choose Create a new operator key when you need mint",
696
+ ]);
697
+ } else if (preset === "sepolia" && generatedKeyFile) {
698
+ nextHint(consumerFundHints({ address, label }));
699
+ } else if (preset === "sepolia") {
700
+ nextHint([
701
+ `If this address needs test ETH: ${BASE_SEPOLIA_FAUCET_URL}`,
702
+ "clanker doctor",
703
+ "clanker whoami",
704
+ `clanker bot mint <label>`,
510
705
  ]);
511
706
  } else {
512
707
  nextHint(["clanker whoami", `clanker bot mint <label>`]);
@@ -520,23 +715,20 @@ export async function runSetupInteractive(argv, opts = {}) {
520
715
  export async function runSetup(argv, opts = {}) {
521
716
  const flags = parseSetupFlags(argv);
522
717
  const isTTY = opts.isTTY ?? Boolean(input.isTTY);
718
+ const hasIdentitySource = Boolean(
719
+ flags.address ||
720
+ flags.keyFile ||
721
+ flags.generateKey ||
722
+ flags.foundryAccount ||
723
+ opts.env?.OPERATOR_PRIVATE_KEY ||
724
+ process.env.OPERATOR_PRIVATE_KEY,
725
+ );
523
726
 
524
- if (
525
- !isTTY ||
526
- (flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile))
527
- ) {
727
+ if (!isTTY || (flags.yes && flags.preset && flags.operator && hasIdentitySource)) {
528
728
  if (!isTTY && !(flags.preset && flags.operator)) {
529
729
  return runSetupNonInteractive(argv, opts);
530
730
  }
531
- if (
532
- flags.yes &&
533
- flags.preset &&
534
- flags.operator &&
535
- (flags.address ||
536
- flags.keyFile ||
537
- opts.env?.OPERATOR_PRIVATE_KEY ||
538
- process.env.OPERATOR_PRIVATE_KEY)
539
- ) {
731
+ if (flags.yes && flags.preset && flags.operator && hasIdentitySource) {
540
732
  return runSetupNonInteractive(argv, opts);
541
733
  }
542
734
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clanker-chain/clanker-cli",
3
- "version": "2026.9.7-4",
3
+ "version": "2026.9.8-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": {