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

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
@@ -25,6 +25,13 @@ import {
25
25
  } from "../lib/identity-query.mjs";
26
26
  import { resolveForRead, resolveOperatorKey, resolveReadIdentity } from "../lib/resolve.mjs";
27
27
  import { runSetup } from "../lib/setup.mjs";
28
+ import { runDoctor } from "../lib/doctor.mjs";
29
+ import {
30
+ c,
31
+ confirmPlan,
32
+ exitCliError,
33
+ nextHint,
34
+ } from "../lib/ui.mjs";
28
35
 
29
36
  function resolveFoundryBinary(name) {
30
37
  const home = os.homedir();
@@ -146,16 +153,17 @@ function usage() {
146
153
 
147
154
  Usage:
148
155
  clanker setup [--preset sepolia|local] [--operator <label>] [--address 0x…] [--key-file path] [--force]
156
+ clanker doctor [--json]
149
157
  clanker init --preset sepolia|local [--force]
150
- clanker whoami [--json] [--operator <label>] [--address 0x…]
151
- clanker operator mint <label> [--json]
152
- clanker operator transfer propose <label> <newOwner>
153
- clanker operator transfer accept <label>
154
- clanker bot mint <label> [operator] [--json]
158
+ clanker whoami [--json] [--operator <label>] [--address 0x…] [--with-bots]
159
+ clanker operator mint <label> [--json] [--yes]
160
+ clanker operator transfer propose <label> <newOwner> [--yes]
161
+ clanker operator transfer accept <label> [--yes]
162
+ clanker bot mint <label> [operator] [--json] [--yes]
155
163
  clanker bots [--json] [--operator <label>] [--address 0x…]
156
164
  clanker bot status <label> [--json]
157
- clanker bot revoke <label> [--json]
158
- clanker bot rotate <label> <newKeyAddress> [--json]
165
+ clanker bot revoke <label> [--json] [--yes]
166
+ clanker bot rotate <label> <newKeyAddress> [--json] [--yes]
159
167
  clanker init-openclaw
160
168
  clanker chain up|deploy|mint-operator|mint-bot|rotate-bot-key|revoke-bot ...
161
169
  clanker check mqtt <bot_id> <operator_id>
@@ -166,11 +174,9 @@ Profile:
166
174
  ~/.clanker/operator.json label + owner + optional key pointer (never raw hex)
167
175
  ~/.clanker/keys/ bot keys (also written to ~/.openclaw/keys/)
168
176
 
169
- Humans: prefer \`clanker setup\` (detects Foundry/OpenClaw hints, writes profile).
170
- Key resolution (mutating commands):
171
- --key > --key-file > OPERATOR_PRIVATE_KEY > profile keyFile > profile env
172
- Anvil account #0 is allowed only on localhost RPC.
173
- Reads (whoami/bots) can use profile owner without a signing key.
177
+ Humans: \`clanker setup\` then \`clanker doctor\` / \`whoami\`.
178
+ Mutates print a plan and confirm unless --yes or --json.
179
+ whoami is fast by default; pass --with-bots to enrich child bots (or use \`clanker bots\`).
174
180
 
175
181
  See docs/operator-cli.md.
176
182
  `);
@@ -223,10 +229,14 @@ async function cmdWhoami(argv) {
223
229
  source = resolved.source;
224
230
  network = resolved.network;
225
231
  } catch (err) {
226
- console.error(err.message);
227
- process.exit(1);
232
+ exitCliError({
233
+ error: err.message,
234
+ because: "whoami needs an owner address from --address, a key, or operator.json",
235
+ try: ["clanker setup", "clanker doctor", "clanker whoami --address 0x…"],
236
+ });
228
237
  }
229
238
 
239
+ const withBots = hasFlag(argv, "--with-bots");
230
240
  const pub = await publicClientFromRpc(network.rpc);
231
241
  const preferred =
232
242
  flagValue(argv, "--operator") ?? loadOperator(network.home)?.label ?? null;
@@ -239,9 +249,14 @@ async function cmdWhoami(argv) {
239
249
  owner: getAddress(address),
240
250
  });
241
251
  if (resolved.error) {
242
- const err = new Error(resolved.error);
243
- err.candidates = resolved.candidates;
244
- throw err;
252
+ exitCliError({
253
+ error: resolved.error,
254
+ because: "preferred operator label did not match this owner on-chain",
255
+ try: [
256
+ "clanker whoami --address 0x…",
257
+ "clanker setup --force # fix label/owner",
258
+ ],
259
+ });
245
260
  }
246
261
  selected = [resolved.operator];
247
262
  } else {
@@ -252,12 +267,14 @@ async function cmdWhoami(argv) {
252
267
  });
253
268
  }
254
269
 
255
- for (const op of selected) {
256
- op.bots = await findBotsByOperator(pub, {
257
- registry: network.registry,
258
- operatorId: op.id,
259
- fromBlock: network.fromBlock,
260
- });
270
+ if (withBots) {
271
+ for (const op of selected) {
272
+ op.bots = await findBotsByOperator(pub, {
273
+ registry: network.registry,
274
+ operatorId: op.id,
275
+ fromBlock: network.fromBlock,
276
+ });
277
+ }
261
278
  }
262
279
 
263
280
  const data = {
@@ -273,18 +290,26 @@ async function cmdWhoami(argv) {
273
290
  active: o.active,
274
291
  registeredAt: o.registeredAt.toString(),
275
292
  revokedAt: o.revokedAt.toString(),
276
- bots: (o.bots ?? []).map((b) => ({
277
- label: b.label,
278
- botKey: b.botKey,
279
- active: b.active,
280
- registeredAt: b.registeredAt.toString(),
281
- revokedAt: b.revokedAt.toString(),
282
- })),
293
+ bots: withBots
294
+ ? (o.bots ?? []).map((b) => ({
295
+ label: b.label,
296
+ botKey: b.botKey,
297
+ active: b.active,
298
+ registeredAt: b.registeredAt.toString(),
299
+ revokedAt: b.revokedAt.toString(),
300
+ }))
301
+ : [],
283
302
  })),
284
303
  };
285
304
 
286
305
  if (hasFlag(argv, "--json")) printJson(data);
287
- else printHumanWhoami(data);
306
+ else {
307
+ printHumanWhoami(data);
308
+ if (!withBots && data.operators.length) {
309
+ console.log(c.dim("(bots omitted — pass --with-bots or run clanker bots)"));
310
+ }
311
+ nextHint(["clanker bots", "clanker doctor"]);
312
+ }
288
313
  }
289
314
 
290
315
  async function cmdBots(argv) {
@@ -362,12 +387,23 @@ async function main() {
362
387
  try {
363
388
  await runSetup(rest);
364
389
  } catch (err) {
365
- console.error(err.message);
366
- process.exit(1);
390
+ exitCliError({
391
+ error: err.message,
392
+ because: "setup could not write a valid local profile",
393
+ try: [
394
+ "clanker setup --preset sepolia --operator org.you --address 0x… --yes --force",
395
+ "clanker doctor",
396
+ ],
397
+ });
367
398
  }
368
399
  return;
369
400
  }
370
401
 
402
+ if (cmd === "doctor") {
403
+ const { exitCode } = await runDoctor(rest);
404
+ process.exit(exitCode);
405
+ }
406
+
371
407
  if (cmd === "init") {
372
408
  const preset = flagValue(rest, "--preset") ?? rest.find((a) => !a.startsWith("--"));
373
409
  if (!preset || preset === "init") {
@@ -414,13 +450,38 @@ async function main() {
414
450
  console.error("Usage: clanker operator mint <label>");
415
451
  process.exit(1);
416
452
  }
453
+ let planRows;
454
+ try {
455
+ const preview = resolveOperatorKey(flags);
456
+ planRows = [
457
+ ["action", "registerOperator"],
458
+ ["label", label],
459
+ ["owner", preview.address],
460
+ ["rpc", preview.network.rpc],
461
+ ["registry", preview.network.registry ?? "(none)"],
462
+ ["key", preview.source],
463
+ ];
464
+ } catch (err) {
465
+ exitCliError({
466
+ error: err.message,
467
+ because: "operator mint needs a signing key on this network",
468
+ try: [
469
+ "export OPERATOR_PRIVATE_KEY=0x…",
470
+ "clanker operator mint " + label + " --key-file ~/.clanker/op.key --yes",
471
+ "clanker doctor",
472
+ ],
473
+ });
474
+ }
475
+ const ok = await confirmPlan(flags, planRows, `Mint operator ${label}?`);
476
+ if (!ok) process.exit(0);
417
477
  const result = await chainMintOperator(label, flags);
418
478
  if (hasFlag(flags, "--json") || hasFlag(opArgv, "--json")) printJson(result);
419
479
  else {
420
- console.log(`Minted operator ${label}`);
480
+ console.log(c.green(`Minted operator ${label}`));
421
481
  console.log(`owner: ${result.owner}`);
422
482
  console.log(`tx: ${result.tx}`);
423
483
  console.log(`Wrote ~/.clanker/operator.json`);
484
+ nextHint([`clanker bot mint <bot_label>`, "clanker whoami"]);
424
485
  }
425
486
  return;
426
487
  }
@@ -432,9 +493,23 @@ async function main() {
432
493
  console.error("Usage: clanker operator transfer propose <label> <newOwner>");
433
494
  process.exit(1);
434
495
  }
496
+ const ok = await confirmPlan(
497
+ flags,
498
+ [
499
+ ["action", "proposeOperatorTransfer"],
500
+ ["label", label],
501
+ ["newOwner", getAddress(newOwner)],
502
+ ],
503
+ `Propose transfer of ${label}?`,
504
+ );
505
+ if (!ok) process.exit(0);
435
506
  const result = await chainProposeOperatorTransfer(label, getAddress(newOwner), flags);
436
507
  if (hasFlag(flags, "--json")) printJson(result);
437
- else console.log(`Proposed transfer of ${label} → ${newOwner}\ntx: ${result.tx}`);
508
+ else {
509
+ console.log(c.green(`Proposed transfer of ${label} → ${newOwner}`));
510
+ console.log(`tx: ${result.tx}`);
511
+ nextHint([`clanker operator transfer accept ${label} # as new owner`]);
512
+ }
438
513
  return;
439
514
  }
440
515
  if (action === "accept") {
@@ -442,9 +517,23 @@ async function main() {
442
517
  console.error("Usage: clanker operator transfer accept <label>");
443
518
  process.exit(1);
444
519
  }
520
+ const ok = await confirmPlan(
521
+ flags,
522
+ [
523
+ ["action", "acceptOperatorTransfer"],
524
+ ["label", label],
525
+ ],
526
+ `Accept transfer of ${label}?`,
527
+ );
528
+ if (!ok) process.exit(0);
445
529
  const result = await chainAcceptOperatorTransfer(label, flags);
446
530
  if (hasFlag(flags, "--json")) printJson(result);
447
- else console.log(`Accepted transfer of ${label}\nowner: ${result.owner}\ntx: ${result.tx}`);
531
+ else {
532
+ console.log(c.green(`Accepted transfer of ${label}`));
533
+ console.log(`owner: ${result.owner}`);
534
+ console.log(`tx: ${result.tx}`);
535
+ nextHint(["clanker whoami", "clanker bots"]);
536
+ }
448
537
  return;
449
538
  }
450
539
  console.error("Usage: clanker operator transfer propose|accept ...");
@@ -484,20 +573,45 @@ async function main() {
484
573
  }
485
574
  let operatorLabel = operatorArg ?? flagValue(flags, "--operator") ?? null;
486
575
  if (!operatorLabel) {
487
- const { address, network } = resolveOperatorKey(flags);
488
- const inferred = await resolveOperatorLabel(flags, address, network);
489
- operatorLabel = inferred.label;
576
+ try {
577
+ const { address, network } = resolveOperatorKey(flags);
578
+ const inferred = await resolveOperatorLabel(flags, address, network);
579
+ operatorLabel = inferred.label;
580
+ } catch (err) {
581
+ exitCliError({
582
+ error: err.message,
583
+ because: "bot mint needs an operator label or a resolvable signing key",
584
+ try: [
585
+ `clanker bot mint ${botLabel} <operator_label> --yes`,
586
+ "clanker setup",
587
+ ],
588
+ });
589
+ }
490
590
  }
591
+ const ok = await confirmPlan(
592
+ flags,
593
+ [
594
+ ["action", "registerBot"],
595
+ ["bot", botLabel],
596
+ ["operator", operatorLabel],
597
+ ],
598
+ `Mint bot ${botLabel} under ${operatorLabel}?`,
599
+ );
600
+ if (!ok) process.exit(0);
491
601
  const result = await chainMintBot(botLabel, operatorLabel, flags);
492
602
  if (hasFlag(flags, "--json")) printJson(result);
493
603
  else {
494
- console.log(`Minted bot ${botLabel} under ${operatorLabel}`);
604
+ console.log(c.green(`Minted bot ${botLabel} under ${operatorLabel}`));
495
605
  console.log(`botKey: ${result.bot_key}`);
496
606
  console.log(`key file: ${result.key_path}`);
497
607
  console.log(`also: ${result.clanker_key_path}`);
498
608
  console.log(`tx: ${result.tx}`);
499
609
  console.log("\nchannels.mqtt stub:");
500
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
+ ]);
501
615
  }
502
616
  return;
503
617
  }
@@ -508,9 +622,22 @@ async function main() {
508
622
  console.error("Usage: clanker bot revoke <label>");
509
623
  process.exit(1);
510
624
  }
625
+ const ok = await confirmPlan(
626
+ flags,
627
+ [
628
+ ["action", "revokeBot"],
629
+ ["bot", botLabel],
630
+ ],
631
+ `Revoke bot ${botLabel}?`,
632
+ );
633
+ if (!ok) process.exit(0);
511
634
  const result = await chainRevokeBot(botLabel, flags);
512
635
  if (hasFlag(flags, "--json")) printJson(result);
513
- else console.log(`Revoked ${botLabel}\ntx: ${result.tx}`);
636
+ else {
637
+ console.log(c.yellow(`Revoked ${botLabel}`));
638
+ console.log(`tx: ${result.tx}`);
639
+ nextHint(["clanker bots", "clanker bot status " + botLabel]);
640
+ }
514
641
  return;
515
642
  }
516
643
 
@@ -520,9 +647,23 @@ async function main() {
520
647
  console.error("Usage: clanker bot rotate <label> <newKeyAddress>");
521
648
  process.exit(1);
522
649
  }
650
+ const ok = await confirmPlan(
651
+ flags,
652
+ [
653
+ ["action", "rotateBotKey"],
654
+ ["bot", botLabel],
655
+ ["newKey", getAddress(newKey)],
656
+ ],
657
+ `Rotate key for ${botLabel}?`,
658
+ );
659
+ if (!ok) process.exit(0);
523
660
  const result = await chainRotateBotKey(botLabel, getAddress(newKey), flags);
524
661
  if (hasFlag(flags, "--json")) printJson(result);
525
- else console.log(`Rotated ${botLabel} → ${newKey}\ntx: ${result.tx}`);
662
+ else {
663
+ console.log(c.green(`Rotated ${botLabel} → ${newKey}`));
664
+ console.log(`tx: ${result.tx}`);
665
+ nextHint(["Update ~/.openclaw/keys/" + botLabel + ".key", "clanker bot status " + botLabel]);
666
+ }
526
667
  return;
527
668
  }
528
669
 
package/lib/doctor.mjs ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * `clanker doctor` — local readiness checks (mise-doctor habit).
3
+ */
4
+
5
+ import { getAddress } from "viem";
6
+ import {
7
+ ANVIL_DEFAULT_ADDRESS,
8
+ isLocalRpc,
9
+ loadConfig,
10
+ loadOperator,
11
+ clankerHome,
12
+ } from "./profile.mjs";
13
+ import { detectSetupHints, formatSetupDetectTable } from "./setup-detect.mjs";
14
+ import { c, nextHint } from "./ui.mjs";
15
+
16
+ /**
17
+ * @param {{ home?: string, env?: NodeJS.ProcessEnv, openclawDir?: string, castBin?: string, spawn?: Function }} [opts]
18
+ */
19
+ export function runDoctorChecks(opts = {}) {
20
+ const env = opts.env ?? process.env;
21
+ const home = opts.home ?? clankerHome(env);
22
+ const hints = detectSetupHints({
23
+ home,
24
+ env,
25
+ openclawDir: opts.openclawDir,
26
+ castBin: opts.castBin,
27
+ spawn: opts.spawn,
28
+ });
29
+ const config = hints.config ?? loadConfig(home);
30
+ const operator = hints.operator ?? loadOperator(home);
31
+
32
+ /** @type {{ id: string, ok: boolean, level: 'pass'|'warn'|'fail', message: string }[]} */
33
+ const checks = [];
34
+
35
+ checks.push({
36
+ id: "config",
37
+ ok: Boolean(config),
38
+ level: config ? "pass" : "fail",
39
+ message: config
40
+ ? `config.json present (preset=${config.preset})`
41
+ : "config.json missing — run clanker setup",
42
+ });
43
+
44
+ const registry = config?.registryAddress;
45
+ const rpc = config?.chainRpcUrl ?? "";
46
+ checks.push({
47
+ id: "registry",
48
+ ok: Boolean(registry && /^0x[0-9a-fA-F]{40}$/.test(registry)),
49
+ level: registry && /^0x[0-9a-fA-F]{40}$/.test(registry) ? "pass" : "fail",
50
+ message:
51
+ registry && /^0x[0-9a-fA-F]{40}$/.test(registry)
52
+ ? `registry ${registry}`
53
+ : "registry missing — run clanker setup --preset sepolia (or set after local deploy)",
54
+ });
55
+
56
+ const hasOwner =
57
+ Boolean(operator?.owner) && /^0x[0-9a-fA-F]{40}$/.test(operator.owner);
58
+ const hasLabel = Boolean(operator?.label);
59
+ checks.push({
60
+ id: "operator",
61
+ ok: hasOwner && hasLabel,
62
+ level: hasOwner && hasLabel ? "pass" : "fail",
63
+ message:
64
+ hasOwner && hasLabel
65
+ ? `operator.json ${operator.label} · ${operator.owner}`
66
+ : "operator.json incomplete — run clanker setup",
67
+ });
68
+
69
+ if (hasOwner && rpc && !isLocalRpc(rpc)) {
70
+ const anvil =
71
+ getAddress(operator.owner).toLowerCase() ===
72
+ ANVIL_DEFAULT_ADDRESS.toLowerCase();
73
+ checks.push({
74
+ id: "anvil_public",
75
+ ok: !anvil,
76
+ level: anvil ? "fail" : "pass",
77
+ message: anvil
78
+ ? "owner is Anvil #0 on a public RPC — refuse for mutate; fix owner address"
79
+ : "owner is not Anvil #0",
80
+ });
81
+ }
82
+
83
+ const hasKey =
84
+ Boolean(operator?.key?.type === "keyFile" && operator.key.value) ||
85
+ Boolean(operator?.key?.type === "env" && operator.key.value) ||
86
+ Boolean(env.OPERATOR_PRIVATE_KEY);
87
+ checks.push({
88
+ id: "signing",
89
+ ok: true,
90
+ level: hasKey ? "pass" : "warn",
91
+ message: hasKey
92
+ ? "signing key pointer available (mint/revoke OK)"
93
+ : "read-only profile — whoami/bots OK; mint needs --key-file or OPERATOR_PRIVATE_KEY",
94
+ });
95
+
96
+ checks.push({
97
+ id: "foundry",
98
+ ok: true,
99
+ level: hints.foundryAvailable ? "pass" : "warn",
100
+ message: hints.foundryAvailable
101
+ ? `Foundry cast OK (${hints.foundryAccounts.length} account(s))`
102
+ : "Foundry cast not on PATH (optional)",
103
+ });
104
+
105
+ const readyWhoami = checks
106
+ .filter((ch) => ch.id === "config" || ch.id === "registry" || ch.id === "operator")
107
+ .every((ch) => ch.ok);
108
+ const readyMint = readyWhoami && hasKey &&
109
+ !checks.some((ch) => ch.id === "anvil_public" && !ch.ok);
110
+
111
+ return {
112
+ home,
113
+ hints,
114
+ checks,
115
+ readyWhoami,
116
+ readyMint,
117
+ ok: readyWhoami,
118
+ };
119
+ }
120
+
121
+ /**
122
+ * @param {string[]} argv
123
+ * @param {{ home?: string, env?: NodeJS.ProcessEnv }} [opts]
124
+ * @returns {Promise<{ exitCode: number, report: object }>}
125
+ */
126
+ export async function runDoctor(argv = [], opts = {}) {
127
+ const json = argv.includes("--json");
128
+ const report = runDoctorChecks(opts);
129
+
130
+ if (json) {
131
+ console.log(
132
+ JSON.stringify(
133
+ {
134
+ ok: report.ok,
135
+ readyWhoami: report.readyWhoami,
136
+ readyMint: report.readyMint,
137
+ home: report.home,
138
+ checks: report.checks,
139
+ },
140
+ null,
141
+ 2,
142
+ ),
143
+ );
144
+ return { exitCode: report.ok ? 0 : 1, report };
145
+ }
146
+
147
+ console.log(c.bold("clanker doctor"));
148
+ console.log("");
149
+ console.log(formatSetupDetectTable(report.hints));
150
+ console.log("");
151
+ console.log(c.bold("Checks"));
152
+ for (const ch of report.checks) {
153
+ const mark =
154
+ ch.level === "pass"
155
+ ? c.green("pass")
156
+ : ch.level === "warn"
157
+ ? c.yellow("warn")
158
+ : c.red("fail");
159
+ console.log(` [${mark}] ${ch.message}`);
160
+ }
161
+ console.log("");
162
+ if (report.readyWhoami) {
163
+ console.log(c.green("Ready for: clanker whoami"));
164
+ } else {
165
+ console.log(c.red("Not ready for whoami"));
166
+ }
167
+ if (report.readyMint) {
168
+ console.log(c.green("Ready for: clanker operator mint / bot mint"));
169
+ } else {
170
+ console.log(c.dim("Mint/revoke: need signing key (and non-Anvil owner on public RPC)"));
171
+ }
172
+
173
+ if (!report.readyWhoami) {
174
+ nextHint(["clanker setup"]);
175
+ } else if (!report.readyMint) {
176
+ nextHint([
177
+ "clanker whoami",
178
+ "clanker setup --key-file ~/.clanker/op.key --force # to enable mint",
179
+ ]);
180
+ } else {
181
+ nextHint(["clanker whoami", "clanker bots"]);
182
+ }
183
+
184
+ return { exitCode: report.ok ? 0 : 1, report };
185
+ }
package/lib/profile.mjs CHANGED
@@ -17,11 +17,14 @@ export const ANVIL_DEFAULT_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
17
17
  export const SEPOLIA_REGISTRY = "0xD650467f9D7A20f37E55ec23Ca1c711598f97958";
18
18
 
19
19
  /**
20
- * Safe floor for Sepolia getLogs (before registry deploy). Documented so scans
21
- * stay cheap; bump only if you redeploy the registry earlier.
20
+ * Safe floor for Sepolia getLogs (before registry deploy). Use for full historical
21
+ * scans via --from-block; default preset uses the faster public-RPC floor below.
22
22
  */
23
23
  export const SEPOLIA_FROM_BLOCK = 35_000_000n;
24
24
 
25
+ /** Default Sepolia fromBlock for public RPC (faster whoami/bots scans). */
26
+ export const SEPOLIA_FAST_FROM_BLOCK = 46_000_000n;
27
+
25
28
  export const PRESETS = {
26
29
  local: {
27
30
  preset: "local",
@@ -37,7 +40,7 @@ export const PRESETS = {
37
40
  chainRpcUrl: "https://sepolia.base.org",
38
41
  brokerUrl: "mqtts://mqtt.clanker-chain.com:8883",
39
42
  mqttAuthServiceUrl: "https://mqtt-auth.clanker-chain.com",
40
- fromBlock: SEPOLIA_FROM_BLOCK,
43
+ fromBlock: SEPOLIA_FAST_FROM_BLOCK,
41
44
  },
42
45
  };
43
46
 
@@ -187,7 +190,7 @@ export function resolveNetwork(argv = [], opts = {}) {
187
190
  }
188
191
 
189
192
  if (fromBlock == null) {
190
- fromBlock = isLocalRpc(rpc) ? 0n : SEPOLIA_FROM_BLOCK;
193
+ fromBlock = isLocalRpc(rpc) ? 0n : SEPOLIA_FAST_FROM_BLOCK;
191
194
  }
192
195
 
193
196
  return {
@@ -12,11 +12,11 @@ import {
12
12
  loadConfig,
13
13
  loadOperator,
14
14
  openclawKeysDir,
15
+ SEPOLIA_FAST_FROM_BLOCK,
15
16
  } from "./profile.mjs";
16
17
  import { normalizePrivateKey } from "./resolve.mjs";
17
18
 
18
- /** Faster Sepolia log floor for public RPC (registry floor remains SEPOLIA_FROM_BLOCK). */
19
- export const SEPOLIA_FAST_FROM_BLOCK = 46_000_000n;
19
+ export { SEPOLIA_FAST_FROM_BLOCK };
20
20
 
21
21
  /**
22
22
  * Parse `cast wallet list` stdout into account names.
@@ -134,3 +134,91 @@ export function detectSetupHints(opts = {}) {
134
134
  anvilDefaultAddress: ANVIL_DEFAULT_ADDRESS,
135
135
  };
136
136
  }
137
+
138
+ /**
139
+ * Pad string to width (truncate with … if longer).
140
+ * @param {string} s
141
+ * @param {number} width
142
+ */
143
+ function cell(s, width) {
144
+ const t = String(s ?? "");
145
+ if (t.length === width) return t;
146
+ if (t.length < width) return t + " ".repeat(width - t.length);
147
+ if (width <= 1) return "…";
148
+ return `${t.slice(0, width - 1)}…`;
149
+ }
150
+
151
+ /**
152
+ * Render detection hints as an aligned two-column table for the terminal.
153
+ * @param {ReturnType<typeof detectSetupHints>} hints
154
+ * @returns {string}
155
+ */
156
+ export function formatSetupDetectTable(hints) {
157
+ /** @type {[string, string][]} */
158
+ const rows = [];
159
+ rows.push(["Profile dir", hints.home]);
160
+
161
+ if (hints.hasConfig) {
162
+ rows.push(["config.json", `present · preset=${hints.config?.preset ?? "?"}`]);
163
+ rows.push([
164
+ "registry",
165
+ hints.config?.registryAddress ? String(hints.config.registryAddress) : "(none)",
166
+ ]);
167
+ } else {
168
+ rows.push(["config.json", "missing · will create"]);
169
+ }
170
+
171
+ if (hints.hasOperator) {
172
+ rows.push([
173
+ "operator.json",
174
+ `present · ${hints.operator?.label ?? "?"} · ${hints.operator?.owner ?? "?"}`,
175
+ ]);
176
+ } else {
177
+ rows.push(["operator.json", "missing · needed for whoami"]);
178
+ }
179
+
180
+ if (hints.foundryAvailable && hints.foundryAccounts.length) {
181
+ rows.push([
182
+ "Foundry accounts",
183
+ `${hints.foundryAccounts.length}: ${hints.foundryAccounts.join(", ")}`,
184
+ ]);
185
+ } else if (hints.foundryAvailable) {
186
+ rows.push(["Foundry accounts", "none listed"]);
187
+ } else {
188
+ rows.push(["Foundry cast", "not on PATH"]);
189
+ }
190
+
191
+ if (hints.openclawBots.length) {
192
+ rows.push([
193
+ "OpenClaw bot keys",
194
+ `${hints.openclawBots.length} files (bot signing only)`,
195
+ ]);
196
+ for (const b of hints.openclawBots) {
197
+ rows.push([" ·", b]);
198
+ }
199
+ } else {
200
+ rows.push(["OpenClaw bot keys", "none"]);
201
+ }
202
+
203
+ if (hints.hasOperatorPrivateKeyEnv) {
204
+ rows.push([
205
+ "OPERATOR_PRIVATE_KEY",
206
+ hints.envAddress ? `set · ${hints.envAddress}` : "set · (invalid)",
207
+ ]);
208
+ } else {
209
+ rows.push(["OPERATOR_PRIVATE_KEY", "unset"]);
210
+ }
211
+
212
+ const col0 = Math.min(
213
+ 22,
214
+ Math.max(4, ...rows.map(([k]) => k.length)),
215
+ );
216
+ const lines = [
217
+ `${cell("What", col0)} Value`,
218
+ `${"-".repeat(col0)} ${"-".repeat(48)}`,
219
+ ];
220
+ for (const [k, v] of rows) {
221
+ lines.push(`${cell(k, col0)} ${v}`);
222
+ }
223
+ return lines.join("\n");
224
+ }