@clanker-chain/clanker-cli 2026.9.7-3 → 2026.9.7

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 CHANGED
@@ -23,8 +23,7 @@ import {
23
23
  readBot,
24
24
  resolvePreferredOperator,
25
25
  } from "../lib/identity-query.mjs";
26
- import { resolveForRead, resolveOperatorKey, resolveReadIdentity } from "../lib/resolve.mjs";
27
- import { runSetup } from "../lib/setup.mjs";
26
+ import { resolveForRead, resolveOperatorKey } from "../lib/resolve.mjs";
28
27
 
29
28
  function resolveFoundryBinary(name) {
30
29
  const home = os.homedir();
@@ -145,7 +144,6 @@ function usage() {
145
144
  console.log(`clanker - clanker-chain helper CLI
146
145
 
147
146
  Usage:
148
- clanker setup [--preset sepolia|local] [--operator <label>] [--address 0x…] [--key-file path] [--force]
149
147
  clanker init --preset sepolia|local [--force]
150
148
  clanker whoami [--json] [--operator <label>] [--address 0x…]
151
149
  clanker operator mint <label> [--json]
@@ -163,14 +161,12 @@ Usage:
163
161
 
164
162
  Profile:
165
163
  ~/.clanker/config.json network preset (registry, RPC, broker URLs)
166
- ~/.clanker/operator.json label + owner + optional key pointer (never raw hex)
164
+ ~/.clanker/operator.json label + owner + key pointer (never raw hex)
167
165
  ~/.clanker/keys/ bot keys (also written to ~/.openclaw/keys/)
168
166
 
169
- Humans: prefer \`clanker setup\` (detects Foundry/OpenClaw hints, writes profile).
170
167
  Key resolution (mutating commands):
171
168
  --key > --key-file > OPERATOR_PRIVATE_KEY > profile keyFile > profile env
172
169
  Anvil account #0 is allowed only on localhost RPC.
173
- Reads (whoami/bots) can use profile owner without a signing key.
174
170
 
175
171
  See docs/operator-cli.md.
176
172
  `);
@@ -217,14 +213,16 @@ async function cmdWhoami(argv) {
217
213
  let address;
218
214
  let source;
219
215
  let network;
220
- try {
221
- const resolved = resolveReadIdentity(argv);
216
+ const addressFlag = flagValue(argv, "--address");
217
+ if (addressFlag) {
218
+ network = resolveForRead(argv);
219
+ address = getAddress(addressFlag);
220
+ source = "--address";
221
+ } else {
222
+ const resolved = resolveOperatorKey(argv);
222
223
  address = resolved.address;
223
224
  source = resolved.source;
224
225
  network = resolved.network;
225
- } catch (err) {
226
- console.error(err.message);
227
- process.exit(1);
228
226
  }
229
227
 
230
228
  const pub = await publicClientFromRpc(network.rpc);
@@ -290,13 +288,14 @@ async function cmdWhoami(argv) {
290
288
  async function cmdBots(argv) {
291
289
  let address;
292
290
  let network;
293
- try {
294
- const resolved = resolveReadIdentity(argv);
291
+ const addressFlag = flagValue(argv, "--address");
292
+ if (addressFlag) {
293
+ network = resolveForRead(argv);
294
+ address = getAddress(addressFlag);
295
+ } else {
296
+ const resolved = resolveOperatorKey(argv);
295
297
  address = resolved.address;
296
298
  network = resolved.network;
297
- } catch (err) {
298
- console.error(err.message);
299
- process.exit(1);
300
299
  }
301
300
  const { label, pub } = await resolveOperatorLabel(argv, address, network);
302
301
  const bots = await findBotsByOperator(pub, {
@@ -358,16 +357,6 @@ async function main() {
358
357
 
359
358
  const repoRoot = findRepoRoot();
360
359
 
361
- if (cmd === "setup") {
362
- try {
363
- await runSetup(rest);
364
- } catch (err) {
365
- console.error(err.message);
366
- process.exit(1);
367
- }
368
- return;
369
- }
370
-
371
360
  if (cmd === "init") {
372
361
  const preset = flagValue(rest, "--preset") ?? rest.find((a) => !a.startsWith("--"));
373
362
  if (!preset || preset === "init") {
package/lib/profile.mjs CHANGED
@@ -63,7 +63,7 @@ export function openclawKeysDir() {
63
63
 
64
64
  /**
65
65
  * @param {string} presetName
66
- * @param {{ force?: boolean, registryAddress?: string|null, home?: string, fromBlock?: bigint|string|number }} [opts]
66
+ * @param {{ force?: boolean, registryAddress?: string|null, home?: string }} [opts]
67
67
  */
68
68
  export function initProfile(presetName, opts = {}) {
69
69
  const name = String(presetName || "").toLowerCase();
@@ -79,15 +79,13 @@ export function initProfile(presetName, opts = {}) {
79
79
  }
80
80
  mkdirSync(home, { recursive: true });
81
81
  const preset = PRESETS[name];
82
- const fromBlock =
83
- opts.fromBlock != null ? BigInt(opts.fromBlock) : preset.fromBlock;
84
82
  const config = {
85
83
  preset: preset.preset,
86
84
  registryAddress: opts.registryAddress ?? preset.registryAddress,
87
85
  chainRpcUrl: preset.chainRpcUrl,
88
86
  brokerUrl: preset.brokerUrl,
89
87
  mqttAuthServiceUrl: preset.mqttAuthServiceUrl,
90
- fromBlock: fromBlock.toString(),
88
+ fromBlock: preset.fromBlock.toString(),
91
89
  };
92
90
  if (name === "local" && !config.registryAddress) {
93
91
  // Placeholder until chain deploy; operator must set after forge deploy.
@@ -122,9 +120,8 @@ export function loadOperator(home = clankerHome()) {
122
120
  }
123
121
 
124
122
  /**
125
- * Write operator profile (label + owner + optional key pointer). Never stores hex keys.
126
- * Omit `key` (or pass null) for a read-only profile; mint/revoke still need a pointer later.
127
- * @param {{ label: string, owner: string, key?: { type: 'env'|'keyFile', value?: string }|null }} op
123
+ * Write operator profile (label + owner + key pointer). Never stores hex keys.
124
+ * @param {{ label: string, owner: string, key?: { type: 'env'|'keyFile', value?: string } }} op
128
125
  * @param {string} [home]
129
126
  */
130
127
  export function writeOperator(op, home = clankerHome()) {
@@ -133,10 +130,8 @@ export function writeOperator(op, home = clankerHome()) {
133
130
  const out = {
134
131
  label: op.label,
135
132
  owner: op.owner,
133
+ key: op.key ?? { type: "env", value: "OPERATOR_PRIVATE_KEY" },
136
134
  };
137
- if (op.key != null) {
138
- out.key = op.key;
139
- }
140
135
  writeFileSync(path, `${JSON.stringify(out, null, 2)}\n`, { mode: 0o600 });
141
136
  return { path, operator: out };
142
137
  }
package/lib/resolve.mjs CHANGED
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  import { existsSync, readFileSync } from "node:fs";
6
- import { getAddress } from "viem";
7
6
  import { privateKeyToAccount } from "viem/accounts";
8
7
  import {
9
8
  ANVIL_DEFAULT_PRIVATE_KEY,
@@ -175,63 +174,8 @@ export function resolveForRead(argv = [], opts = {}) {
175
174
  if (!network.registry || !/^0x[0-9a-fA-F]{40}$/.test(network.registry)) {
176
175
  throw new Error(
177
176
  "REGISTRY_ADDRESS or --registry is required (0x + 40 hex). " +
178
- "Run `clanker init --preset sepolia` or `clanker setup`, or set registry after deploy.",
177
+ "Run `clanker init --preset sepolia` or set registry after `clanker chain deploy`.",
179
178
  );
180
179
  }
181
180
  return network;
182
181
  }
183
-
184
- /**
185
- * Resolve address for read-only commands.
186
- * Order: --address > signing key > operator.json owner > error (point at setup).
187
- *
188
- * @param {string[]} argv
189
- * @param {{ home?: string, env?: NodeJS.ProcessEnv }} [opts]
190
- * @returns {{ address: string, source: string, network: object }}
191
- */
192
- export function resolveReadIdentity(argv = [], opts = {}) {
193
- const network = resolveForRead(argv, opts);
194
- const home = opts.home ?? network.home;
195
-
196
- let addressFlag = null;
197
- for (let i = 0; i < argv.length; i += 1) {
198
- if (argv[i] === "--address" && argv[i + 1] && !argv[i + 1].startsWith("--")) {
199
- addressFlag = argv[++i];
200
- }
201
- }
202
- if (addressFlag) {
203
- return {
204
- address: getAddress(addressFlag),
205
- source: "--address",
206
- network,
207
- };
208
- }
209
-
210
- try {
211
- const resolved = resolveOperatorKey(argv, { ...opts, home });
212
- return {
213
- address: resolved.address,
214
- source: resolved.source,
215
- network: resolved.network,
216
- };
217
- } catch (err) {
218
- const msg = err?.message ?? String(err);
219
- if (!/private key required|Anvil account #0|Key file not found|Profile keyFile/i.test(msg)) {
220
- throw err;
221
- }
222
- }
223
-
224
- const operator = loadOperator(home);
225
- if (operator?.owner && /^0x[0-9a-fA-F]{40}$/.test(operator.owner)) {
226
- return {
227
- address: getAddress(operator.owner),
228
- source: "profile owner",
229
- network,
230
- };
231
- }
232
-
233
- throw new Error(
234
- "No operator identity configured. Run `clanker setup`, pass --address 0x…, " +
235
- "or set a signing key (OPERATOR_PRIVATE_KEY / --key-file / operator.json key pointer).",
236
- );
237
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clanker-chain/clanker-cli",
3
- "version": "2026.9.7-3",
3
+ "version": "2026.9.7",
4
4
  "description": "CLI for wiring clanker-chain identity and MQTT into OpenClaw and other agentic stacks.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,224 +0,0 @@
1
- /**
2
- * Local identity hints for `clanker setup` (no secrets printed).
3
- */
4
-
5
- import { existsSync, readdirSync, readFileSync } from "node:fs";
6
- import { basename, join } from "node:path";
7
- import { spawnSync } from "node:child_process";
8
- import { privateKeyToAccount } from "viem/accounts";
9
- import {
10
- ANVIL_DEFAULT_ADDRESS,
11
- clankerHome,
12
- loadConfig,
13
- loadOperator,
14
- openclawKeysDir,
15
- } from "./profile.mjs";
16
- import { normalizePrivateKey } from "./resolve.mjs";
17
-
18
- /** Faster Sepolia log floor for public RPC (registry floor remains SEPOLIA_FROM_BLOCK). */
19
- export const SEPOLIA_FAST_FROM_BLOCK = 46_000_000n;
20
-
21
- /**
22
- * Parse `cast wallet list` stdout into account names.
23
- * @param {string} stdout
24
- * @returns {string[]}
25
- */
26
- export function parseCastWalletList(stdout) {
27
- const names = [];
28
- for (const line of String(stdout ?? "").split(/\r?\n/)) {
29
- const trimmed = line.trim();
30
- if (!trimmed) continue;
31
- // Formats: "name (Local)" or "0xname (Local)" or just "name"
32
- const m = trimmed.match(/^(\S+)/);
33
- if (!m) continue;
34
- let name = m[1];
35
- if (name.startsWith("0x") && name.length > 2 && !/^0x[a-fA-F0-9]{40}$/.test(name)) {
36
- // cast sometimes prefixes 0x to the account name display
37
- name = name.slice(2);
38
- }
39
- if (/^0x[a-fA-F0-9]{40}$/.test(name)) continue;
40
- names.push(name);
41
- }
42
- return [...new Set(names)];
43
- }
44
-
45
- /**
46
- * @param {{ castBin?: string, spawn?: typeof spawnSync }} [opts]
47
- * @returns {{ available: boolean, accounts: string[] }}
48
- */
49
- export function listFoundryAccounts(opts = {}) {
50
- const spawn = opts.spawn ?? spawnSync;
51
- const castBin = opts.castBin ?? "cast";
52
- const result = spawn(castBin, ["wallet", "list"], {
53
- encoding: "utf8",
54
- shell: false,
55
- });
56
- if (result.error || result.status !== 0) {
57
- return { available: false, accounts: [] };
58
- }
59
- return { available: true, accounts: parseCastWalletList(result.stdout ?? "") };
60
- }
61
-
62
- /**
63
- * Bot key basenames under ~/.openclaw/keys (context only).
64
- * @param {{ openclawDir?: string }} [opts]
65
- * @returns {string[]}
66
- */
67
- export function listOpenclawBotKeys(opts = {}) {
68
- const dir = opts.openclawDir ?? openclawKeysDir();
69
- if (!existsSync(dir)) return [];
70
- return readdirSync(dir)
71
- .filter((f) => f.endsWith(".key"))
72
- .map((f) => basename(f, ".key"))
73
- .sort();
74
- }
75
-
76
- /**
77
- * Derive address from a key file (first line). Does not log the key.
78
- * @param {string} path
79
- * @returns {string}
80
- */
81
- export function addressFromKeyFile(path) {
82
- if (!existsSync(path)) throw new Error(`Key file not found: ${path}`);
83
- const key = normalizePrivateKey(readFileSync(path, "utf8").split(/\r?\n/)[0]);
84
- return privateKeyToAccount(key).address;
85
- }
86
-
87
- /**
88
- * Derive address from OPERATOR_PRIVATE_KEY (or named env).
89
- * @param {NodeJS.ProcessEnv} [env]
90
- * @param {string} [envName]
91
- * @returns {string|null}
92
- */
93
- export function addressFromEnv(env = process.env, envName = "OPERATOR_PRIVATE_KEY") {
94
- const raw = env[envName];
95
- if (!raw) return null;
96
- const key = normalizePrivateKey(raw);
97
- return privateKeyToAccount(key).address;
98
- }
99
-
100
- /**
101
- * Snapshot of local hints for setup UX.
102
- * @param {{ home?: string, env?: NodeJS.ProcessEnv, openclawDir?: string, castBin?: string, spawn?: typeof spawnSync }} [opts]
103
- */
104
- export function detectSetupHints(opts = {}) {
105
- const env = opts.env ?? process.env;
106
- const home = opts.home ?? clankerHome(env);
107
- const config = loadConfig(home);
108
- const operator = loadOperator(home);
109
- const foundry = listFoundryAccounts({
110
- castBin: opts.castBin,
111
- spawn: opts.spawn,
112
- });
113
- const openclawBots = listOpenclawBotKeys({ openclawDir: opts.openclawDir });
114
- let envAddress = null;
115
- try {
116
- envAddress = addressFromEnv(env);
117
- } catch {
118
- envAddress = null;
119
- }
120
-
121
- return {
122
- home,
123
- configPath: join(home, "config.json"),
124
- operatorPath: join(home, "operator.json"),
125
- hasConfig: Boolean(config),
126
- hasOperator: Boolean(operator),
127
- config,
128
- operator,
129
- hasOperatorPrivateKeyEnv: Boolean(env.OPERATOR_PRIVATE_KEY),
130
- envAddress,
131
- foundryAvailable: foundry.available,
132
- foundryAccounts: foundry.accounts,
133
- openclawBots,
134
- anvilDefaultAddress: ANVIL_DEFAULT_ADDRESS,
135
- };
136
- }
137
-
138
- /**
139
- * Pad string to width (truncate with … if longer).
140
- * @param {string} s
141
- * @param {number} width
142
- */
143
- function cell(s, width) {
144
- const t = String(s ?? "");
145
- if (t.length === width) return t;
146
- if (t.length < width) return t + " ".repeat(width - t.length);
147
- if (width <= 1) return "…";
148
- return `${t.slice(0, width - 1)}…`;
149
- }
150
-
151
- /**
152
- * Render detection hints as an aligned two-column table for the terminal.
153
- * @param {ReturnType<typeof detectSetupHints>} hints
154
- * @returns {string}
155
- */
156
- export function formatSetupDetectTable(hints) {
157
- /** @type {[string, string][]} */
158
- const rows = [];
159
- rows.push(["Profile dir", hints.home]);
160
-
161
- if (hints.hasConfig) {
162
- rows.push(["config.json", `present · preset=${hints.config?.preset ?? "?"}`]);
163
- rows.push([
164
- "registry",
165
- hints.config?.registryAddress ? String(hints.config.registryAddress) : "(none)",
166
- ]);
167
- } else {
168
- rows.push(["config.json", "missing · will create"]);
169
- }
170
-
171
- if (hints.hasOperator) {
172
- rows.push([
173
- "operator.json",
174
- `present · ${hints.operator?.label ?? "?"} · ${hints.operator?.owner ?? "?"}`,
175
- ]);
176
- } else {
177
- rows.push(["operator.json", "missing · needed for whoami"]);
178
- }
179
-
180
- if (hints.foundryAvailable && hints.foundryAccounts.length) {
181
- rows.push([
182
- "Foundry accounts",
183
- `${hints.foundryAccounts.length}: ${hints.foundryAccounts.join(", ")}`,
184
- ]);
185
- } else if (hints.foundryAvailable) {
186
- rows.push(["Foundry accounts", "none listed"]);
187
- } else {
188
- rows.push(["Foundry cast", "not on PATH"]);
189
- }
190
-
191
- if (hints.openclawBots.length) {
192
- rows.push([
193
- "OpenClaw bot keys",
194
- `${hints.openclawBots.length} files (bot signing only)`,
195
- ]);
196
- for (const b of hints.openclawBots) {
197
- rows.push([" ·", b]);
198
- }
199
- } else {
200
- rows.push(["OpenClaw bot keys", "none"]);
201
- }
202
-
203
- if (hints.hasOperatorPrivateKeyEnv) {
204
- rows.push([
205
- "OPERATOR_PRIVATE_KEY",
206
- hints.envAddress ? `set · ${hints.envAddress}` : "set · (invalid)",
207
- ]);
208
- } else {
209
- rows.push(["OPERATOR_PRIVATE_KEY", "unset"]);
210
- }
211
-
212
- const col0 = Math.min(
213
- 22,
214
- Math.max(4, ...rows.map(([k]) => k.length)),
215
- );
216
- const lines = [
217
- `${cell("What", col0)} Value`,
218
- `${"-".repeat(col0)} ${"-".repeat(48)}`,
219
- ];
220
- for (const [k, v] of rows) {
221
- lines.push(`${cell(k, col0)} ${v}`);
222
- }
223
- return lines.join("\n");
224
- }
package/lib/setup.mjs DELETED
@@ -1,527 +0,0 @@
1
- /**
2
- * Interactive / flag-driven `clanker setup`.
3
- */
4
-
5
- import { createInterface } from "node:readline/promises";
6
- import { stdin as input, stdout as output } from "node:process";
7
- import { existsSync } from "node:fs";
8
- import { getAddress } from "viem";
9
- import {
10
- ANVIL_DEFAULT_ADDRESS,
11
- PRESETS,
12
- clankerHome,
13
- configPath,
14
- initProfile,
15
- isLocalRpc,
16
- operatorPath,
17
- writeOperator,
18
- } from "./profile.mjs";
19
- import {
20
- publicClientFromRpc,
21
- readOperator,
22
- resolvePreferredOperator,
23
- } from "./identity-query.mjs";
24
- import {
25
- SEPOLIA_FAST_FROM_BLOCK,
26
- addressFromEnv,
27
- addressFromKeyFile,
28
- detectSetupHints,
29
- formatSetupDetectTable,
30
- } from "./setup-detect.mjs";
31
-
32
- /**
33
- * @param {string[]} argv
34
- */
35
- export function parseSetupFlags(argv) {
36
- let preset = null;
37
- let operator = null;
38
- let address = null;
39
- let keyFile = null;
40
- let keyEnv = null;
41
- let fromBlock = null;
42
- let force = false;
43
- let yes = false;
44
- let skipKey = false;
45
- let skipChainCheck = false;
46
-
47
- for (let i = 0; i < argv.length; i += 1) {
48
- const a = argv[i];
49
- if (a === "--preset" && argv[i + 1]) preset = argv[++i];
50
- else if (a === "--operator" && argv[i + 1]) operator = argv[++i];
51
- else if (a === "--address" && argv[i + 1]) address = argv[++i];
52
- else if (a === "--key-file" && argv[i + 1]) keyFile = argv[++i];
53
- else if (a === "--key-env" && argv[i + 1]) keyEnv = argv[++i];
54
- else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
55
- else if (a === "--force") force = true;
56
- else if (a === "--yes" || a === "-y") yes = true;
57
- else if (a === "--skip-key") skipKey = true;
58
- else if (a === "--skip-chain-check") skipChainCheck = true;
59
- }
60
-
61
- return {
62
- preset,
63
- operator,
64
- address,
65
- keyFile,
66
- keyEnv,
67
- fromBlock,
68
- force,
69
- yes,
70
- skipKey,
71
- skipChainCheck,
72
- };
73
- }
74
-
75
- /**
76
- * Validate address + label against registry (and Anvil-on-public).
77
- * @param {{ rpc: string, registry: string, label: string, address: string, skipChainCheck?: boolean }} opts
78
- */
79
- export async function assertSetupIdentity(opts) {
80
- const address = getAddress(opts.address);
81
- const local = isLocalRpc(opts.rpc);
82
-
83
- if (!local && address.toLowerCase() === ANVIL_DEFAULT_ADDRESS.toLowerCase()) {
84
- throw new Error(
85
- "Refusing Anvil account #0 address on a non-local RPC. Use your real operator owner address.",
86
- );
87
- }
88
-
89
- if (opts.skipChainCheck) {
90
- return { address, operator: null, skipped: true };
91
- }
92
-
93
- if (!opts.registry || !/^0x[0-9a-fA-F]{40}$/.test(opts.registry)) {
94
- if (local) {
95
- return { address, operator: null, skipped: true };
96
- }
97
- throw new Error("Registry address required for chain check on public RPC.");
98
- }
99
-
100
- const pub = await publicClientFromRpc(opts.rpc);
101
- const op = await readOperator(pub, opts.registry, opts.label);
102
-
103
- if (op.registeredAt === 0n) {
104
- // New operator — fine; mint later
105
- return { address, operator: op, skipped: false, unregistered: true };
106
- }
107
-
108
- const resolved = await resolvePreferredOperator(pub, {
109
- registry: opts.registry,
110
- label: opts.label,
111
- owner: address,
112
- });
113
- if (resolved.error) {
114
- throw new Error(resolved.error);
115
- }
116
- return { address, operator: resolved.operator, skipped: false, unregistered: false };
117
- }
118
-
119
- /**
120
- * Build key pointer from flags / choices.
121
- * @returns {{ type: 'env'|'keyFile', value: string }|null}
122
- */
123
- export function buildKeyPointer({ keyFile, keyEnv, skipKey, env = process.env }) {
124
- if (skipKey) return null;
125
- if (keyFile) {
126
- if (!existsSync(keyFile)) throw new Error(`Key file not found: ${keyFile}`);
127
- // validate readable
128
- addressFromKeyFile(keyFile);
129
- return { type: "keyFile", value: keyFile };
130
- }
131
- if (keyEnv) {
132
- return { type: "env", value: keyEnv };
133
- }
134
- if (env.OPERATOR_PRIVATE_KEY) {
135
- return { type: "env", value: "OPERATOR_PRIVATE_KEY" };
136
- }
137
- return null;
138
- }
139
-
140
- /**
141
- * Apply setup choices to disk.
142
- */
143
- export function applySetup(choices, opts = {}) {
144
- const home = opts.home ?? clankerHome(opts.env);
145
- const force = Boolean(choices.force);
146
-
147
- if (existsSync(configPath(home)) && !force) {
148
- throw new Error(
149
- `Profile already exists at ${configPath(home)}. Pass --force to overwrite.`,
150
- );
151
- }
152
- if (existsSync(operatorPath(home)) && !force) {
153
- throw new Error(
154
- `Operator profile already exists at ${operatorPath(home)}. Pass --force to overwrite.`,
155
- );
156
- }
157
-
158
- const { path: configFile, config } = initProfile(choices.preset, {
159
- force: true,
160
- home,
161
- registryAddress: choices.registryAddress,
162
- fromBlock: choices.fromBlock,
163
- });
164
-
165
- const { path: opFile, operator } = writeOperator(
166
- {
167
- label: choices.label,
168
- owner: getAddress(choices.address),
169
- key: choices.key,
170
- },
171
- home,
172
- );
173
-
174
- return { configPath: configFile, config, operatorPath: opFile, operator };
175
- }
176
-
177
- /**
178
- * Non-interactive setup (agents / CI).
179
- * @param {string[]} argv
180
- * @param {{ home?: string, env?: NodeJS.ProcessEnv, skipChainCheck?: boolean }} [opts]
181
- */
182
- export async function runSetupNonInteractive(argv, opts = {}) {
183
- const flags = parseSetupFlags(argv);
184
- const env = opts.env ?? process.env;
185
- const home = opts.home ?? clankerHome(env);
186
-
187
- if (!flags.preset || !PRESETS[flags.preset]) {
188
- throw new Error(
189
- "Non-interactive setup requires --preset sepolia|local (stdin is not a TTY). " +
190
- "Also pass --operator <label> and --address 0x…",
191
- );
192
- }
193
- if (!flags.operator) {
194
- throw new Error("Non-interactive setup requires --operator <label>");
195
- }
196
- if (!flags.address && !flags.keyFile && !env.OPERATOR_PRIVATE_KEY) {
197
- throw new Error(
198
- "Non-interactive setup requires --address 0x…, --key-file, or OPERATOR_PRIVATE_KEY",
199
- );
200
- }
201
-
202
- let address = flags.address;
203
- if (!address && flags.keyFile) address = addressFromKeyFile(flags.keyFile);
204
- if (!address && env.OPERATOR_PRIVATE_KEY) address = addressFromEnv(env);
205
-
206
- const preset = PRESETS[flags.preset];
207
- let fromBlock = flags.fromBlock;
208
- if (fromBlock == null && flags.preset === "sepolia") {
209
- fromBlock = SEPOLIA_FAST_FROM_BLOCK;
210
- }
211
-
212
- const rpc = preset.chainRpcUrl;
213
- const registry = preset.registryAddress;
214
-
215
- await assertSetupIdentity({
216
- rpc,
217
- registry,
218
- label: flags.operator,
219
- address,
220
- skipChainCheck: flags.skipChainCheck || opts.skipChainCheck || !registry,
221
- });
222
-
223
- const key = buildKeyPointer({
224
- keyFile: flags.keyFile,
225
- keyEnv: flags.keyEnv,
226
- skipKey: flags.skipKey,
227
- env,
228
- });
229
-
230
- // Address from key must match --address when both set
231
- if (flags.keyFile && flags.address) {
232
- const fromKey = getAddress(addressFromKeyFile(flags.keyFile));
233
- if (fromKey !== getAddress(flags.address)) {
234
- throw new Error(
235
- `Key file address ${fromKey} does not match --address ${getAddress(flags.address)}`,
236
- );
237
- }
238
- }
239
-
240
- return applySetup(
241
- {
242
- preset: flags.preset,
243
- force: flags.force || flags.yes,
244
- fromBlock,
245
- registryAddress: registry,
246
- label: flags.operator,
247
- address,
248
- key,
249
- },
250
- { home, env },
251
- );
252
- }
253
-
254
- /**
255
- * Interactive wizard when stdin is a TTY.
256
- */
257
- export async function runSetupInteractive(argv, opts = {}) {
258
- const flags = parseSetupFlags(argv);
259
- const env = opts.env ?? process.env;
260
- const home = opts.home ?? clankerHome(env);
261
- const hints = detectSetupHints({
262
- home,
263
- env,
264
- castBin: opts.castBin,
265
- spawn: opts.spawn,
266
- openclawDir: opts.openclawDir,
267
- });
268
-
269
- const rl =
270
- opts.rl ??
271
- createInterface({ input, output });
272
- const ask = async (q, def) => {
273
- const suffix = def != null && def !== "" ? ` [${def}]` : "";
274
- const ans = (await rl.question(`${q}${suffix}: `)).trim();
275
- return ans || def || "";
276
- };
277
- const askYesNo = async (q, defaultYes = true) => {
278
- const def = defaultYes ? "Y/n" : "y/N";
279
- const ans = (await rl.question(`${q} (${def}): `)).trim().toLowerCase();
280
- if (!ans) return defaultYes;
281
- return ans === "y" || ans === "yes";
282
- };
283
-
284
- try {
285
- console.log("");
286
- console.log("clanker setup — create your local operator profile");
287
- console.log("");
288
- console.log(
289
- "This writes ~/.clanker/config.json (network) and operator.json (who you are).",
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;
326
- }
327
- console.log("");
328
- }
329
-
330
- console.log("— Network —");
331
- let preset =
332
- flags.preset ||
333
- hints.config?.preset ||
334
- (await ask("Which network? sepolia (public hub) or local (Anvil)", "sepolia"));
335
- preset = String(preset).toLowerCase();
336
- if (!PRESETS[preset]) throw new Error(`Unknown preset "${preset}"`);
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;
349
- }
350
- }
351
-
352
- console.log("");
353
- console.log("— Operator identity —");
354
- console.log(
355
- "We need the wallet address that owns (or will own) your on-chain operator label.",
356
- );
357
- let address = flags.address ?? null;
358
- if (!address && flags.keyFile) {
359
- address = addressFromKeyFile(flags.keyFile);
360
- console.log(`Using address from --key-file: ${address}`);
361
- }
362
- if (!address && hints.envAddress) {
363
- const useEnv = await askYesNo(
364
- `Use address from OPERATOR_PRIVATE_KEY (${hints.envAddress})?`,
365
- true,
366
- );
367
- if (useEnv) address = hints.envAddress;
368
- }
369
- if (!address && hints.operator?.owner) {
370
- const useOp = await askYesNo(
371
- `Keep existing operator.json owner (${hints.operator.owner})?`,
372
- true,
373
- );
374
- if (useOp) address = hints.operator.owner;
375
- }
376
- if (!address && hints.foundryAccounts.length) {
377
- console.log("");
378
- console.log("Foundry accounts on this machine (passwords are never stored here):");
379
- hints.foundryAccounts.forEach((n, i) => console.log(` ${i + 1}. ${n}`));
380
- const pick = await ask(
381
- "Type a Foundry account name to use, or leave blank to paste an address",
382
- hints.foundryAccounts[0] ?? "",
383
- );
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
- }
397
- address = getAddress(address);
398
-
399
- let label =
400
- flags.operator ||
401
- hints.operator?.label ||
402
- (await ask("Operator label on-chain (e.g. org.openclaw.pat or org.you)", ""));
403
- if (!label) throw new Error("Operator label is required");
404
-
405
- const presetCfg = PRESETS[preset];
406
- console.log("");
407
- console.log("— Chain check —");
408
- console.log(`Looking up "${label}" on ${preset}…`);
409
- const check = await assertSetupIdentity({
410
- rpc: presetCfg.chainRpcUrl,
411
- registry: presetCfg.registryAddress,
412
- label,
413
- address,
414
- skipChainCheck: flags.skipChainCheck || !presetCfg.registryAddress,
415
- });
416
- if (check.unregistered) {
417
- console.log(
418
- `OK: "${label}" is not registered yet — after setup, run: clanker operator mint ${label}`,
419
- );
420
- } else if (check.operator) {
421
- console.log(`OK: "${label}" is active on-chain and owned by this address.`);
422
- }
423
-
424
- console.log("");
425
- console.log("— Signing key (optional) —");
426
- console.log(
427
- "Only needed for mint/revoke/transfer. Skip for read-only whoami/bots.",
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
- }
448
- }
449
-
450
- const key = buildKeyPointer({ keyFile, keyEnv, skipKey, env });
451
- if (key?.type === "keyFile") {
452
- const fromKey = getAddress(addressFromKeyFile(key.value));
453
- if (fromKey !== address) {
454
- throw new Error(`Key file address ${fromKey} does not match chosen owner ${address}`);
455
- }
456
- }
457
-
458
- console.log("");
459
- console.log("— Summary (about to write) —");
460
- console.log(` network: ${preset}`);
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 },
481
- );
482
-
483
- console.log("");
484
- console.log(`Wrote ${result.configPath}`);
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");
495
- }
496
- return result;
497
- } finally {
498
- if (!opts.rl) rl.close();
499
- }
500
- }
501
-
502
- /**
503
- * Entry: interactive if TTY (unless --yes with full flags), else non-interactive.
504
- */
505
- export async function runSetup(argv, opts = {}) {
506
- const flags = parseSetupFlags(argv);
507
- const isTTY = opts.isTTY ?? Boolean(input.isTTY);
508
-
509
- // Fully flagged non-interactive path
510
- if (
511
- !isTTY ||
512
- (flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile))
513
- ) {
514
- if (!isTTY && !(flags.preset && flags.operator)) {
515
- return runSetupNonInteractive(argv, opts);
516
- }
517
- if (flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile || opts.env?.OPERATOR_PRIVATE_KEY || process.env.OPERATOR_PRIVATE_KEY)) {
518
- return runSetupNonInteractive(argv, opts);
519
- }
520
- }
521
-
522
- if (!isTTY) {
523
- return runSetupNonInteractive(argv, opts);
524
- }
525
-
526
- return runSetupInteractive(argv, opts);
527
- }