@net-protocol/cli 0.1.13 → 0.1.14

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.
@@ -0,0 +1,1357 @@
1
+ import chalk12 from 'chalk';
2
+ import { FeedRegistryClient, FeedClient } from '@net-protocol/feeds';
3
+ import { NULL_ADDRESS, getChainRpcUrls, NetClient } from '@net-protocol/core';
4
+ import '@net-protocol/storage';
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ import { encodeFunctionData, createWalletClient, http } from 'viem';
9
+ import { privateKeyToAccount } from 'viem/accounts';
10
+ import * as readline from 'readline';
11
+
12
+ // src/commands/feed/list.ts
13
+ var DEFAULT_CHAIN_ID = 8453;
14
+ function getChainIdWithDefault(optionValue) {
15
+ if (optionValue) {
16
+ return optionValue;
17
+ }
18
+ const envChainId = process.env.BOTCHAN_CHAIN_ID || process.env.NET_CHAIN_ID;
19
+ if (envChainId) {
20
+ return parseInt(envChainId, 10);
21
+ }
22
+ return DEFAULT_CHAIN_ID;
23
+ }
24
+ function getRpcUrlWithBotchanFallback(optionValue) {
25
+ return optionValue || process.env.BOTCHAN_RPC_URL || process.env.NET_RPC_URL;
26
+ }
27
+ function parseReadOnlyOptionsWithDefault(options) {
28
+ return {
29
+ chainId: getChainIdWithDefault(options.chainId),
30
+ rpcUrl: getRpcUrlWithBotchanFallback(options.rpcUrl)
31
+ };
32
+ }
33
+ function parseCommonOptionsWithDefault(options, supportsEncodeOnly = false) {
34
+ const privateKey = options.privateKey || process.env.BOTCHAN_PRIVATE_KEY || process.env.NET_PRIVATE_KEY || process.env.PRIVATE_KEY;
35
+ if (!privateKey) {
36
+ const encodeOnlyHint = supportsEncodeOnly ? ", or use --encode-only to output transaction data without submitting" : "";
37
+ console.error(
38
+ chalk12.red(
39
+ `Error: Private key is required. Provide via --private-key flag or NET_PRIVATE_KEY/BOTCHAN_PRIVATE_KEY environment variable${encodeOnlyHint}`
40
+ )
41
+ );
42
+ process.exit(1);
43
+ }
44
+ if (!privateKey.startsWith("0x") || privateKey.length !== 66) {
45
+ console.error(
46
+ chalk12.red(
47
+ "Error: Invalid private key format (must be 0x-prefixed, 66 characters)"
48
+ )
49
+ );
50
+ process.exit(1);
51
+ }
52
+ if (options.privateKey) {
53
+ console.warn(
54
+ chalk12.yellow(
55
+ "Warning: Private key provided via command line. Consider using NET_PRIVATE_KEY environment variable instead."
56
+ )
57
+ );
58
+ }
59
+ return {
60
+ privateKey,
61
+ chainId: getChainIdWithDefault(options.chainId),
62
+ rpcUrl: getRpcUrlWithBotchanFallback(options.rpcUrl)
63
+ };
64
+ }
65
+ function createFeedClient(options) {
66
+ return new FeedClient({
67
+ chainId: options.chainId,
68
+ overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
69
+ });
70
+ }
71
+ function createFeedRegistryClient(options) {
72
+ return new FeedRegistryClient({
73
+ chainId: options.chainId,
74
+ overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
75
+ });
76
+ }
77
+ function createNetClient(options) {
78
+ return new NetClient({
79
+ chainId: options.chainId,
80
+ overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
81
+ });
82
+ }
83
+ function exitWithError(message) {
84
+ console.error(chalk12.red(`Error: ${message}`));
85
+ process.exit(1);
86
+ }
87
+ function truncateAddress(address) {
88
+ if (address.length <= 12) return address;
89
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
90
+ }
91
+ function formatTimestamp(timestamp) {
92
+ return new Date(Number(timestamp) * 1e3).toISOString().replace("T", " ").slice(0, 19);
93
+ }
94
+ function parseTopic(topic) {
95
+ const commentMatch = topic.match(/^(.+?):comments:/);
96
+ if (commentMatch) {
97
+ return { feedName: commentMatch[1], isComment: true };
98
+ }
99
+ return { feedName: topic, isComment: false };
100
+ }
101
+ function formatPost(post, index, options = {}) {
102
+ const { commentCount, showTopic } = options;
103
+ const timestamp = formatTimestamp(post.timestamp);
104
+ const lines = [
105
+ chalk12.cyan(`[${index}]`) + ` ${chalk12.gray(timestamp)}`,
106
+ ` ${chalk12.white("Sender:")} ${post.sender}`,
107
+ ` ${chalk12.white("Text:")} ${post.text}`
108
+ ];
109
+ if (showTopic && post.topic) {
110
+ const { feedName, isComment } = parseTopic(post.topic);
111
+ const prefix = isComment ? "Comment on" : "Feed";
112
+ lines.push(` ${chalk12.white(prefix + ":")} ${chalk12.magenta(feedName)}`);
113
+ }
114
+ if (commentCount !== void 0) {
115
+ lines.push(` ${chalk12.white("Comments:")} ${commentCount}`);
116
+ }
117
+ if (post.data && post.data !== "0x") {
118
+ lines.push(` ${chalk12.white("Data:")} ${post.data}`);
119
+ }
120
+ return lines.join("\n");
121
+ }
122
+ function formatFeed(feed, index) {
123
+ const timestamp = formatTimestamp(feed.timestamp);
124
+ const lines = [
125
+ chalk12.cyan(`[${index}]`) + ` ${chalk12.white(feed.feedName)}`,
126
+ ` ${chalk12.gray("Registrant:")} ${feed.registrant}`,
127
+ ` ${chalk12.gray("Registered:")} ${timestamp}`
128
+ ];
129
+ return lines.join("\n");
130
+ }
131
+ function formatComment(comment, depth) {
132
+ const indent = " ".repeat(depth + 1);
133
+ const timestamp = formatTimestamp(comment.timestamp);
134
+ const lines = [
135
+ `${indent}${chalk12.gray(timestamp)} ${chalk12.blue(truncateAddress(comment.sender))}`,
136
+ `${indent}${comment.text}`
137
+ ];
138
+ return lines.join("\n");
139
+ }
140
+ function postToJson(post, index, commentCount) {
141
+ const result = {
142
+ index,
143
+ sender: post.sender,
144
+ text: post.text,
145
+ timestamp: Number(post.timestamp),
146
+ topic: post.topic
147
+ };
148
+ if (commentCount !== void 0) {
149
+ result.commentCount = commentCount;
150
+ }
151
+ if (post.data && post.data !== "0x") {
152
+ result.data = post.data;
153
+ }
154
+ return result;
155
+ }
156
+ function feedToJson(feed, index) {
157
+ return {
158
+ index,
159
+ feedName: feed.feedName,
160
+ registrant: feed.registrant,
161
+ timestamp: feed.timestamp
162
+ };
163
+ }
164
+ function commentToJson(comment, depth) {
165
+ return {
166
+ sender: comment.sender,
167
+ text: comment.text,
168
+ timestamp: Number(comment.timestamp),
169
+ depth,
170
+ data: comment.data !== "0x" ? comment.data : void 0
171
+ };
172
+ }
173
+ function printJson(data) {
174
+ console.log(JSON.stringify(data, null, 2));
175
+ }
176
+
177
+ // src/commands/feed/list.ts
178
+ async function executeFeedList(options) {
179
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
180
+ chainId: options.chainId,
181
+ rpcUrl: options.rpcUrl
182
+ });
183
+ const client = createFeedRegistryClient(readOnlyOptions);
184
+ try {
185
+ const feeds = await client.getRegisteredFeeds({
186
+ maxFeeds: options.limit ?? 50
187
+ });
188
+ if (options.json) {
189
+ printJson(feeds.map((feed, i) => feedToJson(feed, i)));
190
+ } else {
191
+ if (feeds.length === 0) {
192
+ console.log(chalk12.yellow("No registered feeds found"));
193
+ return;
194
+ }
195
+ console.log(chalk12.white(`Found ${feeds.length} registered feed(s):
196
+ `));
197
+ feeds.forEach((feed, i) => {
198
+ console.log(formatFeed(feed, i));
199
+ if (i < feeds.length - 1) {
200
+ console.log();
201
+ }
202
+ });
203
+ }
204
+ } catch (error) {
205
+ exitWithError(
206
+ `Failed to fetch feeds: ${error instanceof Error ? error.message : String(error)}`
207
+ );
208
+ }
209
+ }
210
+ function registerFeedListCommand(parent, commandName = "list") {
211
+ parent.command(commandName).description("List registered feeds").option(
212
+ "--limit <n>",
213
+ "Maximum number of feeds to display",
214
+ (value) => parseInt(value, 10)
215
+ ).option(
216
+ "--chain-id <id>",
217
+ "Chain ID (default: 8453 for Base)",
218
+ (value) => parseInt(value, 10)
219
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--json", "Output in JSON format").action(async (options) => {
220
+ await executeFeedList(options);
221
+ });
222
+ }
223
+ var MAX_HISTORY_ENTRIES = 100;
224
+ var STATE_DIR = path.join(os.homedir(), ".botchan");
225
+ var STATE_FILE = path.join(STATE_DIR, "state.json");
226
+ function ensureStateDir() {
227
+ if (!fs.existsSync(STATE_DIR)) {
228
+ fs.mkdirSync(STATE_DIR, { recursive: true });
229
+ }
230
+ }
231
+ function loadState() {
232
+ try {
233
+ if (fs.existsSync(STATE_FILE)) {
234
+ const data = fs.readFileSync(STATE_FILE, "utf-8");
235
+ return JSON.parse(data);
236
+ }
237
+ } catch {
238
+ }
239
+ return { feeds: {} };
240
+ }
241
+ function saveState(state) {
242
+ ensureStateDir();
243
+ const tempFile = `${STATE_FILE}.tmp`;
244
+ fs.writeFileSync(tempFile, JSON.stringify(state, null, 2));
245
+ fs.renameSync(tempFile, STATE_FILE);
246
+ }
247
+ function getLastSeenTimestamp(feedName) {
248
+ const state = loadState();
249
+ return state.feeds[feedName]?.lastSeenTimestamp ?? null;
250
+ }
251
+ function setLastSeenTimestamp(feedName, timestamp) {
252
+ const state = loadState();
253
+ if (!state.feeds[feedName]) {
254
+ state.feeds[feedName] = { lastSeenTimestamp: timestamp };
255
+ } else {
256
+ state.feeds[feedName].lastSeenTimestamp = timestamp;
257
+ }
258
+ saveState(state);
259
+ }
260
+ function markFeedSeen(feedName, posts) {
261
+ if (posts.length === 0) return;
262
+ const maxTimestamp = posts.reduce(
263
+ (max, post) => post.timestamp > max ? post.timestamp : max,
264
+ posts[0].timestamp
265
+ );
266
+ setLastSeenTimestamp(feedName, Number(maxTimestamp));
267
+ }
268
+ function getMyAddress() {
269
+ const state = loadState();
270
+ return state.myAddress ?? null;
271
+ }
272
+ function setMyAddress(address) {
273
+ const state = loadState();
274
+ state.myAddress = address.toLowerCase();
275
+ saveState(state);
276
+ }
277
+ function clearMyAddress() {
278
+ const state = loadState();
279
+ delete state.myAddress;
280
+ saveState(state);
281
+ }
282
+ function getFullState() {
283
+ return loadState();
284
+ }
285
+ function resetState() {
286
+ if (fs.existsSync(STATE_FILE)) {
287
+ fs.unlinkSync(STATE_FILE);
288
+ }
289
+ }
290
+ function getStateFilePath() {
291
+ return STATE_FILE;
292
+ }
293
+ function addHistoryEntry(entry) {
294
+ const state = loadState();
295
+ const history = state.history ?? [];
296
+ const newEntry = {
297
+ ...entry,
298
+ timestamp: Math.floor(Date.now() / 1e3)
299
+ };
300
+ history.unshift(newEntry);
301
+ if (history.length > MAX_HISTORY_ENTRIES) {
302
+ history.length = MAX_HISTORY_ENTRIES;
303
+ }
304
+ state.history = history;
305
+ saveState(state);
306
+ }
307
+ function getHistory(limit) {
308
+ const state = loadState();
309
+ const history = state.history ?? [];
310
+ return limit ? history.slice(0, limit) : history;
311
+ }
312
+ function getHistoryByType(type, limit) {
313
+ const history = getHistory();
314
+ const filtered = history.filter((entry) => entry.type === type);
315
+ return limit ? filtered.slice(0, limit) : filtered;
316
+ }
317
+ function clearHistory() {
318
+ const state = loadState();
319
+ state.history = [];
320
+ saveState(state);
321
+ }
322
+ function getHistoryCount() {
323
+ const state = loadState();
324
+ return state.history?.length ?? 0;
325
+ }
326
+ function isWalletAddress(feed) {
327
+ return /^0x[a-fA-F0-9]{40}$/.test(feed);
328
+ }
329
+ function getContacts() {
330
+ const history = getHistory();
331
+ const contactMap = /* @__PURE__ */ new Map();
332
+ for (const entry of history) {
333
+ if (entry.type === "post" && isWalletAddress(entry.feed)) {
334
+ const address = entry.feed.toLowerCase();
335
+ const existing = contactMap.get(address);
336
+ if (existing) {
337
+ existing.interactionCount++;
338
+ if (entry.timestamp > existing.lastInteraction) {
339
+ existing.lastInteraction = entry.timestamp;
340
+ }
341
+ if (entry.timestamp < existing.firstInteraction) {
342
+ existing.firstInteraction = entry.timestamp;
343
+ }
344
+ } else {
345
+ contactMap.set(address, {
346
+ address,
347
+ lastInteraction: entry.timestamp,
348
+ firstInteraction: entry.timestamp,
349
+ interactionCount: 1
350
+ });
351
+ }
352
+ }
353
+ }
354
+ return Array.from(contactMap.values()).sort(
355
+ (a, b) => b.lastInteraction - a.lastInteraction
356
+ );
357
+ }
358
+ function getActiveFeeds() {
359
+ const history = getHistory();
360
+ const feedMap = /* @__PURE__ */ new Map();
361
+ for (const entry of history) {
362
+ if (isWalletAddress(entry.feed)) continue;
363
+ if (entry.type === "register") continue;
364
+ const feedName = entry.feed.toLowerCase();
365
+ const existing = feedMap.get(feedName);
366
+ if (existing) {
367
+ if (entry.type === "post") existing.postCount++;
368
+ if (entry.type === "comment") existing.commentCount++;
369
+ if (entry.timestamp > existing.lastActivity) {
370
+ existing.lastActivity = entry.timestamp;
371
+ }
372
+ if (entry.timestamp < existing.firstActivity) {
373
+ existing.firstActivity = entry.timestamp;
374
+ }
375
+ } else {
376
+ feedMap.set(feedName, {
377
+ feed: feedName,
378
+ postCount: entry.type === "post" ? 1 : 0,
379
+ commentCount: entry.type === "comment" ? 1 : 0,
380
+ lastActivity: entry.timestamp,
381
+ firstActivity: entry.timestamp
382
+ });
383
+ }
384
+ }
385
+ return Array.from(feedMap.values()).sort(
386
+ (a, b) => b.lastActivity - a.lastActivity
387
+ );
388
+ }
389
+
390
+ // src/commands/feed/types.ts
391
+ function normalizeFeedName(feed) {
392
+ return feed.toLowerCase();
393
+ }
394
+
395
+ // src/commands/feed/read.ts
396
+ async function executeFeedRead(feed, options) {
397
+ const normalizedFeed = normalizeFeedName(feed);
398
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
399
+ chainId: options.chainId,
400
+ rpcUrl: options.rpcUrl
401
+ });
402
+ const client = createFeedClient(readOnlyOptions);
403
+ const limit = options.limit ?? 20;
404
+ try {
405
+ const count = await client.getFeedPostCount(normalizedFeed);
406
+ if (count === 0) {
407
+ if (options.json) {
408
+ printJson([]);
409
+ } else {
410
+ console.log(chalk12.yellow(`No posts found in feed "${normalizedFeed}"`));
411
+ }
412
+ return;
413
+ }
414
+ const fetchLimit = options.sender ? Math.max(limit * 5, 100) : limit;
415
+ let posts = await client.getFeedPosts({
416
+ topic: normalizedFeed,
417
+ maxPosts: fetchLimit
418
+ });
419
+ if (options.sender) {
420
+ const senderLower = options.sender.toLowerCase();
421
+ posts = posts.filter(
422
+ (post) => post.sender.toLowerCase() === senderLower
423
+ );
424
+ posts = posts.slice(0, limit);
425
+ }
426
+ if (options.unseen) {
427
+ const lastSeen = getLastSeenTimestamp(normalizedFeed);
428
+ const myAddress = getMyAddress();
429
+ posts = posts.filter((post) => {
430
+ const isNew = lastSeen === null || Number(post.timestamp) > lastSeen;
431
+ const isFromOther = !myAddress || post.sender.toLowerCase() !== myAddress;
432
+ return isNew && isFromOther;
433
+ });
434
+ }
435
+ if (options.markSeen) {
436
+ const allPosts = await client.getFeedPosts({
437
+ topic: normalizedFeed,
438
+ maxPosts: 1
439
+ // Just need the latest
440
+ });
441
+ if (allPosts.length > 0) {
442
+ markFeedSeen(normalizedFeed, allPosts);
443
+ }
444
+ }
445
+ const commentCounts = await Promise.all(
446
+ posts.map((post) => client.getCommentCount(post))
447
+ );
448
+ if (options.json) {
449
+ printJson(
450
+ posts.map((post, i) => postToJson(post, i, commentCounts[i]))
451
+ );
452
+ } else {
453
+ if (posts.length === 0) {
454
+ const senderNote2 = options.sender ? ` by ${options.sender}` : "";
455
+ console.log(chalk12.yellow(`No posts found in feed "${normalizedFeed}"${senderNote2}`));
456
+ return;
457
+ }
458
+ const senderNote = options.sender ? ` by ${options.sender}` : "";
459
+ console.log(
460
+ chalk12.white(`Found ${posts.length} post(s) in feed "${normalizedFeed}"${senderNote}:
461
+ `)
462
+ );
463
+ posts.forEach((post, i) => {
464
+ console.log(formatPost(post, i, { commentCount: commentCounts[i] }));
465
+ if (i < posts.length - 1) {
466
+ console.log();
467
+ }
468
+ });
469
+ }
470
+ } catch (error) {
471
+ exitWithError(
472
+ `Failed to read feed: ${error instanceof Error ? error.message : String(error)}`
473
+ );
474
+ }
475
+ }
476
+ function registerFeedReadCommand(parent) {
477
+ parent.command("read <feed>").description("Read posts from a feed").option(
478
+ "--limit <n>",
479
+ "Maximum number of posts to display",
480
+ (value) => parseInt(value, 10)
481
+ ).option(
482
+ "--chain-id <id>",
483
+ "Chain ID (default: 8453 for Base)",
484
+ (value) => parseInt(value, 10)
485
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--sender <address>", "Filter posts by sender address").option("--unseen", "Only show posts not yet seen (newer than last --mark-seen)").option("--mark-seen", "Mark the feed as seen up to the latest post").option("--json", "Output in JSON format").action(async (feed, options) => {
486
+ await executeFeedRead(feed, options);
487
+ });
488
+ }
489
+ function createWallet(privateKey, chainId, rpcUrl) {
490
+ const account = privateKeyToAccount(privateKey);
491
+ const rpcUrls = getChainRpcUrls({
492
+ chainId,
493
+ rpcUrl
494
+ });
495
+ return createWalletClient({
496
+ account,
497
+ transport: http(rpcUrls[0])
498
+ });
499
+ }
500
+ async function executeTransaction(walletClient, txConfig) {
501
+ const hash = await walletClient.writeContract({
502
+ address: txConfig.to,
503
+ abi: txConfig.abi,
504
+ functionName: txConfig.functionName,
505
+ args: txConfig.args,
506
+ value: txConfig.value,
507
+ chain: null
508
+ });
509
+ return hash;
510
+ }
511
+ function encodeTransaction(config, chainId) {
512
+ const calldata = encodeFunctionData({
513
+ abi: config.abi,
514
+ functionName: config.functionName,
515
+ args: config.args
516
+ });
517
+ return {
518
+ to: config.to,
519
+ data: calldata,
520
+ chainId,
521
+ value: config.value?.toString() ?? "0"
522
+ };
523
+ }
524
+
525
+ // src/commands/feed/post.ts
526
+ var MAX_MESSAGE_LENGTH = 4e3;
527
+ async function executeFeedPost(feed, message, options) {
528
+ const normalizedFeed = normalizeFeedName(feed);
529
+ if (message.length === 0) {
530
+ exitWithError("Message cannot be empty");
531
+ }
532
+ const fullMessage = options.body ? `${message}
533
+
534
+ ${options.body}` : message;
535
+ if (fullMessage.length > MAX_MESSAGE_LENGTH) {
536
+ exitWithError(
537
+ `Message too long (${fullMessage.length} chars). Maximum is ${MAX_MESSAGE_LENGTH} characters.`
538
+ );
539
+ }
540
+ if (options.encodeOnly) {
541
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
542
+ chainId: options.chainId,
543
+ rpcUrl: options.rpcUrl
544
+ });
545
+ const client2 = createFeedClient(readOnlyOptions);
546
+ const txConfig2 = client2.preparePostToFeed({
547
+ topic: normalizedFeed,
548
+ text: fullMessage,
549
+ data: options.data
550
+ });
551
+ const encoded = encodeTransaction(txConfig2, readOnlyOptions.chainId);
552
+ printJson(encoded);
553
+ return;
554
+ }
555
+ const commonOptions = parseCommonOptionsWithDefault(
556
+ {
557
+ privateKey: options.privateKey,
558
+ chainId: options.chainId,
559
+ rpcUrl: options.rpcUrl
560
+ },
561
+ true
562
+ // supports --encode-only
563
+ );
564
+ const client = createFeedClient(commonOptions);
565
+ const txConfig = client.preparePostToFeed({
566
+ topic: normalizedFeed,
567
+ text: fullMessage,
568
+ data: options.data
569
+ });
570
+ const walletClient = createWallet(
571
+ commonOptions.privateKey,
572
+ commonOptions.chainId,
573
+ commonOptions.rpcUrl
574
+ );
575
+ console.log(chalk12.blue(`Posting to feed "${normalizedFeed}"...`));
576
+ try {
577
+ const hash = await executeTransaction(walletClient, txConfig);
578
+ const senderAddress = walletClient.account.address;
579
+ let postId;
580
+ try {
581
+ const posts = await client.getFeedPosts({
582
+ topic: normalizedFeed,
583
+ maxPosts: 10
584
+ });
585
+ const ourPost = posts.find(
586
+ (p) => p.sender.toLowerCase() === senderAddress.toLowerCase() && p.text === fullMessage
587
+ );
588
+ if (ourPost) {
589
+ postId = `${ourPost.sender}:${ourPost.timestamp}`;
590
+ }
591
+ } catch {
592
+ }
593
+ addHistoryEntry({
594
+ type: "post",
595
+ txHash: hash,
596
+ chainId: commonOptions.chainId,
597
+ feed: normalizedFeed,
598
+ sender: senderAddress,
599
+ text: fullMessage,
600
+ postId
601
+ // Now we have the actual post ID for checking comments later
602
+ });
603
+ const displayText = options.body ? `${message} (+ body)` : message;
604
+ const postIdInfo = postId ? `
605
+ Post ID: ${postId}` : "";
606
+ console.log(
607
+ chalk12.green(
608
+ `Message posted successfully!
609
+ Transaction: ${hash}
610
+ Feed: ${normalizedFeed}${postIdInfo}
611
+ Text: ${displayText}`
612
+ )
613
+ );
614
+ } catch (error) {
615
+ exitWithError(
616
+ `Failed to post message: ${error instanceof Error ? error.message : String(error)}`
617
+ );
618
+ }
619
+ }
620
+ function registerFeedPostCommand(parent) {
621
+ parent.command("post <feed> <message>").description("Post a message to a feed").option(
622
+ "--chain-id <id>",
623
+ "Chain ID (default: 8453 for Base)",
624
+ (value) => parseInt(value, 10)
625
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
626
+ "--encode-only",
627
+ "Output transaction data as JSON instead of executing"
628
+ ).option("--data <data>", "Optional data to attach to the post").option("--body <text>", "Post body (message becomes the title)").action(async (feed, message, options) => {
629
+ await executeFeedPost(feed, message, options);
630
+ });
631
+ }
632
+
633
+ // src/shared/postId.ts
634
+ function parsePostId(postId) {
635
+ const parts = postId.split(":");
636
+ if (parts.length !== 2) {
637
+ throw new Error(
638
+ `Invalid post ID format. Expected {sender}:{timestamp}, got: ${postId}`
639
+ );
640
+ }
641
+ const [sender, timestampStr] = parts;
642
+ if (!sender.startsWith("0x") || sender.length !== 42) {
643
+ throw new Error(`Invalid sender address in post ID: ${sender}`);
644
+ }
645
+ const timestamp = BigInt(timestampStr);
646
+ if (timestamp <= 0) {
647
+ throw new Error(`Invalid timestamp in post ID: ${timestampStr}`);
648
+ }
649
+ return {
650
+ sender,
651
+ timestamp
652
+ };
653
+ }
654
+ function findPostByParsedId(posts, parsedId) {
655
+ return posts.find(
656
+ (p) => p.sender.toLowerCase() === parsedId.sender.toLowerCase() && p.timestamp === parsedId.timestamp
657
+ );
658
+ }
659
+
660
+ // src/commands/feed/comment-write.ts
661
+ var MAX_MESSAGE_LENGTH2 = 4e3;
662
+ async function executeFeedCommentWrite(feed, postId, message, options) {
663
+ const normalizedFeed = normalizeFeedName(feed);
664
+ if (message.length === 0) {
665
+ exitWithError("Comment cannot be empty");
666
+ }
667
+ if (message.length > MAX_MESSAGE_LENGTH2) {
668
+ exitWithError(
669
+ `Comment too long (${message.length} chars). Maximum is ${MAX_MESSAGE_LENGTH2} characters.`
670
+ );
671
+ }
672
+ let parsedId;
673
+ try {
674
+ parsedId = parsePostId(postId);
675
+ } catch (error) {
676
+ exitWithError(
677
+ error instanceof Error ? error.message : "Invalid post ID format"
678
+ );
679
+ }
680
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
681
+ chainId: options.chainId,
682
+ rpcUrl: options.rpcUrl
683
+ });
684
+ const client = createFeedClient(readOnlyOptions);
685
+ const count = await client.getFeedPostCount(normalizedFeed);
686
+ if (count === 0) {
687
+ exitWithError(
688
+ `Feed "${normalizedFeed}" has no posts. Cannot find post ${postId}.`
689
+ );
690
+ }
691
+ const posts = await client.getFeedPosts({
692
+ topic: normalizedFeed,
693
+ maxPosts: 100
694
+ // Fetch enough to find the post
695
+ });
696
+ const targetPost = findPostByParsedId(posts, parsedId);
697
+ if (!targetPost) {
698
+ exitWithError(
699
+ `Post not found with ID ${postId} in feed "${normalizedFeed}". Make sure the sender and timestamp are correct.`
700
+ );
701
+ }
702
+ const txConfig = client.prepareComment({
703
+ post: targetPost,
704
+ text: message
705
+ });
706
+ if (options.encodeOnly) {
707
+ const encoded = encodeTransaction(txConfig, readOnlyOptions.chainId);
708
+ printJson(encoded);
709
+ return;
710
+ }
711
+ const commonOptions = parseCommonOptionsWithDefault(
712
+ {
713
+ privateKey: options.privateKey,
714
+ chainId: options.chainId,
715
+ rpcUrl: options.rpcUrl
716
+ },
717
+ true
718
+ // supports --encode-only
719
+ );
720
+ const walletClient = createWallet(
721
+ commonOptions.privateKey,
722
+ commonOptions.chainId,
723
+ commonOptions.rpcUrl
724
+ );
725
+ console.log(chalk12.blue(`Commenting on post ${postId}...`));
726
+ try {
727
+ const hash = await executeTransaction(walletClient, txConfig);
728
+ addHistoryEntry({
729
+ type: "comment",
730
+ txHash: hash,
731
+ chainId: commonOptions.chainId,
732
+ feed: normalizedFeed,
733
+ sender: walletClient.account.address,
734
+ text: message,
735
+ postId
736
+ });
737
+ console.log(
738
+ chalk12.green(
739
+ `Comment posted successfully!
740
+ Transaction: ${hash}
741
+ Post: ${postId}
742
+ Comment: ${message}`
743
+ )
744
+ );
745
+ } catch (error) {
746
+ exitWithError(
747
+ `Failed to post comment: ${error instanceof Error ? error.message : String(error)}`
748
+ );
749
+ }
750
+ }
751
+ function registerFeedCommentWriteCommand(parent) {
752
+ parent.command("comment <feed> <post-id> <message>").description(
753
+ "Comment on a post. Post ID format: {sender}:{timestamp}"
754
+ ).option(
755
+ "--chain-id <id>",
756
+ "Chain ID (default: 8453 for Base)",
757
+ (value) => parseInt(value, 10)
758
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
759
+ "--encode-only",
760
+ "Output transaction data as JSON instead of executing"
761
+ ).action(async (feed, postId, message, options) => {
762
+ await executeFeedCommentWrite(feed, postId, message, options);
763
+ });
764
+ }
765
+ async function executeFeedCommentRead(feed, postId, options) {
766
+ const normalizedFeed = normalizeFeedName(feed);
767
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
768
+ chainId: options.chainId,
769
+ rpcUrl: options.rpcUrl
770
+ });
771
+ const client = createFeedClient(readOnlyOptions);
772
+ let parsedId;
773
+ try {
774
+ parsedId = parsePostId(postId);
775
+ } catch (error) {
776
+ exitWithError(
777
+ error instanceof Error ? error.message : "Invalid post ID format"
778
+ );
779
+ }
780
+ try {
781
+ const count = await client.getFeedPostCount(normalizedFeed);
782
+ if (count === 0) {
783
+ exitWithError(
784
+ `Feed "${normalizedFeed}" has no posts. Cannot find post ${postId}.`
785
+ );
786
+ }
787
+ const posts = await client.getFeedPosts({
788
+ topic: normalizedFeed,
789
+ maxPosts: 100
790
+ // Fetch enough to find the post
791
+ });
792
+ const targetPost = findPostByParsedId(posts, parsedId);
793
+ if (!targetPost) {
794
+ exitWithError(
795
+ `Post not found with ID ${postId} in feed "${normalizedFeed}". Make sure the sender and timestamp are correct.`
796
+ );
797
+ }
798
+ const commentCount = await client.getCommentCount(targetPost);
799
+ if (commentCount === 0) {
800
+ if (options.json) {
801
+ printJson([]);
802
+ } else {
803
+ console.log(chalk12.yellow(`No comments found for post ${postId}`));
804
+ }
805
+ return;
806
+ }
807
+ const comments = await client.getComments({
808
+ post: targetPost,
809
+ maxComments: options.limit ?? 50
810
+ });
811
+ const commentsWithDepth = comments.map((comment) => ({
812
+ comment,
813
+ depth: 0
814
+ }));
815
+ if (options.json) {
816
+ printJson(
817
+ commentsWithDepth.map(
818
+ ({ comment, depth }) => commentToJson(comment, depth)
819
+ )
820
+ );
821
+ } else {
822
+ if (comments.length === 0) {
823
+ console.log(chalk12.yellow(`No comments found for post ${postId}`));
824
+ return;
825
+ }
826
+ console.log(
827
+ chalk12.white(`Found ${comments.length} comment(s) for post ${postId}:
828
+ `)
829
+ );
830
+ commentsWithDepth.forEach(({ comment, depth }, i) => {
831
+ console.log(formatComment(comment, depth));
832
+ if (i < commentsWithDepth.length - 1) {
833
+ console.log();
834
+ }
835
+ });
836
+ }
837
+ } catch (error) {
838
+ if (error.message?.includes("Post not found")) {
839
+ throw error;
840
+ }
841
+ exitWithError(
842
+ `Failed to fetch comments: ${error instanceof Error ? error.message : String(error)}`
843
+ );
844
+ }
845
+ }
846
+ function registerFeedCommentReadCommand(parent) {
847
+ parent.command("comments <feed> <post-id>").description(
848
+ "Read comments on a post. Post ID format: {sender}:{timestamp}"
849
+ ).option(
850
+ "--limit <n>",
851
+ "Maximum number of comments to display",
852
+ (value) => parseInt(value, 10)
853
+ ).option(
854
+ "--chain-id <id>",
855
+ "Chain ID (default: 8453 for Base)",
856
+ (value) => parseInt(value, 10)
857
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--json", "Output in JSON format").action(async (feed, postId, options) => {
858
+ await executeFeedCommentRead(feed, postId, options);
859
+ });
860
+ }
861
+ async function executeFeedRegister(feedName, options) {
862
+ if (feedName.length > 64) {
863
+ exitWithError("Feed name cannot exceed 64 characters");
864
+ }
865
+ if (feedName.length === 0) {
866
+ exitWithError("Feed name cannot be empty");
867
+ }
868
+ if (options.encodeOnly) {
869
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
870
+ chainId: options.chainId,
871
+ rpcUrl: options.rpcUrl
872
+ });
873
+ const client2 = createFeedRegistryClient(readOnlyOptions);
874
+ const txConfig2 = client2.prepareRegisterFeed({ feedName });
875
+ const encoded = encodeTransaction(txConfig2, readOnlyOptions.chainId);
876
+ printJson(encoded);
877
+ return;
878
+ }
879
+ const commonOptions = parseCommonOptionsWithDefault(
880
+ {
881
+ privateKey: options.privateKey,
882
+ chainId: options.chainId,
883
+ rpcUrl: options.rpcUrl
884
+ },
885
+ true
886
+ // supports --encode-only
887
+ );
888
+ const client = createFeedRegistryClient(commonOptions);
889
+ const isRegistered = await client.isFeedRegistered(feedName);
890
+ if (isRegistered) {
891
+ exitWithError(`Feed "${feedName}" is already registered`);
892
+ }
893
+ const txConfig = client.prepareRegisterFeed({ feedName });
894
+ const walletClient = createWallet(
895
+ commonOptions.privateKey,
896
+ commonOptions.chainId,
897
+ commonOptions.rpcUrl
898
+ );
899
+ console.log(chalk12.blue(`Registering feed "${feedName}"...`));
900
+ try {
901
+ const hash = await executeTransaction(walletClient, txConfig);
902
+ addHistoryEntry({
903
+ type: "register",
904
+ txHash: hash,
905
+ chainId: commonOptions.chainId,
906
+ feed: feedName,
907
+ sender: walletClient.account.address
908
+ });
909
+ console.log(
910
+ chalk12.green(
911
+ `Feed registered successfully!
912
+ Transaction: ${hash}
913
+ Feed: ${feedName}`
914
+ )
915
+ );
916
+ } catch (error) {
917
+ exitWithError(
918
+ `Failed to register feed: ${error instanceof Error ? error.message : String(error)}`
919
+ );
920
+ }
921
+ }
922
+ function registerFeedRegisterCommand(parent) {
923
+ parent.command("register <feed-name>").description("Register a new feed").option(
924
+ "--chain-id <id>",
925
+ "Chain ID (default: 8453 for Base)",
926
+ (value) => parseInt(value, 10)
927
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
928
+ "--encode-only",
929
+ "Output transaction data as JSON instead of executing"
930
+ ).action(async (feedName, options) => {
931
+ await executeFeedRegister(feedName, options);
932
+ });
933
+ }
934
+ function truncateText(text, maxLen) {
935
+ if (text.length <= maxLen) return text;
936
+ return text.slice(0, maxLen) + "...";
937
+ }
938
+ async function executeFeedReplies(options) {
939
+ const postHistory = getHistoryByType("post", options.limit ?? 10);
940
+ const postsWithIds = postHistory.filter(
941
+ (entry) => !!entry.postId
942
+ );
943
+ if (postsWithIds.length === 0) {
944
+ if (options.json) {
945
+ printJson([]);
946
+ } else {
947
+ console.log(chalk12.gray("No posts with trackable IDs found in history."));
948
+ console.log(
949
+ chalk12.gray("Post IDs are captured when you post with a wallet.")
950
+ );
951
+ }
952
+ return;
953
+ }
954
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
955
+ chainId: options.chainId,
956
+ rpcUrl: options.rpcUrl
957
+ });
958
+ const client = createFeedClient(readOnlyOptions);
959
+ console.log(chalk12.blue(`Checking replies on ${postsWithIds.length} posts...
960
+ `));
961
+ const results = [];
962
+ for (const entry of postsWithIds) {
963
+ try {
964
+ const [sender, timestampStr] = entry.postId.split(":");
965
+ const timestamp = BigInt(timestampStr);
966
+ const postObj = {
967
+ sender,
968
+ timestamp,
969
+ text: entry.text ?? "",
970
+ topic: entry.feed,
971
+ app: "",
972
+ data: "0x"
973
+ };
974
+ const commentCount = await client.getCommentCount(postObj);
975
+ results.push({
976
+ feed: entry.feed,
977
+ postId: entry.postId,
978
+ text: entry.text ?? "",
979
+ postedAt: entry.timestamp,
980
+ commentCount: Number(commentCount)
981
+ });
982
+ } catch {
983
+ }
984
+ }
985
+ if (options.json) {
986
+ printJson(results);
987
+ return;
988
+ }
989
+ const postsWithReplies = results.filter((r) => r.commentCount > 0);
990
+ const totalReplies = results.reduce((sum, r) => sum + r.commentCount, 0);
991
+ console.log(
992
+ chalk12.cyan(
993
+ `Found ${totalReplies} total replies across ${postsWithReplies.length} posts
994
+ `
995
+ )
996
+ );
997
+ if (results.length === 0) {
998
+ console.log(chalk12.gray("Could not check any posts."));
999
+ return;
1000
+ }
1001
+ results.sort((a, b) => b.commentCount - a.commentCount);
1002
+ for (const post of results) {
1003
+ const timeAgo = formatTimestamp(post.postedAt);
1004
+ const replyText = post.commentCount === 0 ? chalk12.gray("no replies") : post.commentCount === 1 ? chalk12.green("1 reply") : chalk12.green(`${post.commentCount} replies`);
1005
+ console.log(
1006
+ `${chalk12.white(post.feed)} ${chalk12.gray("\u2022")} ${replyText} ${chalk12.gray(`\u2022 ${timeAgo}`)}`
1007
+ );
1008
+ console.log(` ${chalk12.gray(truncateText(post.text, 60))}`);
1009
+ if (post.commentCount > 0) {
1010
+ console.log(
1011
+ chalk12.blue(` \u2192 netp feed comments ${post.feed} ${post.postId}`)
1012
+ );
1013
+ }
1014
+ console.log("");
1015
+ }
1016
+ }
1017
+ function registerFeedRepliesCommand(parent) {
1018
+ parent.command("replies").description("Check for replies on your recent posts").option(
1019
+ "--chain-id <id>",
1020
+ "Chain ID (default: 8453 for Base)",
1021
+ (value) => parseInt(value, 10)
1022
+ ).option("--rpc-url <url>", "Custom RPC URL").option(
1023
+ "--limit <n>",
1024
+ "Number of recent posts to check (default: 10)",
1025
+ (value) => parseInt(value, 10)
1026
+ ).option("--json", "Output as JSON").action(async (options) => {
1027
+ await executeFeedReplies(options);
1028
+ });
1029
+ }
1030
+ async function executeFeedPosts(address, options) {
1031
+ if (!address.startsWith("0x") || address.length !== 42) {
1032
+ exitWithError("Invalid address format. Must be 0x-prefixed, 42 characters");
1033
+ }
1034
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
1035
+ chainId: options.chainId,
1036
+ rpcUrl: options.rpcUrl
1037
+ });
1038
+ const client = createNetClient(readOnlyOptions);
1039
+ const limit = options.limit ?? 20;
1040
+ try {
1041
+ const count = await client.getMessageCount({
1042
+ filter: {
1043
+ appAddress: NULL_ADDRESS,
1044
+ maker: address
1045
+ }
1046
+ });
1047
+ if (count === 0) {
1048
+ if (options.json) {
1049
+ printJson([]);
1050
+ } else {
1051
+ console.log(chalk12.yellow(`No posts found for address ${address}`));
1052
+ }
1053
+ return;
1054
+ }
1055
+ const startIndex = count > limit ? count - limit : 0;
1056
+ const messages = await client.getMessages({
1057
+ filter: {
1058
+ appAddress: NULL_ADDRESS,
1059
+ maker: address
1060
+ },
1061
+ startIndex,
1062
+ endIndex: count
1063
+ });
1064
+ if (options.json) {
1065
+ printJson(messages.map((msg, i) => postToJson(msg, i)));
1066
+ } else {
1067
+ console.log(
1068
+ chalk12.white(`Found ${messages.length} post(s) by ${address}:
1069
+ `)
1070
+ );
1071
+ messages.forEach((msg, i) => {
1072
+ console.log(formatPost(msg, i, { showTopic: true }));
1073
+ if (i < messages.length - 1) {
1074
+ console.log();
1075
+ }
1076
+ });
1077
+ }
1078
+ } catch (error) {
1079
+ exitWithError(
1080
+ `Failed to fetch posts: ${error instanceof Error ? error.message : String(error)}`
1081
+ );
1082
+ }
1083
+ }
1084
+ function registerFeedPostsCommand(parent) {
1085
+ parent.command("posts <address>").description("View posts by an address").option(
1086
+ "--limit <n>",
1087
+ "Maximum number of posts to display",
1088
+ (value) => parseInt(value, 10)
1089
+ ).option(
1090
+ "--chain-id <id>",
1091
+ "Chain ID (default: 8453 for Base)",
1092
+ (value) => parseInt(value, 10)
1093
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--json", "Output in JSON format").action(async (address, options) => {
1094
+ await executeFeedPosts(address, options);
1095
+ });
1096
+ }
1097
+ async function confirm(message) {
1098
+ const rl = readline.createInterface({
1099
+ input: process.stdin,
1100
+ output: process.stdout
1101
+ });
1102
+ return new Promise((resolve) => {
1103
+ rl.question(`${message} (y/N): `, (answer) => {
1104
+ rl.close();
1105
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
1106
+ });
1107
+ });
1108
+ }
1109
+
1110
+ // src/commands/feed/config.ts
1111
+ async function executeFeedConfig(options) {
1112
+ if (options.reset) {
1113
+ const statePath = getStateFilePath();
1114
+ console.log(chalk12.yellow(`This will delete all stored state at:`));
1115
+ console.log(chalk12.white(` ${statePath}`));
1116
+ console.log(chalk12.yellow(`
1117
+ This includes:`));
1118
+ console.log(chalk12.white(` - All "last seen" timestamps for feeds`));
1119
+ console.log(chalk12.white(` - Your configured address`));
1120
+ console.log(chalk12.white(` - Your activity history`));
1121
+ if (!options.force) {
1122
+ const confirmed = await confirm(chalk12.red("\nAre you sure you want to reset?"));
1123
+ if (!confirmed) {
1124
+ console.log(chalk12.gray("Cancelled."));
1125
+ return;
1126
+ }
1127
+ }
1128
+ resetState();
1129
+ console.log(chalk12.green("State reset successfully."));
1130
+ return;
1131
+ }
1132
+ if (options.myAddress) {
1133
+ if (!options.myAddress.match(/^0x[a-fA-F0-9]{40}$/)) {
1134
+ console.error(chalk12.red("Invalid address format. Expected 0x followed by 40 hex characters."));
1135
+ process.exit(1);
1136
+ }
1137
+ setMyAddress(options.myAddress);
1138
+ console.log(chalk12.green(`Set my address to: ${options.myAddress}`));
1139
+ return;
1140
+ }
1141
+ if (options.clearAddress) {
1142
+ clearMyAddress();
1143
+ console.log(chalk12.green("Cleared my address."));
1144
+ return;
1145
+ }
1146
+ const state = getFullState();
1147
+ const myAddress = getMyAddress();
1148
+ console.log(chalk12.cyan("Feed Configuration\n"));
1149
+ console.log(chalk12.white(`State file: ${getStateFilePath()}`));
1150
+ console.log(chalk12.white(`My address: ${myAddress ?? chalk12.gray("(not set)")}`));
1151
+ const feedCount = Object.keys(state.feeds).length;
1152
+ console.log(chalk12.white(`Tracked feeds: ${feedCount}`));
1153
+ const historyCount = getHistoryCount();
1154
+ console.log(chalk12.white(`History entries: ${historyCount}`));
1155
+ if (feedCount > 0 && feedCount <= 20) {
1156
+ console.log(chalk12.gray("\nLast seen timestamps:"));
1157
+ for (const [feed, data] of Object.entries(state.feeds)) {
1158
+ const date = new Date(data.lastSeenTimestamp * 1e3);
1159
+ console.log(chalk12.gray(` ${feed}: ${date.toLocaleString()}`));
1160
+ }
1161
+ } else if (feedCount > 20) {
1162
+ console.log(chalk12.gray(`
1163
+ (${feedCount} feeds tracked, use --json for full list)`));
1164
+ }
1165
+ const activeFeeds = getActiveFeeds();
1166
+ if (activeFeeds.length > 0) {
1167
+ console.log(chalk12.cyan("\nActive Feeds:"));
1168
+ const displayFeeds = activeFeeds.slice(0, 10);
1169
+ for (const feed of displayFeeds) {
1170
+ const activity = [];
1171
+ if (feed.postCount > 0) activity.push(`${feed.postCount} post${feed.postCount !== 1 ? "s" : ""}`);
1172
+ if (feed.commentCount > 0) activity.push(`${feed.commentCount} comment${feed.commentCount !== 1 ? "s" : ""}`);
1173
+ const lastActive = formatTimestamp(feed.lastActivity);
1174
+ console.log(chalk12.white(` ${feed.feed}`) + chalk12.gray(` \u2022 ${activity.join(", ")} \u2022 ${lastActive}`));
1175
+ }
1176
+ if (activeFeeds.length > 10) {
1177
+ console.log(chalk12.gray(` ... and ${activeFeeds.length - 10} more`));
1178
+ }
1179
+ }
1180
+ const contacts = getContacts();
1181
+ if (contacts.length > 0) {
1182
+ console.log(chalk12.cyan("\nRecent Contacts (DMs):"));
1183
+ const displayContacts = contacts.slice(0, 10);
1184
+ for (const contact of displayContacts) {
1185
+ const truncAddr = `${contact.address.slice(0, 6)}...${contact.address.slice(-4)}`;
1186
+ const msgCount = contact.interactionCount;
1187
+ const lastActive = formatTimestamp(contact.lastInteraction);
1188
+ console.log(
1189
+ chalk12.white(` ${truncAddr}`) + chalk12.gray(` \u2022 ${msgCount} message${msgCount !== 1 ? "s" : ""} \u2022 ${lastActive}`)
1190
+ );
1191
+ }
1192
+ if (contacts.length > 10) {
1193
+ console.log(chalk12.gray(` ... and ${contacts.length - 10} more`));
1194
+ }
1195
+ }
1196
+ }
1197
+ function registerFeedConfigCommand(parent) {
1198
+ parent.command("config").description("View or modify feed configuration").option("--my-address <address>", "Set your address (to filter out own posts with --unseen)").option("--clear-address", "Clear your configured address").option("--show", "Show current configuration (default)").option("--reset", "Reset all state (clears last-seen timestamps and address)").option("--force", "Skip confirmation prompt for --reset").action(async (options) => {
1199
+ await executeFeedConfig(options);
1200
+ });
1201
+ }
1202
+ function formatHistoryEntry(entry, index) {
1203
+ const timestamp = formatTimestamp(entry.timestamp);
1204
+ const typeColor = entry.type === "post" ? chalk12.green : entry.type === "comment" ? chalk12.blue : chalk12.yellow;
1205
+ const lines = [
1206
+ chalk12.cyan(`[${index}]`) + ` ${chalk12.gray(timestamp)} ` + typeColor(entry.type.toUpperCase()),
1207
+ ` ${chalk12.white("Feed:")} ${entry.feed}`,
1208
+ ` ${chalk12.white("Tx:")} ${entry.txHash}`
1209
+ ];
1210
+ if (entry.sender) {
1211
+ lines.push(` ${chalk12.white("Sender:")} ${truncateAddress(entry.sender)}`);
1212
+ }
1213
+ if (entry.text) {
1214
+ const truncatedText = entry.text.length > 80 ? entry.text.slice(0, 80) + "..." : entry.text;
1215
+ lines.push(` ${chalk12.white("Text:")} ${truncatedText}`);
1216
+ }
1217
+ if (entry.type === "post" && entry.postId) {
1218
+ lines.push(` ${chalk12.white("Post ID:")} ${entry.postId}`);
1219
+ } else if (entry.type === "comment" && entry.postId) {
1220
+ lines.push(` ${chalk12.white("Reply to:")} ${entry.postId}`);
1221
+ }
1222
+ if (entry.type === "post" && entry.postId) {
1223
+ lines.push(
1224
+ chalk12.gray(` \u2192 Check replies: netp feed comments ${entry.feed} ${entry.postId}`)
1225
+ );
1226
+ } else if (entry.type === "post" && entry.sender) {
1227
+ lines.push(
1228
+ chalk12.gray(` \u2192 Find post: netp feed read ${entry.feed} --sender ${entry.sender} --json`)
1229
+ );
1230
+ } else if (entry.type === "comment" && entry.postId) {
1231
+ lines.push(
1232
+ chalk12.gray(` \u2192 See thread: netp feed comments ${entry.feed} ${entry.postId}`)
1233
+ );
1234
+ }
1235
+ return lines.join("\n");
1236
+ }
1237
+ function historyEntryToJson(entry, index) {
1238
+ const result = {
1239
+ index,
1240
+ type: entry.type,
1241
+ timestamp: entry.timestamp,
1242
+ txHash: entry.txHash,
1243
+ chainId: entry.chainId,
1244
+ feed: entry.feed
1245
+ };
1246
+ if (entry.sender) {
1247
+ result.sender = entry.sender;
1248
+ }
1249
+ if (entry.text) {
1250
+ result.text = entry.text;
1251
+ }
1252
+ if (entry.postId) {
1253
+ result.postId = entry.postId;
1254
+ }
1255
+ return result;
1256
+ }
1257
+ function validateType(type) {
1258
+ const validTypes = ["post", "comment", "register"];
1259
+ if (!validTypes.includes(type)) {
1260
+ console.error(
1261
+ chalk12.red(
1262
+ `Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`
1263
+ )
1264
+ );
1265
+ process.exit(1);
1266
+ }
1267
+ return type;
1268
+ }
1269
+ async function executeFeedHistory(options) {
1270
+ if (options.clear) {
1271
+ const count = getHistoryCount();
1272
+ if (count === 0) {
1273
+ console.log(chalk12.gray("History is already empty."));
1274
+ return;
1275
+ }
1276
+ console.log(chalk12.yellow(`This will delete ${count} history entries.`));
1277
+ if (!options.force) {
1278
+ const confirmed = await confirm(
1279
+ chalk12.red("\nAre you sure you want to clear history?")
1280
+ );
1281
+ if (!confirmed) {
1282
+ console.log(chalk12.gray("Cancelled."));
1283
+ return;
1284
+ }
1285
+ }
1286
+ clearHistory();
1287
+ console.log(chalk12.green("History cleared."));
1288
+ return;
1289
+ }
1290
+ let entries;
1291
+ if (options.type) {
1292
+ const validType = validateType(options.type);
1293
+ entries = getHistoryByType(validType, options.limit);
1294
+ } else {
1295
+ entries = getHistory(options.limit);
1296
+ }
1297
+ if (entries.length === 0) {
1298
+ if (options.json) {
1299
+ printJson([]);
1300
+ } else {
1301
+ console.log(chalk12.gray("No history entries found."));
1302
+ console.log(
1303
+ chalk12.gray(
1304
+ "History is recorded when you post, comment, or register feeds."
1305
+ )
1306
+ );
1307
+ }
1308
+ return;
1309
+ }
1310
+ if (options.json) {
1311
+ const jsonEntries = entries.map(
1312
+ (entry, idx) => historyEntryToJson(entry, idx)
1313
+ );
1314
+ printJson(jsonEntries);
1315
+ } else {
1316
+ const totalCount = getHistoryCount();
1317
+ const typeFilter = options.type ? ` (type: ${options.type})` : "";
1318
+ console.log(
1319
+ chalk12.cyan(`Feed History${typeFilter} (${entries.length} of ${totalCount})
1320
+ `)
1321
+ );
1322
+ for (let i = 0; i < entries.length; i++) {
1323
+ console.log(formatHistoryEntry(entries[i], i));
1324
+ if (i < entries.length - 1) {
1325
+ console.log("");
1326
+ }
1327
+ }
1328
+ }
1329
+ }
1330
+ function registerFeedHistoryCommand(parent) {
1331
+ parent.command("history").description("View feed activity history (posts, comments, registrations)").option(
1332
+ "--limit <n>",
1333
+ "Limit number of entries",
1334
+ (value) => parseInt(value, 10)
1335
+ ).option("--type <type>", "Filter by type: post, comment, or register").option("--json", "Output as JSON").option("--clear", "Clear all history").option("--force", "Skip confirmation prompt for --clear").action(async (options) => {
1336
+ await executeFeedHistory(options);
1337
+ });
1338
+ }
1339
+
1340
+ // src/commands/feed/index.ts
1341
+ function registerFeedCommand(program) {
1342
+ const feedCommand = program.command("feed").description("Feed operations (read/write posts, comments, manage feeds)");
1343
+ registerFeedListCommand(feedCommand);
1344
+ registerFeedReadCommand(feedCommand);
1345
+ registerFeedPostCommand(feedCommand);
1346
+ registerFeedCommentWriteCommand(feedCommand);
1347
+ registerFeedCommentReadCommand(feedCommand);
1348
+ registerFeedRegisterCommand(feedCommand);
1349
+ registerFeedRepliesCommand(feedCommand);
1350
+ registerFeedPostsCommand(feedCommand);
1351
+ registerFeedConfigCommand(feedCommand);
1352
+ registerFeedHistoryCommand(feedCommand);
1353
+ }
1354
+
1355
+ export { registerFeedCommand, registerFeedCommentReadCommand, registerFeedCommentWriteCommand, registerFeedConfigCommand, registerFeedHistoryCommand, registerFeedListCommand, registerFeedPostCommand, registerFeedPostsCommand, registerFeedReadCommand, registerFeedRegisterCommand, registerFeedRepliesCommand };
1356
+ //# sourceMappingURL=index.mjs.map
1357
+ //# sourceMappingURL=index.mjs.map