@clanker-chain/clanker-cli 2026.9.12-1 → 2026.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,41 +3,14 @@
3
3
  * On-chain identity commands (ClankerIdentity contract).
4
4
  */
5
5
 
6
- import {
7
- createPublicClient,
8
- createWalletClient,
9
- defineChain,
10
- http,
11
- } from "viem";
12
6
  import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
13
7
  import { clankerIdentityAbi } from "../lib/clanker-identity-abi.mjs";
14
8
  import { assertSafeBotLabel, writeBotKeyFiles } from "../lib/keys.mjs";
15
9
  import { harnessSnippet, writeOperator } from "../lib/profile.mjs";
16
10
  import { labelToId } from "../lib/identity-query.mjs";
11
+ import { resolveOperatorSigner } from "../lib/operator-signer.mjs";
17
12
  import { resolveOperatorKey } from "../lib/resolve.mjs";
18
13
 
19
- async function chainFromRpc(rpc) {
20
- const publicClient = createPublicClient({ transport: http(rpc) });
21
- const id = await publicClient.getChainId();
22
- return defineChain({
23
- id,
24
- name: `clanker-chain-${id}`,
25
- nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
26
- rpcUrls: { default: { http: [rpc] } },
27
- });
28
- }
29
-
30
- async function walletFromKey(rpc, key) {
31
- const account = privateKeyToAccount(key);
32
- const transport = http(rpc);
33
- const chain = await chainFromRpc(rpc);
34
- return {
35
- account,
36
- public: createPublicClient({ chain, transport }),
37
- wallet: createWalletClient({ account, chain, transport }),
38
- };
39
- }
40
-
41
14
  async function waitOk(pub, hash) {
42
15
  const receipt = await pub.waitForTransactionReceipt({ hash });
43
16
  if (receipt.status !== "success") {
@@ -48,6 +21,7 @@ async function waitOk(pub, hash) {
48
21
 
49
22
  /**
50
23
  * Shared resolve for mutating chain commands (Anvil-guarded).
24
+ * Prefer resolveOperatorSigner for Privy + local.
51
25
  * @param {string[]} argv
52
26
  */
53
27
  export function resolveForWrite(argv) {
@@ -55,15 +29,15 @@ export function resolveForWrite(argv) {
55
29
  }
56
30
 
57
31
  export async function chainMintOperator(label, argv, opts = {}) {
58
- const { key, address, network, keyPointer } = resolveOperatorKey(argv, opts);
59
- const { public: pub, wallet } = await walletFromKey(network.rpc, key);
32
+ const signer = await resolveOperatorSigner(argv, opts);
33
+ const pub = signer.publicClient;
60
34
  const operatorFee = await pub.readContract({
61
- address: network.registry,
35
+ address: signer.network.registry,
62
36
  abi: clankerIdentityAbi,
63
37
  functionName: "operatorFee",
64
38
  });
65
- const hash = await wallet.writeContract({
66
- address: network.registry,
39
+ const hash = await signer.writeContract({
40
+ address: signer.network.registry,
67
41
  abi: clankerIdentityAbi,
68
42
  functionName: "registerOperator",
69
43
  args: [label],
@@ -72,23 +46,27 @@ export async function chainMintOperator(label, argv, opts = {}) {
72
46
  await waitOk(pub, hash);
73
47
 
74
48
  writeOperator(
75
- { label, owner: address, key: opts.keyPointer ?? keyPointer },
76
- opts.home ?? network.home,
49
+ {
50
+ label,
51
+ owner: signer.address,
52
+ key: opts.keyPointer ?? signer.keyPointer,
53
+ },
54
+ opts.home ?? signer.network.home,
77
55
  );
78
56
 
79
57
  return {
80
58
  ok: true,
81
59
  label,
82
60
  operator_id: labelToId(label),
83
- owner: address,
61
+ owner: signer.address,
84
62
  tx: hash,
85
63
  };
86
64
  }
87
65
 
88
66
  export async function chainMintBot(botLabel, operatorLabel, argv, opts = {}) {
89
67
  assertSafeBotLabel(botLabel);
90
- const { key, network } = resolveOperatorKey(argv, opts);
91
- const { public: pub, wallet } = await walletFromKey(network.rpc, key);
68
+ const signer = await resolveOperatorSigner(argv, opts);
69
+ const pub = signer.publicClient;
92
70
  let botPrivateKey;
93
71
  for (let i = 0; i < argv.length; i += 1) {
94
72
  const a = argv[i];
@@ -114,12 +92,12 @@ export async function chainMintBot(botLabel, operatorLabel, argv, opts = {}) {
114
92
  );
115
93
 
116
94
  const botFee = await pub.readContract({
117
- address: network.registry,
95
+ address: signer.network.registry,
118
96
  abi: clankerIdentityAbi,
119
97
  functionName: "botFee",
120
98
  });
121
- const hash = await wallet.writeContract({
122
- address: network.registry,
99
+ const hash = await signer.writeContract({
100
+ address: signer.network.registry,
123
101
  abi: clankerIdentityAbi,
124
102
  functionName: "registerBot",
125
103
  args: [operatorIdBytes, botLabel, botAccount.address],
@@ -130,7 +108,7 @@ export async function chainMintBot(botLabel, operatorLabel, argv, opts = {}) {
130
108
  const channelsMqtt = harnessSnippet({
131
109
  botId: botLabel,
132
110
  operatorId: operatorLabel,
133
- network,
111
+ network: signer.network,
134
112
  });
135
113
 
136
114
  return {
@@ -146,11 +124,11 @@ export async function chainMintBot(botLabel, operatorLabel, argv, opts = {}) {
146
124
  }
147
125
 
148
126
  export async function chainRotateBotKey(botLabel, newKey, argv, opts = {}) {
149
- const { key, network } = resolveOperatorKey(argv, opts);
150
- const { public: pub, wallet } = await walletFromKey(network.rpc, key);
127
+ const signer = await resolveOperatorSigner(argv, opts);
128
+ const pub = signer.publicClient;
151
129
  const botIdBytes = labelToId(botLabel);
152
- const hash = await wallet.writeContract({
153
- address: network.registry,
130
+ const hash = await signer.writeContract({
131
+ address: signer.network.registry,
154
132
  abi: clankerIdentityAbi,
155
133
  functionName: "rotateBotKey",
156
134
  args: [botIdBytes, newKey],
@@ -160,11 +138,11 @@ export async function chainRotateBotKey(botLabel, newKey, argv, opts = {}) {
160
138
  }
161
139
 
162
140
  export async function chainRevokeBot(botLabel, argv, opts = {}) {
163
- const { key, network } = resolveOperatorKey(argv, opts);
164
- const { public: pub, wallet } = await walletFromKey(network.rpc, key);
141
+ const signer = await resolveOperatorSigner(argv, opts);
142
+ const pub = signer.publicClient;
165
143
  const botIdBytes = labelToId(botLabel);
166
- const hash = await wallet.writeContract({
167
- address: network.registry,
144
+ const hash = await signer.writeContract({
145
+ address: signer.network.registry,
168
146
  abi: clankerIdentityAbi,
169
147
  functionName: "revokeBot",
170
148
  args: [botIdBytes],
@@ -174,11 +152,11 @@ export async function chainRevokeBot(botLabel, argv, opts = {}) {
174
152
  }
175
153
 
176
154
  export async function chainProposeOperatorTransfer(label, newOwner, argv, opts = {}) {
177
- const { key, network } = resolveOperatorKey(argv, opts);
178
- const { public: pub, wallet } = await walletFromKey(network.rpc, key);
155
+ const signer = await resolveOperatorSigner(argv, opts);
156
+ const pub = signer.publicClient;
179
157
  const id = labelToId(label);
180
- const hash = await wallet.writeContract({
181
- address: network.registry,
158
+ const hash = await signer.writeContract({
159
+ address: signer.network.registry,
182
160
  abi: clankerIdentityAbi,
183
161
  functionName: "proposeOperatorTransfer",
184
162
  args: [id, newOwner],
@@ -188,11 +166,11 @@ export async function chainProposeOperatorTransfer(label, newOwner, argv, opts =
188
166
  }
189
167
 
190
168
  export async function chainAcceptOperatorTransfer(label, argv, opts = {}) {
191
- const { key, address, network, keyPointer } = resolveOperatorKey(argv, opts);
192
- const { public: pub, wallet } = await walletFromKey(network.rpc, key);
169
+ const signer = await resolveOperatorSigner(argv, opts);
170
+ const pub = signer.publicClient;
193
171
  const id = labelToId(label);
194
- const hash = await wallet.writeContract({
195
- address: network.registry,
172
+ const hash = await signer.writeContract({
173
+ address: signer.network.registry,
196
174
  abi: clankerIdentityAbi,
197
175
  functionName: "acceptOperatorTransfer",
198
176
  args: [id],
@@ -201,12 +179,12 @@ export async function chainAcceptOperatorTransfer(label, argv, opts = {}) {
201
179
  writeOperator(
202
180
  {
203
181
  label,
204
- owner: address,
205
- key: opts.keyPointer ?? keyPointer,
182
+ owner: signer.address,
183
+ key: opts.keyPointer ?? signer.keyPointer,
206
184
  },
207
- opts.home ?? network.home,
185
+ opts.home ?? signer.network.home,
208
186
  );
209
- return { ok: true, label, owner: address, tx: hash };
187
+ return { ok: true, label, owner: signer.address, tx: hash };
210
188
  }
211
189
 
212
190
  export { labelToId };
package/bin/clanker.mjs CHANGED
@@ -24,9 +24,11 @@ import {
24
24
  resolvePreferredOperator,
25
25
  } from "../lib/identity-query.mjs";
26
26
  import { resolveForRead, resolveOperatorKey, resolveReadIdentity } from "../lib/resolve.mjs";
27
+ import { resolveOperatorSigner } from "../lib/operator-signer.mjs";
27
28
  import { runSetup } from "../lib/setup.mjs";
28
29
  import { runDoctor } from "../lib/doctor.mjs";
29
30
  import { runFund } from "../lib/fund.mjs";
31
+ import { runLogin, runLogout } from "../lib/login.mjs";
30
32
  import {
31
33
  assessMintBudget,
32
34
  formatEthTrim,
@@ -171,6 +173,8 @@ function usage() {
171
173
 
172
174
  Usage:
173
175
  clanker setup [--preset sepolia|local] [--operator <label>] [--address 0x…] [--skip-key] [--bot-key path] [--key-file path] [--force]
176
+ clanker login [--no-open] [--operator <label>] [--json]
177
+ clanker logout [--json]
174
178
  clanker fund [--address 0x…] [--no-open] [--timeout ms] [--json]
175
179
  clanker doctor [--json]
176
180
  clanker init --preset sepolia|local [--force]
@@ -197,7 +201,7 @@ Profile:
197
201
  ~/.clanker/operator.json label + owner + optional key pointer (never raw hex)
198
202
  ~/.clanker/keys/ bot keys (also written to ~/.openclaw/keys/)
199
203
 
200
- Humans: \`clanker setup\` then \`clanker fund\` / \`doctor\` / \`whoami\`.
204
+ Humans: \`clanker login\` (email vault) or \`clanker setup --generate-key\`, then \`fund\` / \`doctor\` / \`whoami\`.
201
205
  Pairing (Policy): both operators run \`clanker pair add\` before DMs deliver on the hub.
202
206
  Mutates print a plan and confirm unless --yes or --json.
203
207
  whoami is fast by default; pass --with-bots to enrich child bots (or use \`clanker bots\`).
@@ -415,7 +419,8 @@ async function main() {
415
419
  error: err.message,
416
420
  because: "setup could not write a valid local profile",
417
421
  try: [
418
- "clanker setup --preset sepolia --operator org.you --address 0x… --yes --force",
422
+ "clanker login",
423
+ "clanker setup --preset sepolia --operator org.you --yes --force",
419
424
  "clanker fund",
420
425
  "clanker doctor",
421
426
  ],
@@ -424,6 +429,36 @@ async function main() {
424
429
  return;
425
430
  }
426
431
 
432
+ if (cmd === "login") {
433
+ try {
434
+ const { exitCode } = await runLogin(rest);
435
+ process.exit(exitCode);
436
+ } catch (err) {
437
+ exitCliError({
438
+ error: err.message,
439
+ because: "login needs Privy CLI access enabled and /authorize approval",
440
+ try: [
441
+ "Open the printed URL and Approve",
442
+ "Privy dashboard: Enable CLI and agent access → Verification URI https://clanker-chain.com/authorize",
443
+ "clanker setup --generate-key # local key door instead",
444
+ ],
445
+ });
446
+ }
447
+ }
448
+
449
+ if (cmd === "logout") {
450
+ try {
451
+ const { exitCode } = await runLogout(rest);
452
+ process.exit(exitCode);
453
+ } catch (err) {
454
+ exitCliError({
455
+ error: err.message,
456
+ because: "logout could not clear the Privy session",
457
+ try: ["clanker logout"],
458
+ });
459
+ }
460
+ }
461
+
427
462
  if (cmd === "fund") {
428
463
  try {
429
464
  const { exitCode } = await runFund(rest);
@@ -581,21 +616,21 @@ async function main() {
581
616
  let planRows;
582
617
  let preview;
583
618
  try {
584
- preview = resolveOperatorKey(flags);
619
+ preview = await resolveOperatorSigner(flags);
585
620
  planRows = [
586
621
  ["action", "registerOperator"],
587
622
  ["label", label],
588
623
  ["owner", preview.address],
589
624
  ["rpc", preview.network.rpc],
590
625
  ["registry", preview.network.registry ?? "(none)"],
591
- ["key", preview.source],
626
+ ["signer", preview.source],
592
627
  ];
593
628
  } catch (err) {
594
629
  exitCliError({
595
630
  error: err.message,
596
- because: "operator mint needs a signing key on this network",
631
+ because: "operator mint needs a signer on this network",
597
632
  try: [
598
- "export OPERATOR_PRIVATE_KEY=0x…",
633
+ "clanker login",
599
634
  "clanker operator mint " + label + " --key-file ~/.clanker/op.key --yes",
600
635
  "clanker fund",
601
636
  "clanker doctor",
@@ -736,14 +771,15 @@ async function main() {
736
771
  let preview = null;
737
772
  if (!operatorLabel) {
738
773
  try {
739
- preview = resolveOperatorKey(flags);
774
+ preview = await resolveOperatorSigner(flags);
740
775
  const inferred = await resolveOperatorLabel(flags, preview.address, preview.network);
741
776
  operatorLabel = inferred.label;
742
777
  } catch (err) {
743
778
  exitCliError({
744
779
  error: err.message,
745
- because: "bot mint needs an operator label or a resolvable signing key",
780
+ because: "bot mint needs an operator label or a resolvable signer",
746
781
  try: [
782
+ "clanker login",
747
783
  `clanker bot mint ${botLabel} <operator_label> --yes`,
748
784
  "clanker setup",
749
785
  ],
@@ -752,12 +788,13 @@ async function main() {
752
788
  }
753
789
  if (!preview) {
754
790
  try {
755
- preview = resolveOperatorKey(flags);
791
+ preview = await resolveOperatorSigner(flags);
756
792
  } catch (err) {
757
793
  exitCliError({
758
794
  error: err.message,
759
- because: "bot mint needs a signing key on this network",
795
+ because: "bot mint needs a signer on this network",
760
796
  try: [
797
+ "clanker login",
761
798
  `clanker bot mint ${botLabel} ${operatorLabel} --key-file ~/.clanker/op.key --yes`,
762
799
  "clanker fund",
763
800
  ],
@@ -770,6 +807,7 @@ async function main() {
770
807
  ["operator", operatorLabel],
771
808
  ["owner", preview.address],
772
809
  ["registry", preview.network.registry ?? "(none)"],
810
+ ["signer", preview.source],
773
811
  ];
774
812
  try {
775
813
  if (preview.network.registry) {
package/lib/doctor.mjs CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  } from "./profile.mjs";
13
13
  import { detectSetupHints, formatSetupDetectTable } from "./setup-detect.mjs";
14
14
  import { publicClientFromRpc } from "./identity-query.mjs";
15
+ import { loadPrivySession } from "./privy-session.mjs";
15
16
  import {
16
17
  assessMintBudget,
17
18
  budgetToJson,
@@ -96,17 +97,24 @@ export async function runDoctorChecks(opts = {}) {
96
97
  });
97
98
  }
98
99
 
100
+ const privySession = loadPrivySession(home);
101
+ const hasPrivy =
102
+ Boolean(operator?.key?.type === "privy" && operator.key.value) ||
103
+ Boolean(privySession?.accessToken && privySession?.walletId);
99
104
  const hasKey =
100
105
  Boolean(operator?.key?.type === "keyFile" && operator.key.value) ||
101
106
  Boolean(operator?.key?.type === "env" && operator.key.value) ||
102
- Boolean(env.OPERATOR_PRIVATE_KEY);
107
+ Boolean(env.OPERATOR_PRIVATE_KEY) ||
108
+ hasPrivy;
103
109
  checks.push({
104
110
  id: "signing",
105
111
  ok: true,
106
112
  level: hasKey ? "pass" : "warn",
107
- message: hasKey
108
- ? "signing key pointer available (mint/pair/rotate OK)"
109
- : "read-only profile — whoami/bots/fund OK; mint/pair/rotate need a key pointer or the /join owner",
113
+ message: hasPrivy
114
+ ? "Privy session available (mint/pair/rotate via email vault)"
115
+ : hasKey
116
+ ? "signing key pointer available (mint/pair/rotate OK)"
117
+ : "read-only profile — whoami/bots/fund OK; mint/pair/rotate need clanker login or a key pointer",
110
118
  });
111
119
 
112
120
  checks.push({
@@ -295,20 +303,20 @@ export async function runDoctor(argv = [], opts = {}) {
295
303
  } else {
296
304
  console.log(
297
305
  c.dim(
298
- "Mint/pair/rotate: need a signing key or stay on /join (non-Anvil owner on public RPC)",
306
+ "Mint/pair/rotate: need clanker login or a signing key (non-Anvil owner on public RPC)",
299
307
  ),
300
308
  );
301
309
  }
302
310
 
303
311
  if (!report.readyWhoami) {
304
- nextHint(["clanker setup"]);
312
+ nextHint(["clanker setup", "clanker login"]);
305
313
  } else if (report.budget && !report.budget.funded && !report.budget.local) {
306
314
  nextHint(["clanker fund", "clanker doctor"]);
307
315
  } else if (!report.readyMint) {
308
316
  nextHint([
317
+ "clanker login",
309
318
  "clanker whoami",
310
- "clanker bots",
311
- "mint/pair/rotate need --key-file / OPERATOR_PRIVATE_KEY or stay on /join",
319
+ "mint/pair/rotate need login or --key-file",
312
320
  ]);
313
321
  } else {
314
322
  nextHint(["clanker whoami", "clanker bots"]);
package/lib/login.mjs ADDED
@@ -0,0 +1,200 @@
1
+ /**
2
+ * `clanker login` / `clanker logout` — Privy device grant for the operator vault.
3
+ */
4
+
5
+ import { getAddress } from "viem";
6
+ import { openUrl } from "./fund.mjs";
7
+ import {
8
+ loadOperator,
9
+ writeOperator,
10
+ clankerHome,
11
+ loadConfig,
12
+ } from "./profile.mjs";
13
+ import {
14
+ authenticateWallets,
15
+ pollDeviceToken,
16
+ requestDeviceAuthorization,
17
+ } from "./privy-client.mjs";
18
+ import {
19
+ clearPrivySession,
20
+ loadPrivySession,
21
+ savePrivySession,
22
+ } from "./privy-session.mjs";
23
+ import { resolvePrivyAppId } from "./privy-constants.mjs";
24
+ import { c, nextHint } from "./ui.mjs";
25
+
26
+ /**
27
+ * @param {string[]} argv
28
+ */
29
+ export function parseLoginFlags(argv) {
30
+ let noOpen = false;
31
+ let json = false;
32
+ let operator = null;
33
+ for (let i = 0; i < argv.length; i += 1) {
34
+ const a = argv[i];
35
+ if (a === "--no-open") noOpen = true;
36
+ else if (a === "--json") json = true;
37
+ else if (a === "--operator" && argv[i + 1]) operator = argv[++i];
38
+ }
39
+ return { noOpen, json, operator };
40
+ }
41
+
42
+ /**
43
+ * @param {string[]} argv
44
+ * @param {{
45
+ * home?: string,
46
+ * env?: NodeJS.ProcessEnv,
47
+ * fetchImpl?: typeof fetch,
48
+ * openUrlImpl?: typeof openUrl,
49
+ * sleep?: (ms: number) => Promise<void>,
50
+ * now?: () => number,
51
+ * }} [opts]
52
+ */
53
+ export async function runLogin(argv = [], opts = {}) {
54
+ const flags = parseLoginFlags(argv);
55
+ const env = opts.env ?? process.env;
56
+ const home = opts.home ?? clankerHome(env);
57
+ const openUrlImpl = opts.openUrlImpl ?? openUrl;
58
+
59
+ const device = await requestDeviceAuthorization({
60
+ appId: resolvePrivyAppId(env),
61
+ fetchImpl: opts.fetchImpl,
62
+ });
63
+
64
+ const url =
65
+ device.verificationUriComplete ||
66
+ `${device.verificationUri}?user_code=${encodeURIComponent(device.userCode)}`;
67
+
68
+ if (!flags.json) {
69
+ console.log(c.bold("clanker login"));
70
+ console.log("");
71
+ console.log("Approve CLI access to your operator wallet (email vault).");
72
+ console.log("");
73
+ console.log(`Visit: ${url}`);
74
+ console.log(`Code: ${device.userCode}`);
75
+ console.log("");
76
+ console.log(c.dim("Waiting for browser approval…"));
77
+ }
78
+
79
+ if (!flags.noOpen) {
80
+ await openUrlImpl(url);
81
+ }
82
+
83
+ const tokens = await pollDeviceToken({
84
+ appId: device.appId,
85
+ deviceCode: device.deviceCode,
86
+ interval: device.interval,
87
+ expiresIn: device.expiresIn,
88
+ fetchImpl: opts.fetchImpl,
89
+ sleep: opts.sleep,
90
+ now: opts.now,
91
+ });
92
+
93
+ const auth = await authenticateWallets({
94
+ appId: device.appId,
95
+ accessToken: tokens.accessToken,
96
+ fetchImpl: opts.fetchImpl,
97
+ });
98
+
99
+ const session = {
100
+ appId: device.appId,
101
+ accessToken: tokens.accessToken,
102
+ refreshToken: tokens.refreshToken,
103
+ expiresAt: Date.now() + tokens.expiresIn * 1000,
104
+ walletId: auth.walletId,
105
+ address: auth.address,
106
+ createdAt: Date.now(),
107
+ };
108
+ const stored = savePrivySession(session, home);
109
+
110
+ const existing = loadOperator(home);
111
+ const label = flags.operator || existing?.label || null;
112
+ if (existing?.owner && getAddress(existing.owner) !== auth.address) {
113
+ throw new Error(
114
+ `Logged-in address ${auth.address} does not match operator.json owner ${existing.owner}. ` +
115
+ `Use a matching login, or clanker setup --force with the new owner.`,
116
+ );
117
+ }
118
+
119
+ if (label || existing) {
120
+ writeOperator(
121
+ {
122
+ label: label || existing.label,
123
+ owner: auth.address,
124
+ key: { type: "privy", value: auth.walletId },
125
+ },
126
+ home,
127
+ );
128
+ } else if (!loadConfig(home)) {
129
+ // Login alone is enough for session; setup still needed for network + label.
130
+ } else {
131
+ // Config exists but no operator yet — leave operator.json to setup / mint.
132
+ }
133
+
134
+ const payload = {
135
+ ok: true,
136
+ address: auth.address,
137
+ walletId: auth.walletId,
138
+ storage: stored.storage,
139
+ label: label || existing?.label || null,
140
+ };
141
+
142
+ if (flags.json) {
143
+ console.log(JSON.stringify(payload, null, 2));
144
+ } else {
145
+ console.log("");
146
+ console.log(c.green(`Logged in as ${auth.address}`));
147
+ console.log(c.dim(`wallet: ${auth.walletId}`));
148
+ console.log(c.dim(`session: ${stored.storage}`));
149
+ if (!label && !existing?.label) {
150
+ nextHint([
151
+ "clanker setup --preset sepolia --operator org.you --yes # if no profile yet",
152
+ "clanker fund",
153
+ "clanker whoami",
154
+ "clanker pair add <peer> --yes",
155
+ ]);
156
+ } else {
157
+ nextHint([
158
+ "clanker fund",
159
+ "clanker whoami",
160
+ "clanker pair add <peer> --yes",
161
+ "clanker operator mint <label> --yes # if not minted yet",
162
+ ]);
163
+ }
164
+ }
165
+ return { exitCode: 0, ...payload, session };
166
+ }
167
+
168
+ /**
169
+ * @param {string[]} argv
170
+ * @param {{ home?: string, env?: NodeJS.ProcessEnv }} [opts]
171
+ */
172
+ export async function runLogout(argv = [], opts = {}) {
173
+ const json = argv.includes("--json");
174
+ const home = opts.home ?? clankerHome(opts.env ?? process.env);
175
+ const had = Boolean(loadPrivySession(home));
176
+ clearPrivySession(home);
177
+ const operator = loadOperator(home);
178
+ if (operator?.key?.type === "privy") {
179
+ writeOperator(
180
+ {
181
+ label: operator.label,
182
+ owner: operator.owner,
183
+ key: null,
184
+ },
185
+ home,
186
+ );
187
+ }
188
+ if (json) {
189
+ console.log(JSON.stringify({ ok: true, cleared: had }, null, 2));
190
+ } else {
191
+ console.log(c.bold("clanker logout"));
192
+ console.log(had ? c.green("Cleared Privy CLI session.") : c.dim("No session stored."));
193
+ console.log(
194
+ c.dim(
195
+ "Revoke grants anytime in the Privy dashboard / account settings if needed.",
196
+ ),
197
+ );
198
+ }
199
+ return { exitCode: 0, cleared: had };
200
+ }