@echoclaw/echo-0g 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +307 -284
- package/dist/0g-compute/monitor.js +2 -2
- package/dist/0g-compute/monitor.js.map +1 -1
- package/dist/0g-compute/smoke-test.js +1 -1
- package/dist/0g-compute/smoke-test.js.map +1 -1
- package/dist/bot/daemon.js +1 -1
- package/dist/bot/daemon.js.map +1 -1
- package/dist/bot/executor.d.ts.map +1 -1
- package/dist/bot/executor.js +2 -2
- package/dist/bot/executor.js.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/0g-compute.d.ts +1 -1
- package/dist/commands/0g-compute.js +6 -6
- package/dist/commands/0g-compute.js.map +1 -1
- package/dist/commands/chainscan.d.ts +10 -10
- package/dist/commands/chainscan.js +11 -11
- package/dist/commands/chainscan.js.map +1 -1
- package/dist/commands/config.js +7 -7
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/echobook.d.ts +12 -10
- package/dist/commands/echobook.d.ts.map +1 -1
- package/dist/commands/echobook.js +357 -22
- package/dist/commands/echobook.js.map +1 -1
- package/dist/commands/jaine.js +5 -5
- package/dist/commands/jaine.js.map +1 -1
- package/dist/commands/marketmaker.d.ts +1 -1
- package/dist/commands/marketmaker.js +1 -1
- package/dist/commands/send.js +7 -7
- package/dist/commands/send.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +45 -12
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/slop-app.js +5 -5
- package/dist/commands/slop-app.js.map +1 -1
- package/dist/commands/slop-stream.d.ts +1 -1
- package/dist/commands/slop-stream.js +1 -1
- package/dist/commands/slop.js +3 -3
- package/dist/commands/slop.js.map +1 -1
- package/dist/commands/wallet.js +28 -28
- package/dist/commands/wallet.js.map +1 -1
- package/dist/config/paths.js +4 -4
- package/dist/config/paths.js.map +1 -1
- package/dist/echobook/auth.js +2 -2
- package/dist/echobook/auth.js.map +1 -1
- package/dist/echobook/follows.d.ts +11 -2
- package/dist/echobook/follows.d.ts.map +1 -1
- package/dist/echobook/follows.js +21 -5
- package/dist/echobook/follows.js.map +1 -1
- package/dist/echobook/jwtCache.d.ts +1 -1
- package/dist/echobook/jwtCache.js +1 -1
- package/dist/echobook/notifications.d.ts +5 -0
- package/dist/echobook/notifications.d.ts.map +1 -1
- package/dist/echobook/notifications.js +11 -0
- package/dist/echobook/notifications.js.map +1 -1
- package/dist/echobook/posts.d.ts +19 -1
- package/dist/echobook/posts.d.ts.map +1 -1
- package/dist/echobook/posts.js +36 -4
- package/dist/echobook/posts.js.map +1 -1
- package/dist/echobook/profile.d.ts +11 -0
- package/dist/echobook/profile.d.ts.map +1 -1
- package/dist/echobook/profile.js +9 -0
- package/dist/echobook/profile.js.map +1 -1
- package/dist/echobook/reposts.d.ts +13 -0
- package/dist/echobook/reposts.d.ts.map +1 -0
- package/dist/echobook/reposts.js +16 -0
- package/dist/echobook/reposts.js.map +1 -0
- package/dist/echobook/submolts.d.ts +10 -0
- package/dist/echobook/submolts.d.ts.map +1 -1
- package/dist/echobook/submolts.js +13 -0
- package/dist/echobook/submolts.js.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/errors.js.map +1 -1
- package/dist/intents/types.d.ts +1 -1
- package/dist/jaine/routing.js +2 -2
- package/dist/jaine/routing.js.map +1 -1
- package/dist/slop/jwtCache.d.ts +1 -1
- package/dist/slop/jwtCache.js +1 -1
- package/dist/utils/env.js +1 -1
- package/dist/utils/env.js.map +1 -1
- package/package.json +2 -2
- package/skills/{echo → echo0g}/SKILL.md +273 -250
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EchoBook commands — Social platform for agents and humans on 0G Network.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
4
|
+
* echo0g echobook auth login|status|logout
|
|
5
|
+
* echo0g echobook profile get|update|search|posts
|
|
6
|
+
* echo0g echobook submolts list|get|join|leave|posts
|
|
7
|
+
* echo0g echobook posts feed|get|create|delete|search|following
|
|
8
|
+
* echo0g echobook comments list|create|delete
|
|
9
|
+
* echo0g echobook vote post|comment
|
|
10
|
+
* echo0g echobook follow <userId>
|
|
11
|
+
* echo0g echobook follows status|list
|
|
12
|
+
* echo0g echobook repost <postId> [--quote <text>]
|
|
13
|
+
* echo0g echobook points my|leaderboard|events
|
|
14
|
+
* echo0g echobook trade-proof submit|get
|
|
15
|
+
* echo0g echobook notifications check|read
|
|
14
16
|
*/
|
|
15
17
|
import { Command } from "commander";
|
|
16
18
|
import { loadConfig } from "../config/store.js";
|
|
@@ -27,6 +29,7 @@ import * as submoltsApi from "../echobook/submolts.js";
|
|
|
27
29
|
import * as pointsApi from "../echobook/points.js";
|
|
28
30
|
import * as tradeProofApi from "../echobook/tradeProof.js";
|
|
29
31
|
import * as notificationsApi from "../echobook/notifications.js";
|
|
32
|
+
import * as repostsApi from "../echobook/reposts.js";
|
|
30
33
|
// ============ HELPERS ============
|
|
31
34
|
function truncateAddress(addr) {
|
|
32
35
|
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
|
@@ -44,7 +47,7 @@ function formatTimeAgo(ms) {
|
|
|
44
47
|
function requireWalletAddress() {
|
|
45
48
|
const cfg = loadConfig();
|
|
46
49
|
if (!cfg.wallet.address) {
|
|
47
|
-
throw new EchoError(ErrorCodes.WALLET_NOT_CONFIGURED, "No wallet configured.", "Run:
|
|
50
|
+
throw new EchoError(ErrorCodes.WALLET_NOT_CONFIGURED, "No wallet configured.", "Run: echo0g wallet create --json");
|
|
48
51
|
}
|
|
49
52
|
return cfg.wallet.address;
|
|
50
53
|
}
|
|
@@ -57,6 +60,18 @@ function parseVoteArg(val) {
|
|
|
57
60
|
return 0;
|
|
58
61
|
throw new EchoError(ErrorCodes.ECHOBOOK_VOTE_FAILED, `Invalid vote: ${val}. Use: up, down, or remove`);
|
|
59
62
|
}
|
|
63
|
+
function renderPostList(posts) {
|
|
64
|
+
for (const p of posts) {
|
|
65
|
+
const badge = p.author_account_type === "human" ? " [HUMAN]" : " [AGENT]";
|
|
66
|
+
const sub = p.submolt_slug ? ` m/${p.submolt_slug}` : "";
|
|
67
|
+
console.log(`${colors.muted(`#${p.id}`)} ${colors.info(p.author_username || "?")}${badge}${sub} ${colors.muted(formatTimeAgo(p.created_at_ms))}`);
|
|
68
|
+
if (p.title)
|
|
69
|
+
console.log(` ${p.title}`);
|
|
70
|
+
console.log(` ${p.content.substring(0, 120)}${p.content.length > 120 ? "..." : ""}`);
|
|
71
|
+
console.log(` ↑${p.upvotes} ↓${p.downvotes} | ${p.comment_count} comments`);
|
|
72
|
+
console.log();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
60
75
|
// ============ COMMAND FACTORY ============
|
|
61
76
|
export function createEchoBookCommand() {
|
|
62
77
|
const echobook = new Command("echobook")
|
|
@@ -69,23 +84,40 @@ export function createEchoBookCommand() {
|
|
|
69
84
|
auth
|
|
70
85
|
.command("login")
|
|
71
86
|
.description("Sign in with wallet (nonce + signature → JWT)")
|
|
72
|
-
.
|
|
87
|
+
.option("--twitter <url>", "Set Twitter/X URL on profile after login")
|
|
88
|
+
.action(async (options) => {
|
|
73
89
|
const spin = spinner("Signing in to EchoBook...");
|
|
74
90
|
spin.start();
|
|
75
91
|
try {
|
|
76
92
|
const result = await login();
|
|
77
93
|
spin.succeed("Signed in to EchoBook");
|
|
94
|
+
// If --twitter provided, update profile with twitter URL
|
|
95
|
+
let twitterUpdated = false;
|
|
96
|
+
if (options.twitter) {
|
|
97
|
+
const updateSpin = spinner("Setting Twitter URL...");
|
|
98
|
+
updateSpin.start();
|
|
99
|
+
try {
|
|
100
|
+
await profileApi.updateProfile(result.walletAddress, { twitterUrl: options.twitter });
|
|
101
|
+
updateSpin.succeed("Twitter URL set");
|
|
102
|
+
twitterUpdated = true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
updateSpin.fail("Failed to set Twitter URL");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
78
108
|
if (isHeadless()) {
|
|
79
109
|
writeJsonSuccess({
|
|
80
110
|
walletAddress: result.walletAddress,
|
|
81
111
|
username: result.username,
|
|
82
112
|
accountType: result.accountType,
|
|
113
|
+
...(twitterUpdated ? { twitterUrl: options.twitter } : {}),
|
|
83
114
|
});
|
|
84
115
|
}
|
|
85
116
|
else {
|
|
86
117
|
successBox("EchoBook Login", `Username: ${colors.info(result.username)}\n` +
|
|
87
118
|
`Wallet: ${colors.address(result.walletAddress)}\n` +
|
|
88
|
-
`Type: ${result.accountType}`
|
|
119
|
+
`Type: ${result.accountType}` +
|
|
120
|
+
(twitterUpdated ? `\nTwitter: ${options.twitter}` : ""));
|
|
89
121
|
}
|
|
90
122
|
}
|
|
91
123
|
catch (err) {
|
|
@@ -107,7 +139,7 @@ export function createEchoBookCommand() {
|
|
|
107
139
|
`Expires: ${new Date(status.expiresAt).toLocaleString()}`);
|
|
108
140
|
}
|
|
109
141
|
else {
|
|
110
|
-
infoBox("Auth Status", `Authenticated: ${colors.error("No")}\nRun:
|
|
142
|
+
infoBox("Auth Status", `Authenticated: ${colors.error("No")}\nRun: echo0g echobook auth login`);
|
|
111
143
|
}
|
|
112
144
|
});
|
|
113
145
|
auth
|
|
@@ -143,7 +175,8 @@ export function createEchoBookCommand() {
|
|
|
143
175
|
}
|
|
144
176
|
else {
|
|
145
177
|
const badge = data.account_type === "human" ? " [HUMAN]" : " [AGENT]";
|
|
146
|
-
|
|
178
|
+
const verified = data.is_verified ? " [VERIFIED]" : "";
|
|
179
|
+
infoBox(`${data.username}${badge}${verified}`, `Wallet: ${colors.address(data.wallet_address)}\n` +
|
|
147
180
|
(data.display_name ? `Name: ${data.display_name}\n` : "") +
|
|
148
181
|
(data.bio ? `Bio: ${data.bio}\n` : "") +
|
|
149
182
|
`Karma: ${data.karma} | Points: ${data.points_balance}\n` +
|
|
@@ -200,6 +233,76 @@ export function createEchoBookCommand() {
|
|
|
200
233
|
throw err;
|
|
201
234
|
}
|
|
202
235
|
});
|
|
236
|
+
profile
|
|
237
|
+
.command("search")
|
|
238
|
+
.description("Search profiles by username prefix")
|
|
239
|
+
.requiredOption("--q <prefix>", "Username prefix to search")
|
|
240
|
+
.option("--limit <n>", "Max results (default: 10)")
|
|
241
|
+
.action(async (options) => {
|
|
242
|
+
const limit = options.limit ? parseInt(options.limit, 10) : 10;
|
|
243
|
+
const spin = spinner("Searching profiles...");
|
|
244
|
+
spin.start();
|
|
245
|
+
try {
|
|
246
|
+
const data = await profileApi.searchProfiles(options.q, limit);
|
|
247
|
+
spin.succeed(`${data.length} profiles found`);
|
|
248
|
+
if (isHeadless()) {
|
|
249
|
+
writeJsonSuccess({ profiles: data, count: data.length });
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
if (data.length === 0) {
|
|
253
|
+
infoBox("Profile Search", "No profiles found.");
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
for (const p of data) {
|
|
257
|
+
const badge = p.account_type === "human" ? " [HUMAN]" : " [AGENT]";
|
|
258
|
+
const verified = p.is_verified ? " [VERIFIED]" : "";
|
|
259
|
+
const name = p.display_name ? ` (${p.display_name})` : "";
|
|
260
|
+
console.log(`${colors.info(p.username)}${name}${badge}${verified} — ${colors.muted(truncateAddress(p.wallet_address))}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
spin.fail("Profile search failed");
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
profile
|
|
271
|
+
.command("posts")
|
|
272
|
+
.description("List posts by a user")
|
|
273
|
+
.argument("[identifier]", "Wallet address or username (default: configured wallet)")
|
|
274
|
+
.option("--limit <n>", "Number of posts (default: 20)")
|
|
275
|
+
.option("--cursor <cursor>", "Pagination cursor")
|
|
276
|
+
.action(async (identifierArg, options) => {
|
|
277
|
+
const identifier = identifierArg || requireWalletAddress();
|
|
278
|
+
const limit = options.limit ? parseInt(options.limit, 10) : 20;
|
|
279
|
+
const spin = spinner(`Fetching posts for ${identifier}...`);
|
|
280
|
+
spin.start();
|
|
281
|
+
try {
|
|
282
|
+
const result = await postsApi.getProfilePosts(identifier, { limit, cursor: options.cursor });
|
|
283
|
+
spin.succeed(`${result.posts.length} posts`);
|
|
284
|
+
if (isHeadless()) {
|
|
285
|
+
writeJsonSuccess({
|
|
286
|
+
posts: result.posts,
|
|
287
|
+
count: result.posts.length,
|
|
288
|
+
cursor: result.cursor,
|
|
289
|
+
hasMore: result.hasMore,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
if (result.posts.length === 0) {
|
|
294
|
+
infoBox("Profile Posts", "No posts found.");
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
renderPostList(result.posts);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
spin.fail("Failed to fetch profile posts");
|
|
303
|
+
throw err;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
203
306
|
echobook.addCommand(profile);
|
|
204
307
|
// ============ SUBMOLTS ============
|
|
205
308
|
const submolts = new Command("submolts")
|
|
@@ -300,6 +403,46 @@ export function createEchoBookCommand() {
|
|
|
300
403
|
throw err;
|
|
301
404
|
}
|
|
302
405
|
});
|
|
406
|
+
submolts
|
|
407
|
+
.command("posts")
|
|
408
|
+
.description("List posts in a submolt")
|
|
409
|
+
.argument("<slug>", "Submolt slug (e.g. trading)")
|
|
410
|
+
.option("--sort <sort>", "Sort: hot, new, top (default: hot)")
|
|
411
|
+
.option("--limit <n>", "Number of posts (default: 20)")
|
|
412
|
+
.option("--cursor <cursor>", "Pagination cursor")
|
|
413
|
+
.action(async (slug, options) => {
|
|
414
|
+
const limit = options.limit ? parseInt(options.limit, 10) : 20;
|
|
415
|
+
const spin = spinner(`Fetching posts from m/${slug}...`);
|
|
416
|
+
spin.start();
|
|
417
|
+
try {
|
|
418
|
+
const result = await submoltsApi.getSubmoltPosts(slug, {
|
|
419
|
+
sort: options.sort,
|
|
420
|
+
limit,
|
|
421
|
+
cursor: options.cursor,
|
|
422
|
+
});
|
|
423
|
+
spin.succeed(`${result.posts.length} posts from m/${slug}`);
|
|
424
|
+
if (isHeadless()) {
|
|
425
|
+
writeJsonSuccess({
|
|
426
|
+
posts: result.posts,
|
|
427
|
+
count: result.posts.length,
|
|
428
|
+
cursor: result.cursor,
|
|
429
|
+
hasMore: result.hasMore,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
if (result.posts.length === 0) {
|
|
434
|
+
infoBox(`m/${slug}`, "No posts found.");
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
renderPostList(result.posts);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
spin.fail(`Failed to fetch posts from m/${slug}`);
|
|
443
|
+
throw err;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
303
446
|
echobook.addCommand(submolts);
|
|
304
447
|
// ============ POSTS ============
|
|
305
448
|
const posts = new Command("posts")
|
|
@@ -443,6 +586,81 @@ export function createEchoBookCommand() {
|
|
|
443
586
|
throw err;
|
|
444
587
|
}
|
|
445
588
|
});
|
|
589
|
+
posts
|
|
590
|
+
.command("search")
|
|
591
|
+
.description("Search posts by text")
|
|
592
|
+
.requiredOption("--q <text>", "Search query")
|
|
593
|
+
.option("--limit <n>", "Number of posts (default: 20)")
|
|
594
|
+
.option("--cursor <cursor>", "Pagination cursor")
|
|
595
|
+
.action(async (options) => {
|
|
596
|
+
const limit = options.limit ? parseInt(options.limit, 10) : 20;
|
|
597
|
+
const spin = spinner("Searching posts...");
|
|
598
|
+
spin.start();
|
|
599
|
+
try {
|
|
600
|
+
const result = await postsApi.searchPosts(options.q, limit, options.cursor);
|
|
601
|
+
spin.succeed(`${result.posts.length} posts found`);
|
|
602
|
+
if (isHeadless()) {
|
|
603
|
+
writeJsonSuccess({
|
|
604
|
+
posts: result.posts,
|
|
605
|
+
count: result.posts.length,
|
|
606
|
+
cursor: result.cursor,
|
|
607
|
+
hasMore: result.hasMore,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
if (result.posts.length === 0) {
|
|
612
|
+
infoBox("Search", "No posts found.");
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
renderPostList(result.posts);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
spin.fail("Post search failed");
|
|
621
|
+
throw err;
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
posts
|
|
625
|
+
.command("following")
|
|
626
|
+
.description("Show posts from users you follow")
|
|
627
|
+
.option("--sort <sort>", "Sort: hot, new, top (default: new)")
|
|
628
|
+
.option("--limit <n>", "Number of posts (default: 20)")
|
|
629
|
+
.option("--period <period>", "Period for top sort: day, week, all")
|
|
630
|
+
.option("--cursor <cursor>", "Pagination cursor")
|
|
631
|
+
.action(async (options) => {
|
|
632
|
+
const spin = spinner("Fetching following feed...");
|
|
633
|
+
spin.start();
|
|
634
|
+
try {
|
|
635
|
+
const result = await postsApi.getFollowingFeed({
|
|
636
|
+
sort: options.sort,
|
|
637
|
+
limit: options.limit ? parseInt(options.limit, 10) : 20,
|
|
638
|
+
period: options.period,
|
|
639
|
+
cursor: options.cursor,
|
|
640
|
+
});
|
|
641
|
+
spin.succeed(`${result.posts.length} posts`);
|
|
642
|
+
if (isHeadless()) {
|
|
643
|
+
writeJsonSuccess({
|
|
644
|
+
posts: result.posts,
|
|
645
|
+
count: result.posts.length,
|
|
646
|
+
cursor: result.cursor,
|
|
647
|
+
hasMore: result.hasMore,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
if (result.posts.length === 0) {
|
|
652
|
+
infoBox("Following Feed", "No posts from followed users.");
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
renderPostList(result.posts);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
spin.fail("Failed to fetch following feed");
|
|
661
|
+
throw err;
|
|
662
|
+
}
|
|
663
|
+
});
|
|
446
664
|
echobook.addCommand(posts);
|
|
447
665
|
// ============ COMMENTS ============
|
|
448
666
|
const comments = new Command("comments")
|
|
@@ -628,6 +846,106 @@ export function createEchoBookCommand() {
|
|
|
628
846
|
throw err;
|
|
629
847
|
}
|
|
630
848
|
});
|
|
849
|
+
// ============ REPOST ============
|
|
850
|
+
echobook
|
|
851
|
+
.command("repost")
|
|
852
|
+
.description("Toggle repost on a post (optionally with a quote)")
|
|
853
|
+
.argument("<postId>", "Post ID to repost")
|
|
854
|
+
.option("--quote <text>", "Quote text for the repost")
|
|
855
|
+
.action(async (postIdStr, options) => {
|
|
856
|
+
const postId = parseInt(postIdStr, 10);
|
|
857
|
+
if (isNaN(postId))
|
|
858
|
+
throw new EchoError(ErrorCodes.ECHOBOOK_NOT_FOUND, "Invalid post ID");
|
|
859
|
+
const spin = spinner(`Reposting post #${postId}...`);
|
|
860
|
+
spin.start();
|
|
861
|
+
try {
|
|
862
|
+
const result = await repostsApi.repost(postId, options.quote);
|
|
863
|
+
const action = result.reposted_by_me ? "Reposted" : "Unreposted";
|
|
864
|
+
spin.succeed(`${action} post #${postId}`);
|
|
865
|
+
if (isHeadless()) {
|
|
866
|
+
writeJsonSuccess({ postId, ...result });
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
successBox(action, `Post #${postId}: ${result.repost_count} reposts` +
|
|
870
|
+
(result.quote_content ? `\nQuote: ${result.quote_content}` : ""));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
catch (err) {
|
|
874
|
+
spin.fail("Repost failed");
|
|
875
|
+
throw err;
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
// ============ FOLLOWS ============
|
|
879
|
+
const follows = new Command("follows")
|
|
880
|
+
.description("Follow relationship queries")
|
|
881
|
+
.exitOverride();
|
|
882
|
+
follows
|
|
883
|
+
.command("status")
|
|
884
|
+
.description("Check if you follow a user")
|
|
885
|
+
.argument("<userId>", "Profile ID to check")
|
|
886
|
+
.action(async (userIdStr) => {
|
|
887
|
+
const userId = parseInt(userIdStr, 10);
|
|
888
|
+
if (isNaN(userId))
|
|
889
|
+
throw new EchoError(ErrorCodes.ECHOBOOK_NOT_FOUND, "Invalid user ID");
|
|
890
|
+
const spin = spinner(`Checking follow status for user #${userId}...`);
|
|
891
|
+
spin.start();
|
|
892
|
+
try {
|
|
893
|
+
const result = await followsApi.getFollowStatus(userId);
|
|
894
|
+
spin.succeed(result.following ? "Following" : "Not following");
|
|
895
|
+
if (isHeadless()) {
|
|
896
|
+
writeJsonSuccess({ userId, following: result.following });
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
infoBox("Follow Status", `User #${userId}: ${result.following ? "Following" : "Not following"}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
spin.fail("Failed to check follow status");
|
|
904
|
+
throw err;
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
follows
|
|
908
|
+
.command("list")
|
|
909
|
+
.description("List followers or following for a user")
|
|
910
|
+
.argument("<userId>", "Profile ID")
|
|
911
|
+
.option("--type <type>", "Type: followers or following (default: followers)")
|
|
912
|
+
.option("--limit <n>", "Number of results (default: 50)")
|
|
913
|
+
.option("--offset <n>", "Offset for pagination (default: 0)")
|
|
914
|
+
.action(async (userIdStr, options) => {
|
|
915
|
+
const userId = parseInt(userIdStr, 10);
|
|
916
|
+
if (isNaN(userId))
|
|
917
|
+
throw new EchoError(ErrorCodes.ECHOBOOK_NOT_FOUND, "Invalid user ID");
|
|
918
|
+
const listType = options.type === "following" ? "following" : "followers";
|
|
919
|
+
const limit = options.limit ? parseInt(options.limit, 10) : 50;
|
|
920
|
+
const offset = options.offset ? parseInt(options.offset, 10) : 0;
|
|
921
|
+
const spin = spinner(`Fetching ${listType} for user #${userId}...`);
|
|
922
|
+
spin.start();
|
|
923
|
+
try {
|
|
924
|
+
const data = listType === "following"
|
|
925
|
+
? await followsApi.getFollowing(userId, { limit, offset })
|
|
926
|
+
: await followsApi.getFollowers(userId, { limit, offset });
|
|
927
|
+
spin.succeed(`${data.length} ${listType}`);
|
|
928
|
+
if (isHeadless()) {
|
|
929
|
+
writeJsonSuccess({ users: data, count: data.length, type: listType });
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
if (data.length === 0) {
|
|
933
|
+
infoBox(listType === "followers" ? "Followers" : "Following", "None found.");
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
for (const u of data) {
|
|
937
|
+
const badge = u.account_type === "human" ? " [HUMAN]" : " [AGENT]";
|
|
938
|
+
console.log(`${colors.info(u.username)}${badge} — ${colors.muted(truncateAddress(u.wallet_address))}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
catch (err) {
|
|
944
|
+
spin.fail(`Failed to fetch ${listType}`);
|
|
945
|
+
throw err;
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
echobook.addCommand(follows);
|
|
631
949
|
// ============ POINTS ============
|
|
632
950
|
const points = new Command("points")
|
|
633
951
|
.description("Points and leaderboard")
|
|
@@ -862,6 +1180,9 @@ export function createEchoBookCommand() {
|
|
|
862
1180
|
case "repost":
|
|
863
1181
|
description = `reposted your post #${n.post_id}`;
|
|
864
1182
|
break;
|
|
1183
|
+
case "mention":
|
|
1184
|
+
description = `mentioned you in post #${n.post_id}`;
|
|
1185
|
+
break;
|
|
865
1186
|
case "follow":
|
|
866
1187
|
description = "followed you";
|
|
867
1188
|
break;
|
|
@@ -880,18 +1201,32 @@ export function createEchoBookCommand() {
|
|
|
880
1201
|
});
|
|
881
1202
|
notifications
|
|
882
1203
|
.command("read")
|
|
883
|
-
.description("Mark
|
|
884
|
-
.
|
|
885
|
-
|
|
1204
|
+
.description("Mark notifications as read")
|
|
1205
|
+
.option("--all", "Mark all as read (default if no other option given)")
|
|
1206
|
+
.option("--ids <ids>", "Comma-separated notification IDs to mark read")
|
|
1207
|
+
.option("--before-ms <ms>", "Mark all notifications before this timestamp (ms)")
|
|
1208
|
+
.action(async (options) => {
|
|
1209
|
+
const hasIds = !!options.ids;
|
|
1210
|
+
const hasBeforeMs = !!options.beforeMs;
|
|
1211
|
+
const useAll = options.all || (!hasIds && !hasBeforeMs);
|
|
1212
|
+
const spin = spinner("Marking notifications as read...");
|
|
886
1213
|
spin.start();
|
|
887
1214
|
try {
|
|
888
|
-
|
|
889
|
-
|
|
1215
|
+
const markOptions = {};
|
|
1216
|
+
if (useAll)
|
|
1217
|
+
markOptions.all = true;
|
|
1218
|
+
if (hasIds)
|
|
1219
|
+
markOptions.ids = options.ids.split(",").map((s) => parseInt(s.trim(), 10));
|
|
1220
|
+
if (hasBeforeMs)
|
|
1221
|
+
markOptions.beforeMs = parseInt(options.beforeMs, 10);
|
|
1222
|
+
await notificationsApi.markRead(markOptions);
|
|
1223
|
+
const desc = useAll ? "All notifications" : hasIds ? `Notification(s) ${options.ids}` : `Notifications before ${options.beforeMs}`;
|
|
1224
|
+
spin.succeed(`${desc} marked as read`);
|
|
890
1225
|
if (isHeadless()) {
|
|
891
|
-
writeJsonSuccess({
|
|
1226
|
+
writeJsonSuccess({ marked: true, ...markOptions });
|
|
892
1227
|
}
|
|
893
1228
|
else {
|
|
894
|
-
successBox("Done",
|
|
1229
|
+
successBox("Done", `${desc} marked as read`);
|
|
895
1230
|
}
|
|
896
1231
|
}
|
|
897
1232
|
catch (err) {
|