@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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +307 -284
  3. package/dist/0g-compute/monitor.js +2 -2
  4. package/dist/0g-compute/monitor.js.map +1 -1
  5. package/dist/0g-compute/smoke-test.js +1 -1
  6. package/dist/0g-compute/smoke-test.js.map +1 -1
  7. package/dist/bot/daemon.js +1 -1
  8. package/dist/bot/daemon.js.map +1 -1
  9. package/dist/bot/executor.d.ts.map +1 -1
  10. package/dist/bot/executor.js +2 -2
  11. package/dist/bot/executor.js.map +1 -1
  12. package/dist/cli.js +3 -3
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands/0g-compute.d.ts +1 -1
  15. package/dist/commands/0g-compute.js +6 -6
  16. package/dist/commands/0g-compute.js.map +1 -1
  17. package/dist/commands/chainscan.d.ts +10 -10
  18. package/dist/commands/chainscan.js +11 -11
  19. package/dist/commands/chainscan.js.map +1 -1
  20. package/dist/commands/config.js +7 -7
  21. package/dist/commands/config.js.map +1 -1
  22. package/dist/commands/echobook.d.ts +12 -10
  23. package/dist/commands/echobook.d.ts.map +1 -1
  24. package/dist/commands/echobook.js +357 -22
  25. package/dist/commands/echobook.js.map +1 -1
  26. package/dist/commands/jaine.js +5 -5
  27. package/dist/commands/jaine.js.map +1 -1
  28. package/dist/commands/marketmaker.d.ts +1 -1
  29. package/dist/commands/marketmaker.js +1 -1
  30. package/dist/commands/send.js +7 -7
  31. package/dist/commands/send.js.map +1 -1
  32. package/dist/commands/setup.d.ts.map +1 -1
  33. package/dist/commands/setup.js +45 -12
  34. package/dist/commands/setup.js.map +1 -1
  35. package/dist/commands/slop-app.js +5 -5
  36. package/dist/commands/slop-app.js.map +1 -1
  37. package/dist/commands/slop-stream.d.ts +1 -1
  38. package/dist/commands/slop-stream.js +1 -1
  39. package/dist/commands/slop.js +3 -3
  40. package/dist/commands/slop.js.map +1 -1
  41. package/dist/commands/wallet.js +28 -28
  42. package/dist/commands/wallet.js.map +1 -1
  43. package/dist/config/paths.js +4 -4
  44. package/dist/config/paths.js.map +1 -1
  45. package/dist/echobook/auth.js +2 -2
  46. package/dist/echobook/auth.js.map +1 -1
  47. package/dist/echobook/follows.d.ts +11 -2
  48. package/dist/echobook/follows.d.ts.map +1 -1
  49. package/dist/echobook/follows.js +21 -5
  50. package/dist/echobook/follows.js.map +1 -1
  51. package/dist/echobook/jwtCache.d.ts +1 -1
  52. package/dist/echobook/jwtCache.js +1 -1
  53. package/dist/echobook/notifications.d.ts +5 -0
  54. package/dist/echobook/notifications.d.ts.map +1 -1
  55. package/dist/echobook/notifications.js +11 -0
  56. package/dist/echobook/notifications.js.map +1 -1
  57. package/dist/echobook/posts.d.ts +19 -1
  58. package/dist/echobook/posts.d.ts.map +1 -1
  59. package/dist/echobook/posts.js +36 -4
  60. package/dist/echobook/posts.js.map +1 -1
  61. package/dist/echobook/profile.d.ts +11 -0
  62. package/dist/echobook/profile.d.ts.map +1 -1
  63. package/dist/echobook/profile.js +9 -0
  64. package/dist/echobook/profile.js.map +1 -1
  65. package/dist/echobook/reposts.d.ts +13 -0
  66. package/dist/echobook/reposts.d.ts.map +1 -0
  67. package/dist/echobook/reposts.js +16 -0
  68. package/dist/echobook/reposts.js.map +1 -0
  69. package/dist/echobook/submolts.d.ts +10 -0
  70. package/dist/echobook/submolts.d.ts.map +1 -1
  71. package/dist/echobook/submolts.js +13 -0
  72. package/dist/echobook/submolts.js.map +1 -1
  73. package/dist/errors.d.ts +1 -0
  74. package/dist/errors.d.ts.map +1 -1
  75. package/dist/errors.js +1 -0
  76. package/dist/errors.js.map +1 -1
  77. package/dist/intents/types.d.ts +1 -1
  78. package/dist/jaine/routing.js +2 -2
  79. package/dist/jaine/routing.js.map +1 -1
  80. package/dist/slop/jwtCache.d.ts +1 -1
  81. package/dist/slop/jwtCache.js +1 -1
  82. package/dist/utils/env.js +1 -1
  83. package/dist/utils/env.js.map +1 -1
  84. package/package.json +2 -2
  85. 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
- * echo echobook auth login|status|logout
5
- * echo echobook profile get|update
6
- * echo echobook submolts list|get|join|leave
7
- * echo echobook posts feed|get|create|delete
8
- * echo echobook comments list|create|delete
9
- * echo echobook vote post|comment
10
- * echo echobook follow <userId>
11
- * echo echobook points my|leaderboard|events
12
- * echo echobook trade-proof submit|get
13
- * echo echobook notifications check|read
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: echo wallet create --json");
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
- .action(async () => {
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: echo echobook auth login`);
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
- infoBox(`${data.username}${badge}`, `Wallet: ${colors.address(data.wallet_address)}\n` +
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 all notifications as read")
884
- .action(async () => {
885
- const spin = spinner("Marking all as read...");
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
- await notificationsApi.markAllRead();
889
- spin.succeed("All notifications marked as read");
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({ markedAllRead: true });
1226
+ writeJsonSuccess({ marked: true, ...markOptions });
892
1227
  }
893
1228
  else {
894
- successBox("Done", "All notifications marked as read");
1229
+ successBox("Done", `${desc} marked as read`);
895
1230
  }
896
1231
  }
897
1232
  catch (err) {