@zoralabs/cli 1.2.0 → 1.3.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.3.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
  }
@@ -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,
@@ -7688,10 +7755,219 @@ var exploreCommand = new Command8("explore").description("Browse top, new, and h
7688
7755
  }
7689
7756
  });
7690
7757
 
7691
- // src/commands/get.tsx
7758
+ // src/commands/follow.ts
7759
+ import { getProfile as getProfile2, setApiKey as setApiKey6 } from "@zoralabs/coins-sdk";
7692
7760
  import { Command as Command9 } from "commander";
7761
+ import { erc20Abi as erc20Abi3, isAddress as isAddress7 } from "viem";
7762
+
7763
+ // src/lib/follow.ts
7764
+ var FOLLOW_MUTATION = "mutation CliFollow($followeeId: String!) { follow(followeeId: $followeeId) { handle profileId vcFollowingStatus } }";
7765
+ var UNFOLLOW_MUTATION = "mutation CliUnfollow($followeeId: String!) { unfollow(followeeId: $followeeId) { handle profileId vcFollowingStatus } }";
7766
+ async function mutateFollow(token, followeeId, mutation, operationName, field) {
7767
+ const { data, errors, status } = await graphqlRequest(
7768
+ token,
7769
+ mutation,
7770
+ operationName,
7771
+ { followeeId }
7772
+ );
7773
+ const profile = data?.[field];
7774
+ if (profile?.profileId) {
7775
+ return {
7776
+ // The API returns the full address as both fields when the target has no
7777
+ // profile; fall back to profileId so handle is never empty.
7778
+ handle: profile.handle ?? profile.profileId,
7779
+ profileId: profile.profileId,
7780
+ followingStatus: profile.vcFollowingStatus ?? "UNKNOWN"
7781
+ };
7782
+ }
7783
+ const lastError = errors?.[0]?.message ?? `HTTP ${status}`;
7784
+ throw new Error(`${field} failed: ${lastError}`);
7785
+ }
7786
+ function followProfile(token, followeeId) {
7787
+ return mutateFollow(
7788
+ token,
7789
+ followeeId,
7790
+ FOLLOW_MUTATION,
7791
+ "CliFollow",
7792
+ "follow"
7793
+ );
7794
+ }
7795
+ function unfollowProfile(token, followeeId) {
7796
+ return mutateFollow(
7797
+ token,
7798
+ followeeId,
7799
+ UNFOLLOW_MUTATION,
7800
+ "CliUnfollow",
7801
+ "unfollow"
7802
+ );
7803
+ }
7804
+
7805
+ // src/commands/follow.ts
7806
+ function isPlaceholderName(name) {
7807
+ return name.startsWith("0x") || name.includes("\u2026") || name.includes("...");
7808
+ }
7809
+ function displayName(result) {
7810
+ return isPlaceholderName(result.handle) ? result.profileId : `@${result.handle}`;
7811
+ }
7812
+ function relationshipNote(status) {
7813
+ if (status === "MUTUAL_FOLLOWING") return "You follow each other.";
7814
+ if (status === "FOLLOWED") return "They still follow you.";
7815
+ return void 0;
7816
+ }
7817
+ async function requireCreatorCoinHolding(json, identifier) {
7818
+ const apiKey = getApiKey();
7819
+ if (apiKey) setApiKey6(apiKey);
7820
+ let profile;
7821
+ try {
7822
+ const response = await getProfile2({ identifier });
7823
+ profile = response?.data?.profile;
7824
+ } catch (err) {
7825
+ return outputErrorAndExit(
7826
+ json,
7827
+ `Couldn't look up "${identifier}": ${formatError(err)}`
7828
+ );
7829
+ }
7830
+ if (!profile) {
7831
+ return outputErrorAndExit(
7832
+ json,
7833
+ `No Zora profile found for "${identifier}".`,
7834
+ "Provide an existing Zora username or wallet address."
7835
+ );
7836
+ }
7837
+ const label = profile.handle && !isPlaceholderName(profile.handle) ? `@${profile.handle}` : identifier;
7838
+ const coinAddress = profile.creatorCoin?.address;
7839
+ if (!coinAddress || !isAddress7(coinAddress)) {
7840
+ return outputErrorAndExit(
7841
+ json,
7842
+ `${label} doesn't have a creator coin yet, so there's nothing to buy.`,
7843
+ "Following requires holding the profile's creator coin."
7844
+ );
7845
+ }
7846
+ const { privateKeyAccount, smartWalletAccount } = await resolveAccounts();
7847
+ const wallet = smartWalletAccount?.address ?? privateKeyAccount.address;
7848
+ const { publicClient } = createClients(privateKeyAccount, smartWalletAccount);
7849
+ let balance;
7850
+ try {
7851
+ balance = await publicClient.readContract({
7852
+ abi: erc20Abi3,
7853
+ address: coinAddress,
7854
+ functionName: "balanceOf",
7855
+ args: [wallet]
7856
+ });
7857
+ } catch (err) {
7858
+ return outputErrorAndExit(
7859
+ json,
7860
+ `Couldn't check your creator-coin balance: ${formatError(err)}`
7861
+ );
7862
+ }
7863
+ if (balance === 0n) {
7864
+ return outputErrorAndExit(
7865
+ json,
7866
+ `You must hold ${label}'s creator coin to follow them.`,
7867
+ `Buy some first: zora buy ${coinAddress} --eth 0.001`
7868
+ );
7869
+ }
7870
+ }
7871
+ async function resolveToken(json, key) {
7872
+ try {
7873
+ const session = await ensurePrivySession({ privateKey: normalizeKey(key) });
7874
+ return session.accessToken;
7875
+ } catch (err) {
7876
+ return outputErrorAndExit(json, `Sign-in failed: ${formatError(err)}`);
7877
+ }
7878
+ }
7879
+ async function runFollow(command, action, identifierArg) {
7880
+ const json = getJson(command);
7881
+ const followeeId = (identifierArg ?? "").replace(/^@/, "").trim();
7882
+ if (!followeeId) {
7883
+ return outputErrorAndExit(
7884
+ json,
7885
+ `Missing user to ${action}.`,
7886
+ `Usage: zora ${action} <username | address>`
7887
+ );
7888
+ }
7889
+ const key = process.env.ZORA_PRIVATE_KEY || getPrivateKey();
7890
+ if (!key) {
7891
+ return outputErrorAndExit(
7892
+ json,
7893
+ "No wallet configured.",
7894
+ "Run 'zora agent create' to set up your Zora agent."
7895
+ );
7896
+ }
7897
+ if (action === "follow") {
7898
+ await requireCreatorCoinHolding(json, followeeId);
7899
+ }
7900
+ const token = await resolveToken(json, key);
7901
+ let result;
7902
+ try {
7903
+ result = action === "follow" ? await followProfile(token, followeeId) : await unfollowProfile(token, followeeId);
7904
+ } catch (err) {
7905
+ track("cli_follow", {
7906
+ action,
7907
+ output_format: json ? "json" : "static",
7908
+ success: false,
7909
+ error_type: err instanceof Error ? err.constructor.name : "unknown"
7910
+ });
7911
+ await shutdownAnalytics();
7912
+ const message = formatError(err);
7913
+ if (action === "follow" && /yourself/i.test(message)) {
7914
+ return outputErrorAndExit(json, "You can't follow yourself.");
7915
+ }
7916
+ return outputErrorAndExit(
7917
+ json,
7918
+ `Failed to ${action} "${followeeId}": ${message}`,
7919
+ "Check the username or address is a real Zora profile and try again."
7920
+ );
7921
+ }
7922
+ if (result.followingStatus === "SELF") {
7923
+ track("cli_follow", {
7924
+ action,
7925
+ output_format: json ? "json" : "static",
7926
+ success: false,
7927
+ error_type: "self"
7928
+ });
7929
+ await shutdownAnalytics();
7930
+ return outputErrorAndExit(json, `You can't ${action} yourself.`);
7931
+ }
7932
+ const label = displayName(result);
7933
+ const profileUrl = isPlaceholderName(result.handle) ? void 0 : `https://zora.co/@${result.handle}`;
7934
+ const note2 = relationshipNote(result.followingStatus);
7935
+ track("cli_follow", {
7936
+ action,
7937
+ output_format: json ? "json" : "static",
7938
+ success: true,
7939
+ following_status: result.followingStatus
7940
+ });
7941
+ outputData(json, {
7942
+ json: {
7943
+ action,
7944
+ followee: result.profileId,
7945
+ handle: result.handle,
7946
+ followingStatus: result.followingStatus,
7947
+ ...profileUrl ? { profileUrl } : {}
7948
+ },
7949
+ render: () => {
7950
+ console.log(
7951
+ `
7952
+ \u2713 ${action === "follow" ? "Following" : "Unfollowed"} ${label}`
7953
+ );
7954
+ if (note2) console.log(` ${note2}`);
7955
+ if (profileUrl) console.log(` ${profileUrl}`);
7956
+ console.log("");
7957
+ }
7958
+ });
7959
+ }
7960
+ 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) {
7961
+ await runFollow(this, "follow", identifier);
7962
+ });
7963
+ 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) {
7964
+ await runFollow(this, "unfollow", identifier);
7965
+ });
7966
+
7967
+ // src/commands/get.tsx
7968
+ import { Command as Command10 } from "commander";
7693
7969
  import { Box as Box12, Text as Text12 } from "ink";
7694
- import { setApiKey as setApiKey6, getCoinHolders, getCoinSwaps } from "@zoralabs/coins-sdk";
7970
+ import { setApiKey as setApiKey7, getCoinHolders, getCoinSwaps } from "@zoralabs/coins-sdk";
7695
7971
 
7696
7972
  // src/components/CoinDetail.tsx
7697
7973
  import { Box as Box7, Text as Text7 } from "ink";
@@ -8186,7 +8462,7 @@ function formatCoinJson(coin) {
8186
8462
  var resolveApiKey2 = () => {
8187
8463
  const apiKey = getApiKey();
8188
8464
  if (apiKey) {
8189
- setApiKey6(apiKey);
8465
+ setApiKey7(apiKey);
8190
8466
  }
8191
8467
  };
8192
8468
  var CoinResolutionError = class extends Error {
@@ -8315,7 +8591,7 @@ async function fetchRecentTrades(address) {
8315
8591
  return [];
8316
8592
  }
8317
8593
  }
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(
8594
+ 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
8595
  "[identifier]",
8320
8596
  "Coin address (0x...) or name (when type prefix is given)"
8321
8597
  ).option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
@@ -8845,15 +9121,15 @@ import confirm6 from "@inquirer/confirm";
8845
9121
  import {
8846
9122
  createQuote as createQuote2,
8847
9123
  getCoin as getCoin4,
8848
- setApiKey as setApiKey7,
9124
+ setApiKey as setApiKey8,
8849
9125
  tradeCoin as tradeCoin2,
8850
9126
  tradeCoinSmartWallet as tradeCoinSmartWallet2
8851
9127
  } from "@zoralabs/coins-sdk";
8852
- import { Command as Command10 } from "commander";
9128
+ import { Command as Command11 } from "commander";
8853
9129
  import {
8854
- erc20Abi as erc20Abi3,
9130
+ erc20Abi as erc20Abi4,
8855
9131
  formatUnits as formatUnits5,
8856
- isAddress as isAddress7,
9132
+ isAddress as isAddress8,
8857
9133
  parseUnits as parseUnits2
8858
9134
  } from "viem";
8859
9135
  function printSellQuote(output, info) {
@@ -8930,7 +9206,7 @@ function printSellResult(output, info) {
8930
9206
  console.log(` Tx ${info.txHash}
8931
9207
  `);
8932
9208
  }
8933
- var sellCommand = new Command10("sell").description("Sell a coin").argument(
9209
+ var sellCommand = new Command11("sell").description("Sell a coin").argument(
8934
9210
  "[typeOrId]",
8935
9211
  "Type prefix (creator-coin, trend) or coin address/name"
8936
9212
  ).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 +9223,12 @@ var sellCommand = new Command10("sell").description("Sell a coin").argument(
8947
9223
  }
8948
9224
  const apiKey = getApiKey();
8949
9225
  if (apiKey) {
8950
- setApiKey7(apiKey);
9226
+ setApiKey8(apiKey);
8951
9227
  }
8952
9228
  let coinAddress;
8953
9229
  let earlyAccounts;
8954
9230
  if (parsed.kind === "address") {
8955
- if (!isAddress7(parsed.address)) {
9231
+ if (!isAddress8(parsed.address)) {
8956
9232
  return outputErrorAndExit(json, `Invalid address: ${parsed.address}`);
8957
9233
  }
8958
9234
  coinAddress = parsed.address;
@@ -8968,7 +9244,7 @@ var sellCommand = new Command10("sell").description("Sell a coin").argument(
8968
9244
  ambResult = await resolveAmbiguousByNameAndBalance(
8969
9245
  parsed.name,
8970
9246
  (addr) => earlyPublicClient.readContract({
8971
- abi: erc20Abi3,
9247
+ abi: erc20Abi4,
8972
9248
  address: addr,
8973
9249
  functionName: "balanceOf",
8974
9250
  args: [earlyWalletAddress]
@@ -9112,7 +9388,7 @@ var sellCommand = new Command10("sell").description("Sell a coin").argument(
9112
9388
  }
9113
9389
  } else {
9114
9390
  const balance = await publicClient.readContract({
9115
- abi: erc20Abi3,
9391
+ abi: erc20Abi4,
9116
9392
  address: coinAddress,
9117
9393
  functionName: "balanceOf",
9118
9394
  args: [walletAddress]
@@ -9374,13 +9650,13 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
9374
9650
  });
9375
9651
 
9376
9652
  // src/commands/profile.tsx
9377
- import { Command as Command11 } from "commander";
9653
+ import { Command as Command12 } from "commander";
9378
9654
  import { Box as Box17, Text as Text17 } from "ink";
9379
9655
  import {
9380
9656
  getProfileCoins,
9381
9657
  getProfileBalances as getProfileBalances2,
9382
9658
  getWalletTradeActivity,
9383
- setApiKey as setApiKey8
9659
+ setApiKey as setApiKey9
9384
9660
  } from "@zoralabs/coins-sdk";
9385
9661
  import { privateKeyToAccount as privateKeyToAccount8 } from "viem/accounts";
9386
9662
 
@@ -9762,7 +10038,7 @@ import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
9762
10038
  var resolveApiKey3 = () => {
9763
10039
  const apiKey = getApiKey();
9764
10040
  if (apiKey) {
9765
- setApiKey8(apiKey);
10041
+ setApiKey9(apiKey);
9766
10042
  }
9767
10043
  };
9768
10044
  var formatTradeJson2 = (trade, rank) => ({
@@ -9885,7 +10161,7 @@ var resolveIdentifier = (identifierArg, json) => {
9885
10161
  );
9886
10162
  }
9887
10163
  };
9888
- var profileCommand = new Command11("profile").description("View profile activity (posts, holdings, and trades)").argument(
10164
+ var profileCommand = new Command12("profile").description("View profile activity (posts, holdings, and trades)").argument(
9889
10165
  "[identifier]",
9890
10166
  "Wallet address or profile handle (defaults to your wallet)"
9891
10167
  ).option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
@@ -10429,18 +10705,18 @@ profileCommand.command("trades").description("View profile trade activity (buys
10429
10705
  // src/commands/send.ts
10430
10706
  import confirm7 from "@inquirer/confirm";
10431
10707
  import {
10432
- getProfile as getProfile2,
10708
+ getProfile as getProfile3,
10433
10709
  prepareUserOperation as prepareUserOperation2,
10434
- setApiKey as setApiKey9,
10710
+ setApiKey as setApiKey10,
10435
10711
  submitUserOperation as submitUserOperation2,
10436
10712
  toGenericCall as toGenericCall2,
10437
10713
  toUserOperationCalls as toUserOperationCalls2
10438
10714
  } from "@zoralabs/coins-sdk";
10439
- import { Command as Command12 } from "commander";
10715
+ import { Command as Command13 } from "commander";
10440
10716
  import {
10441
- erc20Abi as erc20Abi4,
10717
+ erc20Abi as erc20Abi5,
10442
10718
  formatUnits as formatUnits6,
10443
- isAddress as isAddress8,
10719
+ isAddress as isAddress9,
10444
10720
  parseUnits as parseUnits3
10445
10721
  } from "viem";
10446
10722
  var SEND_AMOUNT_CHECKS = {
@@ -10449,16 +10725,16 @@ var SEND_AMOUNT_CHECKS = {
10449
10725
  all: (opts) => opts.all === true
10450
10726
  };
10451
10727
  var KNOWN_TOKEN_NAMES = /* @__PURE__ */ new Set(["eth", "usdc", "zora"]);
10452
- function isPlaceholderName(name) {
10728
+ function isPlaceholderName2(name) {
10453
10729
  return name.startsWith("0x") || name.includes("\u2026") || name.includes("...");
10454
10730
  }
10455
10731
  async function resolveRecipient(identifier, json = false) {
10456
- const isIdentifierAddress = isAddress8(identifier);
10732
+ const isIdentifierAddress = isAddress9(identifier);
10457
10733
  try {
10458
- const response = await getProfile2({ identifier });
10734
+ const response = await getProfile3({ identifier });
10459
10735
  const profile = response?.data?.profile;
10460
10736
  const address = isIdentifierAddress ? identifier : profile?.publicWallet?.walletAddress;
10461
- if (!address || !isAddress8(address)) {
10737
+ if (!address || !isAddress9(address)) {
10462
10738
  return outputErrorAndExit(
10463
10739
  json,
10464
10740
  !address ? `No Zora profile or wallet found for "${identifier}".` : "Provide a valid 0x address or an existing Zora profile name."
@@ -10466,9 +10742,10 @@ async function resolveRecipient(identifier, json = false) {
10466
10742
  }
10467
10743
  return {
10468
10744
  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
10745
+ handle: profile?.handle && !isPlaceholderName2(profile.handle) ? `@${profile.handle}` : void 0,
10746
+ username: profile?.username && !isPlaceholderName2(profile.username) ? profile.username : void 0,
10747
+ displayName: profile?.displayName && !isPlaceholderName2(profile.displayName) ? profile.displayName : void 0,
10748
+ platformBlocked: profile?.platformBlocked ?? false
10472
10749
  };
10473
10750
  } catch (err) {
10474
10751
  return isIdentifierAddress ? { address: identifier } : outputErrorAndExit(
@@ -10547,7 +10824,7 @@ async function sendCallViaSmartWallet(call, bundlerClient, account) {
10547
10824
  }
10548
10825
  return receipt.receipt.transactionHash;
10549
10826
  }
10550
- var sendCommand = new Command12("send").description("Send coins or ETH to an address or Zora profile").argument(
10827
+ var sendCommand = new Command13("send").description("Send coins or ETH to an address or Zora profile").argument(
10551
10828
  "[typeOrId]",
10552
10829
  "Token (eth, usdc, zora), type prefix (creator-coin, trend), or coin address/name"
10553
10830
  ).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 +10838,22 @@ var sendCommand = new Command12("send").description("Send coins or ETH to an add
10561
10838
  }
10562
10839
  const apiKey = getApiKey();
10563
10840
  if (apiKey) {
10564
- setApiKey9(apiKey);
10841
+ setApiKey10(apiKey);
10565
10842
  }
10566
10843
  const resolvedRecipient = await resolveRecipient(opts.to, json);
10844
+ if (resolvedRecipient.platformBlocked) {
10845
+ track("cli_send", {
10846
+ output_format: json ? "json" : "text",
10847
+ success: false,
10848
+ blocked_profile: true
10849
+ });
10850
+ return outputErrorAndExit(
10851
+ json,
10852
+ bannedProfileMessage(
10853
+ resolvedRecipient.handle ?? resolvedRecipient.address
10854
+ )
10855
+ );
10856
+ }
10567
10857
  const amountMode = getAmountMode(
10568
10858
  json,
10569
10859
  opts,
@@ -10824,7 +11114,7 @@ var sendCommand = new Command12("send").description("Send coins or ETH to an add
10824
11114
  let symbol;
10825
11115
  if (knownToken) {
10826
11116
  balance = await publicClient.readContract({
10827
- abi: erc20Abi4,
11117
+ abi: erc20Abi5,
10828
11118
  address: tokenAddress,
10829
11119
  functionName: "balanceOf",
10830
11120
  args: [walletAddress]
@@ -10834,18 +11124,18 @@ var sendCommand = new Command12("send").description("Send coins or ETH to an add
10834
11124
  } else {
10835
11125
  const results = await Promise.all([
10836
11126
  publicClient.readContract({
10837
- abi: erc20Abi4,
11127
+ abi: erc20Abi5,
10838
11128
  address: tokenAddress,
10839
11129
  functionName: "balanceOf",
10840
11130
  args: [walletAddress]
10841
11131
  }),
10842
11132
  publicClient.readContract({
10843
- abi: erc20Abi4,
11133
+ abi: erc20Abi5,
10844
11134
  address: tokenAddress,
10845
11135
  functionName: "decimals"
10846
11136
  }),
10847
11137
  publicClient.readContract({
10848
- abi: erc20Abi4,
11138
+ abi: erc20Abi5,
10849
11139
  address: tokenAddress,
10850
11140
  functionName: "symbol"
10851
11141
  })
@@ -10955,7 +11245,7 @@ var sendCommand = new Command12("send").description("Send coins or ETH to an add
10955
11245
  try {
10956
11246
  txHash = smartWalletAccount ? await sendCallViaSmartWallet(
10957
11247
  {
10958
- abi: erc20Abi4,
11248
+ abi: erc20Abi5,
10959
11249
  address: tokenAddress,
10960
11250
  functionName: "transfer",
10961
11251
  args: [resolvedRecipient.address, amount]
@@ -10963,7 +11253,7 @@ var sendCommand = new Command12("send").description("Send coins or ETH to an add
10963
11253
  bundlerClient,
10964
11254
  smartWalletAccount
10965
11255
  ) : await walletClient.writeContract({
10966
- abi: erc20Abi4,
11256
+ abi: erc20Abi5,
10967
11257
  address: tokenAddress,
10968
11258
  functionName: "transfer",
10969
11259
  args: [resolvedRecipient.address, amount]
@@ -11028,7 +11318,7 @@ var sendCommand = new Command12("send").description("Send coins or ETH to an add
11028
11318
  });
11029
11319
 
11030
11320
  // src/commands/setup.tsx
11031
- import { Command as Command13 } from "commander";
11321
+ import { Command as Command14 } from "commander";
11032
11322
  import { Text as Text18, Box as Box18 } from "ink";
11033
11323
 
11034
11324
  // src/lib/strings.ts
@@ -11196,7 +11486,7 @@ ${BOLD}${DIM}[${step}/${total}]${RESET} ${BOLD}${title}${RESET}`
11196
11486
  console.log(`${DIM}${"\u2500".repeat(Math.max(cols, 20))}${RESET}
11197
11487
  `);
11198
11488
  }
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) {
11489
+ 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
11490
  const json = getJson(this);
11201
11491
  const nonInteractive = getYes(this);
11202
11492
  if (!json) stepLine(1, 3, "Set up wallet");
@@ -11333,7 +11623,8 @@ async function promptAndSaveApiKey(json, nonInteractive = false) {
11333
11623
  }
11334
11624
 
11335
11625
  // src/commands/skills.ts
11336
- import { Command as Command14 } from "commander";
11626
+ import { Command as Command15 } from "commander";
11627
+ import { createHash as createHash2 } from "crypto";
11337
11628
  import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
11338
11629
  import { resolve, join as join3 } from "path";
11339
11630
  var DEFAULT_SKILLS_BASE_URL = "https://agents.zora.com/skill";
@@ -11343,81 +11634,96 @@ var SKILLS = [
11343
11634
  {
11344
11635
  name: "onboarding",
11345
11636
  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"
11637
+ 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",
11638
+ integrity: "sha256-8ZSloIyoC232S4QZDyb6PXL94n3OWlhBopHuTpt4Txo="
11347
11639
  },
11348
11640
  // Discovery
11349
11641
  {
11350
11642
  name: "early-buyer",
11351
11643
  category: "Discovery",
11352
- description: "Auto-buy new coin launches from creators you follow"
11644
+ description: "Auto-buy new coin launches from creators you follow",
11645
+ integrity: "sha256-MsU1e7kShm2X8jLY4nqNh8N+2ZNTKs0FVTzOVXf/rTQ="
11353
11646
  },
11354
11647
  {
11355
11648
  name: "watchlist",
11356
11649
  category: "Discovery",
11357
- description: "Track coins and alert when market cap hits configured thresholds"
11650
+ description: "Track coins and alert when market cap hits configured thresholds",
11651
+ integrity: "sha256-jWtGdWJ5gZBE4449BPOA6csCLP8b6dq5svubs/cljBk="
11358
11652
  },
11359
11653
  {
11360
11654
  name: "trend-sniper",
11361
11655
  category: "Discovery",
11362
- description: "Watch the global trending feed and snipe new trend coins on appearance or a volume spike"
11656
+ description: "Watch the global trending feed and snipe new trend coins on appearance or a volume spike",
11657
+ integrity: "sha256-eb6f+uK43inyv10TEXCLOTxZcaMiatnrbhilUdkIm/0="
11363
11658
  },
11364
11659
  {
11365
11660
  name: "new-coin-screener",
11366
11661
  category: "Discovery",
11367
- description: "Poll the global new-coin feed and auto-buy launches that pass a market-cap/holder screen"
11662
+ description: "Poll the global new-coin feed and auto-buy launches that pass a market-cap/holder screen",
11663
+ integrity: "sha256-P/IZ6vn94w+vTwNhxhxMyJInv90ZTlFJJbDJbWGNESs="
11368
11664
  },
11369
11665
  {
11370
11666
  name: "whale-watcher",
11371
11667
  category: "Discovery",
11372
- description: "Watch top holders and large trades on chosen coins, then alert or auto-trade on whale moves"
11668
+ description: "Watch top holders and large trades on chosen coins, then alert or auto-trade on whale moves",
11669
+ integrity: "sha256-9SMJMageM2VlxlMmoZpja2s0VecSymwSQUP0XSaodfI="
11373
11670
  },
11374
11671
  // Social
11375
11672
  {
11376
11673
  name: "copy-trader",
11377
11674
  category: "Social",
11378
- description: "Mirror another user's trades \u2014 existing holdings, future trades, or both"
11675
+ description: "Mirror another user's trades \u2014 existing holdings, future trades, or both",
11676
+ integrity: "sha256-Pj6Idrr52zzdwC+byLwRFjcS9EfVItemygm1q2+hbmQ="
11379
11677
  },
11380
11678
  {
11381
11679
  name: "dm-responder",
11382
11680
  category: "Social",
11383
- description: "Auto-triage and respond to DMs \u2014 approve/deny requests, greet new conversations, and flag keyword matches by rule"
11681
+ description: "Auto-triage and respond to DMs \u2014 approve/deny requests, greet new conversations, and flag keyword matches by rule",
11682
+ integrity: "sha256-ankYlTwsh6xdjL+uZZhKn4y9jZSwHRzYXxAcfrb3yqU="
11384
11683
  },
11385
11684
  {
11386
11685
  name: "comment-engager",
11387
11686
  category: "Social",
11388
- description: "Read and reply to comments on coins you hold, in your own voice, to build social presence"
11687
+ description: "Read and reply to comments on coins you hold, in your own voice, to build social presence",
11688
+ integrity: "sha256-oHbXs3JIOwaEJ/yubo/xIDC3rMIU5Rq7u3SGT7vIKv0="
11389
11689
  },
11390
11690
  {
11391
11691
  name: "social-trader",
11392
11692
  category: "Social",
11393
- description: "Follow specific creators and buy their new post coins or growing creator coins"
11693
+ description: "Follow specific creators and buy their new post coins or growing creator coins",
11694
+ integrity: "sha256-T/txP3ctq2NNaWkXOr7nWj4JuZJlKTMMMCWjh6qsmk8="
11394
11695
  },
11395
11696
  {
11396
11697
  name: "auto-poster",
11397
11698
  category: "Social",
11398
- description: "Publish a new post on a schedule to keep your agent active and in-character"
11699
+ description: "Publish a new post on a schedule to keep your agent active and in-character",
11700
+ integrity: "sha256-7vhcFa9fCArAOKu5hDUaMmR0Pw4SvTjXeVXU8BB9550="
11399
11701
  },
11400
11702
  // Risk
11401
11703
  {
11402
11704
  name: "take-profit",
11403
11705
  category: "Risk",
11404
- description: "Auto-sell positions at configured take-profit or stop-loss price targets"
11706
+ description: "Auto-sell positions at configured take-profit or stop-loss price targets",
11707
+ integrity: "sha256-CHtXieeBhnmfYAzazwIGHk7af1sJYHO2ox3nVDJD3yg="
11405
11708
  },
11406
11709
  {
11407
11710
  name: "dca",
11408
11711
  category: "Risk",
11409
- description: "Dollar-cost-average a fixed amount into chosen coins each iteration, with budget caps"
11712
+ description: "Dollar-cost-average a fixed amount into chosen coins each iteration, with budget caps",
11713
+ integrity: "sha256-xiSCYid81h0btfQV5gNlfsV0sLjc+1DvQSU3ORgYGJI="
11410
11714
  },
11411
11715
  {
11412
11716
  name: "portfolio-rebalancer",
11413
11717
  category: "Risk",
11414
- description: "Rebalance holdings back to target allocations when they drift past a tolerance band"
11718
+ description: "Rebalance holdings back to target allocations when they drift past a tolerance band",
11719
+ integrity: "sha256-LzeWI1kfOhOr1oeXmLABAfrrUtJ3jm+llOb+wGjo5/Q="
11415
11720
  },
11416
11721
  // Reporting
11417
11722
  {
11418
11723
  name: "portfolio-digest",
11419
11724
  category: "Reporting",
11420
- description: "Read-only periodic portfolio and PnL digest, optionally delivered to the operator by DM"
11725
+ description: "Read-only periodic portfolio and PnL digest, optionally delivered to the operator by DM",
11726
+ integrity: "sha256-tp8GQTSzRfqdKKT1H/ox8GOv3nRHSDLXbh9iaHTmjp4="
11421
11727
  }
11422
11728
  ];
11423
11729
  var AGENT_ORDER = [
@@ -11448,7 +11754,11 @@ var detectAgent = (cwd) => {
11448
11754
  }
11449
11755
  return null;
11450
11756
  };
11451
- var fetchSkill = async (name) => {
11757
+ var computeIntegrity = (content) => {
11758
+ const hash = createHash2("sha256").update(content, "utf8").digest("base64");
11759
+ return `sha256-${hash}`;
11760
+ };
11761
+ var fetchSkill = async (name, expectedIntegrity, skipVerify) => {
11452
11762
  const url = `${getSkillsBaseUrl()}/${name}.md`;
11453
11763
  const response = await fetch(url);
11454
11764
  if (!response.ok) {
@@ -11456,17 +11766,30 @@ var fetchSkill = async (name) => {
11456
11766
  `Failed to fetch ${url}: ${response.status} ${response.statusText}`
11457
11767
  );
11458
11768
  }
11459
- return response.text();
11769
+ const content = await response.text();
11770
+ if (!skipVerify) {
11771
+ const actual = computeIntegrity(content);
11772
+ if (actual !== expectedIntegrity) {
11773
+ throw new Error(
11774
+ `Skill integrity check failed for "${name}".
11775
+ Expected: ${expectedIntegrity}
11776
+ Received: ${actual}
11777
+ This could indicate a compromised download. If you trust the source, use --skip-verify.`
11778
+ );
11779
+ }
11780
+ }
11781
+ return content;
11460
11782
  };
11461
- var skillsCommand = new Command14("skills").description(
11783
+ var skillsCommand = new Command15("skills").description(
11462
11784
  "Install pre-built agent skills \u2014 onboarding plus discovery, social, risk, and reporting strategies (run `skills list` to see them all)"
11463
11785
  ).action(function() {
11464
11786
  this.outputHelp();
11465
11787
  });
11788
+ var getPublicSkills = () => SKILLS.map(({ integrity: _, ...rest }) => rest);
11466
11789
  skillsCommand.command("list").description("List available skills").action(function() {
11467
11790
  const json = getJson(this);
11468
11791
  outputData(json, {
11469
- json: { skills: SKILLS },
11792
+ json: { skills: getPublicSkills() },
11470
11793
  render: () => {
11471
11794
  console.log("Available skills:\n");
11472
11795
  for (const s of SKILLS) {
@@ -11482,12 +11805,13 @@ skillsCommand.command("add [name]").description(
11482
11805
  ).option("--all", "Install all skills").option(
11483
11806
  "--agent <agent>",
11484
11807
  "Target agent: claude, cursor, windsurf, openclaw, hermes (default: auto-detect)"
11485
- ).option("--dir <path>", "Explicit directory to install into").action(async function(name) {
11808
+ ).option("--dir <path>", "Explicit directory to install into").option("--skip-verify", "Skip integrity verification (development only)").action(async function(name) {
11486
11809
  const json = getJson(this);
11487
11810
  const opts = this.opts();
11488
11811
  const installAll = opts.all === true;
11489
11812
  const agentFlag = opts.agent;
11490
11813
  const dirFlag = opts.dir;
11814
+ const skipVerify = opts.skipVerify === true;
11491
11815
  if (!installAll && !name) {
11492
11816
  return outputErrorAndExit(
11493
11817
  json,
@@ -11534,28 +11858,72 @@ skillsCommand.command("add [name]").description(
11534
11858
  mkdirSync3(outDir, { recursive: true });
11535
11859
  const installed = [];
11536
11860
  const errors = [];
11537
- for (const file of names) {
11861
+ for (const skillName of names) {
11862
+ const skill = SKILLS.find((s) => s.name === skillName);
11538
11863
  try {
11539
- const content = await fetchSkill(file);
11540
- const skillDir = join3(outDir, `${SKILL_PREFIX}${file}`);
11864
+ const content = await fetchSkill(
11865
+ skillName,
11866
+ skill.integrity,
11867
+ skipVerify
11868
+ );
11869
+ const skillDir = join3(outDir, `${SKILL_PREFIX}${skillName}`);
11541
11870
  mkdirSync3(skillDir, { recursive: true });
11542
11871
  const outPath = join3(skillDir, "SKILL.md");
11543
11872
  writeFileSync3(outPath, content);
11544
- installed.push({ name: `${SKILL_PREFIX}${file}`, path: outPath });
11873
+ installed.push({ name: `${SKILL_PREFIX}${skillName}`, path: outPath });
11545
11874
  } catch (err) {
11546
11875
  errors.push({
11547
- name: file,
11876
+ name: skillName,
11548
11877
  error: err instanceof Error ? err.message : String(err)
11549
11878
  });
11550
11879
  }
11551
11880
  }
11552
11881
  if (errors.length > 0 && installed.length === 0) {
11882
+ const integrityError = errors.find(
11883
+ (e) => e.error.includes("integrity check failed")
11884
+ );
11553
11885
  return outputErrorAndExit(
11554
11886
  json,
11555
11887
  `Failed to install: ${errors.map((e) => e.name).join(", ")}`,
11556
- "Check your network connection and retry."
11888
+ integrityError ? integrityError.error : "Check your network connection and retry."
11557
11889
  );
11558
11890
  }
11891
+ const hasIntegrityErrors = errors.some(
11892
+ (e) => e.error.includes("integrity check failed")
11893
+ );
11894
+ if (hasIntegrityErrors) {
11895
+ outputData(json, {
11896
+ json: {
11897
+ installed,
11898
+ errors,
11899
+ agent: resolvedAgent,
11900
+ dir: outDir
11901
+ },
11902
+ render: () => {
11903
+ if (resolvedAgent && resolvedAgent !== "custom") {
11904
+ console.log(`\x1B[2mDetected agent: ${resolvedAgent}\x1B[0m`);
11905
+ }
11906
+ for (const { name: name2, path } of installed) {
11907
+ console.log(`\x1B[32m\u2713\x1B[0m Installed ${name2} \u2192 ${path}`);
11908
+ }
11909
+ for (const { name: name2, error } of errors) {
11910
+ console.error(`\x1B[31m\u2717\x1B[0m ${name2}: ${error}`);
11911
+ }
11912
+ console.error(
11913
+ "\n\x1B[31mIntegrity check failed for some skills. This could indicate compromised downloads.\x1B[0m"
11914
+ );
11915
+ }
11916
+ });
11917
+ track("cli_skills_add", {
11918
+ installed_count: installed.length,
11919
+ error_count: errors.length,
11920
+ integrity_errors: true,
11921
+ all: installAll,
11922
+ agent: resolvedAgent ?? "unknown",
11923
+ output_format: json ? "json" : "text"
11924
+ });
11925
+ process.exit(1);
11926
+ }
11559
11927
  outputData(json, {
11560
11928
  json: {
11561
11929
  installed,
@@ -11592,8 +11960,8 @@ Invoke by typing /${firstSkill} in your agent to get started.`
11592
11960
  });
11593
11961
 
11594
11962
  // src/commands/wallet.ts
11595
- import { Command as Command15 } from "commander";
11596
- import { isAddress as isAddress9 } from "viem";
11963
+ import { Command as Command16 } from "commander";
11964
+ import { isAddress as isAddress10 } from "viem";
11597
11965
  import { privateKeyToAccount as privateKeyToAccount10 } from "viem/accounts";
11598
11966
  var resolvePrivateKey2 = () => {
11599
11967
  const envKey = process.env.ZORA_PRIVATE_KEY;
@@ -11609,15 +11977,15 @@ var resolvePrivateKey2 = () => {
11609
11977
  var resolveSmartWalletAddress3 = () => {
11610
11978
  const envAddress = process.env.ZORA_SMART_WALLET_ADDRESS;
11611
11979
  if (envAddress) {
11612
- return isAddress9(envAddress) ? { address: envAddress, source: "env" } : { invalid: true, source: "env" };
11980
+ return isAddress10(envAddress) ? { address: envAddress, source: "env" } : { invalid: true, source: "env" };
11613
11981
  }
11614
11982
  const fileAddress = getSmartWalletAddress();
11615
11983
  if (fileAddress !== void 0) {
11616
- return isAddress9(fileAddress) ? { address: fileAddress, source: "file" } : { invalid: true, source: "file" };
11984
+ return isAddress10(fileAddress) ? { address: fileAddress, source: "file" } : { invalid: true, source: "file" };
11617
11985
  }
11618
11986
  return void 0;
11619
11987
  };
11620
- var walletCommand = new Command15("wallet").description("Manage your Zora wallet").action(function() {
11988
+ var walletCommand = new Command16("wallet").description("Manage your Zora wallet").action(function() {
11621
11989
  this.outputHelp();
11622
11990
  });
11623
11991
  walletCommand.command("info").description("Show wallet address and storage location").action(function() {
@@ -12154,7 +12522,7 @@ import { jsx as jsx24 } from "react/jsx-runtime";
12154
12522
  if (process.env.ZORA_API_TARGET) {
12155
12523
  setApiBaseUrl(process.env.ZORA_API_TARGET);
12156
12524
  }
12157
- var version = true ? "1.2.0" : JSON.parse(
12525
+ var version = true ? "1.3.0" : JSON.parse(
12158
12526
  readFileSync5(new URL("../package.json", import.meta.url), "utf-8")
12159
12527
  ).version;
12160
12528
  function styledHelpWriteOut(showHeader) {
@@ -12174,7 +12542,7 @@ function styledHelpWriteOut(showHeader) {
12174
12542
  };
12175
12543
  }
12176
12544
  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);
12545
+ 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
12546
  const helpWidth = (process.stdout.columns || 80) - 4;
12179
12547
  program2.configureHelp({
12180
12548
  helpWidth,
@@ -12195,6 +12563,8 @@ var buildProgram = () => {
12195
12563
  program2.addCommand(createCommand);
12196
12564
  program2.addCommand(dmCommand);
12197
12565
  program2.addCommand(exploreCommand);
12566
+ program2.addCommand(followCommand);
12567
+ program2.addCommand(unfollowCommand);
12198
12568
  program2.addCommand(getCommand);
12199
12569
  program2.addCommand(profileCommand);
12200
12570
  program2.addCommand(setupCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoralabs/cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Zora CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Generates SHA-256 integrity hashes for all skills.
3
+ *
4
+ * Usage:
5
+ * npx tsx scripts/generate-skill-hashes.ts # Print hashes to console
6
+ * npx tsx scripts/generate-skill-hashes.ts --write # Update skills.ts directly
7
+ * npx tsx scripts/generate-skill-hashes.ts --check # Check if hashes are up-to-date (CI)
8
+ */
9
+
10
+ import { createHash } from "node:crypto";
11
+ import { readFileSync, writeFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const SKILLS_TS_PATH = join(__dirname, "../src/commands/skills.ts");
17
+
18
+ const SKILLS_URL = "https://agents.zora.com/skill";
19
+
20
+ const computeIntegrity = (content: string): string => {
21
+ const hash = createHash("sha256").update(content, "utf8").digest("base64");
22
+ return `sha256-${hash}`;
23
+ };
24
+
25
+ /**
26
+ * Parse skill names from skills.ts to avoid import dependency chain.
27
+ * Single source of truth - names are extracted from the SKILLS array.
28
+ */
29
+ function parseSkillNamesFromFile(): string[] {
30
+ const content = readFileSync(SKILLS_TS_PATH, "utf8");
31
+ const names: string[] = [];
32
+
33
+ // Match all name: "skillname" patterns within the SKILLS array
34
+ const namePattern = /name:\s*"([^"]+)"/g;
35
+ let match;
36
+ while ((match = namePattern.exec(content)) !== null) {
37
+ names.push(match[1]);
38
+ }
39
+
40
+ if (names.length === 0) {
41
+ throw new Error("No skill names found in skills.ts");
42
+ }
43
+
44
+ return names;
45
+ }
46
+
47
+ /**
48
+ * Parse current hashes from skills.ts
49
+ */
50
+ function getCurrentHashes(): Map<string, string> {
51
+ const content = readFileSync(SKILLS_TS_PATH, "utf8");
52
+ const hashes = new Map<string, string>();
53
+ const skillNames = parseSkillNamesFromFile();
54
+
55
+ for (const name of skillNames) {
56
+ // Use lazy matching to find the integrity field for each skill
57
+ const pattern = new RegExp(
58
+ `name:\\s*"${name}"[\\s\\S]*?integrity:\\s*"([^"]*)"`,
59
+ );
60
+ const match = content.match(pattern);
61
+ if (match) {
62
+ hashes.set(name, match[1]);
63
+ }
64
+ }
65
+
66
+ return hashes;
67
+ }
68
+
69
+ async function fetchAllHashes(): Promise<Map<string, string>> {
70
+ const hashes = new Map<string, string>();
71
+ const skillNames = parseSkillNamesFromFile();
72
+
73
+ for (const name of skillNames) {
74
+ const url = `${SKILLS_URL}/${name}.md`;
75
+ const response = await fetch(url);
76
+ if (!response.ok) {
77
+ throw new Error(`Failed to fetch ${name}: HTTP ${response.status}`);
78
+ }
79
+ const content = await response.text();
80
+ hashes.set(name, computeIntegrity(content));
81
+ }
82
+
83
+ return hashes;
84
+ }
85
+
86
+ function updateSkillsFile(hashes: Map<string, string>): boolean {
87
+ const content = readFileSync(SKILLS_TS_PATH, "utf8");
88
+ let updated = content;
89
+ let changed = false;
90
+ const errors: string[] = [];
91
+
92
+ for (const [name, hash] of hashes) {
93
+ // First, verify this skill has an integrity field by checking
94
+ // that we can find it within its own object block (before the next skill)
95
+ // Find the position of this skill's name
96
+ const namePattern = new RegExp(`name:\\s*"${name}"`);
97
+ const nameMatch = namePattern.exec(updated);
98
+ if (!nameMatch) {
99
+ errors.push(`Skill "${name}" not found in skills.ts`);
100
+ continue;
101
+ }
102
+
103
+ const namePos = nameMatch.index;
104
+
105
+ // Find the next skill's name (if any) to bound our search
106
+ const remainingContent = updated.slice(namePos + nameMatch[0].length);
107
+ const nextSkillMatch = /name:\s*"[^"]+"/.exec(remainingContent);
108
+ const searchBound = nextSkillMatch
109
+ ? namePos + nameMatch[0].length + nextSkillMatch.index
110
+ : updated.length;
111
+
112
+ // Extract the bounded region for this skill
113
+ const skillRegion = updated.slice(namePos, searchBound);
114
+
115
+ // Check if integrity field exists in this skill's region
116
+ const integrityInRegion = /integrity:\s*"[^"]*"/.exec(skillRegion);
117
+ if (!integrityInRegion) {
118
+ errors.push(
119
+ `Skill "${name}" is missing integrity field - add 'integrity: "sha256-PLACEHOLDER"' to the skill definition`,
120
+ );
121
+ continue;
122
+ }
123
+
124
+ // Now safely replace the integrity value within the bounded region
125
+ const updatedRegion = skillRegion.replace(
126
+ /(integrity:\s*)"[^"]*"/,
127
+ `$1"${hash}"`,
128
+ );
129
+
130
+ if (updatedRegion !== skillRegion) {
131
+ changed = true;
132
+ updated = updated.slice(0, namePos) + updatedRegion + updated.slice(searchBound);
133
+ }
134
+ }
135
+
136
+ if (errors.length > 0) {
137
+ console.error("\nErrors found:");
138
+ for (const err of errors) {
139
+ console.error(` - ${err}`);
140
+ }
141
+ process.exit(1);
142
+ }
143
+
144
+ if (changed) {
145
+ writeFileSync(SKILLS_TS_PATH, updated);
146
+ }
147
+
148
+ return changed;
149
+ }
150
+
151
+ async function main() {
152
+ const args = process.argv.slice(2);
153
+ const writeMode = args.includes("--write");
154
+ const checkMode = args.includes("--check");
155
+
156
+ console.log("Fetching skills from production...\n");
157
+
158
+ let remoteHashes: Map<string, string>;
159
+ try {
160
+ remoteHashes = await fetchAllHashes();
161
+ } catch (err) {
162
+ console.error("Failed to fetch skills:", err);
163
+ process.exit(1);
164
+ }
165
+
166
+ if (checkMode) {
167
+ const currentHashes = getCurrentHashes();
168
+ let hasChanges = false;
169
+
170
+ for (const [name, remoteHash] of remoteHashes) {
171
+ const currentHash = currentHashes.get(name);
172
+ if (currentHash !== remoteHash) {
173
+ console.log(`${name}: CHANGED`);
174
+ console.log(` Current: ${currentHash}`);
175
+ console.log(` Expected: ${remoteHash}\n`);
176
+ hasChanges = true;
177
+ }
178
+ }
179
+
180
+ if (hasChanges) {
181
+ console.log("Skill hashes are out of date. Run with --write to update.");
182
+ process.exit(1);
183
+ } else {
184
+ console.log("All skill hashes are up to date.");
185
+ process.exit(0);
186
+ }
187
+ }
188
+
189
+ if (writeMode) {
190
+ const changed = updateSkillsFile(remoteHashes);
191
+ if (changed) {
192
+ console.log("Updated skills.ts with new hashes:");
193
+ for (const [name, hash] of remoteHashes) {
194
+ console.log(` ${name}: ${hash}`);
195
+ }
196
+ } else {
197
+ console.log("No changes needed - hashes are already up to date.");
198
+ }
199
+ } else {
200
+ console.log("Generated hashes (run with --write to update skills.ts):\n");
201
+ for (const [name, hash] of remoteHashes) {
202
+ console.log(` ${name}: ${hash}`);
203
+ }
204
+ }
205
+ }
206
+
207
+ main().catch((err) => {
208
+ console.error("Fatal error:", err);
209
+ process.exit(1);
210
+ });