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

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.
@@ -134,3 +134,91 @@ export function detectSetupHints(opts = {}) {
134
134
  anvilDefaultAddress: ANVIL_DEFAULT_ADDRESS,
135
135
  };
136
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 CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  addressFromEnv,
27
27
  addressFromKeyFile,
28
28
  detectSetupHints,
29
+ formatSetupDetectTable,
29
30
  } from "./setup-detect.mjs";
30
31
 
31
32
  /**
@@ -281,37 +282,56 @@ export async function runSetupInteractive(argv, opts = {}) {
281
282
  };
282
283
 
283
284
  try {
284
- console.log("clanker setup — local profile wizard");
285
- console.log(`home: ${home}`);
286
- if (hints.hasConfig) console.log(`existing config: ${hints.configPath}`);
287
- if (hints.hasOperator) console.log(`existing operator: ${hints.operatorPath}`);
288
- if (hints.foundryAvailable && hints.foundryAccounts.length) {
289
- console.log(`Foundry accounts: ${hints.foundryAccounts.join(", ")}`);
290
- } else if (!hints.foundryAvailable) {
291
- console.log("Foundry cast: not found (optional)");
292
- }
293
- if (hints.openclawBots.length) {
294
- console.log(
295
- `OpenClaw bot keys: ${hints.openclawBots.join(", ")} (bot keys ≠ operator owner)`,
296
- );
297
- }
298
- if (hints.hasOperatorPrivateKeyEnv) {
299
- console.log(`OPERATOR_PRIVATE_KEY: set → ${hints.envAddress ?? "(invalid)"}`);
300
- }
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));
301
299
  console.log("");
302
300
 
303
301
  if ((hints.hasConfig || hints.hasOperator) && !flags.force) {
304
- const overwrite = await askYesNo("Overwrite existing ~/.clanker profile?", false);
305
- if (!overwrite) {
306
- throw new Error("Aborted (profile exists). Re-run with --force to overwrite.");
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;
307
326
  }
308
- flags.force = true;
327
+ console.log("");
309
328
  }
310
329
 
330
+ console.log("— Network —");
311
331
  let preset =
312
332
  flags.preset ||
313
333
  hints.config?.preset ||
314
- (await ask("Preset (sepolia|local)", "sepolia"));
334
+ (await ask("Which network? sepolia (public hub) or local (Anvil)", "sepolia"));
315
335
  preset = String(preset).toLowerCase();
316
336
  if (!PRESETS[preset]) throw new Error(`Unknown preset "${preset}"`);
317
337
 
@@ -321,7 +341,7 @@ export async function runSetupInteractive(argv, opts = {}) {
321
341
  fromBlock != null
322
342
  ? false
323
343
  : await askYesNo(
324
- `Use faster fromBlock ${SEPOLIA_FAST_FROM_BLOCK} for public RPC scans?`,
344
+ `Speed up chain scans? Use fromBlock ${SEPOLIA_FAST_FROM_BLOCK} (recommended on public RPC)`,
325
345
  true,
326
346
  );
327
347
  if (fromBlock == null) {
@@ -329,25 +349,41 @@ export async function runSetupInteractive(argv, opts = {}) {
329
349
  }
330
350
  }
331
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
+ );
332
357
  let address = flags.address ?? null;
333
358
  if (!address && flags.keyFile) {
334
359
  address = addressFromKeyFile(flags.keyFile);
360
+ console.log(`Using address from --key-file: ${address}`);
335
361
  }
336
362
  if (!address && hints.envAddress) {
337
- const useEnv = await askYesNo(`Use address from OPERATOR_PRIVATE_KEY (${hints.envAddress})?`, true);
363
+ const useEnv = await askYesNo(
364
+ `Use address from OPERATOR_PRIVATE_KEY (${hints.envAddress})?`,
365
+ true,
366
+ );
338
367
  if (useEnv) address = hints.envAddress;
339
368
  }
340
369
  if (!address && hints.operator?.owner) {
341
- const useOp = await askYesNo(`Use existing operator.json owner (${hints.operator.owner})?`, true);
370
+ const useOp = await askYesNo(
371
+ `Keep existing operator.json owner (${hints.operator.owner})?`,
372
+ true,
373
+ );
342
374
  if (useOp) address = hints.operator.owner;
343
375
  }
344
376
  if (!address && hints.foundryAccounts.length) {
345
- console.log("Foundry accounts (signing still needs --key-file or OPERATOR_PRIVATE_KEY):");
377
+ console.log("");
378
+ console.log("Foundry accounts on this machine (passwords are never stored here):");
346
379
  hints.foundryAccounts.forEach((n, i) => console.log(` ${i + 1}. ${n}`));
347
- const pick = await ask("Foundry account name to associate (or leave blank)", "");
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
+ );
348
384
  if (pick) {
349
385
  address = await ask(
350
- `Address for Foundry account "${pick}" (paste 0x…; unlock via cast if needed)`,
386
+ `Paste the 0x address for "${pick}" (cast wallet address ${pick} after unlock)`,
351
387
  "",
352
388
  );
353
389
  }
@@ -363,11 +399,13 @@ export async function runSetupInteractive(argv, opts = {}) {
363
399
  let label =
364
400
  flags.operator ||
365
401
  hints.operator?.label ||
366
- (await ask("Operator label (e.g. org.you)", ""));
402
+ (await ask("Operator label on-chain (e.g. org.openclaw.pat or org.you)", ""));
367
403
  if (!label) throw new Error("Operator label is required");
368
404
 
369
405
  const presetCfg = PRESETS[preset];
370
- console.log("Checking chain…");
406
+ console.log("");
407
+ console.log("— Chain check —");
408
+ console.log(`Looking up "${label}" on ${preset}…`);
371
409
  const check = await assertSetupIdentity({
372
410
  rpc: presetCfg.chainRpcUrl,
373
411
  registry: presetCfg.registryAddress,
@@ -376,22 +414,32 @@ export async function runSetupInteractive(argv, opts = {}) {
376
414
  skipChainCheck: flags.skipChainCheck || !presetCfg.registryAddress,
377
415
  });
378
416
  if (check.unregistered) {
379
- console.log(`Note: "${label}" is not registered yet — run clanker operator mint after setup.`);
417
+ console.log(
418
+ `OK: "${label}" is not registered yet — after setup, run: clanker operator mint ${label}`,
419
+ );
380
420
  } else if (check.operator) {
381
- console.log(`On-chain: ${label} active, owner matches.`);
421
+ console.log(`OK: "${label}" is active on-chain and owned by this address.`);
382
422
  }
383
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
+ );
384
429
  let keyFile = flags.keyFile;
385
430
  let keyEnv = flags.keyEnv;
386
431
  let skipKey = flags.skipKey;
387
432
  if (!skipKey && !keyFile && !keyEnv) {
388
433
  if (hints.hasOperatorPrivateKeyEnv) {
389
- const use = await askYesNo("Store env pointer OPERATOR_PRIVATE_KEY for signing?", true);
434
+ const use = await askYesNo(
435
+ "Remember OPERATOR_PRIVATE_KEY as the signing pointer in operator.json?",
436
+ true,
437
+ );
390
438
  if (use) keyEnv = "OPERATOR_PRIVATE_KEY";
391
439
  else skipKey = true;
392
440
  } else {
393
441
  const path = await ask(
394
- "Path to operator key file for signing (leave blank for read-only whoami)",
442
+ "Path to operator private-key file (blank = read-only profile)",
395
443
  "",
396
444
  );
397
445
  if (path) keyFile = path;
@@ -407,13 +455,16 @@ export async function runSetupInteractive(argv, opts = {}) {
407
455
  }
408
456
  }
409
457
 
410
- console.log("\nWill write:");
411
- console.log(` preset: ${preset}`);
458
+ console.log("");
459
+ console.log("— Summary (about to write) —");
460
+ console.log(` network: ${preset}`);
412
461
  console.log(` fromBlock:${fromBlock ?? presetCfg.fromBlock}`);
413
462
  console.log(` label: ${label}`);
414
463
  console.log(` owner: ${address}`);
415
- console.log(` key: ${key ? `${key.type}=${key.value}` : "(none — read-only)"}`);
416
- const ok = flags.yes || (await askYesNo("Write profile?", true));
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));
417
468
  if (!ok) throw new Error("Aborted");
418
469
 
419
470
  const result = applySetup(
@@ -429,12 +480,18 @@ export async function runSetupInteractive(argv, opts = {}) {
429
480
  { home, env },
430
481
  );
431
482
 
432
- console.log(`\nWrote ${result.configPath}`);
483
+ console.log("");
484
+ console.log(`Wrote ${result.configPath}`);
433
485
  console.log(`Wrote ${result.operatorPath}`);
434
486
  if (!key) {
435
- console.log("Read-only profile: `clanker whoami` works; mint/revoke need --key-file or OPERATOR_PRIVATE_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
+ );
436
492
  } else {
437
- console.log("Try: clanker whoami");
493
+ console.log("");
494
+ console.log("Next: clanker whoami");
438
495
  }
439
496
  return result;
440
497
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clanker-chain/clanker-cli",
3
- "version": "2026.9.7-1",
3
+ "version": "2026.9.7-3",
4
4
  "description": "CLI for wiring clanker-chain identity and MQTT into OpenClaw and other agentic stacks.",
5
5
  "type": "module",
6
6
  "bin": {