@clanker-chain/clanker-cli 2026.9.12 → 2026.9.13
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/chain-identity.mjs +40 -62
- package/bin/clanker.mjs +50 -12
- package/lib/doctor.mjs +20 -7
- package/lib/fund.mjs +61 -24
- package/lib/login.mjs +200 -0
- package/lib/operator-key.mjs +2 -3
- package/lib/operator-signer.mjs +284 -0
- package/lib/pair.mjs +8 -20
- package/lib/privy-client.mjs +419 -0
- package/lib/privy-constants.mjs +20 -0
- package/lib/privy-hpke.mjs +101 -0
- package/lib/privy-session.mjs +151 -0
- package/lib/profile.mjs +1 -1
- package/lib/resolve.mjs +11 -6
- package/lib/setup.mjs +122 -16
- package/package.json +6 -1
package/lib/resolve.mjs
CHANGED
|
@@ -78,6 +78,11 @@ export function keyPointerFromSource(opts) {
|
|
|
78
78
|
return { type: "env", value: "OPERATOR_PRIVATE_KEY" };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Honest error when mint/pair/rotate/transfer have no owner signer. */
|
|
82
|
+
export const OWNER_SIGNER_REQUIRED =
|
|
83
|
+
"Owner actions need clanker login (email vault), or a signing key (--key-file / OPERATOR_PRIVATE_KEY). " +
|
|
84
|
+
"A read-only profile can still run whoami, bots, fund, and doctor.";
|
|
85
|
+
|
|
81
86
|
/**
|
|
82
87
|
* Resolve the operator signing key with Anvil guard.
|
|
83
88
|
*
|
|
@@ -133,11 +138,7 @@ export function resolveOperatorKey(argv = [], opts = {}) {
|
|
|
133
138
|
key = normalizePrivateKey(ANVIL_DEFAULT_PRIVATE_KEY);
|
|
134
139
|
source = "anvil-default (local RPC)";
|
|
135
140
|
} else {
|
|
136
|
-
throw new Error(
|
|
137
|
-
"Operator private key required for non-local RPC. Set OPERATOR_PRIVATE_KEY, " +
|
|
138
|
-
"pass --key / --key-file, or configure ~/.clanker/operator.json key pointer. " +
|
|
139
|
-
"Anvil account #0 is not used on public networks.",
|
|
140
|
-
);
|
|
141
|
+
throw new Error(OWNER_SIGNER_REQUIRED);
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
|
|
@@ -216,7 +217,11 @@ export function resolveReadIdentity(argv = [], opts = {}) {
|
|
|
216
217
|
};
|
|
217
218
|
} catch (err) {
|
|
218
219
|
const msg = err?.message ?? String(err);
|
|
219
|
-
if (
|
|
220
|
+
if (
|
|
221
|
+
!/private key required|Owner actions need|clanker login|Anvil account #0|Key file not found|Profile keyFile/i.test(
|
|
222
|
+
msg,
|
|
223
|
+
)
|
|
224
|
+
) {
|
|
220
225
|
throw err;
|
|
221
226
|
}
|
|
222
227
|
}
|
package/lib/setup.mjs
CHANGED
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { stdin as input } from "node:process";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
chmodSync,
|
|
8
|
+
copyFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { basename, join } from "node:path";
|
|
7
14
|
import { getAddress } from "viem";
|
|
8
15
|
import * as clack from "@clack/prompts";
|
|
9
16
|
import {
|
|
@@ -37,8 +44,9 @@ import {
|
|
|
37
44
|
consumerFundHints,
|
|
38
45
|
generateOperatorKeyFile,
|
|
39
46
|
} from "./operator-key.mjs";
|
|
47
|
+
import { wireOpenClawMqtt } from "./openclaw-wire.mjs";
|
|
48
|
+
import { runLogin } from "./login.mjs";
|
|
40
49
|
import { c, nextHint } from "./ui.mjs";
|
|
41
|
-
import { join } from "node:path";
|
|
42
50
|
|
|
43
51
|
/**
|
|
44
52
|
* @param {string[]} argv
|
|
@@ -57,6 +65,7 @@ export function parseSetupFlags(argv) {
|
|
|
57
65
|
let foundryAccount = null;
|
|
58
66
|
let exportKey = false;
|
|
59
67
|
let generateKey = false;
|
|
68
|
+
let botKey = null;
|
|
60
69
|
|
|
61
70
|
for (let i = 0; i < argv.length; i += 1) {
|
|
62
71
|
const a = argv[i];
|
|
@@ -67,6 +76,7 @@ export function parseSetupFlags(argv) {
|
|
|
67
76
|
else if (a === "--key-env" && argv[i + 1]) keyEnv = argv[++i];
|
|
68
77
|
else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
|
|
69
78
|
else if (a === "--foundry-account" && argv[i + 1]) foundryAccount = argv[++i];
|
|
79
|
+
else if (a === "--bot-key" && argv[i + 1]) botKey = argv[++i];
|
|
70
80
|
else if (a === "--export-key") exportKey = true;
|
|
71
81
|
else if (a === "--generate-key") generateKey = true;
|
|
72
82
|
else if (a === "--force") force = true;
|
|
@@ -89,6 +99,7 @@ export function parseSetupFlags(argv) {
|
|
|
89
99
|
foundryAccount,
|
|
90
100
|
exportKey,
|
|
91
101
|
generateKey,
|
|
102
|
+
botKey,
|
|
92
103
|
};
|
|
93
104
|
}
|
|
94
105
|
|
|
@@ -162,6 +173,39 @@ export function buildKeyPointer({ keyFile, keyEnv, skipKey, env = process.env })
|
|
|
162
173
|
return null;
|
|
163
174
|
}
|
|
164
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Copy a downloaded bot key into ~/.openclaw/keys and wire channels.mqtt.
|
|
178
|
+
* @param {string} botKeyPath
|
|
179
|
+
* @param {{
|
|
180
|
+
* operatorLabel: string,
|
|
181
|
+
* network: { rpc: string, registry: string|null, brokerUrl?: string, mqttAuthServiceUrl?: string },
|
|
182
|
+
* openclawHome?: string,
|
|
183
|
+
* }} opts
|
|
184
|
+
*/
|
|
185
|
+
export function attachBotKeyFile(botKeyPath, opts) {
|
|
186
|
+
if (!existsSync(botKeyPath)) {
|
|
187
|
+
throw new Error(`Bot key file not found: ${botKeyPath}`);
|
|
188
|
+
}
|
|
189
|
+
const botId = basename(botKeyPath).replace(/\.key$/i, "");
|
|
190
|
+
if (!botId || botId.includes("/") || botId.includes("..")) {
|
|
191
|
+
throw new Error(`Invalid bot key basename: ${botKeyPath}`);
|
|
192
|
+
}
|
|
193
|
+
const openclawHome = opts.openclawHome ?? join(homedir(), ".openclaw");
|
|
194
|
+
const destDir = join(openclawHome, "keys");
|
|
195
|
+
mkdirSync(destDir, { recursive: true });
|
|
196
|
+
const dest = join(destDir, `${botId}.key`);
|
|
197
|
+
copyFileSync(botKeyPath, dest);
|
|
198
|
+
chmodSync(dest, 0o600);
|
|
199
|
+
const wired = wireOpenClawMqtt({
|
|
200
|
+
botId,
|
|
201
|
+
operatorId: opts.operatorLabel,
|
|
202
|
+
network: opts.network,
|
|
203
|
+
keyPath: dest,
|
|
204
|
+
openclawHome,
|
|
205
|
+
});
|
|
206
|
+
return { botId, keyPath: dest, openclawPath: wired.path };
|
|
207
|
+
}
|
|
208
|
+
|
|
165
209
|
/**
|
|
166
210
|
* Apply setup choices to disk.
|
|
167
211
|
*/
|
|
@@ -299,7 +343,7 @@ export async function runSetupNonInteractive(argv, opts = {}) {
|
|
|
299
343
|
}
|
|
300
344
|
}
|
|
301
345
|
|
|
302
|
-
|
|
346
|
+
const result = applySetup(
|
|
303
347
|
{
|
|
304
348
|
preset: flags.preset,
|
|
305
349
|
force: flags.force || flags.yes,
|
|
@@ -311,6 +355,34 @@ export async function runSetupNonInteractive(argv, opts = {}) {
|
|
|
311
355
|
},
|
|
312
356
|
{ home, env },
|
|
313
357
|
);
|
|
358
|
+
return finishSetupResult(result, flags, { ...opts, quiet: true });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* @param {object} result
|
|
363
|
+
* @param {{ botKey?: string|null, operator?: string|null }} flags
|
|
364
|
+
* @param {{ openclawHome?: string, quiet?: boolean }} [opts]
|
|
365
|
+
*/
|
|
366
|
+
async function finishSetupResult(result, flags, opts = {}) {
|
|
367
|
+
if (flags.botKey) {
|
|
368
|
+
const attached = attachBotKeyFile(flags.botKey, {
|
|
369
|
+
operatorLabel: result.operator?.label ?? flags.operator,
|
|
370
|
+
network: {
|
|
371
|
+
rpc: result.config.chainRpcUrl,
|
|
372
|
+
registry: result.config.registryAddress,
|
|
373
|
+
brokerUrl: result.config.brokerUrl,
|
|
374
|
+
mqttAuthServiceUrl: result.config.mqttAuthServiceUrl,
|
|
375
|
+
},
|
|
376
|
+
openclawHome: opts.openclawHome,
|
|
377
|
+
});
|
|
378
|
+
result.botKey = attached;
|
|
379
|
+
if (!opts.quiet) {
|
|
380
|
+
clack.log.success(
|
|
381
|
+
`Bot key → ${attached.keyPath}; wired ${attached.openclawPath}`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
314
386
|
}
|
|
315
387
|
|
|
316
388
|
/**
|
|
@@ -334,7 +406,7 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
334
406
|
);
|
|
335
407
|
clack.log.message(
|
|
336
408
|
c.dim(
|
|
337
|
-
"
|
|
409
|
+
"Sign in with email (recommended), attach an existing 0x, or create ~/.clanker/op.key.",
|
|
338
410
|
),
|
|
339
411
|
);
|
|
340
412
|
clack.log.message(c.dim(`Profile: ${home}`));
|
|
@@ -395,6 +467,7 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
395
467
|
let address = flags.address ?? null;
|
|
396
468
|
let foundryAccountUsed = null;
|
|
397
469
|
let generatedKeyFile = null;
|
|
470
|
+
let privyKeyPointer = null;
|
|
398
471
|
if (!address && flags.generateKey) {
|
|
399
472
|
const dest = flags.keyFile || defaultOperatorKeyPath(home);
|
|
400
473
|
let forceGen = flags.force;
|
|
@@ -440,10 +513,20 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
440
513
|
}
|
|
441
514
|
if (!address) {
|
|
442
515
|
const options = [
|
|
516
|
+
{
|
|
517
|
+
value: "__login__",
|
|
518
|
+
label: "Sign in (email — recommended)",
|
|
519
|
+
hint: "Privy vault — no op.key on disk",
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
value: "__paste__",
|
|
523
|
+
label: "I already have an owner address",
|
|
524
|
+
hint: "/join or any 0x — usually read-only until login",
|
|
525
|
+
},
|
|
443
526
|
{
|
|
444
527
|
value: "__generate__",
|
|
445
528
|
label: "Create a new operator key for me",
|
|
446
|
-
hint: "writes ~/.clanker/op.key (
|
|
529
|
+
hint: "writes ~/.clanker/op.key (self-custody)",
|
|
447
530
|
},
|
|
448
531
|
{
|
|
449
532
|
value: "__keyfile__",
|
|
@@ -466,21 +549,27 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
466
549
|
hint: "advanced — install Foundry first",
|
|
467
550
|
});
|
|
468
551
|
}
|
|
469
|
-
options.push({
|
|
470
|
-
value: "__paste__",
|
|
471
|
-
label: "Paste an address…",
|
|
472
|
-
hint: "read-only unless you add a key later",
|
|
473
|
-
});
|
|
474
552
|
|
|
475
553
|
const pick = cancelIf(
|
|
476
554
|
await clack.select({
|
|
477
555
|
message: "How do you want to set your operator identity?",
|
|
478
556
|
options,
|
|
479
|
-
initialValue: "
|
|
557
|
+
initialValue: "__login__",
|
|
480
558
|
}),
|
|
481
559
|
);
|
|
482
560
|
|
|
483
|
-
|
|
561
|
+
let attachReadOnly = false;
|
|
562
|
+
if (pick === "__login__") {
|
|
563
|
+
clack.log.step("Opening browser for clanker login…");
|
|
564
|
+
const logged = await runLogin([], {
|
|
565
|
+
home,
|
|
566
|
+
env,
|
|
567
|
+
});
|
|
568
|
+
address = logged.address;
|
|
569
|
+
privyKeyPointer = { type: "privy", value: logged.walletId };
|
|
570
|
+
flags.skipKey = true;
|
|
571
|
+
clack.log.success(`Signed in as ${address}`);
|
|
572
|
+
} else if (pick === "__generate__") {
|
|
484
573
|
const dest = defaultOperatorKeyPath(home);
|
|
485
574
|
let forceGen = flags.force;
|
|
486
575
|
if (existsSync(dest) && !forceGen) {
|
|
@@ -526,15 +615,17 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
526
615
|
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
527
616
|
}),
|
|
528
617
|
);
|
|
618
|
+
attachReadOnly = true;
|
|
529
619
|
} else if (pick === "__paste__") {
|
|
530
620
|
address = cancelIf(
|
|
531
621
|
await clack.text({
|
|
532
|
-
message: "
|
|
622
|
+
message: "Owner address (from /join or any EOA)",
|
|
533
623
|
placeholder: "0x…",
|
|
534
624
|
validate: (v) =>
|
|
535
625
|
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
536
626
|
}),
|
|
537
627
|
);
|
|
628
|
+
attachReadOnly = true;
|
|
538
629
|
} else {
|
|
539
630
|
foundryAccountUsed = pick;
|
|
540
631
|
try {
|
|
@@ -557,6 +648,9 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
557
648
|
);
|
|
558
649
|
}
|
|
559
650
|
}
|
|
651
|
+
if (attachReadOnly) {
|
|
652
|
+
flags.skipKey = true;
|
|
653
|
+
}
|
|
560
654
|
}
|
|
561
655
|
address = getAddress(address);
|
|
562
656
|
|
|
@@ -645,7 +739,9 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
645
739
|
}
|
|
646
740
|
}
|
|
647
741
|
|
|
648
|
-
const key =
|
|
742
|
+
const key =
|
|
743
|
+
privyKeyPointer ||
|
|
744
|
+
buildKeyPointer({ keyFile, keyEnv, skipKey, env });
|
|
649
745
|
if (key?.type === "keyFile") {
|
|
650
746
|
const fromKey = getAddress(addressFromKeyFile(key.value));
|
|
651
747
|
if (fromKey !== address) {
|
|
@@ -687,11 +783,21 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
687
783
|
{ home, env },
|
|
688
784
|
);
|
|
689
785
|
|
|
786
|
+
await finishSetupResult(result, { ...flags, operator: label }, opts);
|
|
787
|
+
|
|
690
788
|
clack.outro(c.green(`Wrote ${result.configPath}\nWrote ${result.operatorPath}`));
|
|
691
|
-
if (
|
|
789
|
+
if (privyKeyPointer) {
|
|
692
790
|
nextHint([
|
|
791
|
+
"clanker fund",
|
|
792
|
+
"clanker whoami",
|
|
793
|
+
"clanker pair add <peer> --yes",
|
|
794
|
+
"clanker operator mint <label> --yes # if not minted yet",
|
|
795
|
+
]);
|
|
796
|
+
} else if (!key) {
|
|
797
|
+
nextHint([
|
|
798
|
+
"clanker login # to enable mint/pair with the email vault",
|
|
799
|
+
"clanker fund",
|
|
693
800
|
"clanker whoami",
|
|
694
|
-
"clanker setup — choose Create a new operator key when you need mint",
|
|
695
801
|
]);
|
|
696
802
|
} else if (preset === "sepolia" && generatedKeyFile) {
|
|
697
803
|
nextHint(consumerFundHints({ address, label }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clanker-chain/clanker-cli",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.13",
|
|
4
4
|
"description": "CLI for wiring clanker-chain identity and MQTT into OpenClaw and other agentic stacks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@clack/prompts": "^1.8.0",
|
|
27
|
+
"@hpke/chacha20poly1305": "^1.8.0",
|
|
28
|
+
"@hpke/core": "^1.9.0",
|
|
29
|
+
"@noble/curves": "^1.9.7",
|
|
30
|
+
"@noble/hashes": "^1.8.0",
|
|
31
|
+
"canonicalize": "^2.1.0",
|
|
27
32
|
"picocolors": "^1.1.1",
|
|
28
33
|
"viem": "^2.56.3"
|
|
29
34
|
},
|