@clanker-chain/clanker-cli 2026.9.7 → 2026.9.8-2

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,
@@ -23,7 +22,21 @@ import {
23
22
  readBot,
24
23
  resolvePreferredOperator,
25
24
  } from "../lib/identity-query.mjs";
26
- import { resolveForRead, resolveOperatorKey } from "../lib/resolve.mjs";
25
+ import { resolveForRead, resolveOperatorKey, resolveReadIdentity } from "../lib/resolve.mjs";
26
+ import { runSetup } from "../lib/setup.mjs";
27
+ import { runDoctor } from "../lib/doctor.mjs";
28
+ import {
29
+ botIdentityCard,
30
+ botIdentityJson,
31
+ hubConnectChecklist,
32
+ wireOpenClawMqtt,
33
+ } from "../lib/openclaw-wire.mjs";
34
+ import {
35
+ c,
36
+ confirmPlan,
37
+ exitCliError,
38
+ nextHint,
39
+ } from "../lib/ui.mjs";
27
40
 
28
41
  function resolveFoundryBinary(name) {
29
42
  const home = os.homedir();
@@ -144,16 +157,18 @@ function usage() {
144
157
  console.log(`clanker - clanker-chain helper CLI
145
158
 
146
159
  Usage:
160
+ clanker setup [--preset sepolia|local] [--operator <label>] [--address 0x…] [--key-file path] [--force]
161
+ clanker doctor [--json]
147
162
  clanker init --preset sepolia|local [--force]
148
- clanker whoami [--json] [--operator <label>] [--address 0x…]
149
- clanker operator mint <label> [--json]
150
- clanker operator transfer propose <label> <newOwner>
151
- clanker operator transfer accept <label>
152
- clanker bot mint <label> [operator] [--json]
163
+ clanker whoami [--json] [--operator <label>] [--address 0x…] [--with-bots]
164
+ clanker operator mint <label> [--json] [--yes]
165
+ clanker operator transfer propose <label> <newOwner> [--yes]
166
+ clanker operator transfer accept <label> [--yes]
167
+ clanker bot mint <label> [operator] [--json] [--yes]
153
168
  clanker bots [--json] [--operator <label>] [--address 0x…]
154
169
  clanker bot status <label> [--json]
155
- clanker bot revoke <label> [--json]
156
- clanker bot rotate <label> <newKeyAddress> [--json]
170
+ clanker bot revoke <label> [--json] [--yes]
171
+ clanker bot rotate <label> <newKeyAddress> [--json] [--yes]
157
172
  clanker init-openclaw
158
173
  clanker chain up|deploy|mint-operator|mint-bot|rotate-bot-key|revoke-bot ...
159
174
  clanker check mqtt <bot_id> <operator_id>
@@ -161,12 +176,12 @@ Usage:
161
176
 
162
177
  Profile:
163
178
  ~/.clanker/config.json network preset (registry, RPC, broker URLs)
164
- ~/.clanker/operator.json label + owner + key pointer (never raw hex)
179
+ ~/.clanker/operator.json label + owner + optional key pointer (never raw hex)
165
180
  ~/.clanker/keys/ bot keys (also written to ~/.openclaw/keys/)
166
181
 
167
- Key resolution (mutating commands):
168
- --key > --key-file > OPERATOR_PRIVATE_KEY > profile keyFile > profile env
169
- Anvil account #0 is allowed only on localhost RPC.
182
+ Humans: \`clanker setup\` then \`clanker doctor\` / \`whoami\`.
183
+ Mutates print a plan and confirm unless --yes or --json.
184
+ whoami is fast by default; pass --with-bots to enrich child bots (or use \`clanker bots\`).
170
185
 
171
186
  See docs/operator-cli.md.
172
187
  `);
@@ -213,18 +228,20 @@ async function cmdWhoami(argv) {
213
228
  let address;
214
229
  let source;
215
230
  let network;
216
- const addressFlag = flagValue(argv, "--address");
217
- if (addressFlag) {
218
- network = resolveForRead(argv);
219
- address = getAddress(addressFlag);
220
- source = "--address";
221
- } else {
222
- const resolved = resolveOperatorKey(argv);
231
+ try {
232
+ const resolved = resolveReadIdentity(argv);
223
233
  address = resolved.address;
224
234
  source = resolved.source;
225
235
  network = resolved.network;
236
+ } catch (err) {
237
+ exitCliError({
238
+ error: err.message,
239
+ because: "whoami needs an owner address from --address, a key, or operator.json",
240
+ try: ["clanker setup", "clanker doctor", "clanker whoami --address 0x…"],
241
+ });
226
242
  }
227
243
 
244
+ const withBots = hasFlag(argv, "--with-bots");
228
245
  const pub = await publicClientFromRpc(network.rpc);
229
246
  const preferred =
230
247
  flagValue(argv, "--operator") ?? loadOperator(network.home)?.label ?? null;
@@ -237,9 +254,14 @@ async function cmdWhoami(argv) {
237
254
  owner: getAddress(address),
238
255
  });
239
256
  if (resolved.error) {
240
- const err = new Error(resolved.error);
241
- err.candidates = resolved.candidates;
242
- throw err;
257
+ exitCliError({
258
+ error: resolved.error,
259
+ because: "preferred operator label did not match this owner on-chain",
260
+ try: [
261
+ "clanker whoami --address 0x…",
262
+ "clanker setup --force # fix label/owner",
263
+ ],
264
+ });
243
265
  }
244
266
  selected = [resolved.operator];
245
267
  } else {
@@ -250,12 +272,14 @@ async function cmdWhoami(argv) {
250
272
  });
251
273
  }
252
274
 
253
- for (const op of selected) {
254
- op.bots = await findBotsByOperator(pub, {
255
- registry: network.registry,
256
- operatorId: op.id,
257
- fromBlock: network.fromBlock,
258
- });
275
+ if (withBots) {
276
+ for (const op of selected) {
277
+ op.bots = await findBotsByOperator(pub, {
278
+ registry: network.registry,
279
+ operatorId: op.id,
280
+ fromBlock: network.fromBlock,
281
+ });
282
+ }
259
283
  }
260
284
 
261
285
  const data = {
@@ -271,31 +295,38 @@ async function cmdWhoami(argv) {
271
295
  active: o.active,
272
296
  registeredAt: o.registeredAt.toString(),
273
297
  revokedAt: o.revokedAt.toString(),
274
- bots: (o.bots ?? []).map((b) => ({
275
- label: b.label,
276
- botKey: b.botKey,
277
- active: b.active,
278
- registeredAt: b.registeredAt.toString(),
279
- revokedAt: b.revokedAt.toString(),
280
- })),
298
+ bots: withBots
299
+ ? (o.bots ?? []).map((b) => ({
300
+ label: b.label,
301
+ botKey: b.botKey,
302
+ active: b.active,
303
+ registeredAt: b.registeredAt.toString(),
304
+ revokedAt: b.revokedAt.toString(),
305
+ }))
306
+ : [],
281
307
  })),
282
308
  };
283
309
 
284
310
  if (hasFlag(argv, "--json")) printJson(data);
285
- else printHumanWhoami(data);
311
+ else {
312
+ printHumanWhoami(data);
313
+ if (!withBots && data.operators.length) {
314
+ console.log(c.dim("(bots omitted — pass --with-bots or run clanker bots)"));
315
+ }
316
+ nextHint(["clanker bots", "clanker doctor"]);
317
+ }
286
318
  }
287
319
 
288
320
  async function cmdBots(argv) {
289
321
  let address;
290
322
  let network;
291
- const addressFlag = flagValue(argv, "--address");
292
- if (addressFlag) {
293
- network = resolveForRead(argv);
294
- address = getAddress(addressFlag);
295
- } else {
296
- const resolved = resolveOperatorKey(argv);
323
+ try {
324
+ const resolved = resolveReadIdentity(argv);
297
325
  address = resolved.address;
298
326
  network = resolved.network;
327
+ } catch (err) {
328
+ console.error(err.message);
329
+ process.exit(1);
299
330
  }
300
331
  const { label, pub } = await resolveOperatorLabel(argv, address, network);
301
332
  const bots = await findBotsByOperator(pub, {
@@ -357,6 +388,27 @@ async function main() {
357
388
 
358
389
  const repoRoot = findRepoRoot();
359
390
 
391
+ if (cmd === "setup") {
392
+ try {
393
+ await runSetup(rest);
394
+ } catch (err) {
395
+ exitCliError({
396
+ error: err.message,
397
+ because: "setup could not write a valid local profile",
398
+ try: [
399
+ "clanker setup --preset sepolia --operator org.you --address 0x… --yes --force",
400
+ "clanker doctor",
401
+ ],
402
+ });
403
+ }
404
+ return;
405
+ }
406
+
407
+ if (cmd === "doctor") {
408
+ const { exitCode } = await runDoctor(rest);
409
+ process.exit(exitCode);
410
+ }
411
+
360
412
  if (cmd === "init") {
361
413
  const preset = flagValue(rest, "--preset") ?? rest.find((a) => !a.startsWith("--"));
362
414
  if (!preset || preset === "init") {
@@ -403,13 +455,38 @@ async function main() {
403
455
  console.error("Usage: clanker operator mint <label>");
404
456
  process.exit(1);
405
457
  }
458
+ let planRows;
459
+ try {
460
+ const preview = resolveOperatorKey(flags);
461
+ planRows = [
462
+ ["action", "registerOperator"],
463
+ ["label", label],
464
+ ["owner", preview.address],
465
+ ["rpc", preview.network.rpc],
466
+ ["registry", preview.network.registry ?? "(none)"],
467
+ ["key", preview.source],
468
+ ];
469
+ } catch (err) {
470
+ exitCliError({
471
+ error: err.message,
472
+ because: "operator mint needs a signing key on this network",
473
+ try: [
474
+ "export OPERATOR_PRIVATE_KEY=0x…",
475
+ "clanker operator mint " + label + " --key-file ~/.clanker/op.key --yes",
476
+ "clanker doctor",
477
+ ],
478
+ });
479
+ }
480
+ const ok = await confirmPlan(flags, planRows, `Mint operator ${label}?`);
481
+ if (!ok) process.exit(0);
406
482
  const result = await chainMintOperator(label, flags);
407
483
  if (hasFlag(flags, "--json") || hasFlag(opArgv, "--json")) printJson(result);
408
484
  else {
409
- console.log(`Minted operator ${label}`);
485
+ console.log(c.green(`Minted operator ${label}`));
410
486
  console.log(`owner: ${result.owner}`);
411
487
  console.log(`tx: ${result.tx}`);
412
488
  console.log(`Wrote ~/.clanker/operator.json`);
489
+ nextHint([`clanker bot mint <bot_label>`, "clanker whoami"]);
413
490
  }
414
491
  return;
415
492
  }
@@ -421,9 +498,23 @@ async function main() {
421
498
  console.error("Usage: clanker operator transfer propose <label> <newOwner>");
422
499
  process.exit(1);
423
500
  }
501
+ const ok = await confirmPlan(
502
+ flags,
503
+ [
504
+ ["action", "proposeOperatorTransfer"],
505
+ ["label", label],
506
+ ["newOwner", getAddress(newOwner)],
507
+ ],
508
+ `Propose transfer of ${label}?`,
509
+ );
510
+ if (!ok) process.exit(0);
424
511
  const result = await chainProposeOperatorTransfer(label, getAddress(newOwner), flags);
425
512
  if (hasFlag(flags, "--json")) printJson(result);
426
- else console.log(`Proposed transfer of ${label} → ${newOwner}\ntx: ${result.tx}`);
513
+ else {
514
+ console.log(c.green(`Proposed transfer of ${label} → ${newOwner}`));
515
+ console.log(`tx: ${result.tx}`);
516
+ nextHint([`clanker operator transfer accept ${label} # as new owner`]);
517
+ }
427
518
  return;
428
519
  }
429
520
  if (action === "accept") {
@@ -431,9 +522,23 @@ async function main() {
431
522
  console.error("Usage: clanker operator transfer accept <label>");
432
523
  process.exit(1);
433
524
  }
525
+ const ok = await confirmPlan(
526
+ flags,
527
+ [
528
+ ["action", "acceptOperatorTransfer"],
529
+ ["label", label],
530
+ ],
531
+ `Accept transfer of ${label}?`,
532
+ );
533
+ if (!ok) process.exit(0);
434
534
  const result = await chainAcceptOperatorTransfer(label, flags);
435
535
  if (hasFlag(flags, "--json")) printJson(result);
436
- else console.log(`Accepted transfer of ${label}\nowner: ${result.owner}\ntx: ${result.tx}`);
536
+ else {
537
+ console.log(c.green(`Accepted transfer of ${label}`));
538
+ console.log(`owner: ${result.owner}`);
539
+ console.log(`tx: ${result.tx}`);
540
+ nextHint(["clanker whoami", "clanker bots"]);
541
+ }
437
542
  return;
438
543
  }
439
544
  console.error("Usage: clanker operator transfer propose|accept ...");
@@ -473,20 +578,76 @@ async function main() {
473
578
  }
474
579
  let operatorLabel = operatorArg ?? flagValue(flags, "--operator") ?? null;
475
580
  if (!operatorLabel) {
476
- const { address, network } = resolveOperatorKey(flags);
477
- const inferred = await resolveOperatorLabel(flags, address, network);
478
- operatorLabel = inferred.label;
581
+ try {
582
+ const { address, network } = resolveOperatorKey(flags);
583
+ const inferred = await resolveOperatorLabel(flags, address, network);
584
+ operatorLabel = inferred.label;
585
+ } catch (err) {
586
+ exitCliError({
587
+ error: err.message,
588
+ because: "bot mint needs an operator label or a resolvable signing key",
589
+ try: [
590
+ `clanker bot mint ${botLabel} <operator_label> --yes`,
591
+ "clanker setup",
592
+ ],
593
+ });
594
+ }
479
595
  }
596
+ const ok = await confirmPlan(
597
+ flags,
598
+ [
599
+ ["action", "registerBot"],
600
+ ["bot", botLabel],
601
+ ["operator", operatorLabel],
602
+ ],
603
+ `Mint bot ${botLabel} under ${operatorLabel}?`,
604
+ );
605
+ if (!ok) process.exit(0);
480
606
  const result = await chainMintBot(botLabel, operatorLabel, flags);
481
- if (hasFlag(flags, "--json")) printJson(result);
482
- else {
483
- console.log(`Minted bot ${botLabel} under ${operatorLabel}`);
607
+ const mqtt = result.channels_mqtt ?? {};
608
+ const wire = wireOpenClawMqtt({
609
+ botId: botLabel,
610
+ operatorId: operatorLabel,
611
+ network: {
612
+ rpc: mqtt.chainRpcUrl,
613
+ registry: mqtt.registryAddress,
614
+ brokerUrl: mqtt.brokerUrl,
615
+ mqttAuthServiceUrl: mqtt.mqttAuthServiceUrl,
616
+ },
617
+ keyPath: result.key_path,
618
+ });
619
+ if (hasFlag(flags, "--json")) {
620
+ printJson({
621
+ ...result,
622
+ openclaw: wire,
623
+ bot_identity: botIdentityJson({
624
+ botId: botLabel,
625
+ keyPath: result.key_path,
626
+ openclawConfigPath: wire.path,
627
+ }),
628
+ });
629
+ } else {
630
+ console.log(c.green(`Minted bot ${botLabel} under ${operatorLabel}`));
484
631
  console.log(`botKey: ${result.bot_key}`);
485
- console.log(`key file: ${result.key_path}`);
486
- console.log(`also: ${result.clanker_key_path}`);
487
632
  console.log(`tx: ${result.tx}`);
488
- console.log("\nchannels.mqtt stub:");
489
- console.log(JSON.stringify(result.channels_mqtt, null, 2));
633
+ console.log(c.dim(`also: ${result.clanker_key_path}`));
634
+ console.log("");
635
+ for (const line of botIdentityCard({
636
+ botId: botLabel,
637
+ operatorId: operatorLabel,
638
+ keyPath: result.key_path,
639
+ openclawPath: wire.path,
640
+ created: wire.created,
641
+ })) {
642
+ console.log(line);
643
+ }
644
+ nextHint(
645
+ hubConnectChecklist({
646
+ botId: botLabel,
647
+ channelsMqtt: wire.channelsMqtt,
648
+ pluginPin: wire.pluginPin,
649
+ }),
650
+ );
490
651
  }
491
652
  return;
492
653
  }
@@ -497,9 +658,22 @@ async function main() {
497
658
  console.error("Usage: clanker bot revoke <label>");
498
659
  process.exit(1);
499
660
  }
661
+ const ok = await confirmPlan(
662
+ flags,
663
+ [
664
+ ["action", "revokeBot"],
665
+ ["bot", botLabel],
666
+ ],
667
+ `Revoke bot ${botLabel}?`,
668
+ );
669
+ if (!ok) process.exit(0);
500
670
  const result = await chainRevokeBot(botLabel, flags);
501
671
  if (hasFlag(flags, "--json")) printJson(result);
502
- else console.log(`Revoked ${botLabel}\ntx: ${result.tx}`);
672
+ else {
673
+ console.log(c.yellow(`Revoked ${botLabel}`));
674
+ console.log(`tx: ${result.tx}`);
675
+ nextHint(["clanker bots", "clanker bot status " + botLabel]);
676
+ }
503
677
  return;
504
678
  }
505
679
 
@@ -509,9 +683,23 @@ async function main() {
509
683
  console.error("Usage: clanker bot rotate <label> <newKeyAddress>");
510
684
  process.exit(1);
511
685
  }
686
+ const ok = await confirmPlan(
687
+ flags,
688
+ [
689
+ ["action", "rotateBotKey"],
690
+ ["bot", botLabel],
691
+ ["newKey", getAddress(newKey)],
692
+ ],
693
+ `Rotate key for ${botLabel}?`,
694
+ );
695
+ if (!ok) process.exit(0);
512
696
  const result = await chainRotateBotKey(botLabel, getAddress(newKey), flags);
513
697
  if (hasFlag(flags, "--json")) printJson(result);
514
- else console.log(`Rotated ${botLabel} → ${newKey}\ntx: ${result.tx}`);
698
+ else {
699
+ console.log(c.green(`Rotated ${botLabel} → ${newKey}`));
700
+ console.log(`tx: ${result.tx}`);
701
+ nextHint(["Update ~/.openclaw/keys/" + botLabel + ".key", "clanker bot status " + botLabel]);
702
+ }
515
703
  return;
516
704
  }
517
705
 
@@ -717,22 +905,6 @@ async function main() {
717
905
  }
718
906
 
719
907
  if (cmd === "init-openclaw") {
720
- const home = os.homedir();
721
- const openclawDir = join(home, ".openclaw");
722
- const cfgPath = join(openclawDir, "openclaw.json");
723
- if (!existsSync(openclawDir)) {
724
- mkdirSync(openclawDir, { recursive: true });
725
- }
726
-
727
- if (existsSync(cfgPath)) {
728
- console.log(`${cfgPath} already exists.`);
729
- console.log(
730
- 'Ensure plugins.enabled includes "mqtt" and "mqtt-tools", and channels.mqtt has botId, operatorId, brokerUrl, chainRpcUrl, registryAddress.',
731
- );
732
- console.log("See SETUP.md and docs/operator-cli.md.");
733
- process.exit(0);
734
- }
735
-
736
908
  const profile = loadConfig();
737
909
  const operator = loadOperator();
738
910
  // No profile: local defaults only — do not mix Sepolia registry with localhost MQTT.
@@ -750,27 +922,21 @@ async function main() {
750
922
  mqttAuthServiceUrl: "http://localhost:9090",
751
923
  };
752
924
 
753
- const mqtt = harnessSnippet({
925
+ const wire = wireOpenClawMqtt({
754
926
  botId: "openclaw.your-bot.local",
755
927
  operatorId: operator?.label ?? "org.openclaw.your-operator",
756
928
  network,
757
929
  });
758
- if (!mqtt.registryAddress) {
759
- mqtt.registryAddress = "0x0000000000000000000000000000000000000000";
760
- }
761
-
762
- const cfg = {
763
- plugins: {
764
- enabled: ["mqtt", "mqtt-tools"],
765
- },
766
- channels: {
767
- mqtt,
768
- },
769
- };
770
- writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
771
- console.log(`Created ${cfgPath} with mqtt + mqtt-tools from ${profile ? "active" : "default"} preset.`);
772
- console.log("Next: clanker bot mint <label>, then set channels.mqtt.botId / operatorId.");
773
- console.log("See docs/operator-cli.md and SETUP.md.");
930
+ console.log(
931
+ wire.created
932
+ ? `Created ${wire.path} with mqtt + mqtt-tools from ${profile ? "active" : "default"} preset.`
933
+ : `Updated channels.mqtt in ${wire.path} (plugins mqtt + mqtt-tools ensured).`,
934
+ );
935
+ nextHint([
936
+ "clanker bot mint <label>",
937
+ "Then channels.mqtt.botId / operatorId / privateKeyFile update automatically",
938
+ "See docs/operator-cli.md and docs/closed-beta-invite.md",
939
+ ]);
774
940
  process.exit(0);
775
941
  }
776
942