@zoralabs/cli 1.2.0 → 1.4.0

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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.tsx
4
- import { Command as Command16 } from "commander";
4
+ import { Command as Command17 } from "commander";
5
5
  import { ExitPromptError } from "@inquirer/core";
6
6
  import "fs";
7
7
  import { setApiBaseUrl } from "@zoralabs/coins-sdk";
@@ -175,6 +175,9 @@ function bannedCoinMessage(address) {
175
175
  function bannedCoinBuyMessage(address) {
176
176
  return `Unable to buy ${address} because it violates the Zora terms of service. Already own this coin? Run zora sell ${address} --all to exit your position.`;
177
177
  }
178
+ function bannedProfileMessage(identifier) {
179
+ return `This account (${identifier}) has been blocked for violating the Zora terms of service.`;
180
+ }
178
181
  function fsErrorMessage(err, path) {
179
182
  if (!(err instanceof Error)) return String(err);
180
183
  const code = err.code;
@@ -1466,7 +1469,7 @@ var getClient = () => {
1466
1469
  return client;
1467
1470
  };
1468
1471
  var commonProperties = () => ({
1469
- cli_version: true ? "1.2.0" : "development",
1472
+ cli_version: true ? "1.4.0" : "development",
1470
1473
  os: process.platform,
1471
1474
  arch: process.arch,
1472
1475
  node_version: process.version
@@ -2754,6 +2757,23 @@ async function createFirstPost(params) {
2754
2757
  };
2755
2758
  }
2756
2759
 
2760
+ // src/lib/agent/api-key.ts
2761
+ var CREATE_API_KEY_MUTATION = "mutation CreateApiKeyMutation($apiKeyName: String!, $hosts: [String!]) { createApiKey(apiKeyName: $apiKeyName, hosts: $hosts) { apiKey }}";
2762
+ async function createApiKey(token, apiKeyName, hosts) {
2763
+ const { data, errors, status } = await graphqlRequest(
2764
+ token,
2765
+ CREATE_API_KEY_MUTATION,
2766
+ "CreateApiKeyMutation",
2767
+ { apiKeyName, hosts: hosts ?? null }
2768
+ );
2769
+ const apiKey = data?.createApiKey?.apiKey;
2770
+ if (apiKey) {
2771
+ return apiKey;
2772
+ }
2773
+ const lastError = errors?.[0]?.message ?? `HTTP ${status}`;
2774
+ throw new Error(`createApiKey failed: ${lastError}`);
2775
+ }
2776
+
2757
2777
  // src/lib/agent/onboard.ts
2758
2778
  var DEFAULT_BASE_RPC = "https://mainnet.base.org";
2759
2779
  var ZORA_BASE_URL = "https://zora.co";
@@ -2777,6 +2797,15 @@ async function onboardAgent(opts) {
2777
2797
  const isNewUser = session.isNewUser;
2778
2798
  progress("profile", "creating the Zora profile");
2779
2799
  let profile = await createAgentProfile(session.accessToken);
2800
+ progress("apiKey", "creating an API key");
2801
+ const apiKey = await createApiKey(session.accessToken, "AGENT_API_KEY");
2802
+ try {
2803
+ saveApiKey(apiKey);
2804
+ } catch (err) {
2805
+ throw new Error(
2806
+ `Failed to save API key: ${fsErrorMessage(err, getConfigPath())}`
2807
+ );
2808
+ }
2780
2809
  if (opts.username !== void 0 || opts.bio !== void 0 || opts.avatar !== void 0) {
2781
2810
  const chosen = [
2782
2811
  opts.username !== void 0 ? "username" : void 0,
@@ -2839,7 +2868,7 @@ async function onboardAgent(opts) {
2839
2868
  dryRun,
2840
2869
  profileUrl: `${ZORA_BASE_URL}/@${profile.username}`
2841
2870
  };
2842
- if (opts.withCoin) {
2871
+ if (!opts.skipCoin) {
2843
2872
  progress(
2844
2873
  "coin",
2845
2874
  dryRun ? "simulating the creator coin" : "minting the creator coin"
@@ -3068,12 +3097,12 @@ function resolveAgentKey(json, override, { allowGenerate = true } = {}) {
3068
3097
  return { key: generated, source: getWalletPath(), generated: true };
3069
3098
  }
3070
3099
  var agentCommand = new Command("agent").description(
3071
- "Create and manage a Zora agent identity.\nStands up an identity from an EOA \u2014 Privy account, profile, and smart wallet \u2014 with no human interaction. The creator coin is opt-in: add it with `agent create --with-coin` or later with `agent coin`."
3100
+ "Create and manage a Zora agent identity.\nStands up an identity from an EOA \u2014 Privy account, profile, smart wallet, and creator coin \u2014 with no human interaction."
3072
3101
  ).action(function() {
3073
3102
  this.outputHelp();
3074
3103
  });
3075
3104
  agentCommand.command("create").description(
3076
- "Create a Zora agent from an EOA, end to end and unattended: headless Privy account, profile, and smart wallet. The creator coin is opt-in (--with-coin, or `agent coin` later); a first post is published when --caption and --image are supplied. Every on-chain step is sponsored, so the agent needs no ETH."
3105
+ "Create a Zora agent from an EOA, end to end and unattended: headless Privy account, profile, smart wallet, and creator coin. A first post is published when --caption and --image are supplied. Every on-chain step is sponsored, so the agent needs no ETH. Skip the coin with --skip-coin."
3077
3106
  ).option(
3078
3107
  "--private-key <key>",
3079
3108
  "EOA private key to sign in with (default: ZORA_PRIVATE_KEY, then your saved wallet, else a new one is generated)"
@@ -3083,8 +3112,8 @@ agentCommand.command("create").description(
3083
3112
  String(DEFAULT_SIWE_CHAIN_ID)
3084
3113
  ).option("--rpc-url <url>", "Base RPC URL (defaults to the public endpoint)").option(
3085
3114
  "--dry-run",
3086
- "Create the account, profile, and smart wallet, but simulate the opted-in coin + post instead of minting them"
3087
- ).option("--with-coin", "Also create the agent's creator coin").option("--skip-post", "Skip publishing the first post").option(
3115
+ "Create the account, profile, and smart wallet, but simulate the coin + post instead of minting them"
3116
+ ).option("--skip-coin", "Skip minting the agent's creator coin").option("--skip-post", "Skip publishing the first post").option(
3088
3117
  "--username <name>",
3089
3118
  "Set the agent's username (also sets the display name; must be available). Default: an auto-assigned handle."
3090
3119
  ).option("--bio <text>", "Set the agent's bio. Default: an auto-assigned bio.").option(
@@ -3158,16 +3187,29 @@ agentCommand.command("create").description(
3158
3187
  }
3159
3188
  }
3160
3189
  const resolved = resolveAgentKey(json, options.privateKey);
3161
- const wouldMint = Boolean(options.withCoin) || hasCaption && hasImage;
3190
+ const wouldMint = !options.skipCoin || hasCaption && hasImage;
3162
3191
  if (!options.dryRun && wouldMint) {
3163
3192
  const existingAgent = peekAgentWallet();
3164
3193
  if (existingAgent) {
3165
3194
  await confirmAgentAction({
3166
3195
  json,
3167
3196
  force: options.force,
3168
- warning: `You already have an agent: @${existingAgent.username} (smart wallet ${existingAgent.smartWalletAddress}).
3169
- Re-running 'agent create' will mint another creator coin and/or first post for it.`,
3170
- question: `Create another coin/post for @${existingAgent.username}?`
3197
+ warning: (() => {
3198
+ const willMintCoin = !options.skipCoin;
3199
+ const willMintPost = hasCaption && hasImage;
3200
+ const what = [
3201
+ willMintCoin && "creator coin",
3202
+ willMintPost && "first post"
3203
+ ].filter(Boolean).join(" and ");
3204
+ return `You already have an agent: @${existingAgent.username} (smart wallet ${existingAgent.smartWalletAddress}).
3205
+ Re-running 'agent create' will mint another ${what} for it.`;
3206
+ })(),
3207
+ question: (() => {
3208
+ const willMintCoin = !options.skipCoin;
3209
+ const willMintPost = hasCaption && hasImage;
3210
+ const what = [willMintCoin && "coin", willMintPost && "post"].filter(Boolean).join("/");
3211
+ return `Create another ${what} for @${existingAgent.username}?`;
3212
+ })()
3171
3213
  });
3172
3214
  }
3173
3215
  }
@@ -3180,7 +3222,7 @@ Re-running 'agent create' will mint another creator coin and/or first post for i
3180
3222
  chainId,
3181
3223
  rpcUrl: options.rpcUrl,
3182
3224
  dryRun: Boolean(options.dryRun),
3183
- withCoin: Boolean(options.withCoin),
3225
+ skipCoin: Boolean(options.skipCoin),
3184
3226
  skipPost: Boolean(options.skipPost),
3185
3227
  username: options.username,
3186
3228
  bio: options.bio,
@@ -3196,7 +3238,7 @@ Re-running 'agent create' will mint another creator coin and/or first post for i
3196
3238
  return outputErrorAndExit(
3197
3239
  json,
3198
3240
  `Agent onboarding failed: ${formatError(err)}`,
3199
- "Re-run to retry \u2014 the profile and smart wallet are idempotent. Add the creator coin later with `zora agent coin`."
3241
+ "Re-run to retry \u2014 the profile and smart wallet are idempotent."
3200
3242
  );
3201
3243
  }
3202
3244
  const walletPath = getWalletPath();
@@ -3224,7 +3266,7 @@ Re-running 'agent create' will mint another creator coin and/or first post for i
3224
3266
  generated_wallet: resolved.generated,
3225
3267
  saved_to_wallet: savedToWallet,
3226
3268
  dry_run: result.dryRun,
3227
- with_coin: Boolean(options.withCoin),
3269
+ skip_coin: Boolean(options.skipCoin),
3228
3270
  minted_coin: Boolean(result.coin?.hash),
3229
3271
  minted_post: Boolean(result.post?.hash),
3230
3272
  coin_failed: Boolean(result.coinError),
@@ -3249,8 +3291,10 @@ Re-running 'agent create' will mint another creator coin and/or first post for i
3249
3291
  },
3250
3292
  render: () => {
3251
3293
  const simulated = result.dryRun && (result.coin || result.post);
3294
+ const simulatedWhat = [result.coin && "coin", result.post && "post"].filter(Boolean).join(" + ");
3252
3295
  console.log(
3253
- simulated ? "\n\u2713 Agent ready (dry run \u2014 coin + post simulated, not minted)" : result.dryRun ? "\n\u2713 Agent ready (dry run \u2014 account + smart wallet created; no coin/post requested)" : "\n\u2713 Agent ready"
3296
+ simulated ? `
3297
+ \u2713 Agent ready (dry run \u2014 ${simulatedWhat} simulated, not minted)` : result.dryRun ? "\n\u2713 Agent ready (dry run \u2014 account + smart wallet created; coin + post skipped)" : "\n\u2713 Agent ready"
3254
3298
  );
3255
3299
  console.log(` Profile: @${result.username}`);
3256
3300
  if (options.bio !== void 0) {
@@ -3270,8 +3314,11 @@ Re-running 'agent create' will mint another creator coin and/or first post for i
3270
3314
  );
3271
3315
  } else if (result.coinError) {
3272
3316
  console.log(` Creator coin: failed \u2014 ${result.coinError}`);
3273
- } else if (!result.dryRun) {
3274
- console.log(" Creator coin: none \u2014 add one with `zora agent coin`");
3317
+ console.log(" Retry with `zora agent coin`.");
3318
+ } else if (!result.dryRun && options.skipCoin) {
3319
+ console.log(
3320
+ " Creator coin: skipped \u2014 add one later with `zora agent coin`"
3321
+ );
3275
3322
  }
3276
3323
  if (result.post) {
3277
3324
  console.log(
@@ -3929,9 +3976,7 @@ budgetCommand.command("record").description(
3929
3976
  "\u2022 No global budget configured \u2014 nothing to record. Set one with `zora agent budget set <amount>`."
3930
3977
  );
3931
3978
  } else {
3932
- console.log(
3933
- "\u2022 Budget opted out (no limit) \u2014 nothing to record."
3934
- );
3979
+ console.log("\u2022 Budget opted out (no limit) \u2014 nothing to record.");
3935
3980
  }
3936
3981
  }
3937
3982
  });
@@ -6917,10 +6962,17 @@ var fetchProfile = async (address) => {
6917
6962
  address,
6918
6963
  handle,
6919
6964
  displayName: profile?.displayName ?? handle,
6920
- avatarUrl: profile?.avatar?.previewImage?.small ?? null
6965
+ avatarUrl: profile?.avatar?.previewImage?.small ?? null,
6966
+ platformBlocked: profile?.platformBlocked ?? false
6921
6967
  };
6922
6968
  } catch {
6923
- return { address, handle: null, displayName: null, avatarUrl: null };
6969
+ return {
6970
+ address,
6971
+ handle: null,
6972
+ displayName: null,
6973
+ avatarUrl: null,
6974
+ platformBlocked: false
6975
+ };
6924
6976
  }
6925
6977
  };
6926
6978
  var resolveProfiles = async (addresses, _token, onProgress) => {
@@ -6935,7 +6987,8 @@ var resolveProfiles = async (addresses, _token, onProgress) => {
6935
6987
  address,
6936
6988
  handle: cached.handle,
6937
6989
  displayName: cached.displayName,
6938
- avatarUrl: cached.avatarUrl
6990
+ avatarUrl: cached.avatarUrl,
6991
+ platformBlocked: cached.platformBlocked ?? false
6939
6992
  });
6940
6993
  } else {
6941
6994
  stale.push(address);
@@ -6960,6 +7013,7 @@ var resolveProfiles = async (addresses, _token, onProgress) => {
6960
7013
  handle: profile.handle,
6961
7014
  displayName: profile.displayName,
6962
7015
  avatarUrl: profile.avatarUrl,
7016
+ platformBlocked: profile.platformBlocked,
6963
7017
  fetchedAt: now
6964
7018
  };
6965
7019
  }
@@ -7085,7 +7139,7 @@ var resolveClient = async (json) => {
7085
7139
  );
7086
7140
  }
7087
7141
  const token = await auth.getApiToken();
7088
- const { createMessagingClient } = await import("./client-4HW4ZUHD.js");
7142
+ const { createMessagingClient } = await import("./client-M3K6L2ZM.js");
7089
7143
  const client2 = await createMessagingClient(auth.signerSpec, {
7090
7144
  // Register the CLI installation with the Zora backend so it shows up in the
7091
7145
  // user's device list and counts against the install cap. Best-effort — see
@@ -7293,6 +7347,19 @@ dmCommand.command("send").description(
7293
7347
  ).argument("<address>", "Recipient: Zora handle (@name) or 0x address").argument("[message]", "Message text").action(async function(address, message) {
7294
7348
  const json = getJson(this);
7295
7349
  const peer = await resolvePeer(json, address);
7350
+ const profiles = await resolveProfiles([peer]);
7351
+ const profile = profiles.get(peer);
7352
+ if (profile?.platformBlocked) {
7353
+ track("cli_dm_send", {
7354
+ output_format: json ? "json" : "text",
7355
+ success: false,
7356
+ blocked_profile: true
7357
+ });
7358
+ return outputErrorAndExit(
7359
+ json,
7360
+ bannedProfileMessage(profile.handle ?? peer)
7361
+ );
7362
+ }
7296
7363
  if (!message || !message.trim()) {
7297
7364
  return outputErrorAndExit(
7298
7365
  json,
@@ -7330,6 +7397,64 @@ dmCommand.command("send").description(
7330
7397
  await client2.close();
7331
7398
  }
7332
7399
  });
7400
+ dmCommand.command("listen").description(
7401
+ "Stream incoming DMs in real time (no polling \u2014 uses XMTP's server-push stream to avoid rate limits)"
7402
+ ).action(async function() {
7403
+ const json = getJson(this);
7404
+ const { client: client2 } = await resolveClient(json);
7405
+ if (!json) {
7406
+ console.log("Listening for new DMs\u2026 (Ctrl+C to stop)\n");
7407
+ }
7408
+ const shutdown = () => {
7409
+ client2.close().finally(() => process.exit(0));
7410
+ };
7411
+ process.on("SIGINT", shutdown);
7412
+ process.on("SIGTERM", shutdown);
7413
+ const labelCache = /* @__PURE__ */ new Map();
7414
+ const cachedPeerLabel = async (peer) => {
7415
+ const cached = labelCache.get(peer);
7416
+ if (cached) return cached;
7417
+ const label = await peerLabel(peer);
7418
+ labelCache.set(peer, label);
7419
+ return label;
7420
+ };
7421
+ let messageCount = 0;
7422
+ track("cli_dm_listen_start", {
7423
+ output_format: json ? "json" : "text"
7424
+ });
7425
+ try {
7426
+ for await (const msg of client2.streamAllMessages()) {
7427
+ if (msg.fromSelf) continue;
7428
+ messageCount++;
7429
+ const who = msg.peerAddress ? await cachedPeerLabel(msg.peerAddress) : "unknown";
7430
+ if (json) {
7431
+ console.log(
7432
+ JSON.stringify({
7433
+ from: who,
7434
+ address: msg.peerAddress,
7435
+ text: msg.text,
7436
+ contentType: msg.contentType,
7437
+ sentAt: new Date(msg.sentAtMs).toISOString()
7438
+ })
7439
+ );
7440
+ } else {
7441
+ const body = msg.text ? sanitizeMessageText(msg.text) : `[${msg.contentType}]`;
7442
+ const [first = "", ...rest] = body.split("\n");
7443
+ console.log(
7444
+ `\u2190 ${who} ${dim(formatAge(msg.sentAtMs))}
7445
+ ${first}`
7446
+ );
7447
+ for (const line of rest) console.log(` ${line}`);
7448
+ }
7449
+ }
7450
+ } finally {
7451
+ track("cli_dm_listen_end", {
7452
+ output_format: json ? "json" : "text",
7453
+ message_count: messageCount
7454
+ });
7455
+ await client2.close();
7456
+ }
7457
+ });
7333
7458
  var consentSubcommand = (name, consent, description) => {
7334
7459
  dmCommand.command(name).description(description).argument("<address>", "Zora handle (@name) or 0x address").action(async function(address) {
7335
7460
  const json = getJson(this);
@@ -7688,10 +7813,219 @@ var exploreCommand = new Command8("explore").description("Browse top, new, and h
7688
7813
  }
7689
7814
  });
7690
7815
 
7691
- // src/commands/get.tsx
7816
+ // src/commands/follow.ts
7817
+ import { getProfile as getProfile2, setApiKey as setApiKey6 } from "@zoralabs/coins-sdk";
7692
7818
  import { Command as Command9 } from "commander";
7819
+ import { erc20Abi as erc20Abi3, isAddress as isAddress7 } from "viem";
7820
+
7821
+ // src/lib/follow.ts
7822
+ var FOLLOW_MUTATION = "mutation CliFollow($followeeId: String!) { follow(followeeId: $followeeId) { handle profileId vcFollowingStatus } }";
7823
+ var UNFOLLOW_MUTATION = "mutation CliUnfollow($followeeId: String!) { unfollow(followeeId: $followeeId) { handle profileId vcFollowingStatus } }";
7824
+ async function mutateFollow(token, followeeId, mutation, operationName, field) {
7825
+ const { data, errors, status } = await graphqlRequest(
7826
+ token,
7827
+ mutation,
7828
+ operationName,
7829
+ { followeeId }
7830
+ );
7831
+ const profile = data?.[field];
7832
+ if (profile?.profileId) {
7833
+ return {
7834
+ // The API returns the full address as both fields when the target has no
7835
+ // profile; fall back to profileId so handle is never empty.
7836
+ handle: profile.handle ?? profile.profileId,
7837
+ profileId: profile.profileId,
7838
+ followingStatus: profile.vcFollowingStatus ?? "UNKNOWN"
7839
+ };
7840
+ }
7841
+ const lastError = errors?.[0]?.message ?? `HTTP ${status}`;
7842
+ throw new Error(`${field} failed: ${lastError}`);
7843
+ }
7844
+ function followProfile(token, followeeId) {
7845
+ return mutateFollow(
7846
+ token,
7847
+ followeeId,
7848
+ FOLLOW_MUTATION,
7849
+ "CliFollow",
7850
+ "follow"
7851
+ );
7852
+ }
7853
+ function unfollowProfile(token, followeeId) {
7854
+ return mutateFollow(
7855
+ token,
7856
+ followeeId,
7857
+ UNFOLLOW_MUTATION,
7858
+ "CliUnfollow",
7859
+ "unfollow"
7860
+ );
7861
+ }
7862
+
7863
+ // src/commands/follow.ts
7864
+ function isPlaceholderName(name) {
7865
+ return name.startsWith("0x") || name.includes("\u2026") || name.includes("...");
7866
+ }
7867
+ function displayName(result) {
7868
+ return isPlaceholderName(result.handle) ? result.profileId : `@${result.handle}`;
7869
+ }
7870
+ function relationshipNote(status) {
7871
+ if (status === "MUTUAL_FOLLOWING") return "You follow each other.";
7872
+ if (status === "FOLLOWED") return "They still follow you.";
7873
+ return void 0;
7874
+ }
7875
+ async function requireCreatorCoinHolding(json, identifier) {
7876
+ const apiKey = getApiKey();
7877
+ if (apiKey) setApiKey6(apiKey);
7878
+ let profile;
7879
+ try {
7880
+ const response = await getProfile2({ identifier });
7881
+ profile = response?.data?.profile;
7882
+ } catch (err) {
7883
+ return outputErrorAndExit(
7884
+ json,
7885
+ `Couldn't look up "${identifier}": ${formatError(err)}`
7886
+ );
7887
+ }
7888
+ if (!profile) {
7889
+ return outputErrorAndExit(
7890
+ json,
7891
+ `No Zora profile found for "${identifier}".`,
7892
+ "Provide an existing Zora username or wallet address."
7893
+ );
7894
+ }
7895
+ const label = profile.handle && !isPlaceholderName(profile.handle) ? `@${profile.handle}` : identifier;
7896
+ const coinAddress = profile.creatorCoin?.address;
7897
+ if (!coinAddress || !isAddress7(coinAddress)) {
7898
+ return outputErrorAndExit(
7899
+ json,
7900
+ `${label} doesn't have a creator coin yet, so there's nothing to buy.`,
7901
+ "Following requires holding the profile's creator coin."
7902
+ );
7903
+ }
7904
+ const { privateKeyAccount, smartWalletAccount } = await resolveAccounts();
7905
+ const wallet = smartWalletAccount?.address ?? privateKeyAccount.address;
7906
+ const { publicClient } = createClients(privateKeyAccount, smartWalletAccount);
7907
+ let balance;
7908
+ try {
7909
+ balance = await publicClient.readContract({
7910
+ abi: erc20Abi3,
7911
+ address: coinAddress,
7912
+ functionName: "balanceOf",
7913
+ args: [wallet]
7914
+ });
7915
+ } catch (err) {
7916
+ return outputErrorAndExit(
7917
+ json,
7918
+ `Couldn't check your creator-coin balance: ${formatError(err)}`
7919
+ );
7920
+ }
7921
+ if (balance === 0n) {
7922
+ return outputErrorAndExit(
7923
+ json,
7924
+ `You must hold ${label}'s creator coin to follow them.`,
7925
+ `Buy some first: zora buy ${coinAddress} --eth 0.001`
7926
+ );
7927
+ }
7928
+ }
7929
+ async function resolveToken(json, key) {
7930
+ try {
7931
+ const session = await ensurePrivySession({ privateKey: normalizeKey(key) });
7932
+ return session.accessToken;
7933
+ } catch (err) {
7934
+ return outputErrorAndExit(json, `Sign-in failed: ${formatError(err)}`);
7935
+ }
7936
+ }
7937
+ async function runFollow(command, action, identifierArg) {
7938
+ const json = getJson(command);
7939
+ const followeeId = (identifierArg ?? "").replace(/^@/, "").trim();
7940
+ if (!followeeId) {
7941
+ return outputErrorAndExit(
7942
+ json,
7943
+ `Missing user to ${action}.`,
7944
+ `Usage: zora ${action} <username | address>`
7945
+ );
7946
+ }
7947
+ const key = process.env.ZORA_PRIVATE_KEY || getPrivateKey();
7948
+ if (!key) {
7949
+ return outputErrorAndExit(
7950
+ json,
7951
+ "No wallet configured.",
7952
+ "Run 'zora agent create' to set up your Zora agent."
7953
+ );
7954
+ }
7955
+ if (action === "follow") {
7956
+ await requireCreatorCoinHolding(json, followeeId);
7957
+ }
7958
+ const token = await resolveToken(json, key);
7959
+ let result;
7960
+ try {
7961
+ result = action === "follow" ? await followProfile(token, followeeId) : await unfollowProfile(token, followeeId);
7962
+ } catch (err) {
7963
+ track("cli_follow", {
7964
+ action,
7965
+ output_format: json ? "json" : "static",
7966
+ success: false,
7967
+ error_type: err instanceof Error ? err.constructor.name : "unknown"
7968
+ });
7969
+ await shutdownAnalytics();
7970
+ const message = formatError(err);
7971
+ if (action === "follow" && /yourself/i.test(message)) {
7972
+ return outputErrorAndExit(json, "You can't follow yourself.");
7973
+ }
7974
+ return outputErrorAndExit(
7975
+ json,
7976
+ `Failed to ${action} "${followeeId}": ${message}`,
7977
+ "Check the username or address is a real Zora profile and try again."
7978
+ );
7979
+ }
7980
+ if (result.followingStatus === "SELF") {
7981
+ track("cli_follow", {
7982
+ action,
7983
+ output_format: json ? "json" : "static",
7984
+ success: false,
7985
+ error_type: "self"
7986
+ });
7987
+ await shutdownAnalytics();
7988
+ return outputErrorAndExit(json, `You can't ${action} yourself.`);
7989
+ }
7990
+ const label = displayName(result);
7991
+ const profileUrl = isPlaceholderName(result.handle) ? void 0 : `https://zora.co/@${result.handle}`;
7992
+ const note2 = relationshipNote(result.followingStatus);
7993
+ track("cli_follow", {
7994
+ action,
7995
+ output_format: json ? "json" : "static",
7996
+ success: true,
7997
+ following_status: result.followingStatus
7998
+ });
7999
+ outputData(json, {
8000
+ json: {
8001
+ action,
8002
+ followee: result.profileId,
8003
+ handle: result.handle,
8004
+ followingStatus: result.followingStatus,
8005
+ ...profileUrl ? { profileUrl } : {}
8006
+ },
8007
+ render: () => {
8008
+ console.log(
8009
+ `
8010
+ \u2713 ${action === "follow" ? "Following" : "Unfollowed"} ${label}`
8011
+ );
8012
+ if (note2) console.log(` ${note2}`);
8013
+ if (profileUrl) console.log(` ${profileUrl}`);
8014
+ console.log("");
8015
+ }
8016
+ });
8017
+ }
8018
+ var followCommand = new Command9("follow").description("Follow a Zora user whose creator coin you hold").argument("[identifier]", "Username (@handle), wallet address, or account id").action(async function(identifier) {
8019
+ await runFollow(this, "follow", identifier);
8020
+ });
8021
+ var unfollowCommand = new Command9("unfollow").description("Unfollow a Zora user by username or address").argument("[identifier]", "Username (@handle), wallet address, or account id").action(async function(identifier) {
8022
+ await runFollow(this, "unfollow", identifier);
8023
+ });
8024
+
8025
+ // src/commands/get.tsx
8026
+ import { Command as Command10 } from "commander";
7693
8027
  import { Box as Box12, Text as Text12 } from "ink";
7694
- import { setApiKey as setApiKey6, getCoinHolders, getCoinSwaps } from "@zoralabs/coins-sdk";
8028
+ import { setApiKey as setApiKey7, getCoinHolders, getCoinSwaps } from "@zoralabs/coins-sdk";
7695
8029
 
7696
8030
  // src/components/CoinDetail.tsx
7697
8031
  import { Box as Box7, Text as Text7 } from "ink";
@@ -8186,7 +8520,7 @@ function formatCoinJson(coin) {
8186
8520
  var resolveApiKey2 = () => {
8187
8521
  const apiKey = getApiKey();
8188
8522
  if (apiKey) {
8189
- setApiKey6(apiKey);
8523
+ setApiKey7(apiKey);
8190
8524
  }
8191
8525
  };
8192
8526
  var CoinResolutionError = class extends Error {
@@ -8315,7 +8649,7 @@ async function fetchRecentTrades(address) {
8315
8649
  return [];
8316
8650
  }
8317
8651
  }
8318
- var getCommand = new Command9("get").description("Look up a coin by address or name").argument("[typeOrId]", "Type prefix (creator-coin, trend) or identifier").argument(
8652
+ var getCommand = new Command10("get").description("Look up a coin by address or name").argument("[typeOrId]", "Type prefix (creator-coin, trend) or identifier").argument(
8319
8653
  "[identifier]",
8320
8654
  "Coin address (0x...) or name (when type prefix is given)"
8321
8655
  ).option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
@@ -8845,15 +9179,15 @@ import confirm6 from "@inquirer/confirm";
8845
9179
  import {
8846
9180
  createQuote as createQuote2,
8847
9181
  getCoin as getCoin4,
8848
- setApiKey as setApiKey7,
9182
+ setApiKey as setApiKey8,
8849
9183
  tradeCoin as tradeCoin2,
8850
9184
  tradeCoinSmartWallet as tradeCoinSmartWallet2
8851
9185
  } from "@zoralabs/coins-sdk";
8852
- import { Command as Command10 } from "commander";
9186
+ import { Command as Command11 } from "commander";
8853
9187
  import {
8854
- erc20Abi as erc20Abi3,
9188
+ erc20Abi as erc20Abi4,
8855
9189
  formatUnits as formatUnits5,
8856
- isAddress as isAddress7,
9190
+ isAddress as isAddress8,
8857
9191
  parseUnits as parseUnits2
8858
9192
  } from "viem";
8859
9193
  function printSellQuote(output, info) {
@@ -8930,7 +9264,7 @@ function printSellResult(output, info) {
8930
9264
  console.log(` Tx ${info.txHash}
8931
9265
  `);
8932
9266
  }
8933
- var sellCommand = new Command10("sell").description("Sell a coin").argument(
9267
+ var sellCommand = new Command11("sell").description("Sell a coin").argument(
8934
9268
  "[typeOrId]",
8935
9269
  "Type prefix (creator-coin, trend) or coin address/name"
8936
9270
  ).argument("[identifier]", "Coin name (when type prefix is given)").option("--amount <value>", "Sell specific number of coins").option("--usd <value>", "Sell USD equivalent worth of coins").option("--percent <value>", "Sell percentage of coin balance").option("--all", "Sell entire coin balance").option("--to <asset>", "Receive asset: eth, usdc, zora", "eth").option("--token <asset>", "Receive asset: eth, usdc, zora (alias for --to)").option("--quote", "Print quote and exit without trading").option("--yes", "Skip confirmation and execute directly").option("--slippage <pct>", "Slippage tolerance percent", "1").option("--debug", "Print full quote request/response JSON").action(async function(typeOrId, identifier, opts) {
@@ -8947,12 +9281,12 @@ var sellCommand = new Command10("sell").description("Sell a coin").argument(
8947
9281
  }
8948
9282
  const apiKey = getApiKey();
8949
9283
  if (apiKey) {
8950
- setApiKey7(apiKey);
9284
+ setApiKey8(apiKey);
8951
9285
  }
8952
9286
  let coinAddress;
8953
9287
  let earlyAccounts;
8954
9288
  if (parsed.kind === "address") {
8955
- if (!isAddress7(parsed.address)) {
9289
+ if (!isAddress8(parsed.address)) {
8956
9290
  return outputErrorAndExit(json, `Invalid address: ${parsed.address}`);
8957
9291
  }
8958
9292
  coinAddress = parsed.address;
@@ -8968,7 +9302,7 @@ var sellCommand = new Command10("sell").description("Sell a coin").argument(
8968
9302
  ambResult = await resolveAmbiguousByNameAndBalance(
8969
9303
  parsed.name,
8970
9304
  (addr) => earlyPublicClient.readContract({
8971
- abi: erc20Abi3,
9305
+ abi: erc20Abi4,
8972
9306
  address: addr,
8973
9307
  functionName: "balanceOf",
8974
9308
  args: [earlyWalletAddress]
@@ -9112,7 +9446,7 @@ var sellCommand = new Command10("sell").description("Sell a coin").argument(
9112
9446
  }
9113
9447
  } else {
9114
9448
  const balance = await publicClient.readContract({
9115
- abi: erc20Abi3,
9449
+ abi: erc20Abi4,
9116
9450
  address: coinAddress,
9117
9451
  functionName: "balanceOf",
9118
9452
  args: [walletAddress]
@@ -9374,13 +9708,13 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
9374
9708
  });
9375
9709
 
9376
9710
  // src/commands/profile.tsx
9377
- import { Command as Command11 } from "commander";
9711
+ import { Command as Command12 } from "commander";
9378
9712
  import { Box as Box17, Text as Text17 } from "ink";
9379
9713
  import {
9380
9714
  getProfileCoins,
9381
9715
  getProfileBalances as getProfileBalances2,
9382
9716
  getWalletTradeActivity,
9383
- setApiKey as setApiKey8
9717
+ setApiKey as setApiKey9
9384
9718
  } from "@zoralabs/coins-sdk";
9385
9719
  import { privateKeyToAccount as privateKeyToAccount8 } from "viem/accounts";
9386
9720
 
@@ -9762,7 +10096,7 @@ import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
9762
10096
  var resolveApiKey3 = () => {
9763
10097
  const apiKey = getApiKey();
9764
10098
  if (apiKey) {
9765
- setApiKey8(apiKey);
10099
+ setApiKey9(apiKey);
9766
10100
  }
9767
10101
  };
9768
10102
  var formatTradeJson2 = (trade, rank) => ({
@@ -9885,7 +10219,7 @@ var resolveIdentifier = (identifierArg, json) => {
9885
10219
  );
9886
10220
  }
9887
10221
  };
9888
- var profileCommand = new Command11("profile").description("View profile activity (posts, holdings, and trades)").argument(
10222
+ var profileCommand = new Command12("profile").description("View profile activity (posts, holdings, and trades)").argument(
9889
10223
  "[identifier]",
9890
10224
  "Wallet address or profile handle (defaults to your wallet)"
9891
10225
  ).option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
@@ -10429,18 +10763,18 @@ profileCommand.command("trades").description("View profile trade activity (buys
10429
10763
  // src/commands/send.ts
10430
10764
  import confirm7 from "@inquirer/confirm";
10431
10765
  import {
10432
- getProfile as getProfile2,
10766
+ getProfile as getProfile3,
10433
10767
  prepareUserOperation as prepareUserOperation2,
10434
- setApiKey as setApiKey9,
10768
+ setApiKey as setApiKey10,
10435
10769
  submitUserOperation as submitUserOperation2,
10436
10770
  toGenericCall as toGenericCall2,
10437
10771
  toUserOperationCalls as toUserOperationCalls2
10438
10772
  } from "@zoralabs/coins-sdk";
10439
- import { Command as Command12 } from "commander";
10773
+ import { Command as Command13 } from "commander";
10440
10774
  import {
10441
- erc20Abi as erc20Abi4,
10775
+ erc20Abi as erc20Abi5,
10442
10776
  formatUnits as formatUnits6,
10443
- isAddress as isAddress8,
10777
+ isAddress as isAddress9,
10444
10778
  parseUnits as parseUnits3
10445
10779
  } from "viem";
10446
10780
  var SEND_AMOUNT_CHECKS = {
@@ -10449,16 +10783,16 @@ var SEND_AMOUNT_CHECKS = {
10449
10783
  all: (opts) => opts.all === true
10450
10784
  };
10451
10785
  var KNOWN_TOKEN_NAMES = /* @__PURE__ */ new Set(["eth", "usdc", "zora"]);
10452
- function isPlaceholderName(name) {
10786
+ function isPlaceholderName2(name) {
10453
10787
  return name.startsWith("0x") || name.includes("\u2026") || name.includes("...");
10454
10788
  }
10455
10789
  async function resolveRecipient(identifier, json = false) {
10456
- const isIdentifierAddress = isAddress8(identifier);
10790
+ const isIdentifierAddress = isAddress9(identifier);
10457
10791
  try {
10458
- const response = await getProfile2({ identifier });
10792
+ const response = await getProfile3({ identifier });
10459
10793
  const profile = response?.data?.profile;
10460
10794
  const address = isIdentifierAddress ? identifier : profile?.publicWallet?.walletAddress;
10461
- if (!address || !isAddress8(address)) {
10795
+ if (!address || !isAddress9(address)) {
10462
10796
  return outputErrorAndExit(
10463
10797
  json,
10464
10798
  !address ? `No Zora profile or wallet found for "${identifier}".` : "Provide a valid 0x address or an existing Zora profile name."
@@ -10466,9 +10800,10 @@ async function resolveRecipient(identifier, json = false) {
10466
10800
  }
10467
10801
  return {
10468
10802
  address,
10469
- handle: profile?.handle && !isPlaceholderName(profile.handle) ? `@${profile.handle}` : void 0,
10470
- username: profile?.username && !isPlaceholderName(profile.username) ? profile.username : void 0,
10471
- displayName: profile?.displayName && !isPlaceholderName(profile.displayName) ? profile.displayName : void 0
10803
+ handle: profile?.handle && !isPlaceholderName2(profile.handle) ? `@${profile.handle}` : void 0,
10804
+ username: profile?.username && !isPlaceholderName2(profile.username) ? profile.username : void 0,
10805
+ displayName: profile?.displayName && !isPlaceholderName2(profile.displayName) ? profile.displayName : void 0,
10806
+ platformBlocked: profile?.platformBlocked ?? false
10472
10807
  };
10473
10808
  } catch (err) {
10474
10809
  return isIdentifierAddress ? { address: identifier } : outputErrorAndExit(
@@ -10547,7 +10882,7 @@ async function sendCallViaSmartWallet(call, bundlerClient, account) {
10547
10882
  }
10548
10883
  return receipt.receipt.transactionHash;
10549
10884
  }
10550
- var sendCommand = new Command12("send").description("Send coins or ETH to an address or Zora profile").argument(
10885
+ var sendCommand = new Command13("send").description("Send coins or ETH to an address or Zora profile").argument(
10551
10886
  "[typeOrId]",
10552
10887
  "Token (eth, usdc, zora), type prefix (creator-coin, trend), or coin address/name"
10553
10888
  ).argument("[identifier]", "Coin name (when type prefix is given)").option("--to <recipient>", "Recipient: address (0x...) or Zora profile name").option("--amount <value>", "Send specific amount").option("--percent <value>", "Send percentage of balance (1-100)").option("--all", "Send entire balance").option("--yes", "Skip confirmation").action(async function(firstArg, secondArg, opts) {
@@ -10561,9 +10896,22 @@ var sendCommand = new Command12("send").description("Send coins or ETH to an add
10561
10896
  }
10562
10897
  const apiKey = getApiKey();
10563
10898
  if (apiKey) {
10564
- setApiKey9(apiKey);
10899
+ setApiKey10(apiKey);
10565
10900
  }
10566
10901
  const resolvedRecipient = await resolveRecipient(opts.to, json);
10902
+ if (resolvedRecipient.platformBlocked) {
10903
+ track("cli_send", {
10904
+ output_format: json ? "json" : "text",
10905
+ success: false,
10906
+ blocked_profile: true
10907
+ });
10908
+ return outputErrorAndExit(
10909
+ json,
10910
+ bannedProfileMessage(
10911
+ resolvedRecipient.handle ?? resolvedRecipient.address
10912
+ )
10913
+ );
10914
+ }
10567
10915
  const amountMode = getAmountMode(
10568
10916
  json,
10569
10917
  opts,
@@ -10824,7 +11172,7 @@ var sendCommand = new Command12("send").description("Send coins or ETH to an add
10824
11172
  let symbol;
10825
11173
  if (knownToken) {
10826
11174
  balance = await publicClient.readContract({
10827
- abi: erc20Abi4,
11175
+ abi: erc20Abi5,
10828
11176
  address: tokenAddress,
10829
11177
  functionName: "balanceOf",
10830
11178
  args: [walletAddress]
@@ -10834,18 +11182,18 @@ var sendCommand = new Command12("send").description("Send coins or ETH to an add
10834
11182
  } else {
10835
11183
  const results = await Promise.all([
10836
11184
  publicClient.readContract({
10837
- abi: erc20Abi4,
11185
+ abi: erc20Abi5,
10838
11186
  address: tokenAddress,
10839
11187
  functionName: "balanceOf",
10840
11188
  args: [walletAddress]
10841
11189
  }),
10842
11190
  publicClient.readContract({
10843
- abi: erc20Abi4,
11191
+ abi: erc20Abi5,
10844
11192
  address: tokenAddress,
10845
11193
  functionName: "decimals"
10846
11194
  }),
10847
11195
  publicClient.readContract({
10848
- abi: erc20Abi4,
11196
+ abi: erc20Abi5,
10849
11197
  address: tokenAddress,
10850
11198
  functionName: "symbol"
10851
11199
  })
@@ -10955,7 +11303,7 @@ var sendCommand = new Command12("send").description("Send coins or ETH to an add
10955
11303
  try {
10956
11304
  txHash = smartWalletAccount ? await sendCallViaSmartWallet(
10957
11305
  {
10958
- abi: erc20Abi4,
11306
+ abi: erc20Abi5,
10959
11307
  address: tokenAddress,
10960
11308
  functionName: "transfer",
10961
11309
  args: [resolvedRecipient.address, amount]
@@ -10963,7 +11311,7 @@ var sendCommand = new Command12("send").description("Send coins or ETH to an add
10963
11311
  bundlerClient,
10964
11312
  smartWalletAccount
10965
11313
  ) : await walletClient.writeContract({
10966
- abi: erc20Abi4,
11314
+ abi: erc20Abi5,
10967
11315
  address: tokenAddress,
10968
11316
  functionName: "transfer",
10969
11317
  args: [resolvedRecipient.address, amount]
@@ -11028,7 +11376,7 @@ var sendCommand = new Command12("send").description("Send coins or ETH to an add
11028
11376
  });
11029
11377
 
11030
11378
  // src/commands/setup.tsx
11031
- import { Command as Command13 } from "commander";
11379
+ import { Command as Command14 } from "commander";
11032
11380
  import { Text as Text18, Box as Box18 } from "ink";
11033
11381
 
11034
11382
  // src/lib/strings.ts
@@ -11196,7 +11544,7 @@ ${BOLD}${DIM}[${step}/${total}]${RESET} ${BOLD}${title}${RESET}`
11196
11544
  console.log(`${DIM}${"\u2500".repeat(Math.max(cols, 20))}${RESET}
11197
11545
  `);
11198
11546
  }
11199
- var setupCommand = new Command13("setup").description("Guided first-time setup").option("--create", "Create a new wallet without prompting").option("--force", "Overwrite existing wallet without prompting").option("--yes", "Skip interactive prompt and execute directly").action(async function(options) {
11547
+ var setupCommand = new Command14("setup").description("Guided first-time setup").option("--create", "Create a new wallet without prompting").option("--force", "Overwrite existing wallet without prompting").option("--yes", "Skip interactive prompt and execute directly").action(async function(options) {
11200
11548
  const json = getJson(this);
11201
11549
  const nonInteractive = getYes(this);
11202
11550
  if (!json) stepLine(1, 3, "Set up wallet");
@@ -11333,91 +11681,114 @@ async function promptAndSaveApiKey(json, nonInteractive = false) {
11333
11681
  }
11334
11682
 
11335
11683
  // src/commands/skills.ts
11336
- import { Command as Command14 } from "commander";
11684
+ import { Command as Command15 } from "commander";
11685
+ import { createHash as createHash2 } from "crypto";
11337
11686
  import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
11338
11687
  import { resolve, join as join3 } from "path";
11339
11688
  var DEFAULT_SKILLS_BASE_URL = "https://agents.zora.com/skill";
11340
11689
  var getSkillsBaseUrl = () => process.env.ZORA_SKILLS_BASE_URL || DEFAULT_SKILLS_BASE_URL;
11341
11690
  var SKILLS = [
11691
+ // Core
11692
+ {
11693
+ name: "cli",
11694
+ category: "Core",
11695
+ description: "The agent's full interface to Zora \u2014 set up an identity and trade, browse, look up coins, send tokens, and handle DMs from the CLI",
11696
+ integrity: "sha256-PyvDxJ7pbQ8PI5Lg/p4k7ryJNGHapqt9lRSjAV1KBDY="
11697
+ },
11342
11698
  // Onboarding
11343
11699
  {
11344
11700
  name: "onboarding",
11345
11701
  category: "Onboarding",
11346
- description: "Set up on Zora for the first time \u2014 publish your profile, create your smart wallet and creator coin, and post your first meme"
11702
+ description: "Set up on Zora for the first time \u2014 publish your profile, create your smart wallet and creator coin, and post your first meme",
11703
+ integrity: "sha256-8ZSloIyoC232S4QZDyb6PXL94n3OWlhBopHuTpt4Txo="
11347
11704
  },
11348
11705
  // Discovery
11349
11706
  {
11350
11707
  name: "early-buyer",
11351
11708
  category: "Discovery",
11352
- description: "Auto-buy new coin launches from creators you follow"
11709
+ description: "Auto-buy new coin launches from creators you follow",
11710
+ integrity: "sha256-MsU1e7kShm2X8jLY4nqNh8N+2ZNTKs0FVTzOVXf/rTQ="
11353
11711
  },
11354
11712
  {
11355
11713
  name: "watchlist",
11356
11714
  category: "Discovery",
11357
- description: "Track coins and alert when market cap hits configured thresholds"
11715
+ description: "Track coins and alert when market cap hits configured thresholds",
11716
+ integrity: "sha256-jWtGdWJ5gZBE4449BPOA6csCLP8b6dq5svubs/cljBk="
11358
11717
  },
11359
11718
  {
11360
11719
  name: "trend-sniper",
11361
11720
  category: "Discovery",
11362
- description: "Watch the global trending feed and snipe new trend coins on appearance or a volume spike"
11721
+ description: "Watch the global trending feed and snipe new trend coins on appearance or a volume spike",
11722
+ integrity: "sha256-eb6f+uK43inyv10TEXCLOTxZcaMiatnrbhilUdkIm/0="
11363
11723
  },
11364
11724
  {
11365
11725
  name: "new-coin-screener",
11366
11726
  category: "Discovery",
11367
- description: "Poll the global new-coin feed and auto-buy launches that pass a market-cap/holder screen"
11727
+ description: "Poll the global new-coin feed and auto-buy launches that pass a market-cap/holder screen",
11728
+ integrity: "sha256-P/IZ6vn94w+vTwNhxhxMyJInv90ZTlFJJbDJbWGNESs="
11368
11729
  },
11369
11730
  {
11370
11731
  name: "whale-watcher",
11371
11732
  category: "Discovery",
11372
- description: "Watch top holders and large trades on chosen coins, then alert or auto-trade on whale moves"
11733
+ description: "Watch top holders and large trades on chosen coins, then alert or auto-trade on whale moves",
11734
+ integrity: "sha256-9SMJMageM2VlxlMmoZpja2s0VecSymwSQUP0XSaodfI="
11373
11735
  },
11374
11736
  // Social
11375
11737
  {
11376
11738
  name: "copy-trader",
11377
11739
  category: "Social",
11378
- description: "Mirror another user's trades \u2014 existing holdings, future trades, or both"
11740
+ description: "Mirror another user's trades \u2014 existing holdings, future trades, or both",
11741
+ integrity: "sha256-Pj6Idrr52zzdwC+byLwRFjcS9EfVItemygm1q2+hbmQ="
11379
11742
  },
11380
11743
  {
11381
11744
  name: "dm-responder",
11382
11745
  category: "Social",
11383
- description: "Auto-triage and respond to DMs \u2014 approve/deny requests, greet new conversations, and flag keyword matches by rule"
11746
+ description: "Auto-triage and respond to DMs \u2014 approve/deny requests, greet new conversations, and flag keyword matches by rule",
11747
+ integrity: "sha256-ankYlTwsh6xdjL+uZZhKn4y9jZSwHRzYXxAcfrb3yqU="
11384
11748
  },
11385
11749
  {
11386
11750
  name: "comment-engager",
11387
11751
  category: "Social",
11388
- description: "Read and reply to comments on coins you hold, in your own voice, to build social presence"
11752
+ description: "Read and reply to comments on coins you hold, in your own voice, to build social presence",
11753
+ integrity: "sha256-oHbXs3JIOwaEJ/yubo/xIDC3rMIU5Rq7u3SGT7vIKv0="
11389
11754
  },
11390
11755
  {
11391
11756
  name: "social-trader",
11392
11757
  category: "Social",
11393
- description: "Follow specific creators and buy their new post coins or growing creator coins"
11758
+ description: "Follow specific creators and buy their new post coins or growing creator coins",
11759
+ integrity: "sha256-T/txP3ctq2NNaWkXOr7nWj4JuZJlKTMMMCWjh6qsmk8="
11394
11760
  },
11395
11761
  {
11396
11762
  name: "auto-poster",
11397
11763
  category: "Social",
11398
- description: "Publish a new post on a schedule to keep your agent active and in-character"
11764
+ description: "Publish a new post on a schedule to keep your agent active and in-character",
11765
+ integrity: "sha256-7vhcFa9fCArAOKu5hDUaMmR0Pw4SvTjXeVXU8BB9550="
11399
11766
  },
11400
11767
  // Risk
11401
11768
  {
11402
11769
  name: "take-profit",
11403
11770
  category: "Risk",
11404
- description: "Auto-sell positions at configured take-profit or stop-loss price targets"
11771
+ description: "Auto-sell positions at configured take-profit or stop-loss price targets",
11772
+ integrity: "sha256-CHtXieeBhnmfYAzazwIGHk7af1sJYHO2ox3nVDJD3yg="
11405
11773
  },
11406
11774
  {
11407
11775
  name: "dca",
11408
11776
  category: "Risk",
11409
- description: "Dollar-cost-average a fixed amount into chosen coins each iteration, with budget caps"
11777
+ description: "Dollar-cost-average a fixed amount into chosen coins each iteration, with budget caps",
11778
+ integrity: "sha256-xiSCYid81h0btfQV5gNlfsV0sLjc+1DvQSU3ORgYGJI="
11410
11779
  },
11411
11780
  {
11412
11781
  name: "portfolio-rebalancer",
11413
11782
  category: "Risk",
11414
- description: "Rebalance holdings back to target allocations when they drift past a tolerance band"
11783
+ description: "Rebalance holdings back to target allocations when they drift past a tolerance band",
11784
+ integrity: "sha256-LzeWI1kfOhOr1oeXmLABAfrrUtJ3jm+llOb+wGjo5/Q="
11415
11785
  },
11416
11786
  // Reporting
11417
11787
  {
11418
11788
  name: "portfolio-digest",
11419
11789
  category: "Reporting",
11420
- description: "Read-only periodic portfolio and PnL digest, optionally delivered to the operator by DM"
11790
+ description: "Read-only periodic portfolio and PnL digest, optionally delivered to the operator by DM",
11791
+ integrity: "sha256-tp8GQTSzRfqdKKT1H/ox8GOv3nRHSDLXbh9iaHTmjp4="
11421
11792
  }
11422
11793
  ];
11423
11794
  var AGENT_ORDER = [
@@ -11448,7 +11819,11 @@ var detectAgent = (cwd) => {
11448
11819
  }
11449
11820
  return null;
11450
11821
  };
11451
- var fetchSkill = async (name) => {
11822
+ var computeIntegrity = (content) => {
11823
+ const hash = createHash2("sha256").update(content, "utf8").digest("base64");
11824
+ return `sha256-${hash}`;
11825
+ };
11826
+ var fetchSkill = async (name, expectedIntegrity, skipVerify) => {
11452
11827
  const url = `${getSkillsBaseUrl()}/${name}.md`;
11453
11828
  const response = await fetch(url);
11454
11829
  if (!response.ok) {
@@ -11456,17 +11831,30 @@ var fetchSkill = async (name) => {
11456
11831
  `Failed to fetch ${url}: ${response.status} ${response.statusText}`
11457
11832
  );
11458
11833
  }
11459
- return response.text();
11834
+ const content = await response.text();
11835
+ if (!skipVerify) {
11836
+ const actual = computeIntegrity(content);
11837
+ if (actual !== expectedIntegrity) {
11838
+ throw new Error(
11839
+ `Skill integrity check failed for "${name}".
11840
+ Expected: ${expectedIntegrity}
11841
+ Received: ${actual}
11842
+ This could indicate a compromised download. If you trust the source, use --skip-verify.`
11843
+ );
11844
+ }
11845
+ }
11846
+ return content;
11460
11847
  };
11461
- var skillsCommand = new Command14("skills").description(
11848
+ var skillsCommand = new Command15("skills").description(
11462
11849
  "Install pre-built agent skills \u2014 onboarding plus discovery, social, risk, and reporting strategies (run `skills list` to see them all)"
11463
11850
  ).action(function() {
11464
11851
  this.outputHelp();
11465
11852
  });
11853
+ var getPublicSkills = () => SKILLS.map(({ integrity: _, ...rest }) => rest);
11466
11854
  skillsCommand.command("list").description("List available skills").action(function() {
11467
11855
  const json = getJson(this);
11468
11856
  outputData(json, {
11469
- json: { skills: SKILLS },
11857
+ json: { skills: getPublicSkills() },
11470
11858
  render: () => {
11471
11859
  console.log("Available skills:\n");
11472
11860
  for (const s of SKILLS) {
@@ -11482,12 +11870,13 @@ skillsCommand.command("add [name]").description(
11482
11870
  ).option("--all", "Install all skills").option(
11483
11871
  "--agent <agent>",
11484
11872
  "Target agent: claude, cursor, windsurf, openclaw, hermes (default: auto-detect)"
11485
- ).option("--dir <path>", "Explicit directory to install into").action(async function(name) {
11873
+ ).option("--dir <path>", "Explicit directory to install into").option("--skip-verify", "Skip integrity verification (development only)").action(async function(name) {
11486
11874
  const json = getJson(this);
11487
11875
  const opts = this.opts();
11488
11876
  const installAll = opts.all === true;
11489
11877
  const agentFlag = opts.agent;
11490
11878
  const dirFlag = opts.dir;
11879
+ const skipVerify = opts.skipVerify === true;
11491
11880
  if (!installAll && !name) {
11492
11881
  return outputErrorAndExit(
11493
11882
  json,
@@ -11534,28 +11923,72 @@ skillsCommand.command("add [name]").description(
11534
11923
  mkdirSync3(outDir, { recursive: true });
11535
11924
  const installed = [];
11536
11925
  const errors = [];
11537
- for (const file of names) {
11926
+ for (const skillName of names) {
11927
+ const skill = SKILLS.find((s) => s.name === skillName);
11538
11928
  try {
11539
- const content = await fetchSkill(file);
11540
- const skillDir = join3(outDir, `${SKILL_PREFIX}${file}`);
11929
+ const content = await fetchSkill(
11930
+ skillName,
11931
+ skill.integrity,
11932
+ skipVerify
11933
+ );
11934
+ const skillDir = join3(outDir, `${SKILL_PREFIX}${skillName}`);
11541
11935
  mkdirSync3(skillDir, { recursive: true });
11542
11936
  const outPath = join3(skillDir, "SKILL.md");
11543
11937
  writeFileSync3(outPath, content);
11544
- installed.push({ name: `${SKILL_PREFIX}${file}`, path: outPath });
11938
+ installed.push({ name: `${SKILL_PREFIX}${skillName}`, path: outPath });
11545
11939
  } catch (err) {
11546
11940
  errors.push({
11547
- name: file,
11941
+ name: skillName,
11548
11942
  error: err instanceof Error ? err.message : String(err)
11549
11943
  });
11550
11944
  }
11551
11945
  }
11552
11946
  if (errors.length > 0 && installed.length === 0) {
11947
+ const integrityError = errors.find(
11948
+ (e) => e.error.includes("integrity check failed")
11949
+ );
11553
11950
  return outputErrorAndExit(
11554
11951
  json,
11555
11952
  `Failed to install: ${errors.map((e) => e.name).join(", ")}`,
11556
- "Check your network connection and retry."
11953
+ integrityError ? integrityError.error : "Check your network connection and retry."
11557
11954
  );
11558
11955
  }
11956
+ const hasIntegrityErrors = errors.some(
11957
+ (e) => e.error.includes("integrity check failed")
11958
+ );
11959
+ if (hasIntegrityErrors) {
11960
+ outputData(json, {
11961
+ json: {
11962
+ installed,
11963
+ errors,
11964
+ agent: resolvedAgent,
11965
+ dir: outDir
11966
+ },
11967
+ render: () => {
11968
+ if (resolvedAgent && resolvedAgent !== "custom") {
11969
+ console.log(`\x1B[2mDetected agent: ${resolvedAgent}\x1B[0m`);
11970
+ }
11971
+ for (const { name: name2, path } of installed) {
11972
+ console.log(`\x1B[32m\u2713\x1B[0m Installed ${name2} \u2192 ${path}`);
11973
+ }
11974
+ for (const { name: name2, error } of errors) {
11975
+ console.error(`\x1B[31m\u2717\x1B[0m ${name2}: ${error}`);
11976
+ }
11977
+ console.error(
11978
+ "\n\x1B[31mIntegrity check failed for some skills. This could indicate compromised downloads.\x1B[0m"
11979
+ );
11980
+ }
11981
+ });
11982
+ track("cli_skills_add", {
11983
+ installed_count: installed.length,
11984
+ error_count: errors.length,
11985
+ integrity_errors: true,
11986
+ all: installAll,
11987
+ agent: resolvedAgent ?? "unknown",
11988
+ output_format: json ? "json" : "text"
11989
+ });
11990
+ process.exit(1);
11991
+ }
11559
11992
  outputData(json, {
11560
11993
  json: {
11561
11994
  installed,
@@ -11592,8 +12025,8 @@ Invoke by typing /${firstSkill} in your agent to get started.`
11592
12025
  });
11593
12026
 
11594
12027
  // src/commands/wallet.ts
11595
- import { Command as Command15 } from "commander";
11596
- import { isAddress as isAddress9 } from "viem";
12028
+ import { Command as Command16 } from "commander";
12029
+ import { isAddress as isAddress10 } from "viem";
11597
12030
  import { privateKeyToAccount as privateKeyToAccount10 } from "viem/accounts";
11598
12031
  var resolvePrivateKey2 = () => {
11599
12032
  const envKey = process.env.ZORA_PRIVATE_KEY;
@@ -11609,15 +12042,15 @@ var resolvePrivateKey2 = () => {
11609
12042
  var resolveSmartWalletAddress3 = () => {
11610
12043
  const envAddress = process.env.ZORA_SMART_WALLET_ADDRESS;
11611
12044
  if (envAddress) {
11612
- return isAddress9(envAddress) ? { address: envAddress, source: "env" } : { invalid: true, source: "env" };
12045
+ return isAddress10(envAddress) ? { address: envAddress, source: "env" } : { invalid: true, source: "env" };
11613
12046
  }
11614
12047
  const fileAddress = getSmartWalletAddress();
11615
12048
  if (fileAddress !== void 0) {
11616
- return isAddress9(fileAddress) ? { address: fileAddress, source: "file" } : { invalid: true, source: "file" };
12049
+ return isAddress10(fileAddress) ? { address: fileAddress, source: "file" } : { invalid: true, source: "file" };
11617
12050
  }
11618
12051
  return void 0;
11619
12052
  };
11620
- var walletCommand = new Command15("wallet").description("Manage your Zora wallet").action(function() {
12053
+ var walletCommand = new Command16("wallet").description("Manage your Zora wallet").action(function() {
11621
12054
  this.outputHelp();
11622
12055
  });
11623
12056
  walletCommand.command("info").description("Show wallet address and storage location").action(function() {
@@ -12113,7 +12546,7 @@ async function maybeNotifyNewDms() {
12113
12546
  privateKey: normalizeKey(key)
12114
12547
  });
12115
12548
  const auth = createSmartWalletAuth(provider);
12116
- const { createMessagingClient } = await import("./client-4HW4ZUHD.js");
12549
+ const { createMessagingClient } = await import("./client-M3K6L2ZM.js");
12117
12550
  const client2 = await createMessagingClient(auth.signerSpec);
12118
12551
  try {
12119
12552
  await client2.sync(["unknown"]);
@@ -12154,7 +12587,7 @@ import { jsx as jsx24 } from "react/jsx-runtime";
12154
12587
  if (process.env.ZORA_API_TARGET) {
12155
12588
  setApiBaseUrl(process.env.ZORA_API_TARGET);
12156
12589
  }
12157
- var version = true ? "1.2.0" : JSON.parse(
12590
+ var version = true ? "1.4.0" : JSON.parse(
12158
12591
  readFileSync5(new URL("../package.json", import.meta.url), "utf-8")
12159
12592
  ).version;
12160
12593
  function styledHelpWriteOut(showHeader) {
@@ -12174,7 +12607,7 @@ function styledHelpWriteOut(showHeader) {
12174
12607
  };
12175
12608
  }
12176
12609
  var buildProgram = () => {
12177
- const program2 = new Command16().name("zora").description("Trade what's trending. Run `zora setup` to get started.").version(version).option("--json", "Output as JSON (for scripts and automation)", false);
12610
+ const program2 = new Command17().name("zora").description("Trade what's trending. Run `zora setup` to get started.").version(version).option("--json", "Output as JSON (for scripts and automation)", false);
12178
12611
  const helpWidth = (process.stdout.columns || 80) - 4;
12179
12612
  program2.configureHelp({
12180
12613
  helpWidth,
@@ -12195,6 +12628,8 @@ var buildProgram = () => {
12195
12628
  program2.addCommand(createCommand);
12196
12629
  program2.addCommand(dmCommand);
12197
12630
  program2.addCommand(exploreCommand);
12631
+ program2.addCommand(followCommand);
12632
+ program2.addCommand(unfollowCommand);
12198
12633
  program2.addCommand(getCommand);
12199
12634
  program2.addCommand(profileCommand);
12200
12635
  program2.addCommand(setupCommand);
@@ -12210,7 +12645,11 @@ var buildProgram = () => {
12210
12645
  }
12211
12646
  };
12212
12647
  applyToSubcommands(program2);
12213
- const argOptionalCommands = /* @__PURE__ */ new Set(["profile", "agent budget set"]);
12648
+ const argOptionalCommands = /* @__PURE__ */ new Set([
12649
+ "profile",
12650
+ "agent budget set",
12651
+ "skills add"
12652
+ ]);
12214
12653
  const fullCommandPath = (cmd) => {
12215
12654
  const parts = [];
12216
12655
  let c = cmd;