@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 +466 -96
- package/package.json +1 -1
- package/scripts/generate-skill-hashes.ts +210 -0
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
|
|
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.
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
|
3087
|
-
).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 =
|
|
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:
|
|
3169
|
-
|
|
3170
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
3274
|
-
|
|
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 {
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
9124
|
+
setApiKey as setApiKey8,
|
|
8849
9125
|
tradeCoin as tradeCoin2,
|
|
8850
9126
|
tradeCoinSmartWallet as tradeCoinSmartWallet2
|
|
8851
9127
|
} from "@zoralabs/coins-sdk";
|
|
8852
|
-
import { Command as
|
|
9128
|
+
import { Command as Command11 } from "commander";
|
|
8853
9129
|
import {
|
|
8854
|
-
erc20Abi as
|
|
9130
|
+
erc20Abi as erc20Abi4,
|
|
8855
9131
|
formatUnits as formatUnits5,
|
|
8856
|
-
isAddress as
|
|
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
|
|
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
|
-
|
|
9226
|
+
setApiKey8(apiKey);
|
|
8951
9227
|
}
|
|
8952
9228
|
let coinAddress;
|
|
8953
9229
|
let earlyAccounts;
|
|
8954
9230
|
if (parsed.kind === "address") {
|
|
8955
|
-
if (!
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
10708
|
+
getProfile as getProfile3,
|
|
10433
10709
|
prepareUserOperation as prepareUserOperation2,
|
|
10434
|
-
setApiKey as
|
|
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
|
|
10715
|
+
import { Command as Command13 } from "commander";
|
|
10440
10716
|
import {
|
|
10441
|
-
erc20Abi as
|
|
10717
|
+
erc20Abi as erc20Abi5,
|
|
10442
10718
|
formatUnits as formatUnits6,
|
|
10443
|
-
isAddress as
|
|
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
|
|
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 =
|
|
10732
|
+
const isIdentifierAddress = isAddress9(identifier);
|
|
10457
10733
|
try {
|
|
10458
|
-
const response = await
|
|
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 || !
|
|
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 && !
|
|
10470
|
-
username: profile?.username && !
|
|
10471
|
-
displayName: profile?.displayName && !
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
11127
|
+
abi: erc20Abi5,
|
|
10838
11128
|
address: tokenAddress,
|
|
10839
11129
|
functionName: "balanceOf",
|
|
10840
11130
|
args: [walletAddress]
|
|
10841
11131
|
}),
|
|
10842
11132
|
publicClient.readContract({
|
|
10843
|
-
abi:
|
|
11133
|
+
abi: erc20Abi5,
|
|
10844
11134
|
address: tokenAddress,
|
|
10845
11135
|
functionName: "decimals"
|
|
10846
11136
|
}),
|
|
10847
11137
|
publicClient.readContract({
|
|
10848
|
-
abi:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
11861
|
+
for (const skillName of names) {
|
|
11862
|
+
const skill = SKILLS.find((s) => s.name === skillName);
|
|
11538
11863
|
try {
|
|
11539
|
-
const content = await fetchSkill(
|
|
11540
|
-
|
|
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}${
|
|
11873
|
+
installed.push({ name: `${SKILL_PREFIX}${skillName}`, path: outPath });
|
|
11545
11874
|
} catch (err) {
|
|
11546
11875
|
errors.push({
|
|
11547
|
-
name:
|
|
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
|
|
11596
|
-
import { isAddress as
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
@@ -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
|
+
});
|