@net-protocol/cli 0.1.42 → 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.
@@ -1,7 +1,7 @@
1
1
  import chalk12 from 'chalk';
2
2
  import { isCommentTopic, parseCommentData, FeedRegistryClient, FeedClient, AgentRegistryClient, COMMENT_TOPIC_SUFFIX, FEED_TOPIC_PREFIX } from '@net-protocol/feeds';
3
3
  import '@net-protocol/chats';
4
- import { NULL_ADDRESS, getPublicClient, getNetContract, getBaseDataSuffix, getChainRpcUrls, NetClient } from '@net-protocol/core';
4
+ import { NULL_ADDRESS, getPublicClient, getNetContract, getBaseDataSuffix, getChainRpcUrls, getChainBlockExplorer, NetClient, getChainSlug } from '@net-protocol/core';
5
5
  import '@net-protocol/storage';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
@@ -91,6 +91,61 @@ function exitWithError(message) {
91
91
  console.error(chalk12.red(`Error: ${message}`));
92
92
  process.exit(1);
93
93
  }
94
+ var WEBSITE_BASE = "https://netprotocol.app";
95
+ function chainSlug(chainId) {
96
+ return getChainSlug({ chainId }) ?? null;
97
+ }
98
+ function stripFeedPrefix(topic) {
99
+ const lower = topic.toLowerCase();
100
+ return lower.startsWith("feed-") ? lower.slice(5) : lower;
101
+ }
102
+ function feedUrl(chainId, feedName) {
103
+ const slug = chainSlug(chainId);
104
+ if (!slug) return null;
105
+ return `${WEBSITE_BASE}/app/feed/${slug}/${stripFeedPrefix(feedName)}`;
106
+ }
107
+ function walletUrl(chainId, address) {
108
+ const slug = chainSlug(chainId);
109
+ if (!slug) return null;
110
+ return `${WEBSITE_BASE}/app/feed/${slug}/${address.toLowerCase()}`;
111
+ }
112
+ function profileUrl(chainId, address) {
113
+ const slug = chainSlug(chainId);
114
+ if (!slug) return null;
115
+ return `${WEBSITE_BASE}/app/profile/${slug}/${address.toLowerCase()}`;
116
+ }
117
+ function explorerTxUrl(chainId, txHash) {
118
+ const base = getChainBlockExplorer({ chainId })?.url;
119
+ if (!base) return null;
120
+ return `${base}/tx/${txHash}`;
121
+ }
122
+ function postIdToCommentParam(postId) {
123
+ const colon = postId.indexOf(":");
124
+ if (colon === -1) return postId;
125
+ return `${postId.slice(0, colon)}-${postId.slice(colon + 1)}`;
126
+ }
127
+ function postPermalink(chainId, opts) {
128
+ const slug = chainSlug(chainId);
129
+ if (!slug) return null;
130
+ const params = new URLSearchParams();
131
+ if (opts.globalIndex != null) {
132
+ params.set("index", String(opts.globalIndex));
133
+ } else if (opts.topic != null && opts.topicIndex != null) {
134
+ params.set("topic", stripFeedPrefix(opts.topic));
135
+ params.set("index", String(opts.topicIndex));
136
+ } else if (opts.user != null && opts.userIndex != null) {
137
+ params.set("user", opts.user.toLowerCase());
138
+ params.set("index", String(opts.userIndex));
139
+ } else {
140
+ return null;
141
+ }
142
+ if (opts.commentId) {
143
+ params.set("commentId", postIdToCommentParam(opts.commentId));
144
+ }
145
+ return `${WEBSITE_BASE}/app/feed/${slug}/post?${params.toString()}`;
146
+ }
147
+
148
+ // src/commands/feed/format.ts
94
149
  function truncateAddress(address) {
95
150
  if (address.length <= 12) return address;
96
151
  return `${address.slice(0, 6)}...${address.slice(-4)}`;
@@ -144,38 +199,70 @@ function formatComment(comment, depth) {
144
199
  ];
145
200
  return lines.join("\n");
146
201
  }
147
- function postToJson(post, index, commentCount) {
202
+ function stripFeedPrefix2(topic) {
203
+ const match = topic.match(/^(.+?):comments:/);
204
+ const base = match ? match[1] : topic;
205
+ return base.replace(/^feed-/i, "");
206
+ }
207
+ function postToJson(post, options) {
208
+ const { chainId, topicIndex, userIndex, globalIndex, commentCount } = options;
209
+ const feedName = stripFeedPrefix2(post.topic);
210
+ const postId = `${post.sender}:${post.timestamp}`;
211
+ const permalink = postPermalink(chainId, {
212
+ globalIndex,
213
+ topic: post.topic,
214
+ topicIndex,
215
+ user: userIndex !== void 0 ? post.sender : void 0,
216
+ userIndex
217
+ });
148
218
  const result = {
149
- index,
219
+ postId,
220
+ permalink,
150
221
  sender: post.sender,
222
+ senderProfileUrl: profileUrl(chainId, post.sender),
223
+ senderWalletUrl: walletUrl(chainId, post.sender),
151
224
  text: post.text,
152
225
  timestamp: Number(post.timestamp),
226
+ feed: feedName,
227
+ feedUrl: feedUrl(chainId, feedName),
153
228
  topic: post.topic
154
229
  };
155
- if (commentCount !== void 0) {
156
- result.commentCount = commentCount;
157
- }
158
- if (post.data && post.data !== "0x") {
159
- result.data = post.data;
160
- }
230
+ if (topicIndex !== void 0) result.topicIndex = topicIndex;
231
+ if (userIndex !== void 0) result.userIndex = userIndex;
232
+ if (globalIndex !== void 0) result.globalIndex = globalIndex;
233
+ if (commentCount !== void 0) result.commentCount = commentCount;
234
+ if (post.data && post.data !== "0x") result.data = post.data;
161
235
  return result;
162
236
  }
163
- function feedToJson(feed, index) {
164
- return {
237
+ function feedToJson(feed, index, chainId) {
238
+ const result = {
165
239
  index,
166
240
  feedName: feed.feedName,
167
241
  registrant: feed.registrant,
168
242
  timestamp: feed.timestamp
169
243
  };
244
+ if (chainId !== void 0) {
245
+ result.feedUrl = feedUrl(chainId, feed.feedName);
246
+ }
247
+ return result;
170
248
  }
171
- function commentToJson(comment, depth) {
172
- return {
249
+ function commentToJson(comment, options) {
250
+ const { chainId, depth, parentPostUrl } = options;
251
+ const commentParam = `${comment.sender}-${comment.timestamp}`;
252
+ const permalink = parentPostUrl ? `${parentPostUrl}${parentPostUrl.includes("?") ? "&" : "?"}commentId=${commentParam}` : null;
253
+ const result = {
254
+ commentId: `${comment.sender}:${comment.timestamp}`,
255
+ permalink,
173
256
  sender: comment.sender,
257
+ senderProfileUrl: profileUrl(chainId, comment.sender),
174
258
  text: comment.text,
175
259
  timestamp: Number(comment.timestamp),
176
- depth,
177
- data: comment.data !== "0x" ? comment.data : void 0
260
+ depth
178
261
  };
262
+ if (comment.data !== "0x") {
263
+ result.data = comment.data;
264
+ }
265
+ return result;
179
266
  }
180
267
  function formatAgent(agent, index) {
181
268
  const timestamp = formatTimestamp(agent.timestamp);
@@ -185,12 +272,17 @@ function formatAgent(agent, index) {
185
272
  ];
186
273
  return lines.join("\n");
187
274
  }
188
- function agentToJson(agent, index) {
189
- return {
275
+ function agentToJson(agent, index, chainId) {
276
+ const result = {
190
277
  index,
191
278
  address: agent.address,
192
279
  timestamp: agent.timestamp
193
280
  };
281
+ if (chainId !== void 0) {
282
+ result.profileUrl = profileUrl(chainId, agent.address);
283
+ result.walletUrl = walletUrl(chainId, agent.address);
284
+ }
285
+ return result;
194
286
  }
195
287
  function printJson(data) {
196
288
  console.log(JSON.stringify(data, null, 2));
@@ -208,7 +300,11 @@ async function executeFeedList(options) {
208
300
  maxFeeds: options.limit ?? 50
209
301
  });
210
302
  if (options.json) {
211
- printJson(feeds.map((feed, i) => feedToJson(feed, i)));
303
+ printJson(
304
+ feeds.map(
305
+ (feed, i) => feedToJson(feed, i, readOnlyOptions.chainId)
306
+ )
307
+ );
212
308
  } else {
213
309
  if (feeds.length === 0) {
214
310
  console.log(chalk12.yellow("No registered feeds found"));
@@ -434,26 +530,31 @@ async function executeFeedRead(feed, options) {
434
530
  return;
435
531
  }
436
532
  const fetchLimit = options.sender ? Math.max(limit * 5, 100) : limit;
437
- let posts = await client.getFeedPosts({
533
+ const fetched = await client.getFeedPostsWithIndex({
438
534
  topic: normalizedFeed,
439
535
  maxPosts: fetchLimit
440
536
  });
537
+ let postsWithIndex = fetched.messages.map((post, i) => ({
538
+ post,
539
+ topicIndex: fetched.startIndex + i
540
+ }));
441
541
  if (options.sender) {
442
542
  const senderLower = options.sender.toLowerCase();
443
- posts = posts.filter(
444
- (post) => post.sender.toLowerCase() === senderLower
543
+ postsWithIndex = postsWithIndex.filter(
544
+ ({ post }) => post.sender.toLowerCase() === senderLower
445
545
  );
446
- posts = posts.slice(0, limit);
546
+ postsWithIndex = postsWithIndex.slice(0, limit);
447
547
  }
448
548
  if (options.unseen) {
449
549
  const lastSeen = getLastSeenTimestamp(normalizedFeed);
450
550
  const myAddress = getMyAddress();
451
- posts = posts.filter((post) => {
551
+ postsWithIndex = postsWithIndex.filter(({ post }) => {
452
552
  const isNew = lastSeen === null || Number(post.timestamp) > lastSeen;
453
553
  const isFromOther = !myAddress || post.sender.toLowerCase() !== myAddress;
454
554
  return isNew && isFromOther;
455
555
  });
456
556
  }
557
+ const posts = postsWithIndex.map(({ post }) => post);
457
558
  if (options.markSeen) {
458
559
  const allPosts = await client.getFeedPosts({
459
560
  topic: normalizedFeed,
@@ -469,7 +570,13 @@ async function executeFeedRead(feed, options) {
469
570
  );
470
571
  if (options.json) {
471
572
  printJson(
472
- posts.map((post, i) => postToJson(post, i, commentCounts[i]))
573
+ postsWithIndex.map(
574
+ ({ post, topicIndex }, i) => postToJson(post, {
575
+ chainId: readOnlyOptions.chainId,
576
+ topicIndex,
577
+ commentCount: commentCounts[i]
578
+ })
579
+ )
473
580
  );
474
581
  } else {
475
582
  if (posts.length === 0) {
@@ -546,6 +653,34 @@ function encodeTransaction(config, chainId) {
546
653
  value: config.value?.toString() ?? "0"
547
654
  };
548
655
  }
656
+ async function getMessageIndicesFromTx(params) {
657
+ const publicClient = getPublicClient({
658
+ chainId: params.chainId,
659
+ rpcUrl: params.rpcUrl
660
+ });
661
+ const netContract = getNetContract(params.chainId);
662
+ const contractAddress = netContract.address.toLowerCase();
663
+ const receipt = await publicClient.waitForTransactionReceipt({
664
+ hash: params.txHash
665
+ });
666
+ const indices = [];
667
+ for (const log of receipt.logs) {
668
+ if (log.address.toLowerCase() !== contractAddress) continue;
669
+ try {
670
+ const decoded = decodeEventLog({
671
+ abi: netContract.abi,
672
+ data: log.data,
673
+ topics: log.topics
674
+ });
675
+ if (decoded.eventName === "MessageSent") {
676
+ const args = decoded.args;
677
+ indices.push(Number(args.messageIndex));
678
+ }
679
+ } catch {
680
+ }
681
+ }
682
+ return indices;
683
+ }
549
684
 
550
685
  // src/commands/feed/post.ts
551
686
  var MAX_MESSAGE_LENGTH = 4e3;
@@ -597,11 +732,23 @@ ${options.body}` : message;
597
732
  commonOptions.chainId,
598
733
  commonOptions.rpcUrl
599
734
  );
600
- console.log(chalk12.blue(`Posting to feed "${normalizedFeed}"...`));
735
+ if (!options.json) {
736
+ console.log(chalk12.blue(`Posting to feed "${normalizedFeed}"...`));
737
+ }
601
738
  try {
602
739
  const hash = await executeTransaction(walletClient, txConfig);
603
740
  const senderAddress = walletClient.account.address;
604
741
  let postId;
742
+ let globalIndex;
743
+ try {
744
+ const indices = await getMessageIndicesFromTx({
745
+ chainId: commonOptions.chainId,
746
+ rpcUrl: commonOptions.rpcUrl,
747
+ txHash: hash
748
+ });
749
+ globalIndex = indices[0];
750
+ } catch {
751
+ }
605
752
  try {
606
753
  const posts = await client.getFeedPosts({
607
754
  topic: normalizedFeed,
@@ -623,19 +770,36 @@ ${options.body}` : message;
623
770
  sender: senderAddress,
624
771
  text: fullMessage,
625
772
  postId
626
- // Now we have the actual post ID for checking comments later
627
773
  });
774
+ const permalink = postPermalink(commonOptions.chainId, {
775
+ globalIndex
776
+ });
777
+ if (options.json) {
778
+ printJson({
779
+ success: true,
780
+ txHash: hash,
781
+ explorerTxUrl: explorerTxUrl(commonOptions.chainId, hash),
782
+ postId,
783
+ globalIndex,
784
+ permalink,
785
+ feed: normalizedFeed,
786
+ feedUrl: feedUrl(commonOptions.chainId, normalizedFeed),
787
+ sender: senderAddress,
788
+ senderProfileUrl: profileUrl(commonOptions.chainId, senderAddress),
789
+ text: fullMessage
790
+ });
791
+ return;
792
+ }
628
793
  const displayText = options.body ? `${message} (+ body)` : message;
629
- const postIdInfo = postId ? `
630
- Post ID: ${postId}` : "";
631
- console.log(
632
- chalk12.green(
633
- `Message posted successfully!
634
- Transaction: ${hash}
635
- Feed: ${normalizedFeed}${postIdInfo}
636
- Text: ${displayText}`
637
- )
638
- );
794
+ const lines = [
795
+ `Message posted successfully!`,
796
+ ` Transaction: ${hash}`,
797
+ ` Feed: ${normalizedFeed}`
798
+ ];
799
+ if (postId) lines.push(` Post ID: ${postId}`);
800
+ if (permalink) lines.push(` Permalink: ${permalink}`);
801
+ lines.push(` Text: ${displayText}`);
802
+ console.log(chalk12.green(lines.join("\n")));
639
803
  } catch (error) {
640
804
  exitWithError(
641
805
  `Failed to post message: ${error instanceof Error ? error.message : String(error)}`
@@ -650,7 +814,10 @@ function registerFeedPostCommand(parent) {
650
814
  ).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
651
815
  "--encode-only",
652
816
  "Output transaction data as JSON instead of executing"
653
- ).option("--data <data>", "Optional data to attach to the post").option("--body <text>", "Post body (message becomes the title)").action(async (feed, message, options) => {
817
+ ).option("--data <data>", "Optional data to attach to the post").option("--body <text>", "Post body (message becomes the title)").option(
818
+ "--json",
819
+ "Output structured JSON (includes permalink and other URLs) after submission"
820
+ ).action(async (feed, message, options) => {
654
821
  await executeFeedPost(feed, message, options);
655
822
  });
656
823
  }
@@ -679,11 +846,6 @@ function parsePostId(postId) {
679
846
  timestamp
680
847
  };
681
848
  }
682
- function findPostByParsedId(posts, parsedId) {
683
- return posts.find(
684
- (p) => p.sender.toLowerCase() === parsedId.sender.toLowerCase() && p.timestamp === parsedId.timestamp
685
- );
686
- }
687
849
 
688
850
  // src/commands/feed/comment-write.ts
689
851
  var MAX_MESSAGE_LENGTH2 = 4e3;
@@ -716,17 +878,20 @@ async function executeFeedCommentWrite(feed, postId, message, options) {
716
878
  `Feed "${normalizedFeed}" has no posts. Cannot find post ${postId}.`
717
879
  );
718
880
  }
719
- const posts = await client.getFeedPosts({
881
+ const fetched = await client.getFeedPostsWithIndex({
720
882
  topic: normalizedFeed,
721
883
  maxPosts: 100
722
- // Fetch enough to find the post
723
884
  });
724
- const targetPost = findPostByParsedId(posts, parsedId);
885
+ const matchOffset = fetched.messages.findIndex(
886
+ (p) => p.sender.toLowerCase() === parsedId.sender.toLowerCase() && p.timestamp === parsedId.timestamp
887
+ );
888
+ const targetPost = matchOffset >= 0 ? fetched.messages[matchOffset] : void 0;
725
889
  if (!targetPost) {
726
890
  exitWithError(
727
891
  `Post not found with ID ${postId} in feed "${normalizedFeed}". Make sure the sender and timestamp are correct.`
728
892
  );
729
893
  }
894
+ const parentTopicIndex = fetched.startIndex + matchOffset;
730
895
  const txConfig = client.prepareComment({
731
896
  post: targetPost,
732
897
  text: message
@@ -750,26 +915,81 @@ async function executeFeedCommentWrite(feed, postId, message, options) {
750
915
  commonOptions.chainId,
751
916
  commonOptions.rpcUrl
752
917
  );
753
- console.log(chalk12.blue(`Commenting on post ${postId}...`));
918
+ if (!options.json) {
919
+ console.log(chalk12.blue(`Commenting on post ${postId}...`));
920
+ }
754
921
  try {
755
922
  const hash = await executeTransaction(walletClient, txConfig);
923
+ const senderAddress = walletClient.account.address;
924
+ let globalIndex;
925
+ try {
926
+ const indices = await getMessageIndicesFromTx({
927
+ chainId: commonOptions.chainId,
928
+ rpcUrl: commonOptions.rpcUrl,
929
+ txHash: hash
930
+ });
931
+ globalIndex = indices[0];
932
+ } catch {
933
+ }
934
+ let commentTimestamp;
935
+ if (globalIndex !== void 0) {
936
+ try {
937
+ const netClient = createNetClient(commonOptions);
938
+ const fetchedComment = await netClient.getMessageAtIndex({
939
+ messageIndex: globalIndex
940
+ });
941
+ if (fetchedComment) {
942
+ commentTimestamp = Number(fetchedComment.timestamp);
943
+ }
944
+ } catch {
945
+ }
946
+ }
756
947
  addHistoryEntry({
757
948
  type: "comment",
758
949
  txHash: hash,
759
950
  chainId: commonOptions.chainId,
760
951
  feed: normalizedFeed,
761
- sender: walletClient.account.address,
952
+ sender: senderAddress,
762
953
  text: message,
763
954
  postId
764
955
  });
765
- console.log(
766
- chalk12.green(
767
- `Comment posted successfully!
768
- Transaction: ${hash}
769
- Post: ${postId}
770
- Comment: ${message}`
771
- )
772
- );
956
+ const parentPostUrl = postPermalink(commonOptions.chainId, {
957
+ topic: normalizedFeed,
958
+ topicIndex: parentTopicIndex
959
+ });
960
+ const commentPermalink = commentTimestamp !== void 0 ? postPermalink(commonOptions.chainId, {
961
+ topic: normalizedFeed,
962
+ topicIndex: parentTopicIndex,
963
+ commentId: `${senderAddress}-${commentTimestamp}`
964
+ }) : postPermalink(commonOptions.chainId, { globalIndex });
965
+ if (options.json) {
966
+ printJson({
967
+ success: true,
968
+ txHash: hash,
969
+ explorerTxUrl: explorerTxUrl(commonOptions.chainId, hash),
970
+ globalIndex,
971
+ permalink: commentPermalink,
972
+ parentPostId: postId,
973
+ parentPostUrl,
974
+ feed: normalizedFeed,
975
+ feedUrl: feedUrl(commonOptions.chainId, normalizedFeed),
976
+ sender: senderAddress,
977
+ senderProfileUrl: profileUrl(commonOptions.chainId, senderAddress),
978
+ text: message,
979
+ ...commentTimestamp !== void 0 && {
980
+ commentId: `${senderAddress}:${commentTimestamp}`
981
+ }
982
+ });
983
+ return;
984
+ }
985
+ const lines = [
986
+ `Comment posted successfully!`,
987
+ ` Transaction: ${hash}`,
988
+ ` Post: ${postId}`
989
+ ];
990
+ if (commentPermalink) lines.push(` Permalink: ${commentPermalink}`);
991
+ lines.push(` Comment: ${message}`);
992
+ console.log(chalk12.green(lines.join("\n")));
773
993
  } catch (error) {
774
994
  exitWithError(
775
995
  `Failed to post comment: ${error instanceof Error ? error.message : String(error)}`
@@ -786,6 +1006,9 @@ function registerFeedCommentWriteCommand(parent) {
786
1006
  ).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
787
1007
  "--encode-only",
788
1008
  "Output transaction data as JSON instead of executing"
1009
+ ).option(
1010
+ "--json",
1011
+ "Output structured JSON (includes permalink and other URLs) after submission"
789
1012
  ).action(async (feed, postId, message, options) => {
790
1013
  await executeFeedCommentWrite(feed, postId, message, options);
791
1014
  });
@@ -812,17 +1035,25 @@ async function executeFeedCommentRead(feed, postId, options) {
812
1035
  `Feed "${normalizedFeed}" has no posts. Cannot find post ${postId}.`
813
1036
  );
814
1037
  }
815
- const posts = await client.getFeedPosts({
1038
+ const fetched = await client.getFeedPostsWithIndex({
816
1039
  topic: normalizedFeed,
817
1040
  maxPosts: 100
818
1041
  // Fetch enough to find the post
819
1042
  });
820
- const targetPost = findPostByParsedId(posts, parsedId);
1043
+ const matchIndex = fetched.messages.findIndex(
1044
+ (p) => p.sender.toLowerCase() === parsedId.sender.toLowerCase() && p.timestamp === parsedId.timestamp
1045
+ );
1046
+ const targetPost = matchIndex >= 0 ? fetched.messages[matchIndex] : void 0;
821
1047
  if (!targetPost) {
822
1048
  exitWithError(
823
1049
  `Post not found with ID ${postId} in feed "${normalizedFeed}". Make sure the sender and timestamp are correct.`
824
1050
  );
825
1051
  }
1052
+ const parentTopicIndex = fetched.startIndex + matchIndex;
1053
+ const parentPostUrl = postPermalink(readOnlyOptions.chainId, {
1054
+ topic: normalizedFeed,
1055
+ topicIndex: parentTopicIndex
1056
+ });
826
1057
  const commentCount = await client.getCommentCount(targetPost);
827
1058
  if (commentCount === 0) {
828
1059
  if (options.json) {
@@ -843,7 +1074,11 @@ async function executeFeedCommentRead(feed, postId, options) {
843
1074
  if (options.json) {
844
1075
  printJson(
845
1076
  commentsWithDepth.map(
846
- ({ comment, depth }) => commentToJson(comment, depth)
1077
+ ({ comment, depth }) => commentToJson(comment, {
1078
+ chainId: readOnlyOptions.chainId,
1079
+ depth,
1080
+ parentPostUrl
1081
+ })
847
1082
  )
848
1083
  );
849
1084
  } else {
@@ -984,8 +1219,10 @@ async function executeFeedReplies(options) {
984
1219
  rpcUrl: options.rpcUrl
985
1220
  });
986
1221
  const client = createFeedClient(readOnlyOptions);
987
- console.log(chalk12.blue(`Checking replies on ${postsWithIds.length} posts...
1222
+ if (!options.json) {
1223
+ console.log(chalk12.blue(`Checking replies on ${postsWithIds.length} posts...
988
1224
  `));
1225
+ }
989
1226
  const results = [];
990
1227
  for (const entry of postsWithIds) {
991
1228
  try {
@@ -1000,12 +1237,31 @@ async function executeFeedReplies(options) {
1000
1237
  data: "0x"
1001
1238
  };
1002
1239
  const commentCount = await client.getCommentCount(postObj);
1240
+ let permalink = null;
1241
+ try {
1242
+ const indices = await getMessageIndicesFromTx({
1243
+ chainId: readOnlyOptions.chainId,
1244
+ rpcUrl: readOnlyOptions.rpcUrl,
1245
+ txHash: entry.txHash
1246
+ });
1247
+ if (indices[0] !== void 0) {
1248
+ permalink = postPermalink(readOnlyOptions.chainId, {
1249
+ globalIndex: indices[0]
1250
+ });
1251
+ }
1252
+ } catch {
1253
+ }
1003
1254
  results.push({
1004
1255
  feed: entry.feed,
1005
1256
  postId: entry.postId,
1006
1257
  text: entry.text ?? "",
1007
1258
  postedAt: entry.timestamp,
1008
- commentCount: Number(commentCount)
1259
+ commentCount: Number(commentCount),
1260
+ permalink,
1261
+ feedUrl: feedUrl(readOnlyOptions.chainId, entry.feed),
1262
+ senderProfileUrl: entry.sender ? profileUrl(readOnlyOptions.chainId, entry.sender) : null,
1263
+ explorerTxUrl: explorerTxUrl(readOnlyOptions.chainId, entry.txHash),
1264
+ txHash: entry.txHash
1009
1265
  });
1010
1266
  } catch {
1011
1267
  }
@@ -1090,7 +1346,14 @@ async function executeFeedPosts(address, options) {
1090
1346
  endIndex: count
1091
1347
  });
1092
1348
  if (options.json) {
1093
- printJson(messages.map((msg, i) => postToJson(msg, i)));
1349
+ printJson(
1350
+ messages.map(
1351
+ (msg, i) => postToJson(msg, {
1352
+ chainId: readOnlyOptions.chainId,
1353
+ userIndex: startIndex + i
1354
+ })
1355
+ )
1356
+ );
1094
1357
  } else {
1095
1358
  console.log(
1096
1359
  chalk12.white(`Found ${messages.length} post(s) by ${address}:
@@ -1268,11 +1531,14 @@ function historyEntryToJson(entry, index) {
1268
1531
  type: entry.type,
1269
1532
  timestamp: entry.timestamp,
1270
1533
  txHash: entry.txHash,
1534
+ explorerTxUrl: explorerTxUrl(entry.chainId, entry.txHash),
1271
1535
  chainId: entry.chainId,
1272
- feed: entry.feed
1536
+ feed: entry.feed,
1537
+ feedUrl: feedUrl(entry.chainId, entry.feed)
1273
1538
  };
1274
1539
  if (entry.sender) {
1275
1540
  result.sender = entry.sender;
1541
+ result.senderProfileUrl = profileUrl(entry.chainId, entry.sender);
1276
1542
  }
1277
1543
  if (entry.text) {
1278
1544
  result.text = entry.text;
@@ -1446,7 +1712,9 @@ async function executeListAgents(options) {
1446
1712
  if (options.json) {
1447
1713
  printJson({
1448
1714
  totalCount,
1449
- agents: agents.map((agent, i) => agentToJson(agent, i))
1715
+ agents: agents.map(
1716
+ (agent, i) => agentToJson(agent, i, readOnlyOptions.chainId)
1717
+ )
1450
1718
  });
1451
1719
  } else {
1452
1720
  if (agents.length === 0) {
@@ -1505,9 +1773,13 @@ async function executeFeedVerifyClaim(txHash, options) {
1505
1773
  const netClient = createNetClient(readOnlyOptions);
1506
1774
  const existingHistory = getHistory();
1507
1775
  if (existingHistory.some((entry) => entry.txHash === txHash)) {
1508
- console.log(
1509
- chalk12.yellow("Transaction already recorded in history. Skipping.")
1510
- );
1776
+ if (options.json) {
1777
+ printJson({ alreadyRecorded: true, txHash });
1778
+ } else {
1779
+ console.log(
1780
+ chalk12.yellow("Transaction already recorded in history. Skipping.")
1781
+ );
1782
+ }
1511
1783
  return;
1512
1784
  }
1513
1785
  const receipt = await publicClient.getTransactionReceipt({ hash: txHash }).catch(
@@ -1542,11 +1814,13 @@ async function executeFeedVerifyClaim(txHash, options) {
1542
1814
  "Transaction does not contain any Net protocol messages."
1543
1815
  );
1544
1816
  }
1545
- console.log(
1546
- chalk12.blue(
1547
- `Found ${messageSentEvents.length} message(s) in transaction. Fetching details...`
1548
- )
1549
- );
1817
+ if (!options.json) {
1818
+ console.log(
1819
+ chalk12.blue(
1820
+ `Found ${messageSentEvents.length} message(s) in transaction. Fetching details...`
1821
+ )
1822
+ );
1823
+ }
1550
1824
  const messages = await Promise.all(
1551
1825
  messageSentEvents.map(
1552
1826
  (event) => netClient.getMessageAtIndex({
@@ -1554,41 +1828,40 @@ async function executeFeedVerifyClaim(txHash, options) {
1554
1828
  })
1555
1829
  )
1556
1830
  );
1831
+ const entries = [];
1557
1832
  let recorded = 0;
1558
1833
  for (let i = 0; i < messages.length; i++) {
1559
1834
  const message = messages[i];
1560
1835
  if (!message) {
1561
- console.log(
1562
- chalk12.yellow(
1563
- ` Could not fetch message at index ${messageSentEvents[i].messageIndex}. Skipping.`
1564
- )
1565
- );
1836
+ if (!options.json) {
1837
+ console.log(
1838
+ chalk12.yellow(
1839
+ ` Could not fetch message at index ${messageSentEvents[i].messageIndex}. Skipping.`
1840
+ )
1841
+ );
1842
+ }
1566
1843
  continue;
1567
1844
  }
1845
+ const globalIndex = Number(messageSentEvents[i].messageIndex);
1568
1846
  const feedName = extractFeedName(message.topic);
1569
1847
  const isComment = isCommentTopic(message.topic);
1570
1848
  let type;
1571
1849
  let postId;
1572
- let label;
1850
+ let parentPostId;
1851
+ let permalink = null;
1573
1852
  if (isComment) {
1574
1853
  type = "comment";
1575
1854
  const commentData = parseCommentData(message.data);
1576
- postId = commentData ? `${commentData.parentSender}:${commentData.parentTimestamp}` : void 0;
1577
- label = `Verified comment:
1578
- Feed: ${feedName}
1579
- Sender: ${message.sender}
1580
- Text: ${message.text}
1581
- Parent post: ${postId ?? "unknown"}
1582
- Tx: ${txHash}`;
1855
+ parentPostId = commentData ? `${commentData.parentSender}:${commentData.parentTimestamp}` : void 0;
1856
+ const commentParam = `${message.sender}-${Number(message.timestamp)}`;
1857
+ permalink = postPermalink(readOnlyOptions.chainId, {
1858
+ globalIndex,
1859
+ commentId: commentParam
1860
+ });
1583
1861
  } else {
1584
1862
  type = "post";
1585
1863
  postId = createPostId(message);
1586
- label = `Verified post:
1587
- Feed: ${feedName}
1588
- Sender: ${message.sender}
1589
- Text: ${message.text}
1590
- Post ID: ${postId}
1591
- Tx: ${txHash}`;
1864
+ permalink = postPermalink(readOnlyOptions.chainId, { globalIndex });
1592
1865
  }
1593
1866
  addHistoryEntry({
1594
1867
  type,
@@ -1597,11 +1870,49 @@ async function executeFeedVerifyClaim(txHash, options) {
1597
1870
  feed: feedName,
1598
1871
  sender: message.sender,
1599
1872
  text: message.text,
1600
- postId
1873
+ postId: type === "comment" ? parentPostId : postId
1601
1874
  });
1602
- console.log(chalk12.green(` ${label}`));
1875
+ const entry = {
1876
+ type,
1877
+ txHash,
1878
+ explorerTxUrl: explorerTxUrl(readOnlyOptions.chainId, txHash),
1879
+ globalIndex,
1880
+ permalink,
1881
+ feed: feedName,
1882
+ feedUrl: feedUrl(readOnlyOptions.chainId, feedName),
1883
+ sender: message.sender,
1884
+ senderProfileUrl: profileUrl(
1885
+ readOnlyOptions.chainId,
1886
+ message.sender
1887
+ ),
1888
+ text: message.text,
1889
+ timestamp: Number(message.timestamp)
1890
+ };
1891
+ if (type === "post") entry.postId = postId;
1892
+ if (type === "comment") entry.parentPostId = parentPostId;
1893
+ entries.push(entry);
1894
+ if (!options.json) {
1895
+ const label = type === "comment" ? `Verified comment:
1896
+ Feed: ${feedName}
1897
+ Sender: ${message.sender}
1898
+ Text: ${message.text}
1899
+ Parent post: ${parentPostId ?? "unknown"}
1900
+ Permalink: ${permalink ?? "(unavailable)"}
1901
+ Tx: ${txHash}` : `Verified post:
1902
+ Feed: ${feedName}
1903
+ Sender: ${message.sender}
1904
+ Text: ${message.text}
1905
+ Post ID: ${postId}
1906
+ Permalink: ${permalink ?? "(unavailable)"}
1907
+ Tx: ${txHash}`;
1908
+ console.log(chalk12.green(` ${label}`));
1909
+ }
1603
1910
  recorded++;
1604
1911
  }
1912
+ if (options.json) {
1913
+ printJson({ recorded, entries });
1914
+ return;
1915
+ }
1605
1916
  if (recorded > 0) {
1606
1917
  console.log(
1607
1918
  chalk12.green(`
@@ -1616,7 +1927,10 @@ function registerFeedVerifyClaimCommand(parent) {
1616
1927
  "--chain-id <id>",
1617
1928
  "Chain ID (default: 8453 for Base)",
1618
1929
  (value) => parseInt(value, 10)
1619
- ).option("--rpc-url <url>", "Custom RPC URL").action(async (txHash, options) => {
1930
+ ).option("--rpc-url <url>", "Custom RPC URL").option(
1931
+ "--json",
1932
+ "Output structured JSON (includes permalink and other URLs)"
1933
+ ).action(async (txHash, options) => {
1620
1934
  await executeFeedVerifyClaim(txHash, options);
1621
1935
  });
1622
1936
  }