@net-protocol/cli 0.1.43 → 0.1.44

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.
@@ -6,10 +6,10 @@ import { createRequire } from 'module';
6
6
  import chalk4 from 'chalk';
7
7
  import * as fs6 from 'fs';
8
8
  import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
9
- import { OPTIMAL_CHUNK_SIZE, StorageClient, detectFileTypeFromBase64, base64ToDataUri, shouldSuggestXmlStorage, getStorageKeyBytes, encodeStorageKeyForUrl, chunkDataForStorage, CHUNKED_STORAGE_CONTRACT, STORAGE_CONTRACT as STORAGE_CONTRACT$1 } from '@net-protocol/storage';
9
+ import { OPTIMAL_CHUNK_SIZE, StorageClient, detectFileTypeFromBase64, base64ToDataUri, shouldSuggestXmlStorage, getStorageKeyBytes, chunkDataForStorage, CHUNKED_STORAGE_CONTRACT, STORAGE_CONTRACT as STORAGE_CONTRACT$1, encodeStorageKeyForUrl } from '@net-protocol/storage';
10
10
  import { createPublicClient, http, createWalletClient, stringToHex, hexToString, parseEther, encodeFunctionData, publicActions, concat, defineChain, decodeEventLog, formatEther, isAddress } from 'viem';
11
+ import { getSupportedChains, getNetContract, getChainName, getChainRpcUrls, getPublicClient, getBaseDataSuffix, NetClient, toBytes32, getChainBlockExplorer, NULL_ADDRESS, getChainSlug } from '@net-protocol/core';
11
12
  import { privateKeyToAccount } from 'viem/accounts';
12
- import { getNetContract, getChainName, getChainRpcUrls, getPublicClient, getBaseDataSuffix, NetClient, toBytes32, NULL_ADDRESS } from '@net-protocol/core';
13
13
  import { fundBackendWallet, checkBackendWalletBalance, createRelayX402Client, createRelaySession, batchTransactions, submitTransactionsViaRelay, waitForConfirmations, retryFailedTransactions as retryFailedTransactions$1 } from '@net-protocol/relay';
14
14
  import { isCommentTopic, parseCommentData, FeedRegistryClient, FeedClient, AgentRegistryClient, COMMENT_TOPIC_SUFFIX, FEED_TOPIC_PREFIX } from '@net-protocol/feeds';
15
15
  import '@net-protocol/chats';
@@ -130,6 +130,75 @@ function parseCommonOptionsWithDefault(options, supportsEncodeOnly = false) {
130
130
  rpcUrl: getRpcUrlWithBotchanFallback(options.rpcUrl)
131
131
  };
132
132
  }
133
+ var WEBSITE_BASE = "https://netprotocol.app";
134
+ var STORAGE_BASE = "https://storedon.net";
135
+ function chainSlug(chainId) {
136
+ return getChainSlug({ chainId }) ?? null;
137
+ }
138
+ function stripFeedPrefix(topic) {
139
+ const lower = topic.toLowerCase();
140
+ return lower.startsWith("feed-") ? lower.slice(5) : lower;
141
+ }
142
+ function feedUrl(chainId, feedName) {
143
+ const slug = chainSlug(chainId);
144
+ if (!slug) return null;
145
+ return `${WEBSITE_BASE}/app/feed/${slug}/${stripFeedPrefix(feedName)}`;
146
+ }
147
+ function walletUrl(chainId, address) {
148
+ const slug = chainSlug(chainId);
149
+ if (!slug) return null;
150
+ return `${WEBSITE_BASE}/app/feed/${slug}/${address.toLowerCase()}`;
151
+ }
152
+ function profileUrl(chainId, address) {
153
+ const slug = chainSlug(chainId);
154
+ if (!slug) return null;
155
+ return `${WEBSITE_BASE}/app/profile/${slug}/${address.toLowerCase()}`;
156
+ }
157
+ function tokenUrl(chainId, tokenAddress) {
158
+ const slug = chainSlug(chainId);
159
+ if (!slug) return null;
160
+ return `${WEBSITE_BASE}/app/token/${slug}/${tokenAddress.toLowerCase()}`;
161
+ }
162
+ function storageUrl(chainId, operatorAddress, key) {
163
+ return `${STORAGE_BASE}/net/${chainId}/storage/load/${operatorAddress.toLowerCase()}/${encodeStorageKeyForUrl(
164
+ key
165
+ )}`;
166
+ }
167
+ function explorerTxUrl(chainId, txHash) {
168
+ const base9 = getChainBlockExplorer({ chainId })?.url;
169
+ if (!base9) return null;
170
+ return `${base9}/tx/${txHash}`;
171
+ }
172
+ function explorerAddressUrl(chainId, address) {
173
+ const base9 = getChainBlockExplorer({ chainId })?.url;
174
+ if (!base9) return null;
175
+ return `${base9}/address/${address}`;
176
+ }
177
+ function postIdToCommentParam(postId) {
178
+ const colon = postId.indexOf(":");
179
+ if (colon === -1) return postId;
180
+ return `${postId.slice(0, colon)}-${postId.slice(colon + 1)}`;
181
+ }
182
+ function postPermalink(chainId, opts) {
183
+ const slug = chainSlug(chainId);
184
+ if (!slug) return null;
185
+ const params = new URLSearchParams();
186
+ if (opts.globalIndex != null) {
187
+ params.set("index", String(opts.globalIndex));
188
+ } else if (opts.topic != null && opts.topicIndex != null) {
189
+ params.set("topic", stripFeedPrefix(opts.topic));
190
+ params.set("index", String(opts.topicIndex));
191
+ } else if (opts.user != null && opts.userIndex != null) {
192
+ params.set("user", opts.user.toLowerCase());
193
+ params.set("index", String(opts.userIndex));
194
+ } else {
195
+ return null;
196
+ }
197
+ if (opts.commentId) {
198
+ params.set("commentId", postIdToCommentParam(opts.commentId));
199
+ }
200
+ return `${WEBSITE_BASE}/app/feed/${slug}/post?${params.toString()}`;
201
+ }
133
202
  async function checkNormalStorageExists(params) {
134
203
  const { storageClient, storageKey, operatorAddress, expectedContent } = params;
135
204
  const existing = await storageClient.get({
@@ -213,7 +282,7 @@ function extractTypedArgsFromTransaction(tx, type) {
213
282
  }
214
283
  function generateStorageUrl(operatorAddress, chainId, storageKey) {
215
284
  if (!operatorAddress) return void 0;
216
- return `https://storedon.net/net/${chainId}/storage/load/${operatorAddress}/${encodeStorageKeyForUrl(storageKey)}`;
285
+ return storageUrl(chainId, operatorAddress, storageKey);
217
286
  }
218
287
  async function checkTransactionExists(params) {
219
288
  const { storageClient, tx, operatorAddress } = params;
@@ -1182,6 +1251,11 @@ async function executeStorageRead(options) {
1182
1251
  key: options.key,
1183
1252
  operator: options.operator,
1184
1253
  chainId: readOnlyOptions.chainId,
1254
+ storageUrl: storageUrl(
1255
+ readOnlyOptions.chainId,
1256
+ options.operator,
1257
+ options.key
1258
+ ),
1185
1259
  text: result.text,
1186
1260
  data: options.raw ? result.data : result.data,
1187
1261
  isXml: result.isXml,
@@ -1374,7 +1448,7 @@ function registerStorageCommand(program2) {
1374
1448
  try {
1375
1449
  console.log(chalk4.blue(`\u{1F4C1} Reading file: ${options.file}`));
1376
1450
  const result = await uploadFile(uploadOptions);
1377
- const storageUrl = generateStorageUrl(
1451
+ const storageUrl2 = generateStorageUrl(
1378
1452
  result.operatorAddress,
1379
1453
  commonOptions.chainId,
1380
1454
  options.key
@@ -1384,8 +1458,8 @@ function registerStorageCommand(program2) {
1384
1458
  chalk4.green(
1385
1459
  `\u2713 All data already stored - skipping upload
1386
1460
  Storage Key: ${options.key}
1387
- Skipped: ${result.transactionsSkipped} transaction(s)${storageUrl ? `
1388
- Storage URL: ${chalk4.cyan(storageUrl)}` : ""}`
1461
+ Skipped: ${result.transactionsSkipped} transaction(s)${storageUrl2 ? `
1462
+ Storage URL: ${chalk4.cyan(storageUrl2)}` : ""}`
1389
1463
  )
1390
1464
  );
1391
1465
  process.exit(0);
@@ -1398,8 +1472,8 @@ function registerStorageCommand(program2) {
1398
1472
  Storage Type: ${result.storageType === "xml" ? "XML" : "Normal"}
1399
1473
  Transactions Sent: ${result.transactionsSent}
1400
1474
  Transactions Skipped: ${result.transactionsSkipped}
1401
- Final Transaction Hash: ${result.finalHash || "N/A"}${storageUrl ? `
1402
- Storage URL: ${chalk4.cyan(storageUrl)}` : ""}`
1475
+ Final Transaction Hash: ${result.finalHash || "N/A"}${storageUrl2 ? `
1476
+ Storage URL: ${chalk4.cyan(storageUrl2)}` : ""}`
1403
1477
  )
1404
1478
  );
1405
1479
  process.exit(0);
@@ -1456,7 +1530,7 @@ function registerStorageCommand(program2) {
1456
1530
  try {
1457
1531
  console.log(chalk4.blue(`\u{1F4C1} Reading file: ${options.file}`));
1458
1532
  const result = await previewFile(previewOptions);
1459
- const storageUrl = generateStorageUrl(
1533
+ const storageUrl2 = generateStorageUrl(
1460
1534
  result.operatorAddress,
1461
1535
  commonOptions.chainId,
1462
1536
  options.key
@@ -1492,8 +1566,8 @@ function registerStorageCommand(program2) {
1492
1566
  console.log(
1493
1567
  ` Operator Address: ${chalk4.gray(result.operatorAddress)}`
1494
1568
  );
1495
- if (storageUrl) {
1496
- console.log(` Storage URL: ${chalk4.cyan(storageUrl)}`);
1569
+ if (storageUrl2) {
1570
+ console.log(` Storage URL: ${chalk4.cyan(storageUrl2)}`);
1497
1571
  }
1498
1572
  if (result.needToStoreChunks === 0 && !result.metadataNeedsStorage) {
1499
1573
  console.log(
@@ -1569,7 +1643,7 @@ function registerStorageCommand(program2) {
1569
1643
  const result = await uploadFileWithRelay(uploadRelayOptions);
1570
1644
  const { privateKeyToAccount: privateKeyToAccount28 } = await import('viem/accounts');
1571
1645
  const userAccount = privateKeyToAccount28(commonOptions.privateKey);
1572
- const storageUrl = generateStorageUrl(
1646
+ const storageUrl2 = generateStorageUrl(
1573
1647
  userAccount.address,
1574
1648
  commonOptions.chainId,
1575
1649
  options.key
@@ -1585,8 +1659,8 @@ function registerStorageCommand(program2) {
1585
1659
  Metadata Submitted: ${result.metadataSubmitted ? "Yes" : "No (already exists)"}
1586
1660
  Backend Wallet: ${result.backendWalletAddress}
1587
1661
  Chunk Transaction Hashes: ${result.chunkTransactionHashes.length > 0 ? result.chunkTransactionHashes.join(", ") : "None"}${result.metadataTransactionHash ? `
1588
- Metadata Transaction Hash: ${result.metadataTransactionHash}` : ""}${storageUrl ? `
1589
- Storage URL: ${chalk4.cyan(storageUrl)}` : ""}`
1662
+ Metadata Transaction Hash: ${result.metadataTransactionHash}` : ""}${storageUrl2 ? `
1663
+ Storage URL: ${chalk4.cyan(storageUrl2)}` : ""}`
1590
1664
  )
1591
1665
  );
1592
1666
  process.exit(0);
@@ -1935,33 +2009,25 @@ function registerMessageCommand(program2) {
1935
2009
  messageCommand.addCommand(readCommand);
1936
2010
  messageCommand.addCommand(countCommand);
1937
2011
  }
1938
- var SUPPORTED_CHAINS = [
1939
- { id: 8453, name: "Base", type: "mainnet" },
1940
- { id: 1, name: "Ethereum", type: "mainnet" },
1941
- { id: 666666666, name: "Degen", type: "mainnet" },
1942
- { id: 5112, name: "Ham", type: "mainnet" },
1943
- { id: 57073, name: "Ink", type: "mainnet" },
1944
- { id: 130, name: "Unichain", type: "mainnet" },
1945
- { id: 999, name: "HyperEVM", type: "mainnet" },
1946
- { id: 9745, name: "Plasma", type: "mainnet" },
1947
- { id: 143, name: "Monad", type: "mainnet" },
1948
- { id: 84532, name: "Base Sepolia", type: "testnet" },
1949
- { id: 11155111, name: "Sepolia", type: "testnet" }
1950
- ];
1951
2012
  function registerChainsCommand(program2) {
1952
2013
  program2.command("chains").description("List supported chains").option("--json", "Output in JSON format").action((options) => {
2014
+ const chains = getSupportedChains();
1953
2015
  if (options.json) {
1954
- console.log(JSON.stringify(SUPPORTED_CHAINS, null, 2));
2016
+ console.log(JSON.stringify(chains, null, 2));
1955
2017
  return;
1956
2018
  }
1957
2019
  console.log(chalk4.white.bold("Supported Chains:\n"));
1958
2020
  console.log(chalk4.cyan("Mainnets:"));
1959
- SUPPORTED_CHAINS.filter((c) => c.type === "mainnet").forEach((chain) => {
1960
- console.log(` ${chalk4.white(chain.name)} ${chalk4.gray(`(${chain.id})`)}`);
2021
+ chains.filter((c) => c.type === "mainnet").forEach((chain) => {
2022
+ console.log(
2023
+ ` ${chalk4.white(chain.name)} ${chalk4.gray(`(${chain.chainId})`)}`
2024
+ );
1961
2025
  });
1962
2026
  console.log(chalk4.cyan("\nTestnets:"));
1963
- SUPPORTED_CHAINS.filter((c) => c.type === "testnet").forEach((chain) => {
1964
- console.log(` ${chalk4.white(chain.name)} ${chalk4.gray(`(${chain.id})`)}`);
2027
+ chains.filter((c) => c.type === "testnet").forEach((chain) => {
2028
+ console.log(
2029
+ ` ${chalk4.white(chain.name)} ${chalk4.gray(`(${chain.chainId})`)}`
2030
+ );
1965
2031
  });
1966
2032
  });
1967
2033
  }
@@ -2225,10 +2291,19 @@ async function executeTokenInfo(options) {
2225
2291
  const output = {
2226
2292
  address: tokenAddress,
2227
2293
  chainId: readOnlyOptions.chainId,
2294
+ tokenUrl: tokenUrl(readOnlyOptions.chainId, tokenAddress),
2295
+ explorerAddressUrl: explorerAddressUrl(
2296
+ readOnlyOptions.chainId,
2297
+ tokenAddress
2298
+ ),
2228
2299
  token: {
2229
2300
  name: token.name,
2230
2301
  symbol: token.symbol,
2231
2302
  deployer: token.deployer,
2303
+ deployerProfileUrl: profileUrl(
2304
+ readOnlyOptions.chainId,
2305
+ token.deployer
2306
+ ),
2232
2307
  image: token.image,
2233
2308
  animation: token.animation || null,
2234
2309
  fid: token.fid.toString(),
@@ -2427,6 +2502,8 @@ async function executeProfileGet(options) {
2427
2502
  const output = {
2428
2503
  address: options.address,
2429
2504
  chainId: readOnlyOptions.chainId,
2505
+ profileUrl: profileUrl(readOnlyOptions.chainId, options.address),
2506
+ walletUrl: walletUrl(readOnlyOptions.chainId, options.address),
2430
2507
  profilePicture: profilePicture || null,
2431
2508
  displayName: displayName || null,
2432
2509
  xUsername: xUsername || null,
@@ -5679,38 +5756,70 @@ function formatComment(comment, depth) {
5679
5756
  ];
5680
5757
  return lines.join("\n");
5681
5758
  }
5682
- function postToJson(post, index, commentCount) {
5759
+ function stripFeedPrefix2(topic) {
5760
+ const match = topic.match(/^(.+?):comments:/);
5761
+ const base9 = match ? match[1] : topic;
5762
+ return base9.replace(/^feed-/i, "");
5763
+ }
5764
+ function postToJson(post, options) {
5765
+ const { chainId, topicIndex, userIndex, globalIndex, commentCount } = options;
5766
+ const feedName = stripFeedPrefix2(post.topic);
5767
+ const postId = `${post.sender}:${post.timestamp}`;
5768
+ const permalink = postPermalink(chainId, {
5769
+ globalIndex,
5770
+ topic: post.topic,
5771
+ topicIndex,
5772
+ user: userIndex !== void 0 ? post.sender : void 0,
5773
+ userIndex
5774
+ });
5683
5775
  const result = {
5684
- index,
5776
+ postId,
5777
+ permalink,
5685
5778
  sender: post.sender,
5779
+ senderProfileUrl: profileUrl(chainId, post.sender),
5780
+ senderWalletUrl: walletUrl(chainId, post.sender),
5686
5781
  text: post.text,
5687
5782
  timestamp: Number(post.timestamp),
5783
+ feed: feedName,
5784
+ feedUrl: feedUrl(chainId, feedName),
5688
5785
  topic: post.topic
5689
5786
  };
5690
- if (commentCount !== void 0) {
5691
- result.commentCount = commentCount;
5692
- }
5693
- if (post.data && post.data !== "0x") {
5694
- result.data = post.data;
5695
- }
5787
+ if (topicIndex !== void 0) result.topicIndex = topicIndex;
5788
+ if (userIndex !== void 0) result.userIndex = userIndex;
5789
+ if (globalIndex !== void 0) result.globalIndex = globalIndex;
5790
+ if (commentCount !== void 0) result.commentCount = commentCount;
5791
+ if (post.data && post.data !== "0x") result.data = post.data;
5696
5792
  return result;
5697
5793
  }
5698
- function feedToJson(feed, index) {
5699
- return {
5794
+ function feedToJson(feed, index, chainId) {
5795
+ const result = {
5700
5796
  index,
5701
5797
  feedName: feed.feedName,
5702
5798
  registrant: feed.registrant,
5703
5799
  timestamp: feed.timestamp
5704
5800
  };
5801
+ if (chainId !== void 0) {
5802
+ result.feedUrl = feedUrl(chainId, feed.feedName);
5803
+ }
5804
+ return result;
5705
5805
  }
5706
- function commentToJson(comment, depth) {
5707
- return {
5806
+ function commentToJson(comment, options) {
5807
+ const { chainId, depth, parentPostUrl } = options;
5808
+ const commentParam = `${comment.sender}-${comment.timestamp}`;
5809
+ const permalink = parentPostUrl ? `${parentPostUrl}${parentPostUrl.includes("?") ? "&" : "?"}commentId=${commentParam}` : null;
5810
+ const result = {
5811
+ commentId: `${comment.sender}:${comment.timestamp}`,
5812
+ permalink,
5708
5813
  sender: comment.sender,
5814
+ senderProfileUrl: profileUrl(chainId, comment.sender),
5709
5815
  text: comment.text,
5710
5816
  timestamp: Number(comment.timestamp),
5711
- depth,
5712
- data: comment.data !== "0x" ? comment.data : void 0
5817
+ depth
5713
5818
  };
5819
+ if (comment.data !== "0x") {
5820
+ result.data = comment.data;
5821
+ }
5822
+ return result;
5714
5823
  }
5715
5824
  function formatAgent(agent, index) {
5716
5825
  const timestamp = formatTimestamp(agent.timestamp);
@@ -5720,12 +5829,17 @@ function formatAgent(agent, index) {
5720
5829
  ];
5721
5830
  return lines.join("\n");
5722
5831
  }
5723
- function agentToJson(agent, index) {
5724
- return {
5832
+ function agentToJson(agent, index, chainId) {
5833
+ const result = {
5725
5834
  index,
5726
5835
  address: agent.address,
5727
5836
  timestamp: agent.timestamp
5728
5837
  };
5838
+ if (chainId !== void 0) {
5839
+ result.profileUrl = profileUrl(chainId, agent.address);
5840
+ result.walletUrl = walletUrl(chainId, agent.address);
5841
+ }
5842
+ return result;
5729
5843
  }
5730
5844
  function printJson(data) {
5731
5845
  console.log(JSON.stringify(data, null, 2));
@@ -5743,7 +5857,11 @@ async function executeFeedList(options) {
5743
5857
  maxFeeds: options.limit ?? 50
5744
5858
  });
5745
5859
  if (options.json) {
5746
- printJson(feeds.map((feed, i) => feedToJson(feed, i)));
5860
+ printJson(
5861
+ feeds.map(
5862
+ (feed, i) => feedToJson(feed, i, readOnlyOptions.chainId)
5863
+ )
5864
+ );
5747
5865
  } else {
5748
5866
  if (feeds.length === 0) {
5749
5867
  console.log(chalk4.yellow("No registered feeds found"));
@@ -5969,26 +6087,31 @@ async function executeFeedRead(feed, options) {
5969
6087
  return;
5970
6088
  }
5971
6089
  const fetchLimit = options.sender ? Math.max(limit * 5, 100) : limit;
5972
- let posts = await client.getFeedPosts({
6090
+ const fetched = await client.getFeedPostsWithIndex({
5973
6091
  topic: normalizedFeed,
5974
6092
  maxPosts: fetchLimit
5975
6093
  });
6094
+ let postsWithIndex = fetched.messages.map((post, i) => ({
6095
+ post,
6096
+ topicIndex: fetched.startIndex + i
6097
+ }));
5976
6098
  if (options.sender) {
5977
6099
  const senderLower = options.sender.toLowerCase();
5978
- posts = posts.filter(
5979
- (post) => post.sender.toLowerCase() === senderLower
6100
+ postsWithIndex = postsWithIndex.filter(
6101
+ ({ post }) => post.sender.toLowerCase() === senderLower
5980
6102
  );
5981
- posts = posts.slice(0, limit);
6103
+ postsWithIndex = postsWithIndex.slice(0, limit);
5982
6104
  }
5983
6105
  if (options.unseen) {
5984
6106
  const lastSeen = getLastSeenTimestamp(normalizedFeed);
5985
6107
  const myAddress = getMyAddress();
5986
- posts = posts.filter((post) => {
6108
+ postsWithIndex = postsWithIndex.filter(({ post }) => {
5987
6109
  const isNew = lastSeen === null || Number(post.timestamp) > lastSeen;
5988
6110
  const isFromOther = !myAddress || post.sender.toLowerCase() !== myAddress;
5989
6111
  return isNew && isFromOther;
5990
6112
  });
5991
6113
  }
6114
+ const posts = postsWithIndex.map(({ post }) => post);
5992
6115
  if (options.markSeen) {
5993
6116
  const allPosts = await client.getFeedPosts({
5994
6117
  topic: normalizedFeed,
@@ -6004,7 +6127,13 @@ async function executeFeedRead(feed, options) {
6004
6127
  );
6005
6128
  if (options.json) {
6006
6129
  printJson(
6007
- posts.map((post, i) => postToJson(post, i, commentCounts[i]))
6130
+ postsWithIndex.map(
6131
+ ({ post, topicIndex }, i) => postToJson(post, {
6132
+ chainId: readOnlyOptions.chainId,
6133
+ topicIndex,
6134
+ commentCount: commentCounts[i]
6135
+ })
6136
+ )
6008
6137
  );
6009
6138
  } else {
6010
6139
  if (posts.length === 0) {
@@ -6066,6 +6195,34 @@ async function executeTransaction(walletClient, txConfig) {
6066
6195
  });
6067
6196
  return hash;
6068
6197
  }
6198
+ async function getMessageIndicesFromTx(params) {
6199
+ const publicClient = getPublicClient({
6200
+ chainId: params.chainId,
6201
+ rpcUrl: params.rpcUrl
6202
+ });
6203
+ const netContract = getNetContract(params.chainId);
6204
+ const contractAddress = netContract.address.toLowerCase();
6205
+ const receipt = await publicClient.waitForTransactionReceipt({
6206
+ hash: params.txHash
6207
+ });
6208
+ const indices = [];
6209
+ for (const log of receipt.logs) {
6210
+ if (log.address.toLowerCase() !== contractAddress) continue;
6211
+ try {
6212
+ const decoded = decodeEventLog({
6213
+ abi: netContract.abi,
6214
+ data: log.data,
6215
+ topics: log.topics
6216
+ });
6217
+ if (decoded.eventName === "MessageSent") {
6218
+ const args = decoded.args;
6219
+ indices.push(Number(args.messageIndex));
6220
+ }
6221
+ } catch {
6222
+ }
6223
+ }
6224
+ return indices;
6225
+ }
6069
6226
 
6070
6227
  // src/commands/feed/post.ts
6071
6228
  var MAX_MESSAGE_LENGTH = 4e3;
@@ -6117,11 +6274,23 @@ ${options.body}` : message;
6117
6274
  commonOptions.chainId,
6118
6275
  commonOptions.rpcUrl
6119
6276
  );
6120
- console.log(chalk4.blue(`Posting to feed "${normalizedFeed}"...`));
6277
+ if (!options.json) {
6278
+ console.log(chalk4.blue(`Posting to feed "${normalizedFeed}"...`));
6279
+ }
6121
6280
  try {
6122
6281
  const hash = await executeTransaction(walletClient, txConfig);
6123
6282
  const senderAddress = walletClient.account.address;
6124
6283
  let postId;
6284
+ let globalIndex;
6285
+ try {
6286
+ const indices = await getMessageIndicesFromTx({
6287
+ chainId: commonOptions.chainId,
6288
+ rpcUrl: commonOptions.rpcUrl,
6289
+ txHash: hash
6290
+ });
6291
+ globalIndex = indices[0];
6292
+ } catch {
6293
+ }
6125
6294
  try {
6126
6295
  const posts = await client.getFeedPosts({
6127
6296
  topic: normalizedFeed,
@@ -6143,19 +6312,36 @@ ${options.body}` : message;
6143
6312
  sender: senderAddress,
6144
6313
  text: fullMessage,
6145
6314
  postId
6146
- // Now we have the actual post ID for checking comments later
6147
6315
  });
6316
+ const permalink = postPermalink(commonOptions.chainId, {
6317
+ globalIndex
6318
+ });
6319
+ if (options.json) {
6320
+ printJson({
6321
+ success: true,
6322
+ txHash: hash,
6323
+ explorerTxUrl: explorerTxUrl(commonOptions.chainId, hash),
6324
+ postId,
6325
+ globalIndex,
6326
+ permalink,
6327
+ feed: normalizedFeed,
6328
+ feedUrl: feedUrl(commonOptions.chainId, normalizedFeed),
6329
+ sender: senderAddress,
6330
+ senderProfileUrl: profileUrl(commonOptions.chainId, senderAddress),
6331
+ text: fullMessage
6332
+ });
6333
+ return;
6334
+ }
6148
6335
  const displayText = options.body ? `${message} (+ body)` : message;
6149
- const postIdInfo = postId ? `
6150
- Post ID: ${postId}` : "";
6151
- console.log(
6152
- chalk4.green(
6153
- `Message posted successfully!
6154
- Transaction: ${hash}
6155
- Feed: ${normalizedFeed}${postIdInfo}
6156
- Text: ${displayText}`
6157
- )
6158
- );
6336
+ const lines = [
6337
+ `Message posted successfully!`,
6338
+ ` Transaction: ${hash}`,
6339
+ ` Feed: ${normalizedFeed}`
6340
+ ];
6341
+ if (postId) lines.push(` Post ID: ${postId}`);
6342
+ if (permalink) lines.push(` Permalink: ${permalink}`);
6343
+ lines.push(` Text: ${displayText}`);
6344
+ console.log(chalk4.green(lines.join("\n")));
6159
6345
  } catch (error) {
6160
6346
  exitWithError(
6161
6347
  `Failed to post message: ${error instanceof Error ? error.message : String(error)}`
@@ -6170,7 +6356,10 @@ function registerFeedPostCommand(parent) {
6170
6356
  ).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
6171
6357
  "--encode-only",
6172
6358
  "Output transaction data as JSON instead of executing"
6173
- ).option("--data <data>", "Optional data to attach to the post").option("--body <text>", "Post body (message becomes the title)").action(async (feed, message, options) => {
6359
+ ).option("--data <data>", "Optional data to attach to the post").option("--body <text>", "Post body (message becomes the title)").option(
6360
+ "--json",
6361
+ "Output structured JSON (includes permalink and other URLs) after submission"
6362
+ ).action(async (feed, message, options) => {
6174
6363
  await executeFeedPost(feed, message, options);
6175
6364
  });
6176
6365
  }
@@ -6199,11 +6388,6 @@ function parsePostId(postId) {
6199
6388
  timestamp
6200
6389
  };
6201
6390
  }
6202
- function findPostByParsedId(posts, parsedId) {
6203
- return posts.find(
6204
- (p) => p.sender.toLowerCase() === parsedId.sender.toLowerCase() && p.timestamp === parsedId.timestamp
6205
- );
6206
- }
6207
6391
 
6208
6392
  // src/commands/feed/comment-write.ts
6209
6393
  var MAX_MESSAGE_LENGTH2 = 4e3;
@@ -6236,17 +6420,20 @@ async function executeFeedCommentWrite(feed, postId, message, options) {
6236
6420
  `Feed "${normalizedFeed}" has no posts. Cannot find post ${postId}.`
6237
6421
  );
6238
6422
  }
6239
- const posts = await client.getFeedPosts({
6423
+ const fetched = await client.getFeedPostsWithIndex({
6240
6424
  topic: normalizedFeed,
6241
6425
  maxPosts: 100
6242
- // Fetch enough to find the post
6243
6426
  });
6244
- const targetPost = findPostByParsedId(posts, parsedId);
6427
+ const matchOffset = fetched.messages.findIndex(
6428
+ (p) => p.sender.toLowerCase() === parsedId.sender.toLowerCase() && p.timestamp === parsedId.timestamp
6429
+ );
6430
+ const targetPost = matchOffset >= 0 ? fetched.messages[matchOffset] : void 0;
6245
6431
  if (!targetPost) {
6246
6432
  exitWithError(
6247
6433
  `Post not found with ID ${postId} in feed "${normalizedFeed}". Make sure the sender and timestamp are correct.`
6248
6434
  );
6249
6435
  }
6436
+ const parentTopicIndex = fetched.startIndex + matchOffset;
6250
6437
  const txConfig = client.prepareComment({
6251
6438
  post: targetPost,
6252
6439
  text: message
@@ -6270,26 +6457,81 @@ async function executeFeedCommentWrite(feed, postId, message, options) {
6270
6457
  commonOptions.chainId,
6271
6458
  commonOptions.rpcUrl
6272
6459
  );
6273
- console.log(chalk4.blue(`Commenting on post ${postId}...`));
6460
+ if (!options.json) {
6461
+ console.log(chalk4.blue(`Commenting on post ${postId}...`));
6462
+ }
6274
6463
  try {
6275
6464
  const hash = await executeTransaction(walletClient, txConfig);
6465
+ const senderAddress = walletClient.account.address;
6466
+ let globalIndex;
6467
+ try {
6468
+ const indices = await getMessageIndicesFromTx({
6469
+ chainId: commonOptions.chainId,
6470
+ rpcUrl: commonOptions.rpcUrl,
6471
+ txHash: hash
6472
+ });
6473
+ globalIndex = indices[0];
6474
+ } catch {
6475
+ }
6476
+ let commentTimestamp;
6477
+ if (globalIndex !== void 0) {
6478
+ try {
6479
+ const netClient = createNetClient(commonOptions);
6480
+ const fetchedComment = await netClient.getMessageAtIndex({
6481
+ messageIndex: globalIndex
6482
+ });
6483
+ if (fetchedComment) {
6484
+ commentTimestamp = Number(fetchedComment.timestamp);
6485
+ }
6486
+ } catch {
6487
+ }
6488
+ }
6276
6489
  addHistoryEntry({
6277
6490
  type: "comment",
6278
6491
  txHash: hash,
6279
6492
  chainId: commonOptions.chainId,
6280
6493
  feed: normalizedFeed,
6281
- sender: walletClient.account.address,
6494
+ sender: senderAddress,
6282
6495
  text: message,
6283
6496
  postId
6284
6497
  });
6285
- console.log(
6286
- chalk4.green(
6287
- `Comment posted successfully!
6288
- Transaction: ${hash}
6289
- Post: ${postId}
6290
- Comment: ${message}`
6291
- )
6292
- );
6498
+ const parentPostUrl = postPermalink(commonOptions.chainId, {
6499
+ topic: normalizedFeed,
6500
+ topicIndex: parentTopicIndex
6501
+ });
6502
+ const commentPermalink = commentTimestamp !== void 0 ? postPermalink(commonOptions.chainId, {
6503
+ topic: normalizedFeed,
6504
+ topicIndex: parentTopicIndex,
6505
+ commentId: `${senderAddress}-${commentTimestamp}`
6506
+ }) : postPermalink(commonOptions.chainId, { globalIndex });
6507
+ if (options.json) {
6508
+ printJson({
6509
+ success: true,
6510
+ txHash: hash,
6511
+ explorerTxUrl: explorerTxUrl(commonOptions.chainId, hash),
6512
+ globalIndex,
6513
+ permalink: commentPermalink,
6514
+ parentPostId: postId,
6515
+ parentPostUrl,
6516
+ feed: normalizedFeed,
6517
+ feedUrl: feedUrl(commonOptions.chainId, normalizedFeed),
6518
+ sender: senderAddress,
6519
+ senderProfileUrl: profileUrl(commonOptions.chainId, senderAddress),
6520
+ text: message,
6521
+ ...commentTimestamp !== void 0 && {
6522
+ commentId: `${senderAddress}:${commentTimestamp}`
6523
+ }
6524
+ });
6525
+ return;
6526
+ }
6527
+ const lines = [
6528
+ `Comment posted successfully!`,
6529
+ ` Transaction: ${hash}`,
6530
+ ` Post: ${postId}`
6531
+ ];
6532
+ if (commentPermalink) lines.push(` Permalink: ${commentPermalink}`);
6533
+ lines.push(` Comment: ${message}`);
6534
+ console.log(chalk4.green(lines.join("\n")));
6293
6535
  } catch (error) {
6294
6536
  exitWithError(
6295
6537
  `Failed to post comment: ${error instanceof Error ? error.message : String(error)}`
@@ -6306,6 +6548,9 @@ function registerFeedCommentWriteCommand(parent) {
6306
6548
  ).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
6307
6549
  "--encode-only",
6308
6550
  "Output transaction data as JSON instead of executing"
6551
+ ).option(
6552
+ "--json",
6553
+ "Output structured JSON (includes permalink and other URLs) after submission"
6309
6554
  ).action(async (feed, postId, message, options) => {
6310
6555
  await executeFeedCommentWrite(feed, postId, message, options);
6311
6556
  });
@@ -6332,17 +6577,25 @@ async function executeFeedCommentRead(feed, postId, options) {
6332
6577
  `Feed "${normalizedFeed}" has no posts. Cannot find post ${postId}.`
6333
6578
  );
6334
6579
  }
6335
- const posts = await client.getFeedPosts({
6580
+ const fetched = await client.getFeedPostsWithIndex({
6336
6581
  topic: normalizedFeed,
6337
6582
  maxPosts: 100
6338
6583
  // Fetch enough to find the post
6339
6584
  });
6340
- const targetPost = findPostByParsedId(posts, parsedId);
6585
+ const matchIndex = fetched.messages.findIndex(
6586
+ (p) => p.sender.toLowerCase() === parsedId.sender.toLowerCase() && p.timestamp === parsedId.timestamp
6587
+ );
6588
+ const targetPost = matchIndex >= 0 ? fetched.messages[matchIndex] : void 0;
6341
6589
  if (!targetPost) {
6342
6590
  exitWithError(
6343
6591
  `Post not found with ID ${postId} in feed "${normalizedFeed}". Make sure the sender and timestamp are correct.`
6344
6592
  );
6345
6593
  }
6594
+ const parentTopicIndex = fetched.startIndex + matchIndex;
6595
+ const parentPostUrl = postPermalink(readOnlyOptions.chainId, {
6596
+ topic: normalizedFeed,
6597
+ topicIndex: parentTopicIndex
6598
+ });
6346
6599
  const commentCount = await client.getCommentCount(targetPost);
6347
6600
  if (commentCount === 0) {
6348
6601
  if (options.json) {
@@ -6363,7 +6616,11 @@ async function executeFeedCommentRead(feed, postId, options) {
6363
6616
  if (options.json) {
6364
6617
  printJson(
6365
6618
  commentsWithDepth.map(
6366
- ({ comment, depth }) => commentToJson(comment, depth)
6619
+ ({ comment, depth }) => commentToJson(comment, {
6620
+ chainId: readOnlyOptions.chainId,
6621
+ depth,
6622
+ parentPostUrl
6623
+ })
6367
6624
  )
6368
6625
  );
6369
6626
  } else {
@@ -6504,8 +6761,10 @@ async function executeFeedReplies(options) {
6504
6761
  rpcUrl: options.rpcUrl
6505
6762
  });
6506
6763
  const client = createFeedClient(readOnlyOptions);
6507
- console.log(chalk4.blue(`Checking replies on ${postsWithIds.length} posts...
6764
+ if (!options.json) {
6765
+ console.log(chalk4.blue(`Checking replies on ${postsWithIds.length} posts...
6508
6766
  `));
6767
+ }
6509
6768
  const results = [];
6510
6769
  for (const entry of postsWithIds) {
6511
6770
  try {
@@ -6520,12 +6779,31 @@ async function executeFeedReplies(options) {
6520
6779
  data: "0x"
6521
6780
  };
6522
6781
  const commentCount = await client.getCommentCount(postObj);
6782
+ let permalink = null;
6783
+ try {
6784
+ const indices = await getMessageIndicesFromTx({
6785
+ chainId: readOnlyOptions.chainId,
6786
+ rpcUrl: readOnlyOptions.rpcUrl,
6787
+ txHash: entry.txHash
6788
+ });
6789
+ if (indices[0] !== void 0) {
6790
+ permalink = postPermalink(readOnlyOptions.chainId, {
6791
+ globalIndex: indices[0]
6792
+ });
6793
+ }
6794
+ } catch {
6795
+ }
6523
6796
  results.push({
6524
6797
  feed: entry.feed,
6525
6798
  postId: entry.postId,
6526
6799
  text: entry.text ?? "",
6527
6800
  postedAt: entry.timestamp,
6528
- commentCount: Number(commentCount)
6801
+ commentCount: Number(commentCount),
6802
+ permalink,
6803
+ feedUrl: feedUrl(readOnlyOptions.chainId, entry.feed),
6804
+ senderProfileUrl: entry.sender ? profileUrl(readOnlyOptions.chainId, entry.sender) : null,
6805
+ explorerTxUrl: explorerTxUrl(readOnlyOptions.chainId, entry.txHash),
6806
+ txHash: entry.txHash
6529
6807
  });
6530
6808
  } catch {
6531
6809
  }
@@ -6610,7 +6888,14 @@ async function executeFeedPosts(address, options) {
6610
6888
  endIndex: count
6611
6889
  });
6612
6890
  if (options.json) {
6613
- printJson(messages.map((msg, i) => postToJson(msg, i)));
6891
+ printJson(
6892
+ messages.map(
6893
+ (msg, i) => postToJson(msg, {
6894
+ chainId: readOnlyOptions.chainId,
6895
+ userIndex: startIndex + i
6896
+ })
6897
+ )
6898
+ );
6614
6899
  } else {
6615
6900
  console.log(
6616
6901
  chalk4.white(`Found ${messages.length} post(s) by ${address}:
@@ -6788,11 +7073,14 @@ function historyEntryToJson(entry, index) {
6788
7073
  type: entry.type,
6789
7074
  timestamp: entry.timestamp,
6790
7075
  txHash: entry.txHash,
7076
+ explorerTxUrl: explorerTxUrl(entry.chainId, entry.txHash),
6791
7077
  chainId: entry.chainId,
6792
- feed: entry.feed
7078
+ feed: entry.feed,
7079
+ feedUrl: feedUrl(entry.chainId, entry.feed)
6793
7080
  };
6794
7081
  if (entry.sender) {
6795
7082
  result.sender = entry.sender;
7083
+ result.senderProfileUrl = profileUrl(entry.chainId, entry.sender);
6796
7084
  }
6797
7085
  if (entry.text) {
6798
7086
  result.text = entry.text;
@@ -6966,7 +7254,9 @@ async function executeListAgents(options) {
6966
7254
  if (options.json) {
6967
7255
  printJson({
6968
7256
  totalCount,
6969
- agents: agents.map((agent, i) => agentToJson(agent, i))
7257
+ agents: agents.map(
7258
+ (agent, i) => agentToJson(agent, i, readOnlyOptions.chainId)
7259
+ )
6970
7260
  });
6971
7261
  } else {
6972
7262
  if (agents.length === 0) {
@@ -7025,9 +7315,13 @@ async function executeFeedVerifyClaim(txHash, options) {
7025
7315
  const netClient = createNetClient(readOnlyOptions);
7026
7316
  const existingHistory = getHistory();
7027
7317
  if (existingHistory.some((entry) => entry.txHash === txHash)) {
7028
- console.log(
7029
- chalk4.yellow("Transaction already recorded in history. Skipping.")
7030
- );
7318
+ if (options.json) {
7319
+ printJson({ alreadyRecorded: true, txHash });
7320
+ } else {
7321
+ console.log(
7322
+ chalk4.yellow("Transaction already recorded in history. Skipping.")
7323
+ );
7324
+ }
7031
7325
  return;
7032
7326
  }
7033
7327
  const receipt = await publicClient.getTransactionReceipt({ hash: txHash }).catch(
@@ -7062,11 +7356,13 @@ async function executeFeedVerifyClaim(txHash, options) {
7062
7356
  "Transaction does not contain any Net protocol messages."
7063
7357
  );
7064
7358
  }
7065
- console.log(
7066
- chalk4.blue(
7067
- `Found ${messageSentEvents.length} message(s) in transaction. Fetching details...`
7068
- )
7069
- );
7359
+ if (!options.json) {
7360
+ console.log(
7361
+ chalk4.blue(
7362
+ `Found ${messageSentEvents.length} message(s) in transaction. Fetching details...`
7363
+ )
7364
+ );
7365
+ }
7070
7366
  const messages = await Promise.all(
7071
7367
  messageSentEvents.map(
7072
7368
  (event) => netClient.getMessageAtIndex({
@@ -7074,41 +7370,40 @@ async function executeFeedVerifyClaim(txHash, options) {
7074
7370
  })
7075
7371
  )
7076
7372
  );
7373
+ const entries = [];
7077
7374
  let recorded = 0;
7078
7375
  for (let i = 0; i < messages.length; i++) {
7079
7376
  const message = messages[i];
7080
7377
  if (!message) {
7081
- console.log(
7082
- chalk4.yellow(
7083
- ` Could not fetch message at index ${messageSentEvents[i].messageIndex}. Skipping.`
7084
- )
7085
- );
7378
+ if (!options.json) {
7379
+ console.log(
7380
+ chalk4.yellow(
7381
+ ` Could not fetch message at index ${messageSentEvents[i].messageIndex}. Skipping.`
7382
+ )
7383
+ );
7384
+ }
7086
7385
  continue;
7087
7386
  }
7387
+ const globalIndex = Number(messageSentEvents[i].messageIndex);
7088
7388
  const feedName = extractFeedName(message.topic);
7089
7389
  const isComment = isCommentTopic(message.topic);
7090
7390
  let type;
7091
7391
  let postId;
7092
- let label;
7392
+ let parentPostId;
7393
+ let permalink = null;
7093
7394
  if (isComment) {
7094
7395
  type = "comment";
7095
7396
  const commentData = parseCommentData(message.data);
7096
- postId = commentData ? `${commentData.parentSender}:${commentData.parentTimestamp}` : void 0;
7097
- label = `Verified comment:
7098
- Feed: ${feedName}
7099
- Sender: ${message.sender}
7100
- Text: ${message.text}
7101
- Parent post: ${postId ?? "unknown"}
7102
- Tx: ${txHash}`;
7397
+ parentPostId = commentData ? `${commentData.parentSender}:${commentData.parentTimestamp}` : void 0;
7398
+ const commentParam = `${message.sender}-${Number(message.timestamp)}`;
7399
+ permalink = postPermalink(readOnlyOptions.chainId, {
7400
+ globalIndex,
7401
+ commentId: commentParam
7402
+ });
7103
7403
  } else {
7104
7404
  type = "post";
7105
7405
  postId = createPostId(message);
7106
- label = `Verified post:
7107
- Feed: ${feedName}
7108
- Sender: ${message.sender}
7109
- Text: ${message.text}
7110
- Post ID: ${postId}
7111
- Tx: ${txHash}`;
7406
+ permalink = postPermalink(readOnlyOptions.chainId, { globalIndex });
7112
7407
  }
7113
7408
  addHistoryEntry({
7114
7409
  type,
@@ -7117,11 +7412,49 @@ async function executeFeedVerifyClaim(txHash, options) {
7117
7412
  feed: feedName,
7118
7413
  sender: message.sender,
7119
7414
  text: message.text,
7120
- postId
7415
+ postId: type === "comment" ? parentPostId : postId
7121
7416
  });
7122
- console.log(chalk4.green(` ${label}`));
7417
+ const entry = {
7418
+ type,
7419
+ txHash,
7420
+ explorerTxUrl: explorerTxUrl(readOnlyOptions.chainId, txHash),
7421
+ globalIndex,
7422
+ permalink,
7423
+ feed: feedName,
7424
+ feedUrl: feedUrl(readOnlyOptions.chainId, feedName),
7425
+ sender: message.sender,
7426
+ senderProfileUrl: profileUrl(
7427
+ readOnlyOptions.chainId,
7428
+ message.sender
7429
+ ),
7430
+ text: message.text,
7431
+ timestamp: Number(message.timestamp)
7432
+ };
7433
+ if (type === "post") entry.postId = postId;
7434
+ if (type === "comment") entry.parentPostId = parentPostId;
7435
+ entries.push(entry);
7436
+ if (!options.json) {
7437
+ const label = type === "comment" ? `Verified comment:
7438
+ Feed: ${feedName}
7439
+ Sender: ${message.sender}
7440
+ Text: ${message.text}
7441
+ Parent post: ${parentPostId ?? "unknown"}
7442
+ Permalink: ${permalink ?? "(unavailable)"}
7443
+ Tx: ${txHash}` : `Verified post:
7444
+ Feed: ${feedName}
7445
+ Sender: ${message.sender}
7446
+ Text: ${message.text}
7447
+ Post ID: ${postId}
7448
+ Permalink: ${permalink ?? "(unavailable)"}
7449
+ Tx: ${txHash}`;
7450
+ console.log(chalk4.green(` ${label}`));
7451
+ }
7123
7452
  recorded++;
7124
7453
  }
7454
+ if (options.json) {
7455
+ printJson({ recorded, entries });
7456
+ return;
7457
+ }
7125
7458
  if (recorded > 0) {
7126
7459
  console.log(
7127
7460
  chalk4.green(`
@@ -7136,7 +7469,10 @@ function registerFeedVerifyClaimCommand(parent) {
7136
7469
  "--chain-id <id>",
7137
7470
  "Chain ID (default: 8453 for Base)",
7138
7471
  (value) => parseInt(value, 10)
7139
- ).option("--rpc-url <url>", "Custom RPC URL").action(async (txHash, options) => {
7472
+ ).option("--rpc-url <url>", "Custom RPC URL").option(
7473
+ "--json",
7474
+ "Output structured JSON (includes permalink and other URLs)"
7475
+ ).action(async (txHash, options) => {
7140
7476
  await executeFeedVerifyClaim(txHash, options);
7141
7477
  });
7142
7478
  }
@@ -7332,6 +7668,7 @@ async function executeGetUpvotes(options) {
7332
7668
  JSON.stringify(
7333
7669
  {
7334
7670
  tokenAddress,
7671
+ tokenUrl: tokenUrl(readOnlyOptions.chainId, tokenAddress),
7335
7672
  scoreKey,
7336
7673
  total,
7337
7674
  strategies: strategyCounts.map((s) => ({
@@ -7500,6 +7837,8 @@ async function executeGetUserUpvotes(options) {
7500
7837
  {
7501
7838
  address: userAddress,
7502
7839
  chainId: readOnlyOptions.chainId,
7840
+ profileUrl: profileUrl(readOnlyOptions.chainId, userAddress),
7841
+ walletUrl: walletUrl(readOnlyOptions.chainId, userAddress),
7503
7842
  upvotesGiven: Number(given),
7504
7843
  upvotesReceived: Number(received),
7505
7844
  upvotePriceWei: upvotePrice.toString(),