@clanker-chain/clanker-cli 2026.9.8 → 2026.9.10
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 +126 -12
- package/lib/identity-query.mjs +1 -1
- package/lib/openclaw-wire.mjs +43 -2
- package/lib/operator-key.mjs +50 -0
- package/lib/pair.mjs +252 -0
- package/lib/setup.mjs +158 -39
- package/package.json +1 -1
package/bin/clanker.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { existsSync, mkdirSync } from "node:fs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import os from "node:os";
|
|
7
8
|
import process from "node:process";
|
|
8
9
|
import { getAddress } from "viem";
|
|
@@ -26,6 +27,8 @@ import { resolveForRead, resolveOperatorKey, resolveReadIdentity } from "../lib/
|
|
|
26
27
|
import { runSetup } from "../lib/setup.mjs";
|
|
27
28
|
import { runDoctor } from "../lib/doctor.mjs";
|
|
28
29
|
import {
|
|
30
|
+
botIdentityCard,
|
|
31
|
+
botIdentityJson,
|
|
29
32
|
hubConnectChecklist,
|
|
30
33
|
wireOpenClawMqtt,
|
|
31
34
|
} from "../lib/openclaw-wire.mjs";
|
|
@@ -95,8 +98,15 @@ function runScript(scriptPath, args = []) {
|
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
function findRepoRoot() {
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
102
|
+
for (let i = 0; i < 8; i++) {
|
|
103
|
+
if (existsSync(join(dir, "chain", "foundry.toml"))) return dir;
|
|
104
|
+
const parent = dirname(dir);
|
|
105
|
+
if (parent === dir) break;
|
|
106
|
+
dir = parent;
|
|
107
|
+
}
|
|
108
|
+
// npm install: package root (no monorepo checkout)
|
|
109
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
/** chain up/deploy and check * need the monorepo; npm installs only ship bin/ + lib/. */
|
|
@@ -167,6 +177,10 @@ Usage:
|
|
|
167
177
|
clanker bot status <label> [--json]
|
|
168
178
|
clanker bot revoke <label> [--json] [--yes]
|
|
169
179
|
clanker bot rotate <label> <newKeyAddress> [--json] [--yes]
|
|
180
|
+
clanker pair add <operator-label> [--yes] [--auth-url URL] [--openclaw-home DIR]
|
|
181
|
+
clanker pair remove <operator-label> [--auth-url URL] [--openclaw-home DIR]
|
|
182
|
+
clanker pair list [--auth-url URL]
|
|
183
|
+
clanker pair status <operator-label> [--auth-url URL]
|
|
170
184
|
clanker init-openclaw
|
|
171
185
|
clanker chain up|deploy|mint-operator|mint-bot|rotate-bot-key|revoke-bot ...
|
|
172
186
|
clanker check mqtt <bot_id> <operator_id>
|
|
@@ -178,10 +192,11 @@ Profile:
|
|
|
178
192
|
~/.clanker/keys/ bot keys (also written to ~/.openclaw/keys/)
|
|
179
193
|
|
|
180
194
|
Humans: \`clanker setup\` then \`clanker doctor\` / \`whoami\`.
|
|
195
|
+
Pairing (Policy): both operators run \`clanker pair add\` before DMs deliver on the hub.
|
|
181
196
|
Mutates print a plan and confirm unless --yes or --json.
|
|
182
197
|
whoami is fast by default; pass --with-bots to enrich child bots (or use \`clanker bots\`).
|
|
183
198
|
|
|
184
|
-
See docs/operator-cli.md.
|
|
199
|
+
See docs/operator-cli.md and docs/trust-model.md.
|
|
185
200
|
`);
|
|
186
201
|
}
|
|
187
202
|
|
|
@@ -439,6 +454,93 @@ async function main() {
|
|
|
439
454
|
return;
|
|
440
455
|
}
|
|
441
456
|
|
|
457
|
+
if (cmd === "pair") {
|
|
458
|
+
const [sub, ...pairRest] = rest;
|
|
459
|
+
if (!sub || !["add", "remove", "list", "status"].includes(sub)) {
|
|
460
|
+
console.error("Usage: clanker pair add|remove|list|status [operator-label]");
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
let peerLabel = null;
|
|
465
|
+
for (let i = 0; i < pairRest.length; i += 1) {
|
|
466
|
+
const a = pairRest[i];
|
|
467
|
+
if (a.startsWith("--")) {
|
|
468
|
+
if (a !== "--json" && a !== "--yes" && pairRest[i + 1] && !pairRest[i + 1].startsWith("--")) {
|
|
469
|
+
i += 1; // skip flag value
|
|
470
|
+
}
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
peerLabel = a;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const { runPairAction } = await import("../lib/pair.mjs");
|
|
478
|
+
try {
|
|
479
|
+
if (sub === "add") {
|
|
480
|
+
const planRows = [
|
|
481
|
+
["action", "pair add (Policy)"],
|
|
482
|
+
["peer", peerLabel ?? "(missing)"],
|
|
483
|
+
["note", "One-way allow; peer must pair you for bidirectional DMs"],
|
|
484
|
+
];
|
|
485
|
+
const ok = await confirmPlan(pairRest, planRows, `Allow operator ${peerLabel}?`);
|
|
486
|
+
if (!ok) process.exit(0);
|
|
487
|
+
}
|
|
488
|
+
const out = await runPairAction(pairRest, sub, peerLabel);
|
|
489
|
+
if (hasFlag(pairRest, "--json")) {
|
|
490
|
+
console.log(JSON.stringify(out, null, 2));
|
|
491
|
+
} else if (sub === "list") {
|
|
492
|
+
console.log(`Operator: ${out.operatorId}`);
|
|
493
|
+
console.log(`Auth: ${out.authUrl}`);
|
|
494
|
+
const allows = out.listed?.allows ?? [];
|
|
495
|
+
if (!allows.length) {
|
|
496
|
+
console.log("(no allows)");
|
|
497
|
+
} else {
|
|
498
|
+
for (const a of allows) {
|
|
499
|
+
console.log(
|
|
500
|
+
` ${a.peer_operator_id} ${a.mutual ? "mutual" : "pending (one-way)"}`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} else if (sub === "status") {
|
|
505
|
+
console.log(`peer: ${out.peerLabel}`);
|
|
506
|
+
console.log(`allowed: ${out.allowed}`);
|
|
507
|
+
console.log(`mutual: ${out.mutual}`);
|
|
508
|
+
if (out.allowed && !out.mutual) {
|
|
509
|
+
console.log("Waiting for peer to run: clanker pair add <your-operator>");
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
console.log(
|
|
513
|
+
`${sub} ${out.peerLabel}: ${out.result?.mutual ? "mutual" : "one-way / pending"}`,
|
|
514
|
+
);
|
|
515
|
+
if (out.openclawSync?.synced) {
|
|
516
|
+
console.log(`Synced allowOperators → ${out.openclawSync.path}`);
|
|
517
|
+
console.log(
|
|
518
|
+
"Restart (or reload) the OpenClaw gateway so channels.mqtt.allowOperators is picked up.",
|
|
519
|
+
);
|
|
520
|
+
} else if (out.openclawSync?.reason === "missing_openclaw_json") {
|
|
521
|
+
console.log(
|
|
522
|
+
`Hub Policy updated, but no openclaw.json at ${out.openclawSync.path} — ` +
|
|
523
|
+
"client allowOperators was not synced. Create OpenClaw config or set allowOperators manually.",
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (sub === "add" && !out.result?.mutual) {
|
|
527
|
+
nextHint(`Ask ${out.peerLabel} to run: clanker pair add ${out.operatorId}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
} catch (err) {
|
|
531
|
+
exitCliError({
|
|
532
|
+
error: err.message,
|
|
533
|
+
because: "pairing needs operator key + mqtt-auth /pair endpoints",
|
|
534
|
+
try: [
|
|
535
|
+
"clanker doctor",
|
|
536
|
+
"clanker pair add org.peer --auth-url http://127.0.0.1:9090 --yes",
|
|
537
|
+
"See docs/trust-model.md",
|
|
538
|
+
],
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
442
544
|
if (cmd === "operator") {
|
|
443
545
|
const [sub, ...opArgv] = rest;
|
|
444
546
|
const {
|
|
@@ -615,18 +717,30 @@ async function main() {
|
|
|
615
717
|
keyPath: result.key_path,
|
|
616
718
|
});
|
|
617
719
|
if (hasFlag(flags, "--json")) {
|
|
618
|
-
printJson({
|
|
720
|
+
printJson({
|
|
721
|
+
...result,
|
|
722
|
+
openclaw: wire,
|
|
723
|
+
bot_identity: botIdentityJson({
|
|
724
|
+
botId: botLabel,
|
|
725
|
+
keyPath: result.key_path,
|
|
726
|
+
openclawConfigPath: wire.path,
|
|
727
|
+
}),
|
|
728
|
+
});
|
|
619
729
|
} else {
|
|
620
730
|
console.log(c.green(`Minted bot ${botLabel} under ${operatorLabel}`));
|
|
621
731
|
console.log(`botKey: ${result.bot_key}`);
|
|
622
|
-
console.log(`key file: ${result.key_path}`);
|
|
623
|
-
console.log(`also: ${result.clanker_key_path}`);
|
|
624
732
|
console.log(`tx: ${result.tx}`);
|
|
625
|
-
console.log(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
733
|
+
console.log(c.dim(`also: ${result.clanker_key_path}`));
|
|
734
|
+
console.log("");
|
|
735
|
+
for (const line of botIdentityCard({
|
|
736
|
+
botId: botLabel,
|
|
737
|
+
operatorId: operatorLabel,
|
|
738
|
+
keyPath: result.key_path,
|
|
739
|
+
openclawPath: wire.path,
|
|
740
|
+
created: wire.created,
|
|
741
|
+
})) {
|
|
742
|
+
console.log(line);
|
|
743
|
+
}
|
|
630
744
|
nextHint(
|
|
631
745
|
hubConnectChecklist({
|
|
632
746
|
botId: botLabel,
|
|
@@ -921,7 +1035,7 @@ async function main() {
|
|
|
921
1035
|
nextHint([
|
|
922
1036
|
"clanker bot mint <label>",
|
|
923
1037
|
"Then channels.mqtt.botId / operatorId / privateKeyFile update automatically",
|
|
924
|
-
"See docs/operator-cli.md and docs/
|
|
1038
|
+
"See docs/operator-cli.md and docs/public-testnet-hub.md",
|
|
925
1039
|
]);
|
|
926
1040
|
process.exit(0);
|
|
927
1041
|
}
|
package/lib/identity-query.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "viem";
|
|
14
14
|
import { clankerIdentityAbi } from "./clanker-identity-abi.mjs";
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/** Public RPC eth_getLogs chunk size — wide ranges are often rejected. */
|
|
17
17
|
export const LOG_CHUNK_BLOCKS = 2000n;
|
|
18
18
|
|
|
19
19
|
export function labelToId(label) {
|
package/lib/openclaw-wire.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { join } from "node:path";
|
|
|
8
8
|
import { harnessSnippet } from "./profile.mjs";
|
|
9
9
|
|
|
10
10
|
/** Current published OpenClaw plugin pins for closed-beta invite. */
|
|
11
|
-
export const OPENCLAW_PLUGIN_PIN = "2026.
|
|
11
|
+
export const OPENCLAW_PLUGIN_PIN = "2026.9.10";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* @param {string} [openclawHome]
|
|
@@ -94,11 +94,52 @@ export function wireOpenClawMqtt(opts) {
|
|
|
94
94
|
export function hubConnectChecklist(opts) {
|
|
95
95
|
const pin = opts.pluginPin ?? OPENCLAW_PLUGIN_PIN;
|
|
96
96
|
return [
|
|
97
|
+
"Your bot login is already wired — install plugins and restart OpenClaw.",
|
|
97
98
|
`openclaw plugins install @clanker-chain/mqtt-channel-plugin@${pin}`,
|
|
98
99
|
`openclaw plugins install @clanker-chain/mqtt-tools@${pin}`,
|
|
99
100
|
"Enable plugin ids mqtt + mqtt-tools (already set in openclaw.json if we wired it)",
|
|
100
101
|
`CONNECT broker: ${opts.channelsMqtt?.brokerUrl ?? "mqtts://mqtt.clanker-chain.com:8883"}`,
|
|
101
|
-
|
|
102
|
+
"Both operators: clanker pair add <peer-operator> (Policy — required before DMs deliver)",
|
|
102
103
|
"DM openclaw.france.prod-1 to smoke the mesh",
|
|
103
104
|
];
|
|
104
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Plain-language card: what the bot uses vs operator mint key.
|
|
109
|
+
* @param {{
|
|
110
|
+
* botId: string,
|
|
111
|
+
* operatorId: string,
|
|
112
|
+
* keyPath: string,
|
|
113
|
+
* openclawPath: string,
|
|
114
|
+
* created?: boolean,
|
|
115
|
+
* }} opts
|
|
116
|
+
* @returns {string[]}
|
|
117
|
+
*/
|
|
118
|
+
export function botIdentityCard(opts) {
|
|
119
|
+
const openclawState = opts.created
|
|
120
|
+
? `${opts.openclawPath} (channels.mqtt created)`
|
|
121
|
+
: `${opts.openclawPath} (channels.mqtt updated)`;
|
|
122
|
+
return [
|
|
123
|
+
"Bot identity (what OpenClaw uses to CONNECT):",
|
|
124
|
+
`bot id: ${opts.botId}`,
|
|
125
|
+
`operator: ${opts.operatorId}`,
|
|
126
|
+
`bot key: ${opts.keyPath}`,
|
|
127
|
+
`openclaw: ${openclawState}`,
|
|
128
|
+
"Do not give the bot ~/.clanker/op.key — that key is only for mint/transfer.",
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Structured bot_identity for --json mint output.
|
|
134
|
+
* @param {{ botId: string, keyPath: string, openclawConfigPath: string }} opts
|
|
135
|
+
*/
|
|
136
|
+
export function botIdentityJson(opts) {
|
|
137
|
+
return {
|
|
138
|
+
botId: opts.botId,
|
|
139
|
+
keyPath: opts.keyPath,
|
|
140
|
+
openclawConfigPath: opts.openclawConfigPath,
|
|
141
|
+
warnOperatorKey:
|
|
142
|
+
"Do not give the bot ~/.clanker/op.key — that key is only for mint/transfer.",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer-path operator key helpers (generate local op.key).
|
|
3
|
+
* Never logs private keys.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
9
|
+
import { defaultOperatorKeyPath } from "./foundry.mjs";
|
|
10
|
+
|
|
11
|
+
export { defaultOperatorKeyPath };
|
|
12
|
+
|
|
13
|
+
/** Coinbase Developer Platform faucet UI (Base Sepolia). */
|
|
14
|
+
export const BASE_SEPOLIA_FAUCET_URL =
|
|
15
|
+
"https://portal.cdp.coinbase.com/products/faucet";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate a new secp256k1 key and write it to destPath (mode 0o600).
|
|
19
|
+
* Does not print the key.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} destPath
|
|
22
|
+
* @param {{ force?: boolean }} [opts]
|
|
23
|
+
* @returns {{ path: string, address: string }}
|
|
24
|
+
*/
|
|
25
|
+
export function generateOperatorKeyFile(destPath, opts = {}) {
|
|
26
|
+
if (existsSync(destPath) && !opts.force) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Key file already exists: ${destPath} (pass --force to overwrite)`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const key = generatePrivateKey();
|
|
32
|
+
const address = privateKeyToAccount(key).address;
|
|
33
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
34
|
+
writeFileSync(destPath, `${key}\n`, { mode: 0o600 });
|
|
35
|
+
return { path: destPath, address };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Plain-language next steps after creating an operator key on Sepolia.
|
|
40
|
+
* @param {{ address: string, label?: string }} opts
|
|
41
|
+
*/
|
|
42
|
+
export function consumerFundHints(opts) {
|
|
43
|
+
const mintLabel = opts.label ? ` ${opts.label}` : "";
|
|
44
|
+
return [
|
|
45
|
+
`Your operator address is ${opts.address} — fund it with Base Sepolia ETH: ${BASE_SEPOLIA_FAUCET_URL}`,
|
|
46
|
+
"clanker doctor",
|
|
47
|
+
`clanker operator mint${mintLabel} --yes`,
|
|
48
|
+
"clanker bot mint <bot_label> --yes",
|
|
49
|
+
];
|
|
50
|
+
}
|
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,11 @@ import {
|
|
|
33
33
|
exportFoundryKey,
|
|
34
34
|
resolveFoundryAddress,
|
|
35
35
|
} from "./foundry.mjs";
|
|
36
|
+
import {
|
|
37
|
+
BASE_SEPOLIA_FAUCET_URL,
|
|
38
|
+
consumerFundHints,
|
|
39
|
+
generateOperatorKeyFile,
|
|
40
|
+
} from "./operator-key.mjs";
|
|
36
41
|
import { c, nextHint } from "./ui.mjs";
|
|
37
42
|
import { join } from "node:path";
|
|
38
43
|
|
|
@@ -52,6 +57,7 @@ export function parseSetupFlags(argv) {
|
|
|
52
57
|
let skipChainCheck = false;
|
|
53
58
|
let foundryAccount = null;
|
|
54
59
|
let exportKey = false;
|
|
60
|
+
let generateKey = false;
|
|
55
61
|
|
|
56
62
|
for (let i = 0; i < argv.length; i += 1) {
|
|
57
63
|
const a = argv[i];
|
|
@@ -63,6 +69,7 @@ export function parseSetupFlags(argv) {
|
|
|
63
69
|
else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
|
|
64
70
|
else if (a === "--foundry-account" && argv[i + 1]) foundryAccount = argv[++i];
|
|
65
71
|
else if (a === "--export-key") exportKey = true;
|
|
72
|
+
else if (a === "--generate-key") generateKey = true;
|
|
66
73
|
else if (a === "--force") force = true;
|
|
67
74
|
else if (a === "--yes" || a === "-y") yes = true;
|
|
68
75
|
else if (a === "--skip-key") skipKey = true;
|
|
@@ -82,6 +89,7 @@ export function parseSetupFlags(argv) {
|
|
|
82
89
|
skipChainCheck,
|
|
83
90
|
foundryAccount,
|
|
84
91
|
exportKey,
|
|
92
|
+
generateKey,
|
|
85
93
|
};
|
|
86
94
|
}
|
|
87
95
|
|
|
@@ -203,7 +211,7 @@ export async function runSetupNonInteractive(argv, opts = {}) {
|
|
|
203
211
|
if (!flags.preset || !PRESETS[flags.preset]) {
|
|
204
212
|
throw new Error(
|
|
205
213
|
"Non-interactive setup requires --preset sepolia|local (stdin is not a TTY). " +
|
|
206
|
-
"Also pass --operator <label> and --address
|
|
214
|
+
"Also pass --operator <label> and --generate-key (or --address / --key-file)",
|
|
207
215
|
);
|
|
208
216
|
}
|
|
209
217
|
if (!flags.operator) {
|
|
@@ -218,6 +226,23 @@ export async function runSetupNonInteractive(argv, opts = {}) {
|
|
|
218
226
|
inheritStdio: false,
|
|
219
227
|
};
|
|
220
228
|
|
|
229
|
+
if (flags.generateKey) {
|
|
230
|
+
if (flags.skipKey) {
|
|
231
|
+
throw new Error("Cannot combine --generate-key with --skip-key");
|
|
232
|
+
}
|
|
233
|
+
const dest = keyFile || defaultOperatorKeyPath(home);
|
|
234
|
+
const created = generateOperatorKeyFile(dest, {
|
|
235
|
+
force: flags.force,
|
|
236
|
+
});
|
|
237
|
+
keyFile = created.path;
|
|
238
|
+
if (address && getAddress(address) !== getAddress(created.address)) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Generated key address ${created.address} does not match --address ${getAddress(address)}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
address = created.address;
|
|
244
|
+
}
|
|
245
|
+
|
|
221
246
|
if (flags.foundryAccount) {
|
|
222
247
|
if (!address) {
|
|
223
248
|
address = resolveFoundryAddress(flags.foundryAccount, castOpts);
|
|
@@ -238,7 +263,7 @@ export async function runSetupNonInteractive(argv, opts = {}) {
|
|
|
238
263
|
if (!address && env.OPERATOR_PRIVATE_KEY) address = addressFromEnv(env);
|
|
239
264
|
if (!address) {
|
|
240
265
|
throw new Error(
|
|
241
|
-
"Non-interactive setup requires --address 0x…, --key-file, OPERATOR_PRIVATE_KEY, or --foundry-account",
|
|
266
|
+
"Non-interactive setup requires --generate-key, --address 0x…, --key-file, OPERATOR_PRIVATE_KEY, or --foundry-account",
|
|
242
267
|
);
|
|
243
268
|
}
|
|
244
269
|
|
|
@@ -305,9 +330,13 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
305
330
|
});
|
|
306
331
|
|
|
307
332
|
clack.intro(c.bold("clanker setup"));
|
|
308
|
-
clack.log.step(
|
|
333
|
+
clack.log.step(
|
|
334
|
+
"Sets up your operator identity (org account) and network for OpenClaw bots.",
|
|
335
|
+
);
|
|
309
336
|
clack.log.message(
|
|
310
|
-
c.dim(
|
|
337
|
+
c.dim(
|
|
338
|
+
"New here? Create a key file — no wallet app or Foundry required. Mint needs a little test ETH later.",
|
|
339
|
+
),
|
|
311
340
|
);
|
|
312
341
|
clack.log.message(c.dim(`Profile: ${home}`));
|
|
313
342
|
console.log("");
|
|
@@ -366,6 +395,28 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
366
395
|
|
|
367
396
|
let address = flags.address ?? null;
|
|
368
397
|
let foundryAccountUsed = null;
|
|
398
|
+
let generatedKeyFile = null;
|
|
399
|
+
if (!address && flags.generateKey) {
|
|
400
|
+
const dest = flags.keyFile || defaultOperatorKeyPath(home);
|
|
401
|
+
let forceGen = flags.force;
|
|
402
|
+
if (existsSync(dest) && !forceGen) {
|
|
403
|
+
forceGen = cancelIf(
|
|
404
|
+
await clack.confirm({
|
|
405
|
+
message: `Overwrite existing ${dest}?`,
|
|
406
|
+
initialValue: false,
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
if (!forceGen) {
|
|
410
|
+
clack.cancel("Aborted.");
|
|
411
|
+
process.exit(0);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const created = generateOperatorKeyFile(dest, { force: true });
|
|
415
|
+
generatedKeyFile = created.path;
|
|
416
|
+
address = created.address;
|
|
417
|
+
clack.log.success(`Created operator key at ${created.path}`);
|
|
418
|
+
clack.log.info(`Your operator address: ${created.address}`);
|
|
419
|
+
}
|
|
369
420
|
if (!address && flags.keyFile) {
|
|
370
421
|
address = addressFromKeyFile(flags.keyFile);
|
|
371
422
|
clack.log.info(`Address from --key-file: ${address}`);
|
|
@@ -388,23 +439,95 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
388
439
|
);
|
|
389
440
|
if (useOp) address = hints.operator.owner;
|
|
390
441
|
}
|
|
391
|
-
if (!address
|
|
442
|
+
if (!address) {
|
|
392
443
|
const options = [
|
|
393
|
-
|
|
394
|
-
value:
|
|
395
|
-
label:
|
|
396
|
-
hint: "
|
|
397
|
-
}
|
|
398
|
-
{
|
|
444
|
+
{
|
|
445
|
+
value: "__generate__",
|
|
446
|
+
label: "Create a new operator key for me",
|
|
447
|
+
hint: "writes ~/.clanker/op.key (recommended)",
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
value: "__keyfile__",
|
|
451
|
+
label: "Use an existing key file…",
|
|
452
|
+
hint: "path to a 0x private key file",
|
|
453
|
+
},
|
|
399
454
|
];
|
|
455
|
+
if (hints.foundryAccounts.length) {
|
|
456
|
+
for (const n of hints.foundryAccounts) {
|
|
457
|
+
options.push({
|
|
458
|
+
value: n,
|
|
459
|
+
label: `Foundry: ${n}`,
|
|
460
|
+
hint: "advanced — cast wallet",
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
options.push({
|
|
465
|
+
value: "__foundry_missing__",
|
|
466
|
+
label: "Foundry account…",
|
|
467
|
+
hint: "advanced — install Foundry first",
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
options.push({
|
|
471
|
+
value: "__paste__",
|
|
472
|
+
label: "Paste an address…",
|
|
473
|
+
hint: "read-only unless you add a key later",
|
|
474
|
+
});
|
|
475
|
+
|
|
400
476
|
const pick = cancelIf(
|
|
401
477
|
await clack.select({
|
|
402
|
-
message: "
|
|
478
|
+
message: "How do you want to set your operator identity?",
|
|
403
479
|
options,
|
|
404
|
-
initialValue:
|
|
480
|
+
initialValue: "__generate__",
|
|
405
481
|
}),
|
|
406
482
|
);
|
|
407
|
-
|
|
483
|
+
|
|
484
|
+
if (pick === "__generate__") {
|
|
485
|
+
const dest = defaultOperatorKeyPath(home);
|
|
486
|
+
let forceGen = flags.force;
|
|
487
|
+
if (existsSync(dest) && !forceGen) {
|
|
488
|
+
forceGen = cancelIf(
|
|
489
|
+
await clack.confirm({
|
|
490
|
+
message: `Overwrite existing ${dest}?`,
|
|
491
|
+
initialValue: false,
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
if (!forceGen) {
|
|
495
|
+
clack.cancel("Aborted.");
|
|
496
|
+
process.exit(0);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const created = generateOperatorKeyFile(dest, { force: true });
|
|
500
|
+
generatedKeyFile = created.path;
|
|
501
|
+
address = created.address;
|
|
502
|
+
clack.log.success(`Created operator key at ${created.path}`);
|
|
503
|
+
clack.log.info(`Your operator address: ${created.address}`);
|
|
504
|
+
} else if (pick === "__keyfile__") {
|
|
505
|
+
const path = cancelIf(
|
|
506
|
+
await clack.text({
|
|
507
|
+
message: "Path to operator key file",
|
|
508
|
+
placeholder: join(home, "op.key"),
|
|
509
|
+
validate: (v) =>
|
|
510
|
+
v && String(v).trim() && existsSync(String(v).trim())
|
|
511
|
+
? undefined
|
|
512
|
+
: "File not found",
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
generatedKeyFile = String(path).trim();
|
|
516
|
+
address = addressFromKeyFile(generatedKeyFile);
|
|
517
|
+
clack.log.info(`Address from key file: ${address}`);
|
|
518
|
+
} else if (pick === "__foundry_missing__") {
|
|
519
|
+
clack.log.warn(
|
|
520
|
+
"Foundry (cast) is not available. Install https://book.getfoundry.sh/ or choose Create a new operator key.",
|
|
521
|
+
);
|
|
522
|
+
address = cancelIf(
|
|
523
|
+
await clack.text({
|
|
524
|
+
message: "Operator owner address",
|
|
525
|
+
placeholder: "0x…",
|
|
526
|
+
validate: (v) =>
|
|
527
|
+
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
528
|
+
}),
|
|
529
|
+
);
|
|
530
|
+
} else if (pick === "__paste__") {
|
|
408
531
|
address = cancelIf(
|
|
409
532
|
await clack.text({
|
|
410
533
|
message: "Operator owner address",
|
|
@@ -436,16 +559,6 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
436
559
|
}
|
|
437
560
|
}
|
|
438
561
|
}
|
|
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
562
|
address = getAddress(address);
|
|
450
563
|
|
|
451
564
|
let label = flags.operator || hints.operator?.label || null;
|
|
@@ -483,7 +596,7 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
483
596
|
throw err;
|
|
484
597
|
}
|
|
485
598
|
|
|
486
|
-
let keyFile = flags.keyFile;
|
|
599
|
+
let keyFile = flags.keyFile || generatedKeyFile;
|
|
487
600
|
let keyEnv = flags.keyEnv;
|
|
488
601
|
let skipKey = flags.skipKey;
|
|
489
602
|
if (!skipKey && !keyFile && !keyEnv) {
|
|
@@ -579,7 +692,16 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
579
692
|
if (!key) {
|
|
580
693
|
nextHint([
|
|
581
694
|
"clanker whoami",
|
|
582
|
-
"clanker setup
|
|
695
|
+
"clanker setup — choose Create a new operator key when you need mint",
|
|
696
|
+
]);
|
|
697
|
+
} else if (preset === "sepolia" && generatedKeyFile) {
|
|
698
|
+
nextHint(consumerFundHints({ address, label }));
|
|
699
|
+
} else if (preset === "sepolia") {
|
|
700
|
+
nextHint([
|
|
701
|
+
`If this address needs test ETH: ${BASE_SEPOLIA_FAUCET_URL}`,
|
|
702
|
+
"clanker doctor",
|
|
703
|
+
"clanker whoami",
|
|
704
|
+
`clanker bot mint <label>`,
|
|
583
705
|
]);
|
|
584
706
|
} else {
|
|
585
707
|
nextHint(["clanker whoami", `clanker bot mint <label>`]);
|
|
@@ -593,23 +715,20 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
593
715
|
export async function runSetup(argv, opts = {}) {
|
|
594
716
|
const flags = parseSetupFlags(argv);
|
|
595
717
|
const isTTY = opts.isTTY ?? Boolean(input.isTTY);
|
|
718
|
+
const hasIdentitySource = Boolean(
|
|
719
|
+
flags.address ||
|
|
720
|
+
flags.keyFile ||
|
|
721
|
+
flags.generateKey ||
|
|
722
|
+
flags.foundryAccount ||
|
|
723
|
+
opts.env?.OPERATOR_PRIVATE_KEY ||
|
|
724
|
+
process.env.OPERATOR_PRIVATE_KEY,
|
|
725
|
+
);
|
|
596
726
|
|
|
597
|
-
if (
|
|
598
|
-
!isTTY ||
|
|
599
|
-
(flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile))
|
|
600
|
-
) {
|
|
727
|
+
if (!isTTY || (flags.yes && flags.preset && flags.operator && hasIdentitySource)) {
|
|
601
728
|
if (!isTTY && !(flags.preset && flags.operator)) {
|
|
602
729
|
return runSetupNonInteractive(argv, opts);
|
|
603
730
|
}
|
|
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
|
-
) {
|
|
731
|
+
if (flags.yes && flags.preset && flags.operator && hasIdentitySource) {
|
|
613
732
|
return runSetupNonInteractive(argv, opts);
|
|
614
733
|
}
|
|
615
734
|
}
|