@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 +183 -42
- package/lib/doctor.mjs +185 -0
- package/lib/profile.mjs +7 -4
- package/lib/setup-detect.mjs +90 -2
- package/lib/setup.mjs +240 -253
- package/lib/ui.mjs +104 -0
- package/package.json +3 -1
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:
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
256
|
-
op
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
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
|
-
|
|
366
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
|
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
|
|
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).
|
|
21
|
-
*
|
|
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:
|
|
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 :
|
|
193
|
+
fromBlock = isLocalRpc(rpc) ? 0n : SEPOLIA_FAST_FROM_BLOCK;
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
return {
|
package/lib/setup-detect.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|