@clanker-chain/clanker-cli 2026.9.8 → 2026.9.12
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 +239 -21
- package/lib/clanker-identity-abi.mjs +18 -0
- package/lib/doctor.mjs +91 -4
- package/lib/fund.mjs +250 -0
- package/lib/identity-query.mjs +1 -1
- package/lib/mint-budget.mjs +237 -0
- package/lib/openclaw-wire.mjs +43 -2
- package/lib/operator-key.mjs +58 -0
- package/lib/pair.mjs +252 -0
- package/lib/setup.mjs +157 -39
- package/package.json +5 -2
package/lib/pair.mjs
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator pairing client (Policy layer — docs/trust-model.md).
|
|
3
|
+
* Signs with the operator owner key; never the bot key.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
10
|
+
import { loadOperator } from "./profile.mjs";
|
|
11
|
+
import { openclawConfigPath } from "./openclaw-wire.mjs";
|
|
12
|
+
import { resolveOperatorKey } from "./resolve.mjs";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} baseUrl
|
|
16
|
+
* @param {{ operatorId: string, action: 'add'|'remove'|'list', peerLabel?: string }} opts
|
|
17
|
+
*/
|
|
18
|
+
export async function fetchPairNonce(baseUrl, opts) {
|
|
19
|
+
const url = new URL("/pair-nonce", baseUrl.replace(/\/$/, ""));
|
|
20
|
+
url.searchParams.set("operator_id", opts.operatorId);
|
|
21
|
+
url.searchParams.set("action", opts.action);
|
|
22
|
+
if (opts.peerLabel) url.searchParams.set("peer_label", opts.peerLabel);
|
|
23
|
+
const res = await fetch(url.toString());
|
|
24
|
+
const text = await res.text();
|
|
25
|
+
let body;
|
|
26
|
+
try {
|
|
27
|
+
body = JSON.parse(text);
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error(`pair-nonce: unexpected response: ${text}`);
|
|
30
|
+
}
|
|
31
|
+
if (!res.ok || !body.nonce || !body.message) {
|
|
32
|
+
throw new Error(body.error || `pair-nonce failed: HTTP ${res.status}`);
|
|
33
|
+
}
|
|
34
|
+
return /** @type {{ nonce: string, message: string, expires_at?: string }} */ (body);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} baseUrl
|
|
39
|
+
* @param {{
|
|
40
|
+
* operatorId: string,
|
|
41
|
+
* peerLabel: string,
|
|
42
|
+
* action: 'add'|'remove',
|
|
43
|
+
* nonce: string,
|
|
44
|
+
* signature: `0x${string}`,
|
|
45
|
+
* }} opts
|
|
46
|
+
*/
|
|
47
|
+
export async function postPair(baseUrl, opts) {
|
|
48
|
+
const res = await fetch(new URL("/pair", baseUrl.replace(/\/$/, "")).toString(), {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "content-type": "application/json" },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
operator_id: opts.operatorId,
|
|
53
|
+
peer_label: opts.peerLabel,
|
|
54
|
+
action: opts.action,
|
|
55
|
+
nonce: opts.nonce,
|
|
56
|
+
signature: opts.signature,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
const text = await res.text();
|
|
60
|
+
let body;
|
|
61
|
+
try {
|
|
62
|
+
body = JSON.parse(text);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new Error(`pair: unexpected response: ${text}`);
|
|
65
|
+
}
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
throw new Error(body.error || `pair failed: HTTP ${res.status}`);
|
|
68
|
+
}
|
|
69
|
+
return body;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} baseUrl
|
|
74
|
+
* @param {{ operatorId: string, nonce: string, signature: `0x${string}` }} opts
|
|
75
|
+
*/
|
|
76
|
+
export async function getPairList(baseUrl, opts) {
|
|
77
|
+
const url = new URL("/pair", baseUrl.replace(/\/$/, ""));
|
|
78
|
+
url.searchParams.set("operator_id", opts.operatorId);
|
|
79
|
+
url.searchParams.set("nonce", opts.nonce);
|
|
80
|
+
url.searchParams.set("signature", opts.signature);
|
|
81
|
+
const res = await fetch(url.toString());
|
|
82
|
+
const text = await res.text();
|
|
83
|
+
let body;
|
|
84
|
+
try {
|
|
85
|
+
body = JSON.parse(text);
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error(`pair list: unexpected response: ${text}`);
|
|
88
|
+
}
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
throw new Error(body.error || `pair list failed: HTTP ${res.status}`);
|
|
91
|
+
}
|
|
92
|
+
return body;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Merge allowOperators + dmPolicy into ~/.openclaw/openclaw.json channels.mqtt.
|
|
97
|
+
* @param {{ peerLabel: string, action: 'add'|'remove', openclawHome?: string }} opts
|
|
98
|
+
*/
|
|
99
|
+
export function syncAllowOperators(opts) {
|
|
100
|
+
const openclawHome = opts.openclawHome ?? join(homedir(), ".openclaw");
|
|
101
|
+
const cfgPath = openclawConfigPath(openclawHome);
|
|
102
|
+
if (!existsSync(cfgPath)) {
|
|
103
|
+
return { synced: false, path: cfgPath, reason: "missing_openclaw_json" };
|
|
104
|
+
}
|
|
105
|
+
const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
106
|
+
const channels = { ...(cfg.channels ?? {}) };
|
|
107
|
+
const mqtt = { ...(channels.mqtt && typeof channels.mqtt === "object" ? channels.mqtt : {}) };
|
|
108
|
+
const prev = Array.isArray(mqtt.allowOperators)
|
|
109
|
+
? mqtt.allowOperators.filter((x) => typeof x === "string")
|
|
110
|
+
: [];
|
|
111
|
+
let next;
|
|
112
|
+
if (opts.action === "add") {
|
|
113
|
+
next = prev.includes(opts.peerLabel) ? prev : [...prev, opts.peerLabel];
|
|
114
|
+
} else {
|
|
115
|
+
next = prev.filter((x) => x !== opts.peerLabel);
|
|
116
|
+
}
|
|
117
|
+
mqtt.allowOperators = next;
|
|
118
|
+
if (!mqtt.dmPolicy) mqtt.dmPolicy = "pairing";
|
|
119
|
+
channels.mqtt = mqtt;
|
|
120
|
+
cfg.channels = channels;
|
|
121
|
+
mkdirSync(openclawHome, { recursive: true });
|
|
122
|
+
writeFileSync(cfgPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
|
|
123
|
+
return { synced: true, path: cfgPath, allowOperators: next };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve mqtt-auth base URL from network / env / flag.
|
|
128
|
+
* @param {string[]} argv
|
|
129
|
+
* @param {{ mqttAuthServiceUrl?: string|null }} network
|
|
130
|
+
*/
|
|
131
|
+
export function resolvePairAuthUrl(argv, network) {
|
|
132
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
133
|
+
if (argv[i] === "--auth-url" && argv[i + 1]) return argv[i + 1];
|
|
134
|
+
}
|
|
135
|
+
if (process.env.MQTT_AUTH_SERVICE_URL) return process.env.MQTT_AUTH_SERVICE_URL;
|
|
136
|
+
if (network.mqttAuthServiceUrl) return network.mqttAuthServiceUrl;
|
|
137
|
+
throw new Error(
|
|
138
|
+
"mqttAuthServiceUrl required for pairing. Set profile via clanker setup, " +
|
|
139
|
+
"pass --auth-url, or set MQTT_AUTH_SERVICE_URL.",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Optional OpenClaw home for allowOperators sync (directory containing openclaw.json).
|
|
145
|
+
* @param {string[]} argv
|
|
146
|
+
* @returns {string|undefined}
|
|
147
|
+
*/
|
|
148
|
+
export function resolveOpenclawHome(argv) {
|
|
149
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
150
|
+
if (argv[i] === "--openclaw-home" && argv[i + 1]) return argv[i + 1];
|
|
151
|
+
}
|
|
152
|
+
if (process.env.OPENCLAW_HOME) return process.env.OPENCLAW_HOME;
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {string[]} argv
|
|
158
|
+
* @param {'add'|'remove'|'list'|'status'} action
|
|
159
|
+
* @param {string|null} peerLabel
|
|
160
|
+
*/
|
|
161
|
+
export async function runPairAction(argv, action, peerLabel) {
|
|
162
|
+
const resolved = resolveOperatorKey(argv, { requireRegistry: true });
|
|
163
|
+
const operator =
|
|
164
|
+
loadOperator(resolved.network.home) ??
|
|
165
|
+
(resolved.network.operatorLabel
|
|
166
|
+
? { label: resolved.network.operatorLabel }
|
|
167
|
+
: null);
|
|
168
|
+
const operatorId =
|
|
169
|
+
argv.includes("--operator") && argv[argv.indexOf("--operator") + 1]
|
|
170
|
+
? argv[argv.indexOf("--operator") + 1]
|
|
171
|
+
: operator?.label;
|
|
172
|
+
if (!operatorId) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
"Operator label required. Run clanker setup / write operator.json, or pass --operator org.you.",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const authUrl = resolvePairAuthUrl(argv, resolved.network);
|
|
179
|
+
const account = privateKeyToAccount(resolved.key);
|
|
180
|
+
|
|
181
|
+
if (action === "list" || action === "status") {
|
|
182
|
+
const { nonce, message } = await fetchPairNonce(authUrl, {
|
|
183
|
+
operatorId,
|
|
184
|
+
action: "list",
|
|
185
|
+
});
|
|
186
|
+
const signature = await account.signMessage({ message });
|
|
187
|
+
const listed = await getPairList(authUrl, {
|
|
188
|
+
operatorId,
|
|
189
|
+
nonce,
|
|
190
|
+
signature,
|
|
191
|
+
});
|
|
192
|
+
if (action === "status") {
|
|
193
|
+
if (!peerLabel) throw new Error("Usage: clanker pair status <operator-label>");
|
|
194
|
+
// Resolve peer by scanning allows — mutual flag is per peer_operator_id;
|
|
195
|
+
// we match by requesting add-style peer label via a second pair-nonce is heavy.
|
|
196
|
+
// Instead: check if any allow entry is mutual when peer label hashes match… we
|
|
197
|
+
// only have peer_operator_id hex. Re-fetch is unnecessary: call add-path status
|
|
198
|
+
// by computing whether peer is in allow list via a dedicated pair status using
|
|
199
|
+
// the peer label through POST-less approach — GET list doesn't include labels.
|
|
200
|
+
// Best effort: show mutual if we can add then remove? No.
|
|
201
|
+
// Call /pair-nonce + post with action that doesn't mutate? Use list + ask user.
|
|
202
|
+
// Simpler: after list, for status we POST nothing — check if peer's keccak is in list.
|
|
203
|
+
// We need peer id: keccak of peerLabel.
|
|
204
|
+
const { keccak256, toBytes } = await import("viem");
|
|
205
|
+
const peerId = keccak256(toBytes(peerLabel)).toLowerCase();
|
|
206
|
+
const hit = (listed.allows ?? []).find(
|
|
207
|
+
(a) => String(a.peer_operator_id).toLowerCase() === peerId,
|
|
208
|
+
);
|
|
209
|
+
return {
|
|
210
|
+
action: "status",
|
|
211
|
+
operatorId,
|
|
212
|
+
peerLabel,
|
|
213
|
+
allowed: Boolean(hit),
|
|
214
|
+
mutual: hit?.mutual === true,
|
|
215
|
+
authUrl,
|
|
216
|
+
listed,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return { action: "list", operatorId, authUrl, listed };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!peerLabel) {
|
|
223
|
+
throw new Error(`Usage: clanker pair ${action} <operator-label>`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const { nonce, message } = await fetchPairNonce(authUrl, {
|
|
227
|
+
operatorId,
|
|
228
|
+
action,
|
|
229
|
+
peerLabel,
|
|
230
|
+
});
|
|
231
|
+
const signature = await account.signMessage({ message });
|
|
232
|
+
const result = await postPair(authUrl, {
|
|
233
|
+
operatorId,
|
|
234
|
+
peerLabel,
|
|
235
|
+
action,
|
|
236
|
+
nonce,
|
|
237
|
+
signature,
|
|
238
|
+
});
|
|
239
|
+
const sync = syncAllowOperators({
|
|
240
|
+
peerLabel,
|
|
241
|
+
action,
|
|
242
|
+
openclawHome: resolveOpenclawHome(argv),
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
action,
|
|
246
|
+
operatorId,
|
|
247
|
+
peerLabel,
|
|
248
|
+
authUrl,
|
|
249
|
+
result,
|
|
250
|
+
openclawSync: sync,
|
|
251
|
+
};
|
|
252
|
+
}
|
package/lib/setup.mjs
CHANGED
|
@@ -33,6 +33,10 @@ import {
|
|
|
33
33
|
exportFoundryKey,
|
|
34
34
|
resolveFoundryAddress,
|
|
35
35
|
} from "./foundry.mjs";
|
|
36
|
+
import {
|
|
37
|
+
consumerFundHints,
|
|
38
|
+
generateOperatorKeyFile,
|
|
39
|
+
} from "./operator-key.mjs";
|
|
36
40
|
import { c, nextHint } from "./ui.mjs";
|
|
37
41
|
import { join } from "node:path";
|
|
38
42
|
|
|
@@ -52,6 +56,7 @@ export function parseSetupFlags(argv) {
|
|
|
52
56
|
let skipChainCheck = false;
|
|
53
57
|
let foundryAccount = null;
|
|
54
58
|
let exportKey = false;
|
|
59
|
+
let generateKey = false;
|
|
55
60
|
|
|
56
61
|
for (let i = 0; i < argv.length; i += 1) {
|
|
57
62
|
const a = argv[i];
|
|
@@ -63,6 +68,7 @@ export function parseSetupFlags(argv) {
|
|
|
63
68
|
else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
|
|
64
69
|
else if (a === "--foundry-account" && argv[i + 1]) foundryAccount = argv[++i];
|
|
65
70
|
else if (a === "--export-key") exportKey = true;
|
|
71
|
+
else if (a === "--generate-key") generateKey = true;
|
|
66
72
|
else if (a === "--force") force = true;
|
|
67
73
|
else if (a === "--yes" || a === "-y") yes = true;
|
|
68
74
|
else if (a === "--skip-key") skipKey = true;
|
|
@@ -82,6 +88,7 @@ export function parseSetupFlags(argv) {
|
|
|
82
88
|
skipChainCheck,
|
|
83
89
|
foundryAccount,
|
|
84
90
|
exportKey,
|
|
91
|
+
generateKey,
|
|
85
92
|
};
|
|
86
93
|
}
|
|
87
94
|
|
|
@@ -203,7 +210,7 @@ export async function runSetupNonInteractive(argv, opts = {}) {
|
|
|
203
210
|
if (!flags.preset || !PRESETS[flags.preset]) {
|
|
204
211
|
throw new Error(
|
|
205
212
|
"Non-interactive setup requires --preset sepolia|local (stdin is not a TTY). " +
|
|
206
|
-
"Also pass --operator <label> and --address
|
|
213
|
+
"Also pass --operator <label> and --generate-key (or --address / --key-file)",
|
|
207
214
|
);
|
|
208
215
|
}
|
|
209
216
|
if (!flags.operator) {
|
|
@@ -218,6 +225,23 @@ export async function runSetupNonInteractive(argv, opts = {}) {
|
|
|
218
225
|
inheritStdio: false,
|
|
219
226
|
};
|
|
220
227
|
|
|
228
|
+
if (flags.generateKey) {
|
|
229
|
+
if (flags.skipKey) {
|
|
230
|
+
throw new Error("Cannot combine --generate-key with --skip-key");
|
|
231
|
+
}
|
|
232
|
+
const dest = keyFile || defaultOperatorKeyPath(home);
|
|
233
|
+
const created = generateOperatorKeyFile(dest, {
|
|
234
|
+
force: flags.force,
|
|
235
|
+
});
|
|
236
|
+
keyFile = created.path;
|
|
237
|
+
if (address && getAddress(address) !== getAddress(created.address)) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Generated key address ${created.address} does not match --address ${getAddress(address)}`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
address = created.address;
|
|
243
|
+
}
|
|
244
|
+
|
|
221
245
|
if (flags.foundryAccount) {
|
|
222
246
|
if (!address) {
|
|
223
247
|
address = resolveFoundryAddress(flags.foundryAccount, castOpts);
|
|
@@ -238,7 +262,7 @@ export async function runSetupNonInteractive(argv, opts = {}) {
|
|
|
238
262
|
if (!address && env.OPERATOR_PRIVATE_KEY) address = addressFromEnv(env);
|
|
239
263
|
if (!address) {
|
|
240
264
|
throw new Error(
|
|
241
|
-
"Non-interactive setup requires --address 0x…, --key-file, OPERATOR_PRIVATE_KEY, or --foundry-account",
|
|
265
|
+
"Non-interactive setup requires --generate-key, --address 0x…, --key-file, OPERATOR_PRIVATE_KEY, or --foundry-account",
|
|
242
266
|
);
|
|
243
267
|
}
|
|
244
268
|
|
|
@@ -305,9 +329,13 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
305
329
|
});
|
|
306
330
|
|
|
307
331
|
clack.intro(c.bold("clanker setup"));
|
|
308
|
-
clack.log.step(
|
|
332
|
+
clack.log.step(
|
|
333
|
+
"Sets up your operator identity (org account) and network for OpenClaw bots.",
|
|
334
|
+
);
|
|
309
335
|
clack.log.message(
|
|
310
|
-
c.dim(
|
|
336
|
+
c.dim(
|
|
337
|
+
"New here? Create a key file — no wallet app or Foundry required. Mint needs a little test ETH later.",
|
|
338
|
+
),
|
|
311
339
|
);
|
|
312
340
|
clack.log.message(c.dim(`Profile: ${home}`));
|
|
313
341
|
console.log("");
|
|
@@ -366,6 +394,28 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
366
394
|
|
|
367
395
|
let address = flags.address ?? null;
|
|
368
396
|
let foundryAccountUsed = null;
|
|
397
|
+
let generatedKeyFile = null;
|
|
398
|
+
if (!address && flags.generateKey) {
|
|
399
|
+
const dest = flags.keyFile || defaultOperatorKeyPath(home);
|
|
400
|
+
let forceGen = flags.force;
|
|
401
|
+
if (existsSync(dest) && !forceGen) {
|
|
402
|
+
forceGen = cancelIf(
|
|
403
|
+
await clack.confirm({
|
|
404
|
+
message: `Overwrite existing ${dest}?`,
|
|
405
|
+
initialValue: false,
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
if (!forceGen) {
|
|
409
|
+
clack.cancel("Aborted.");
|
|
410
|
+
process.exit(0);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const created = generateOperatorKeyFile(dest, { force: true });
|
|
414
|
+
generatedKeyFile = created.path;
|
|
415
|
+
address = created.address;
|
|
416
|
+
clack.log.success(`Created operator key at ${created.path}`);
|
|
417
|
+
clack.log.info(`Your operator address: ${created.address}`);
|
|
418
|
+
}
|
|
369
419
|
if (!address && flags.keyFile) {
|
|
370
420
|
address = addressFromKeyFile(flags.keyFile);
|
|
371
421
|
clack.log.info(`Address from --key-file: ${address}`);
|
|
@@ -388,23 +438,95 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
388
438
|
);
|
|
389
439
|
if (useOp) address = hints.operator.owner;
|
|
390
440
|
}
|
|
391
|
-
if (!address
|
|
441
|
+
if (!address) {
|
|
392
442
|
const options = [
|
|
393
|
-
|
|
394
|
-
value:
|
|
395
|
-
label:
|
|
396
|
-
hint: "
|
|
397
|
-
}
|
|
398
|
-
{
|
|
443
|
+
{
|
|
444
|
+
value: "__generate__",
|
|
445
|
+
label: "Create a new operator key for me",
|
|
446
|
+
hint: "writes ~/.clanker/op.key (recommended)",
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
value: "__keyfile__",
|
|
450
|
+
label: "Use an existing key file…",
|
|
451
|
+
hint: "path to a 0x private key file",
|
|
452
|
+
},
|
|
399
453
|
];
|
|
454
|
+
if (hints.foundryAccounts.length) {
|
|
455
|
+
for (const n of hints.foundryAccounts) {
|
|
456
|
+
options.push({
|
|
457
|
+
value: n,
|
|
458
|
+
label: `Foundry: ${n}`,
|
|
459
|
+
hint: "advanced — cast wallet",
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
options.push({
|
|
464
|
+
value: "__foundry_missing__",
|
|
465
|
+
label: "Foundry account…",
|
|
466
|
+
hint: "advanced — install Foundry first",
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
options.push({
|
|
470
|
+
value: "__paste__",
|
|
471
|
+
label: "Paste an address…",
|
|
472
|
+
hint: "read-only unless you add a key later",
|
|
473
|
+
});
|
|
474
|
+
|
|
400
475
|
const pick = cancelIf(
|
|
401
476
|
await clack.select({
|
|
402
|
-
message: "
|
|
477
|
+
message: "How do you want to set your operator identity?",
|
|
403
478
|
options,
|
|
404
|
-
initialValue:
|
|
479
|
+
initialValue: "__generate__",
|
|
405
480
|
}),
|
|
406
481
|
);
|
|
407
|
-
|
|
482
|
+
|
|
483
|
+
if (pick === "__generate__") {
|
|
484
|
+
const dest = defaultOperatorKeyPath(home);
|
|
485
|
+
let forceGen = flags.force;
|
|
486
|
+
if (existsSync(dest) && !forceGen) {
|
|
487
|
+
forceGen = cancelIf(
|
|
488
|
+
await clack.confirm({
|
|
489
|
+
message: `Overwrite existing ${dest}?`,
|
|
490
|
+
initialValue: false,
|
|
491
|
+
}),
|
|
492
|
+
);
|
|
493
|
+
if (!forceGen) {
|
|
494
|
+
clack.cancel("Aborted.");
|
|
495
|
+
process.exit(0);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const created = generateOperatorKeyFile(dest, { force: true });
|
|
499
|
+
generatedKeyFile = created.path;
|
|
500
|
+
address = created.address;
|
|
501
|
+
clack.log.success(`Created operator key at ${created.path}`);
|
|
502
|
+
clack.log.info(`Your operator address: ${created.address}`);
|
|
503
|
+
} else if (pick === "__keyfile__") {
|
|
504
|
+
const path = cancelIf(
|
|
505
|
+
await clack.text({
|
|
506
|
+
message: "Path to operator key file",
|
|
507
|
+
placeholder: join(home, "op.key"),
|
|
508
|
+
validate: (v) =>
|
|
509
|
+
v && String(v).trim() && existsSync(String(v).trim())
|
|
510
|
+
? undefined
|
|
511
|
+
: "File not found",
|
|
512
|
+
}),
|
|
513
|
+
);
|
|
514
|
+
generatedKeyFile = String(path).trim();
|
|
515
|
+
address = addressFromKeyFile(generatedKeyFile);
|
|
516
|
+
clack.log.info(`Address from key file: ${address}`);
|
|
517
|
+
} else if (pick === "__foundry_missing__") {
|
|
518
|
+
clack.log.warn(
|
|
519
|
+
"Foundry (cast) is not available. Install https://book.getfoundry.sh/ or choose Create a new operator key.",
|
|
520
|
+
);
|
|
521
|
+
address = cancelIf(
|
|
522
|
+
await clack.text({
|
|
523
|
+
message: "Operator owner address",
|
|
524
|
+
placeholder: "0x…",
|
|
525
|
+
validate: (v) =>
|
|
526
|
+
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
527
|
+
}),
|
|
528
|
+
);
|
|
529
|
+
} else if (pick === "__paste__") {
|
|
408
530
|
address = cancelIf(
|
|
409
531
|
await clack.text({
|
|
410
532
|
message: "Operator owner address",
|
|
@@ -436,16 +558,6 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
436
558
|
}
|
|
437
559
|
}
|
|
438
560
|
}
|
|
439
|
-
if (!address) {
|
|
440
|
-
address = cancelIf(
|
|
441
|
-
await clack.text({
|
|
442
|
-
message: "Operator owner address",
|
|
443
|
-
placeholder: "0x…",
|
|
444
|
-
validate: (v) =>
|
|
445
|
-
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
446
|
-
}),
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
561
|
address = getAddress(address);
|
|
450
562
|
|
|
451
563
|
let label = flags.operator || hints.operator?.label || null;
|
|
@@ -483,7 +595,7 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
483
595
|
throw err;
|
|
484
596
|
}
|
|
485
597
|
|
|
486
|
-
let keyFile = flags.keyFile;
|
|
598
|
+
let keyFile = flags.keyFile || generatedKeyFile;
|
|
487
599
|
let keyEnv = flags.keyEnv;
|
|
488
600
|
let skipKey = flags.skipKey;
|
|
489
601
|
if (!skipKey && !keyFile && !keyEnv) {
|
|
@@ -579,7 +691,16 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
579
691
|
if (!key) {
|
|
580
692
|
nextHint([
|
|
581
693
|
"clanker whoami",
|
|
582
|
-
"clanker setup
|
|
694
|
+
"clanker setup — choose Create a new operator key when you need mint",
|
|
695
|
+
]);
|
|
696
|
+
} else if (preset === "sepolia" && generatedKeyFile) {
|
|
697
|
+
nextHint(consumerFundHints({ address, label }));
|
|
698
|
+
} else if (preset === "sepolia") {
|
|
699
|
+
nextHint([
|
|
700
|
+
"clanker fund",
|
|
701
|
+
"clanker doctor",
|
|
702
|
+
"clanker whoami",
|
|
703
|
+
`clanker bot mint <label>`,
|
|
583
704
|
]);
|
|
584
705
|
} else {
|
|
585
706
|
nextHint(["clanker whoami", `clanker bot mint <label>`]);
|
|
@@ -593,23 +714,20 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
593
714
|
export async function runSetup(argv, opts = {}) {
|
|
594
715
|
const flags = parseSetupFlags(argv);
|
|
595
716
|
const isTTY = opts.isTTY ?? Boolean(input.isTTY);
|
|
717
|
+
const hasIdentitySource = Boolean(
|
|
718
|
+
flags.address ||
|
|
719
|
+
flags.keyFile ||
|
|
720
|
+
flags.generateKey ||
|
|
721
|
+
flags.foundryAccount ||
|
|
722
|
+
opts.env?.OPERATOR_PRIVATE_KEY ||
|
|
723
|
+
process.env.OPERATOR_PRIVATE_KEY,
|
|
724
|
+
);
|
|
596
725
|
|
|
597
|
-
if (
|
|
598
|
-
!isTTY ||
|
|
599
|
-
(flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile))
|
|
600
|
-
) {
|
|
726
|
+
if (!isTTY || (flags.yes && flags.preset && flags.operator && hasIdentitySource)) {
|
|
601
727
|
if (!isTTY && !(flags.preset && flags.operator)) {
|
|
602
728
|
return runSetupNonInteractive(argv, opts);
|
|
603
729
|
}
|
|
604
|
-
if (
|
|
605
|
-
flags.yes &&
|
|
606
|
-
flags.preset &&
|
|
607
|
-
flags.operator &&
|
|
608
|
-
(flags.address ||
|
|
609
|
-
flags.keyFile ||
|
|
610
|
-
opts.env?.OPERATOR_PRIVATE_KEY ||
|
|
611
|
-
process.env.OPERATOR_PRIVATE_KEY)
|
|
612
|
-
) {
|
|
730
|
+
if (flags.yes && flags.preset && flags.operator && hasIdentitySource) {
|
|
613
731
|
return runSetupNonInteractive(argv, opts);
|
|
614
732
|
}
|
|
615
733
|
}
|
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.12",
|
|
4
4
|
"description": "CLI for wiring clanker-chain identity and MQTT into OpenClaw and other agentic stacks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@clack/prompts": "^1.8.0",
|
|
27
27
|
"picocolors": "^1.1.1",
|
|
28
|
-
"viem": "^2.
|
|
28
|
+
"viem": "^2.56.3"
|
|
29
|
+
},
|
|
30
|
+
"overrides": {
|
|
31
|
+
"ws": "^8.21.0"
|
|
29
32
|
}
|
|
30
33
|
}
|