@clanker-chain/clanker-cli 2026.9.7-3 → 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 +2 -2
- package/lib/setup.mjs +242 -220
- 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.
|
package/lib/setup.mjs
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* Interactive / flag-driven `clanker setup`.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
5
|
+
import { stdin as input } from "node:process";
|
|
7
6
|
import { existsSync } from "node:fs";
|
|
8
7
|
import { getAddress } from "viem";
|
|
8
|
+
import * as clack from "@clack/prompts";
|
|
9
9
|
import {
|
|
10
10
|
ANVIL_DEFAULT_ADDRESS,
|
|
11
11
|
PRESETS,
|
|
12
|
+
SEPOLIA_FAST_FROM_BLOCK,
|
|
12
13
|
clankerHome,
|
|
13
14
|
configPath,
|
|
14
15
|
initProfile,
|
|
@@ -22,12 +23,12 @@ import {
|
|
|
22
23
|
resolvePreferredOperator,
|
|
23
24
|
} from "./identity-query.mjs";
|
|
24
25
|
import {
|
|
25
|
-
SEPOLIA_FAST_FROM_BLOCK,
|
|
26
26
|
addressFromEnv,
|
|
27
27
|
addressFromKeyFile,
|
|
28
28
|
detectSetupHints,
|
|
29
29
|
formatSetupDetectTable,
|
|
30
30
|
} from "./setup-detect.mjs";
|
|
31
|
+
import { c, nextHint } from "./ui.mjs";
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* @param {string[]} argv
|
|
@@ -72,6 +73,14 @@ export function parseSetupFlags(argv) {
|
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
function cancelIf(value) {
|
|
77
|
+
if (clack.isCancel(value)) {
|
|
78
|
+
clack.cancel("Setup aborted.");
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
/**
|
|
76
85
|
* Validate address + label against registry (and Anvil-on-public).
|
|
77
86
|
* @param {{ rpc: string, registry: string, label: string, address: string, skipChainCheck?: boolean }} opts
|
|
@@ -101,7 +110,6 @@ export async function assertSetupIdentity(opts) {
|
|
|
101
110
|
const op = await readOperator(pub, opts.registry, opts.label);
|
|
102
111
|
|
|
103
112
|
if (op.registeredAt === 0n) {
|
|
104
|
-
// New operator — fine; mint later
|
|
105
113
|
return { address, operator: op, skipped: false, unregistered: true };
|
|
106
114
|
}
|
|
107
115
|
|
|
@@ -117,14 +125,12 @@ export async function assertSetupIdentity(opts) {
|
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
/**
|
|
120
|
-
* Build key pointer from flags / choices.
|
|
121
128
|
* @returns {{ type: 'env'|'keyFile', value: string }|null}
|
|
122
129
|
*/
|
|
123
130
|
export function buildKeyPointer({ keyFile, keyEnv, skipKey, env = process.env }) {
|
|
124
131
|
if (skipKey) return null;
|
|
125
132
|
if (keyFile) {
|
|
126
133
|
if (!existsSync(keyFile)) throw new Error(`Key file not found: ${keyFile}`);
|
|
127
|
-
// validate readable
|
|
128
134
|
addressFromKeyFile(keyFile);
|
|
129
135
|
return { type: "keyFile", value: keyFile };
|
|
130
136
|
}
|
|
@@ -176,8 +182,6 @@ export function applySetup(choices, opts = {}) {
|
|
|
176
182
|
|
|
177
183
|
/**
|
|
178
184
|
* Non-interactive setup (agents / CI).
|
|
179
|
-
* @param {string[]} argv
|
|
180
|
-
* @param {{ home?: string, env?: NodeJS.ProcessEnv, skipChainCheck?: boolean }} [opts]
|
|
181
185
|
*/
|
|
182
186
|
export async function runSetupNonInteractive(argv, opts = {}) {
|
|
183
187
|
const flags = parseSetupFlags(argv);
|
|
@@ -227,7 +231,6 @@ export async function runSetupNonInteractive(argv, opts = {}) {
|
|
|
227
231
|
env,
|
|
228
232
|
});
|
|
229
233
|
|
|
230
|
-
// Address from key must match --address when both set
|
|
231
234
|
if (flags.keyFile && flags.address) {
|
|
232
235
|
const fromKey = getAddress(addressFromKeyFile(flags.keyFile));
|
|
233
236
|
if (fromKey !== getAddress(flags.address)) {
|
|
@@ -252,7 +255,7 @@ export async function runSetupNonInteractive(argv, opts = {}) {
|
|
|
252
255
|
}
|
|
253
256
|
|
|
254
257
|
/**
|
|
255
|
-
* Interactive wizard when stdin is a TTY.
|
|
258
|
+
* Interactive wizard (Clack) when stdin is a TTY.
|
|
256
259
|
*/
|
|
257
260
|
export async function runSetupInteractive(argv, opts = {}) {
|
|
258
261
|
const flags = parseSetupFlags(argv);
|
|
@@ -266,237 +269,249 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
266
269
|
openclawDir: opts.openclawDir,
|
|
267
270
|
});
|
|
268
271
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (!
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
);
|
|
291
|
-
console.log(
|
|
292
|
-
"Reads like `clanker whoami` can use the owner address without a private key;",
|
|
293
|
-
);
|
|
294
|
-
console.log("mint/revoke still need a key file or OPERATOR_PRIVATE_KEY later.");
|
|
295
|
-
console.log("");
|
|
296
|
-
console.log(`Profile directory: ${home}`);
|
|
297
|
-
console.log("");
|
|
298
|
-
console.log(formatSetupDetectTable(hints));
|
|
299
|
-
console.log("");
|
|
300
|
-
|
|
301
|
-
if ((hints.hasConfig || hints.hasOperator) && !flags.force) {
|
|
302
|
-
if (hints.hasConfig && !hints.hasOperator) {
|
|
303
|
-
console.log(
|
|
304
|
-
"You already have network settings from `clanker init`, but no operator identity.",
|
|
305
|
-
);
|
|
306
|
-
console.log(
|
|
307
|
-
"Continuing will refresh config.json if needed and create operator.json.",
|
|
308
|
-
);
|
|
309
|
-
const cont = await askYesNo("Continue setup?", true);
|
|
310
|
-
if (!cont) {
|
|
311
|
-
throw new Error("Aborted. Re-run `clanker setup` when ready.");
|
|
312
|
-
}
|
|
313
|
-
flags.force = true;
|
|
314
|
-
} else {
|
|
315
|
-
console.log(
|
|
316
|
-
"A full profile already exists. Continuing will REPLACE config.json and/or operator.json",
|
|
317
|
-
);
|
|
318
|
-
console.log("with the answers you give next (same files, new contents).");
|
|
319
|
-
const overwrite = await askYesNo("Replace existing profile files?", false);
|
|
320
|
-
if (!overwrite) {
|
|
321
|
-
throw new Error(
|
|
322
|
-
"Aborted. Pass --force to replace, or edit ~/.clanker/*.json by hand.",
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
flags.force = true;
|
|
272
|
+
clack.intro(c.bold("clanker setup"));
|
|
273
|
+
clack.log.step("Creates ~/.clanker/config.json (network) and operator.json (identity).");
|
|
274
|
+
clack.log.message(
|
|
275
|
+
c.dim("whoami works from owner without a key; mint/revoke need a key pointer later."),
|
|
276
|
+
);
|
|
277
|
+
clack.log.message(c.dim(`Profile: ${home}`));
|
|
278
|
+
console.log("");
|
|
279
|
+
console.log(c.dim(formatSetupDetectTable(hints)));
|
|
280
|
+
console.log("");
|
|
281
|
+
|
|
282
|
+
if ((hints.hasConfig || hints.hasOperator) && !flags.force) {
|
|
283
|
+
if (hints.hasConfig && !hints.hasOperator) {
|
|
284
|
+
const cont = cancelIf(
|
|
285
|
+
await clack.confirm({
|
|
286
|
+
message: "Network config exists; continue to create operator.json?",
|
|
287
|
+
initialValue: true,
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
if (!cont) {
|
|
291
|
+
clack.cancel("Aborted.");
|
|
292
|
+
process.exit(0);
|
|
326
293
|
}
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
let fromBlock = flags.fromBlock;
|
|
339
|
-
if (preset === "sepolia") {
|
|
340
|
-
const useFast =
|
|
341
|
-
fromBlock != null
|
|
342
|
-
? false
|
|
343
|
-
: await askYesNo(
|
|
344
|
-
`Speed up chain scans? Use fromBlock ${SEPOLIA_FAST_FROM_BLOCK} (recommended on public RPC)`,
|
|
345
|
-
true,
|
|
346
|
-
);
|
|
347
|
-
if (fromBlock == null) {
|
|
348
|
-
fromBlock = useFast ? SEPOLIA_FAST_FROM_BLOCK : PRESETS.sepolia.fromBlock;
|
|
294
|
+
flags.force = true;
|
|
295
|
+
} else {
|
|
296
|
+
const overwrite = cancelIf(
|
|
297
|
+
await clack.confirm({
|
|
298
|
+
message: "Replace existing config.json / operator.json?",
|
|
299
|
+
initialValue: false,
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
if (!overwrite) {
|
|
303
|
+
clack.cancel("Aborted. Pass --force to replace.");
|
|
304
|
+
process.exit(0);
|
|
349
305
|
}
|
|
306
|
+
flags.force = true;
|
|
350
307
|
}
|
|
308
|
+
}
|
|
351
309
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
310
|
+
let preset = flags.preset || hints.config?.preset || null;
|
|
311
|
+
if (!preset) {
|
|
312
|
+
preset = cancelIf(
|
|
313
|
+
await clack.select({
|
|
314
|
+
message: "Network",
|
|
315
|
+
options: [
|
|
316
|
+
{ value: "sepolia", label: "sepolia", hint: "public closed-beta hub" },
|
|
317
|
+
{ value: "local", label: "local", hint: "Anvil" },
|
|
318
|
+
],
|
|
319
|
+
initialValue: "sepolia",
|
|
320
|
+
}),
|
|
356
321
|
);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
322
|
+
}
|
|
323
|
+
preset = String(preset).toLowerCase();
|
|
324
|
+
if (!PRESETS[preset]) throw new Error(`Unknown preset "${preset}"`);
|
|
325
|
+
|
|
326
|
+
// Fast Sepolia default — no interactive fromBlock question
|
|
327
|
+
let fromBlock = flags.fromBlock;
|
|
328
|
+
if (fromBlock == null && preset === "sepolia") {
|
|
329
|
+
fromBlock = SEPOLIA_FAST_FROM_BLOCK;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let address = flags.address ?? null;
|
|
333
|
+
if (!address && flags.keyFile) {
|
|
334
|
+
address = addressFromKeyFile(flags.keyFile);
|
|
335
|
+
clack.log.info(`Address from --key-file: ${address}`);
|
|
336
|
+
}
|
|
337
|
+
if (!address && hints.envAddress) {
|
|
338
|
+
const useEnv = cancelIf(
|
|
339
|
+
await clack.confirm({
|
|
340
|
+
message: `Use OPERATOR_PRIVATE_KEY address (${hints.envAddress})?`,
|
|
341
|
+
initialValue: true,
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
if (useEnv) address = hints.envAddress;
|
|
345
|
+
}
|
|
346
|
+
if (!address && hints.operator?.owner) {
|
|
347
|
+
const useOp = cancelIf(
|
|
348
|
+
await clack.confirm({
|
|
349
|
+
message: `Keep existing owner (${hints.operator.owner})?`,
|
|
350
|
+
initialValue: true,
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
353
|
+
if (useOp) address = hints.operator.owner;
|
|
354
|
+
}
|
|
355
|
+
if (!address && hints.foundryAccounts.length) {
|
|
356
|
+
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…" },
|
|
363
|
+
];
|
|
364
|
+
const pick = cancelIf(
|
|
365
|
+
await clack.select({
|
|
366
|
+
message: "Operator owner source",
|
|
367
|
+
options,
|
|
368
|
+
initialValue: hints.foundryAccounts[0],
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
371
|
+
if (pick === "__paste__") {
|
|
372
|
+
address = cancelIf(
|
|
373
|
+
await clack.text({
|
|
374
|
+
message: "Operator owner address",
|
|
375
|
+
placeholder: "0x…",
|
|
376
|
+
validate: (v) =>
|
|
377
|
+
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
378
|
+
}),
|
|
373
379
|
);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
hints.foundryAccounts[0] ?? "",
|
|
380
|
+
} else {
|
|
381
|
+
address = cancelIf(
|
|
382
|
+
await clack.text({
|
|
383
|
+
message: `Paste 0x address for Foundry account "${pick}"`,
|
|
384
|
+
placeholder: "0x…",
|
|
385
|
+
validate: (v) =>
|
|
386
|
+
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
387
|
+
}),
|
|
383
388
|
);
|
|
384
|
-
if (pick) {
|
|
385
|
-
address = await ask(
|
|
386
|
-
`Paste the 0x address for "${pick}" (cast wallet address ${pick} after unlock)`,
|
|
387
|
-
"",
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
if (!address) {
|
|
392
|
-
address = await ask("Operator owner address (0x…)", "");
|
|
393
|
-
}
|
|
394
|
-
if (!address || !/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
395
|
-
throw new Error("A valid 0x operator address is required");
|
|
396
389
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
390
|
+
}
|
|
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
|
+
address = getAddress(address);
|
|
402
|
+
|
|
403
|
+
let label = flags.operator || hints.operator?.label || null;
|
|
404
|
+
if (!label) {
|
|
405
|
+
label = cancelIf(
|
|
406
|
+
await clack.text({
|
|
407
|
+
message: "Operator label",
|
|
408
|
+
placeholder: "org.openclaw.pat",
|
|
409
|
+
validate: (v) => (v && v.trim() ? undefined : "Label required"),
|
|
410
|
+
}),
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const presetCfg = PRESETS[preset];
|
|
415
|
+
const spin = clack.spinner();
|
|
416
|
+
spin.start(`Looking up "${label}" on ${preset}`);
|
|
417
|
+
let check;
|
|
418
|
+
try {
|
|
419
|
+
check = await assertSetupIdentity({
|
|
410
420
|
rpc: presetCfg.chainRpcUrl,
|
|
411
421
|
registry: presetCfg.registryAddress,
|
|
412
422
|
label,
|
|
413
423
|
address,
|
|
414
424
|
skipChainCheck: flags.skipChainCheck || !presetCfg.registryAddress,
|
|
415
425
|
});
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
`
|
|
426
|
+
spin.stop(
|
|
427
|
+
check.unregistered
|
|
428
|
+
? c.yellow(`"${label}" not registered yet — mint after setup`)
|
|
429
|
+
: check.skipped
|
|
430
|
+
? "Chain check skipped"
|
|
431
|
+
: c.green(`"${label}" active and owned by this address`),
|
|
432
|
+
);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
spin.stop(c.red("Chain check failed"));
|
|
435
|
+
throw err;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let keyFile = flags.keyFile;
|
|
439
|
+
let keyEnv = flags.keyEnv;
|
|
440
|
+
let skipKey = flags.skipKey;
|
|
441
|
+
if (!skipKey && !keyFile && !keyEnv) {
|
|
442
|
+
if (hints.hasOperatorPrivateKeyEnv) {
|
|
443
|
+
const use = cancelIf(
|
|
444
|
+
await clack.confirm({
|
|
445
|
+
message: "Store OPERATOR_PRIVATE_KEY pointer for signing?",
|
|
446
|
+
initialValue: true,
|
|
447
|
+
}),
|
|
419
448
|
);
|
|
420
|
-
|
|
421
|
-
|
|
449
|
+
if (use) keyEnv = "OPERATOR_PRIVATE_KEY";
|
|
450
|
+
else skipKey = true;
|
|
451
|
+
} else {
|
|
452
|
+
const path = cancelIf(
|
|
453
|
+
await clack.text({
|
|
454
|
+
message: "Operator key file path (Enter = read-only)",
|
|
455
|
+
placeholder: "~/.clanker/op.key",
|
|
456
|
+
}),
|
|
457
|
+
);
|
|
458
|
+
if (path && String(path).trim()) keyFile = String(path).trim();
|
|
459
|
+
else skipKey = true;
|
|
422
460
|
}
|
|
461
|
+
}
|
|
423
462
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
let keyFile = flags.keyFile;
|
|
430
|
-
let keyEnv = flags.keyEnv;
|
|
431
|
-
let skipKey = flags.skipKey;
|
|
432
|
-
if (!skipKey && !keyFile && !keyEnv) {
|
|
433
|
-
if (hints.hasOperatorPrivateKeyEnv) {
|
|
434
|
-
const use = await askYesNo(
|
|
435
|
-
"Remember OPERATOR_PRIVATE_KEY as the signing pointer in operator.json?",
|
|
436
|
-
true,
|
|
437
|
-
);
|
|
438
|
-
if (use) keyEnv = "OPERATOR_PRIVATE_KEY";
|
|
439
|
-
else skipKey = true;
|
|
440
|
-
} else {
|
|
441
|
-
const path = await ask(
|
|
442
|
-
"Path to operator private-key file (blank = read-only profile)",
|
|
443
|
-
"",
|
|
444
|
-
);
|
|
445
|
-
if (path) keyFile = path;
|
|
446
|
-
else skipKey = true;
|
|
447
|
-
}
|
|
463
|
+
const key = buildKeyPointer({ keyFile, keyEnv, skipKey, env });
|
|
464
|
+
if (key?.type === "keyFile") {
|
|
465
|
+
const fromKey = getAddress(addressFromKeyFile(key.value));
|
|
466
|
+
if (fromKey !== address) {
|
|
467
|
+
throw new Error(`Key file address ${fromKey} does not match chosen owner ${address}`);
|
|
448
468
|
}
|
|
469
|
+
}
|
|
449
470
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
456
|
-
|
|
471
|
+
clack.note(
|
|
472
|
+
[
|
|
473
|
+
`network: ${preset}`,
|
|
474
|
+
`fromBlock: ${fromBlock ?? presetCfg.fromBlock}`,
|
|
475
|
+
`label: ${label}`,
|
|
476
|
+
`owner: ${address}`,
|
|
477
|
+
`signing: ${key ? `${key.type}=${key.value}` : "none (read-only)"}`,
|
|
478
|
+
].join("\n"),
|
|
479
|
+
"Summary",
|
|
480
|
+
);
|
|
457
481
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
console.log(` fromBlock:${fromBlock ?? presetCfg.fromBlock}`);
|
|
462
|
-
console.log(` label: ${label}`);
|
|
463
|
-
console.log(` owner: ${address}`);
|
|
464
|
-
console.log(
|
|
465
|
-
` signing: ${key ? `${key.type}=${key.value}` : "none (read-only whoami/bots)"}`,
|
|
466
|
-
);
|
|
467
|
-
const ok = flags.yes || (await askYesNo("Write these files now?", true));
|
|
468
|
-
if (!ok) throw new Error("Aborted");
|
|
469
|
-
|
|
470
|
-
const result = applySetup(
|
|
471
|
-
{
|
|
472
|
-
preset,
|
|
473
|
-
force: true,
|
|
474
|
-
fromBlock: fromBlock ?? undefined,
|
|
475
|
-
registryAddress: presetCfg.registryAddress,
|
|
476
|
-
label,
|
|
477
|
-
address,
|
|
478
|
-
key,
|
|
479
|
-
},
|
|
480
|
-
{ home, env },
|
|
482
|
+
if (!flags.yes) {
|
|
483
|
+
const ok = cancelIf(
|
|
484
|
+
await clack.confirm({ message: "Write these files?", initialValue: true }),
|
|
481
485
|
);
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
console.log(`Wrote ${result.operatorPath}`);
|
|
486
|
-
if (!key) {
|
|
487
|
-
console.log("");
|
|
488
|
-
console.log("Next: clanker whoami");
|
|
489
|
-
console.log(
|
|
490
|
-
"For mint/revoke later: re-run setup with a key file, or pass --key-file / OPERATOR_PRIVATE_KEY.",
|
|
491
|
-
);
|
|
492
|
-
} else {
|
|
493
|
-
console.log("");
|
|
494
|
-
console.log("Next: clanker whoami");
|
|
486
|
+
if (!ok) {
|
|
487
|
+
clack.cancel("Aborted.");
|
|
488
|
+
process.exit(0);
|
|
495
489
|
}
|
|
496
|
-
return result;
|
|
497
|
-
} finally {
|
|
498
|
-
if (!opts.rl) rl.close();
|
|
499
490
|
}
|
|
491
|
+
|
|
492
|
+
const result = applySetup(
|
|
493
|
+
{
|
|
494
|
+
preset,
|
|
495
|
+
force: true,
|
|
496
|
+
fromBlock: fromBlock ?? undefined,
|
|
497
|
+
registryAddress: presetCfg.registryAddress,
|
|
498
|
+
label,
|
|
499
|
+
address,
|
|
500
|
+
key,
|
|
501
|
+
},
|
|
502
|
+
{ home, env },
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
clack.outro(c.green(`Wrote ${result.configPath}\nWrote ${result.operatorPath}`));
|
|
506
|
+
if (!key) {
|
|
507
|
+
nextHint([
|
|
508
|
+
"clanker whoami",
|
|
509
|
+
"clanker setup --key-file ~/.clanker/op.key --force # when you need mint",
|
|
510
|
+
]);
|
|
511
|
+
} else {
|
|
512
|
+
nextHint(["clanker whoami", `clanker bot mint <label>`]);
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
500
515
|
}
|
|
501
516
|
|
|
502
517
|
/**
|
|
@@ -506,7 +521,6 @@ export async function runSetup(argv, opts = {}) {
|
|
|
506
521
|
const flags = parseSetupFlags(argv);
|
|
507
522
|
const isTTY = opts.isTTY ?? Boolean(input.isTTY);
|
|
508
523
|
|
|
509
|
-
// Fully flagged non-interactive path
|
|
510
524
|
if (
|
|
511
525
|
!isTTY ||
|
|
512
526
|
(flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile))
|
|
@@ -514,7 +528,15 @@ export async function runSetup(argv, opts = {}) {
|
|
|
514
528
|
if (!isTTY && !(flags.preset && flags.operator)) {
|
|
515
529
|
return runSetupNonInteractive(argv, opts);
|
|
516
530
|
}
|
|
517
|
-
if (
|
|
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
|
+
) {
|
|
518
540
|
return runSetupNonInteractive(argv, opts);
|
|
519
541
|
}
|
|
520
542
|
}
|
package/lib/ui.mjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared human-path CLI UX (color, plans, next hints, structured errors).
|
|
3
|
+
* Respects NO_COLOR / non-TTY via picocolors.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import * as clack from "@clack/prompts";
|
|
8
|
+
|
|
9
|
+
export const c = {
|
|
10
|
+
dim: (s) => pc.dim(String(s)),
|
|
11
|
+
green: (s) => pc.green(String(s)),
|
|
12
|
+
yellow: (s) => pc.yellow(String(s)),
|
|
13
|
+
red: (s) => pc.red(String(s)),
|
|
14
|
+
bold: (s) => pc.bold(String(s)),
|
|
15
|
+
cyan: (s) => pc.cyan(String(s)),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string[]} argv
|
|
20
|
+
* @param {{ stdinTTY?: boolean }} [opts]
|
|
21
|
+
*/
|
|
22
|
+
export function isInteractive(argv = [], opts = {}) {
|
|
23
|
+
const tty = opts.stdinTTY ?? Boolean(process.stdin.isTTY);
|
|
24
|
+
if (!tty) return false;
|
|
25
|
+
if (argv.includes("--yes") || argv.includes("-y")) return false;
|
|
26
|
+
if (argv.includes("--json")) return false;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string|string[]} lines
|
|
32
|
+
*/
|
|
33
|
+
export function nextHint(lines) {
|
|
34
|
+
const list = Array.isArray(lines) ? lines : [lines];
|
|
35
|
+
console.log("");
|
|
36
|
+
console.log(c.bold("Next:"));
|
|
37
|
+
for (const line of list) {
|
|
38
|
+
console.log(` ${c.cyan(line)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Cargo-style human error.
|
|
44
|
+
* @param {{ error: string, because?: string, try?: string[] }} opts
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function formatCliError(opts) {
|
|
48
|
+
const lines = [c.red(`error: ${opts.error}`)];
|
|
49
|
+
if (opts.because) {
|
|
50
|
+
lines.push(c.dim(`because: ${opts.because}`));
|
|
51
|
+
}
|
|
52
|
+
if (opts.try?.length) {
|
|
53
|
+
lines.push(c.yellow("try:"));
|
|
54
|
+
for (const t of opts.try) {
|
|
55
|
+
lines.push(` ${t}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Print a plan table (key/value rows).
|
|
63
|
+
* @param {[string, string][]} rows
|
|
64
|
+
* @param {string} [title]
|
|
65
|
+
*/
|
|
66
|
+
export function printPlan(rows, title = "Plan") {
|
|
67
|
+
console.log("");
|
|
68
|
+
console.log(c.bold(title));
|
|
69
|
+
const w = Math.min(18, Math.max(4, ...rows.map(([k]) => k.length)));
|
|
70
|
+
for (const [k, v] of rows) {
|
|
71
|
+
const pad = k + " ".repeat(Math.max(0, w - k.length));
|
|
72
|
+
console.log(` ${c.dim(pad)} ${v}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Confirm a mutating plan. Skips when not interactive.
|
|
78
|
+
* @param {string[]} argv
|
|
79
|
+
* @param {[string, string][]} rows
|
|
80
|
+
* @param {string} [message]
|
|
81
|
+
* @returns {Promise<boolean>}
|
|
82
|
+
*/
|
|
83
|
+
export async function confirmPlan(argv, rows, message = "Proceed?") {
|
|
84
|
+
printPlan(rows);
|
|
85
|
+
if (!isInteractive(argv)) return true;
|
|
86
|
+
const ok = await clack.confirm({
|
|
87
|
+
message,
|
|
88
|
+
initialValue: true,
|
|
89
|
+
});
|
|
90
|
+
if (clack.isCancel(ok) || ok === false) {
|
|
91
|
+
clack.cancel("Aborted.");
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Print structured error to stderr and exit.
|
|
99
|
+
* @param {{ error: string, because?: string, try?: string[], status?: number }} opts
|
|
100
|
+
*/
|
|
101
|
+
export function exitCliError(opts) {
|
|
102
|
+
console.error(formatCliError(opts));
|
|
103
|
+
process.exit(opts.status ?? 1);
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clanker-chain/clanker-cli",
|
|
3
|
-
"version": "2026.9.7-
|
|
3
|
+
"version": "2026.9.7-4",
|
|
4
4
|
"description": "CLI for wiring clanker-chain identity and MQTT into OpenClaw and other agentic stacks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"mint-bot:sepolia": "bash ./scripts/with-sepolia-env.sh mint-bot"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"@clack/prompts": "^1.8.0",
|
|
27
|
+
"picocolors": "^1.1.1",
|
|
26
28
|
"viem": "^2.21.0"
|
|
27
29
|
}
|
|
28
30
|
}
|