@clanker-chain/clanker-cli 2026.9.7 → 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,
@@ -23,7 +22,19 @@ 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
+ hubConnectChecklist,
30
+ wireOpenClawMqtt,
31
+ } from "../lib/openclaw-wire.mjs";
32
+ import {
33
+ c,
34
+ confirmPlan,
35
+ exitCliError,
36
+ nextHint,
37
+ } from "../lib/ui.mjs";
27
38
 
28
39
  function resolveFoundryBinary(name) {
29
40
  const home = os.homedir();
@@ -144,16 +155,18 @@ function usage() {
144
155
  console.log(`clanker - clanker-chain helper CLI
145
156
 
146
157
  Usage:
158
+ clanker setup [--preset sepolia|local] [--operator <label>] [--address 0x…] [--key-file path] [--force]
159
+ clanker doctor [--json]
147
160
  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]
161
+ clanker whoami [--json] [--operator <label>] [--address 0x…] [--with-bots]
162
+ clanker operator mint <label> [--json] [--yes]
163
+ clanker operator transfer propose <label> <newOwner> [--yes]
164
+ clanker operator transfer accept <label> [--yes]
165
+ clanker bot mint <label> [operator] [--json] [--yes]
153
166
  clanker bots [--json] [--operator <label>] [--address 0x…]
154
167
  clanker bot status <label> [--json]
155
- clanker bot revoke <label> [--json]
156
- clanker bot rotate <label> <newKeyAddress> [--json]
168
+ clanker bot revoke <label> [--json] [--yes]
169
+ clanker bot rotate <label> <newKeyAddress> [--json] [--yes]
157
170
  clanker init-openclaw
158
171
  clanker chain up|deploy|mint-operator|mint-bot|rotate-bot-key|revoke-bot ...
159
172
  clanker check mqtt <bot_id> <operator_id>
@@ -161,12 +174,12 @@ Usage:
161
174
 
162
175
  Profile:
163
176
  ~/.clanker/config.json network preset (registry, RPC, broker URLs)
164
- ~/.clanker/operator.json label + owner + key pointer (never raw hex)
177
+ ~/.clanker/operator.json label + owner + optional key pointer (never raw hex)
165
178
  ~/.clanker/keys/ bot keys (also written to ~/.openclaw/keys/)
166
179
 
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.
180
+ Humans: \`clanker setup\` then \`clanker doctor\` / \`whoami\`.
181
+ Mutates print a plan and confirm unless --yes or --json.
182
+ whoami is fast by default; pass --with-bots to enrich child bots (or use \`clanker bots\`).
170
183
 
171
184
  See docs/operator-cli.md.
172
185
  `);
@@ -213,18 +226,20 @@ async function cmdWhoami(argv) {
213
226
  let address;
214
227
  let source;
215
228
  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);
229
+ try {
230
+ const resolved = resolveReadIdentity(argv);
223
231
  address = resolved.address;
224
232
  source = resolved.source;
225
233
  network = resolved.network;
234
+ } catch (err) {
235
+ exitCliError({
236
+ error: err.message,
237
+ because: "whoami needs an owner address from --address, a key, or operator.json",
238
+ try: ["clanker setup", "clanker doctor", "clanker whoami --address 0x…"],
239
+ });
226
240
  }
227
241
 
242
+ const withBots = hasFlag(argv, "--with-bots");
228
243
  const pub = await publicClientFromRpc(network.rpc);
229
244
  const preferred =
230
245
  flagValue(argv, "--operator") ?? loadOperator(network.home)?.label ?? null;
@@ -237,9 +252,14 @@ async function cmdWhoami(argv) {
237
252
  owner: getAddress(address),
238
253
  });
239
254
  if (resolved.error) {
240
- const err = new Error(resolved.error);
241
- err.candidates = resolved.candidates;
242
- throw err;
255
+ exitCliError({
256
+ error: resolved.error,
257
+ because: "preferred operator label did not match this owner on-chain",
258
+ try: [
259
+ "clanker whoami --address 0x…",
260
+ "clanker setup --force # fix label/owner",
261
+ ],
262
+ });
243
263
  }
244
264
  selected = [resolved.operator];
245
265
  } else {
@@ -250,12 +270,14 @@ async function cmdWhoami(argv) {
250
270
  });
251
271
  }
252
272
 
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
- });
273
+ if (withBots) {
274
+ for (const op of selected) {
275
+ op.bots = await findBotsByOperator(pub, {
276
+ registry: network.registry,
277
+ operatorId: op.id,
278
+ fromBlock: network.fromBlock,
279
+ });
280
+ }
259
281
  }
260
282
 
261
283
  const data = {
@@ -271,31 +293,38 @@ async function cmdWhoami(argv) {
271
293
  active: o.active,
272
294
  registeredAt: o.registeredAt.toString(),
273
295
  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
- })),
296
+ bots: withBots
297
+ ? (o.bots ?? []).map((b) => ({
298
+ label: b.label,
299
+ botKey: b.botKey,
300
+ active: b.active,
301
+ registeredAt: b.registeredAt.toString(),
302
+ revokedAt: b.revokedAt.toString(),
303
+ }))
304
+ : [],
281
305
  })),
282
306
  };
283
307
 
284
308
  if (hasFlag(argv, "--json")) printJson(data);
285
- else printHumanWhoami(data);
309
+ else {
310
+ printHumanWhoami(data);
311
+ if (!withBots && data.operators.length) {
312
+ console.log(c.dim("(bots omitted — pass --with-bots or run clanker bots)"));
313
+ }
314
+ nextHint(["clanker bots", "clanker doctor"]);
315
+ }
286
316
  }
287
317
 
288
318
  async function cmdBots(argv) {
289
319
  let address;
290
320
  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);
321
+ try {
322
+ const resolved = resolveReadIdentity(argv);
297
323
  address = resolved.address;
298
324
  network = resolved.network;
325
+ } catch (err) {
326
+ console.error(err.message);
327
+ process.exit(1);
299
328
  }
300
329
  const { label, pub } = await resolveOperatorLabel(argv, address, network);
301
330
  const bots = await findBotsByOperator(pub, {
@@ -357,6 +386,27 @@ async function main() {
357
386
 
358
387
  const repoRoot = findRepoRoot();
359
388
 
389
+ if (cmd === "setup") {
390
+ try {
391
+ await runSetup(rest);
392
+ } catch (err) {
393
+ exitCliError({
394
+ error: err.message,
395
+ because: "setup could not write a valid local profile",
396
+ try: [
397
+ "clanker setup --preset sepolia --operator org.you --address 0x… --yes --force",
398
+ "clanker doctor",
399
+ ],
400
+ });
401
+ }
402
+ return;
403
+ }
404
+
405
+ if (cmd === "doctor") {
406
+ const { exitCode } = await runDoctor(rest);
407
+ process.exit(exitCode);
408
+ }
409
+
360
410
  if (cmd === "init") {
361
411
  const preset = flagValue(rest, "--preset") ?? rest.find((a) => !a.startsWith("--"));
362
412
  if (!preset || preset === "init") {
@@ -403,13 +453,38 @@ async function main() {
403
453
  console.error("Usage: clanker operator mint <label>");
404
454
  process.exit(1);
405
455
  }
456
+ let planRows;
457
+ try {
458
+ const preview = resolveOperatorKey(flags);
459
+ planRows = [
460
+ ["action", "registerOperator"],
461
+ ["label", label],
462
+ ["owner", preview.address],
463
+ ["rpc", preview.network.rpc],
464
+ ["registry", preview.network.registry ?? "(none)"],
465
+ ["key", preview.source],
466
+ ];
467
+ } catch (err) {
468
+ exitCliError({
469
+ error: err.message,
470
+ because: "operator mint needs a signing key on this network",
471
+ try: [
472
+ "export OPERATOR_PRIVATE_KEY=0x…",
473
+ "clanker operator mint " + label + " --key-file ~/.clanker/op.key --yes",
474
+ "clanker doctor",
475
+ ],
476
+ });
477
+ }
478
+ const ok = await confirmPlan(flags, planRows, `Mint operator ${label}?`);
479
+ if (!ok) process.exit(0);
406
480
  const result = await chainMintOperator(label, flags);
407
481
  if (hasFlag(flags, "--json") || hasFlag(opArgv, "--json")) printJson(result);
408
482
  else {
409
- console.log(`Minted operator ${label}`);
483
+ console.log(c.green(`Minted operator ${label}`));
410
484
  console.log(`owner: ${result.owner}`);
411
485
  console.log(`tx: ${result.tx}`);
412
486
  console.log(`Wrote ~/.clanker/operator.json`);
487
+ nextHint([`clanker bot mint <bot_label>`, "clanker whoami"]);
413
488
  }
414
489
  return;
415
490
  }
@@ -421,9 +496,23 @@ async function main() {
421
496
  console.error("Usage: clanker operator transfer propose <label> <newOwner>");
422
497
  process.exit(1);
423
498
  }
499
+ const ok = await confirmPlan(
500
+ flags,
501
+ [
502
+ ["action", "proposeOperatorTransfer"],
503
+ ["label", label],
504
+ ["newOwner", getAddress(newOwner)],
505
+ ],
506
+ `Propose transfer of ${label}?`,
507
+ );
508
+ if (!ok) process.exit(0);
424
509
  const result = await chainProposeOperatorTransfer(label, getAddress(newOwner), flags);
425
510
  if (hasFlag(flags, "--json")) printJson(result);
426
- else console.log(`Proposed transfer of ${label} → ${newOwner}\ntx: ${result.tx}`);
511
+ else {
512
+ console.log(c.green(`Proposed transfer of ${label} → ${newOwner}`));
513
+ console.log(`tx: ${result.tx}`);
514
+ nextHint([`clanker operator transfer accept ${label} # as new owner`]);
515
+ }
427
516
  return;
428
517
  }
429
518
  if (action === "accept") {
@@ -431,9 +520,23 @@ async function main() {
431
520
  console.error("Usage: clanker operator transfer accept <label>");
432
521
  process.exit(1);
433
522
  }
523
+ const ok = await confirmPlan(
524
+ flags,
525
+ [
526
+ ["action", "acceptOperatorTransfer"],
527
+ ["label", label],
528
+ ],
529
+ `Accept transfer of ${label}?`,
530
+ );
531
+ if (!ok) process.exit(0);
434
532
  const result = await chainAcceptOperatorTransfer(label, flags);
435
533
  if (hasFlag(flags, "--json")) printJson(result);
436
- else console.log(`Accepted transfer of ${label}\nowner: ${result.owner}\ntx: ${result.tx}`);
534
+ else {
535
+ console.log(c.green(`Accepted transfer of ${label}`));
536
+ console.log(`owner: ${result.owner}`);
537
+ console.log(`tx: ${result.tx}`);
538
+ nextHint(["clanker whoami", "clanker bots"]);
539
+ }
437
540
  return;
438
541
  }
439
542
  console.error("Usage: clanker operator transfer propose|accept ...");
@@ -473,20 +576,64 @@ async function main() {
473
576
  }
474
577
  let operatorLabel = operatorArg ?? flagValue(flags, "--operator") ?? null;
475
578
  if (!operatorLabel) {
476
- const { address, network } = resolveOperatorKey(flags);
477
- const inferred = await resolveOperatorLabel(flags, address, network);
478
- operatorLabel = inferred.label;
579
+ try {
580
+ const { address, network } = resolveOperatorKey(flags);
581
+ const inferred = await resolveOperatorLabel(flags, address, network);
582
+ operatorLabel = inferred.label;
583
+ } catch (err) {
584
+ exitCliError({
585
+ error: err.message,
586
+ because: "bot mint needs an operator label or a resolvable signing key",
587
+ try: [
588
+ `clanker bot mint ${botLabel} <operator_label> --yes`,
589
+ "clanker setup",
590
+ ],
591
+ });
592
+ }
479
593
  }
594
+ const ok = await confirmPlan(
595
+ flags,
596
+ [
597
+ ["action", "registerBot"],
598
+ ["bot", botLabel],
599
+ ["operator", operatorLabel],
600
+ ],
601
+ `Mint bot ${botLabel} under ${operatorLabel}?`,
602
+ );
603
+ if (!ok) process.exit(0);
480
604
  const result = await chainMintBot(botLabel, operatorLabel, flags);
481
- if (hasFlag(flags, "--json")) printJson(result);
482
- else {
483
- console.log(`Minted bot ${botLabel} under ${operatorLabel}`);
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 {
620
+ console.log(c.green(`Minted bot ${botLabel} under ${operatorLabel}`));
484
621
  console.log(`botKey: ${result.bot_key}`);
485
622
  console.log(`key file: ${result.key_path}`);
486
623
  console.log(`also: ${result.clanker_key_path}`);
487
624
  console.log(`tx: ${result.tx}`);
488
- console.log("\nchannels.mqtt stub:");
489
- console.log(JSON.stringify(result.channels_mqtt, null, 2));
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
+ );
490
637
  }
491
638
  return;
492
639
  }
@@ -497,9 +644,22 @@ async function main() {
497
644
  console.error("Usage: clanker bot revoke <label>");
498
645
  process.exit(1);
499
646
  }
647
+ const ok = await confirmPlan(
648
+ flags,
649
+ [
650
+ ["action", "revokeBot"],
651
+ ["bot", botLabel],
652
+ ],
653
+ `Revoke bot ${botLabel}?`,
654
+ );
655
+ if (!ok) process.exit(0);
500
656
  const result = await chainRevokeBot(botLabel, flags);
501
657
  if (hasFlag(flags, "--json")) printJson(result);
502
- else console.log(`Revoked ${botLabel}\ntx: ${result.tx}`);
658
+ else {
659
+ console.log(c.yellow(`Revoked ${botLabel}`));
660
+ console.log(`tx: ${result.tx}`);
661
+ nextHint(["clanker bots", "clanker bot status " + botLabel]);
662
+ }
503
663
  return;
504
664
  }
505
665
 
@@ -509,9 +669,23 @@ async function main() {
509
669
  console.error("Usage: clanker bot rotate <label> <newKeyAddress>");
510
670
  process.exit(1);
511
671
  }
672
+ const ok = await confirmPlan(
673
+ flags,
674
+ [
675
+ ["action", "rotateBotKey"],
676
+ ["bot", botLabel],
677
+ ["newKey", getAddress(newKey)],
678
+ ],
679
+ `Rotate key for ${botLabel}?`,
680
+ );
681
+ if (!ok) process.exit(0);
512
682
  const result = await chainRotateBotKey(botLabel, getAddress(newKey), flags);
513
683
  if (hasFlag(flags, "--json")) printJson(result);
514
- else console.log(`Rotated ${botLabel} → ${newKey}\ntx: ${result.tx}`);
684
+ else {
685
+ console.log(c.green(`Rotated ${botLabel} → ${newKey}`));
686
+ console.log(`tx: ${result.tx}`);
687
+ nextHint(["Update ~/.openclaw/keys/" + botLabel + ".key", "clanker bot status " + botLabel]);
688
+ }
515
689
  return;
516
690
  }
517
691
 
@@ -717,22 +891,6 @@ async function main() {
717
891
  }
718
892
 
719
893
  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
894
  const profile = loadConfig();
737
895
  const operator = loadOperator();
738
896
  // No profile: local defaults only — do not mix Sepolia registry with localhost MQTT.
@@ -750,27 +908,21 @@ async function main() {
750
908
  mqttAuthServiceUrl: "http://localhost:9090",
751
909
  };
752
910
 
753
- const mqtt = harnessSnippet({
911
+ const wire = wireOpenClawMqtt({
754
912
  botId: "openclaw.your-bot.local",
755
913
  operatorId: operator?.label ?? "org.openclaw.your-operator",
756
914
  network,
757
915
  });
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.");
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
+ ]);
774
926
  process.exit(0);
775
927
  }
776
928