@net-protocol/cli 0.1.12 → 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.
- package/README.md +38 -0
- package/dist/cli/index.mjs +2353 -125
- package/dist/cli/index.mjs.map +1 -1
- package/dist/feed/index.mjs +1357 -0
- package/dist/feed/index.mjs.map +1 -0
- package/dist/profile/index.mjs +970 -0
- package/dist/profile/index.mjs.map +1 -0
- package/package.json +8 -2
|
@@ -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
|