@mepuka/skygent 0.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 (114) hide show
  1. package/README.md +59 -0
  2. package/index.ts +146 -0
  3. package/package.json +56 -0
  4. package/src/cli/app.ts +75 -0
  5. package/src/cli/config-command.ts +140 -0
  6. package/src/cli/config.ts +91 -0
  7. package/src/cli/derive.ts +205 -0
  8. package/src/cli/doc/annotation.ts +36 -0
  9. package/src/cli/doc/filter.ts +69 -0
  10. package/src/cli/doc/index.ts +9 -0
  11. package/src/cli/doc/post.ts +155 -0
  12. package/src/cli/doc/primitives.ts +25 -0
  13. package/src/cli/doc/render.ts +18 -0
  14. package/src/cli/doc/table.ts +114 -0
  15. package/src/cli/doc/thread.ts +46 -0
  16. package/src/cli/doc/tree.ts +126 -0
  17. package/src/cli/errors.ts +59 -0
  18. package/src/cli/exit-codes.ts +52 -0
  19. package/src/cli/feed.ts +177 -0
  20. package/src/cli/filter-dsl.ts +1411 -0
  21. package/src/cli/filter-errors.ts +208 -0
  22. package/src/cli/filter-help.ts +70 -0
  23. package/src/cli/filter-input.ts +54 -0
  24. package/src/cli/filter.ts +435 -0
  25. package/src/cli/graph.ts +472 -0
  26. package/src/cli/help.ts +14 -0
  27. package/src/cli/interval.ts +35 -0
  28. package/src/cli/jetstream.ts +173 -0
  29. package/src/cli/layers.ts +180 -0
  30. package/src/cli/logging.ts +136 -0
  31. package/src/cli/output-format.ts +26 -0
  32. package/src/cli/output.ts +82 -0
  33. package/src/cli/parse.ts +80 -0
  34. package/src/cli/post.ts +193 -0
  35. package/src/cli/preferences.ts +11 -0
  36. package/src/cli/query-fields.ts +247 -0
  37. package/src/cli/query.ts +415 -0
  38. package/src/cli/range.ts +44 -0
  39. package/src/cli/search.ts +465 -0
  40. package/src/cli/shared-options.ts +169 -0
  41. package/src/cli/shared.ts +20 -0
  42. package/src/cli/store-errors.ts +80 -0
  43. package/src/cli/store-tree.ts +392 -0
  44. package/src/cli/store.ts +395 -0
  45. package/src/cli/sync-factory.ts +107 -0
  46. package/src/cli/sync.ts +366 -0
  47. package/src/cli/view-thread.ts +196 -0
  48. package/src/cli/view.ts +47 -0
  49. package/src/cli/watch.ts +344 -0
  50. package/src/db/migrations/store-catalog/001_init.ts +14 -0
  51. package/src/db/migrations/store-index/001_init.ts +34 -0
  52. package/src/db/migrations/store-index/002_event_log.ts +24 -0
  53. package/src/db/migrations/store-index/003_fts_and_derived.ts +52 -0
  54. package/src/db/migrations/store-index/004_query_indexes.ts +9 -0
  55. package/src/db/migrations/store-index/005_post_lang.ts +15 -0
  56. package/src/db/migrations/store-index/006_has_embed.ts +10 -0
  57. package/src/db/migrations/store-index/007_event_seq_and_checkpoints.ts +68 -0
  58. package/src/domain/bsky.ts +467 -0
  59. package/src/domain/config.ts +11 -0
  60. package/src/domain/credentials.ts +6 -0
  61. package/src/domain/defaults.ts +8 -0
  62. package/src/domain/derivation.ts +55 -0
  63. package/src/domain/errors.ts +71 -0
  64. package/src/domain/events.ts +55 -0
  65. package/src/domain/extract.ts +64 -0
  66. package/src/domain/filter-describe.ts +551 -0
  67. package/src/domain/filter-explain.ts +9 -0
  68. package/src/domain/filter.ts +797 -0
  69. package/src/domain/format.ts +91 -0
  70. package/src/domain/index.ts +13 -0
  71. package/src/domain/indexes.ts +17 -0
  72. package/src/domain/policies.ts +16 -0
  73. package/src/domain/post.ts +88 -0
  74. package/src/domain/primitives.ts +50 -0
  75. package/src/domain/raw.ts +140 -0
  76. package/src/domain/store.ts +103 -0
  77. package/src/domain/sync.ts +211 -0
  78. package/src/domain/text-width.ts +56 -0
  79. package/src/services/app-config.ts +278 -0
  80. package/src/services/bsky-client.ts +2113 -0
  81. package/src/services/credential-store.ts +408 -0
  82. package/src/services/derivation-engine.ts +502 -0
  83. package/src/services/derivation-settings.ts +61 -0
  84. package/src/services/derivation-validator.ts +68 -0
  85. package/src/services/filter-compiler.ts +269 -0
  86. package/src/services/filter-library.ts +371 -0
  87. package/src/services/filter-runtime.ts +821 -0
  88. package/src/services/filter-settings.ts +30 -0
  89. package/src/services/identity-resolver.ts +563 -0
  90. package/src/services/jetstream-sync.ts +636 -0
  91. package/src/services/lineage-store.ts +89 -0
  92. package/src/services/link-validator.ts +244 -0
  93. package/src/services/output-manager.ts +274 -0
  94. package/src/services/post-parser.ts +62 -0
  95. package/src/services/profile-resolver.ts +223 -0
  96. package/src/services/resource-monitor.ts +106 -0
  97. package/src/services/shared.ts +69 -0
  98. package/src/services/store-cleaner.ts +43 -0
  99. package/src/services/store-commit.ts +168 -0
  100. package/src/services/store-db.ts +248 -0
  101. package/src/services/store-event-log.ts +285 -0
  102. package/src/services/store-index-sql.ts +289 -0
  103. package/src/services/store-index.ts +1152 -0
  104. package/src/services/store-keys.ts +4 -0
  105. package/src/services/store-manager.ts +358 -0
  106. package/src/services/store-stats.ts +522 -0
  107. package/src/services/store-writer.ts +200 -0
  108. package/src/services/sync-checkpoint-store.ts +169 -0
  109. package/src/services/sync-engine.ts +547 -0
  110. package/src/services/sync-reporter.ts +16 -0
  111. package/src/services/sync-settings.ts +72 -0
  112. package/src/services/trending-topics.ts +226 -0
  113. package/src/services/view-checkpoint-store.ts +238 -0
  114. package/src/typeclass/chunk.ts +84 -0
@@ -0,0 +1,289 @@
1
+ import { Effect, Schema } from "effect";
2
+ import type * as SqlClient from "@effect/sql/SqlClient";
3
+ import { StoreIndexError } from "../domain/errors.js";
4
+ import { Post } from "../domain/post.js";
5
+ import type { PostUri } from "../domain/primitives.js";
6
+
7
+ const toStoreIndexError = (message: string) => (cause: unknown) =>
8
+ StoreIndexError.make({ message, cause });
9
+
10
+ const toIso = (value: Date | string) =>
11
+ value instanceof Date ? value.toISOString() : new Date(value).toISOString();
12
+
13
+ const encodePostJson = (post: Post) =>
14
+ Schema.encode(Schema.parseJson(Post))(post).pipe(
15
+ Effect.mapError(toStoreIndexError("StoreIndex.post encode failed"))
16
+ );
17
+
18
+ const embedTag = (embed: Post["embed"]): string | undefined => {
19
+ if (!embed || typeof embed !== "object" || !("_tag" in embed)) {
20
+ return undefined;
21
+ }
22
+ const tag = (embed as { readonly _tag?: unknown })._tag;
23
+ return typeof tag === "string" ? tag : undefined;
24
+ };
25
+
26
+ const embedMediaTag = (embed: Post["embed"]): string | undefined => {
27
+ if (!embed || typeof embed !== "object" || !("_tag" in embed)) {
28
+ return undefined;
29
+ }
30
+ const tag = (embed as { readonly _tag?: unknown })._tag;
31
+ if (tag !== "RecordWithMedia") {
32
+ return undefined;
33
+ }
34
+ const media = (embed as { readonly media?: unknown }).media;
35
+ if (!media || typeof media !== "object" || !("_tag" in media)) {
36
+ return undefined;
37
+ }
38
+ const mediaTag = (media as { readonly _tag?: unknown })._tag;
39
+ return typeof mediaTag === "string" ? mediaTag : undefined;
40
+ };
41
+
42
+ const hasExternalLink = (post: Post) => {
43
+ if (post.links.length > 0) {
44
+ return true;
45
+ }
46
+ const tag = embedTag(post.embed);
47
+ if (tag === "External") {
48
+ return true;
49
+ }
50
+ return embedMediaTag(post.embed) === "External";
51
+ };
52
+
53
+ const hasImages = (post: Post) => {
54
+ const tag = embedTag(post.embed);
55
+ if (tag === "Images") {
56
+ return true;
57
+ }
58
+ return embedMediaTag(post.embed) === "Images";
59
+ };
60
+
61
+ const hasVideo = (post: Post) => {
62
+ const tag = embedTag(post.embed);
63
+ if (tag === "Video") {
64
+ return true;
65
+ }
66
+ return embedMediaTag(post.embed) === "Video";
67
+ };
68
+
69
+ const hasMedia = (post: Post) =>
70
+ hasImages(post) || hasVideo(post) || hasExternalLink(post);
71
+
72
+ const hasEmbed = (post: Post) =>
73
+ post.embed != null || post.recordEmbed != null;
74
+
75
+ const normalizeLangs = (langs: ReadonlyArray<string> | undefined) =>
76
+ Array.from(
77
+ new Set(
78
+ (langs ?? [])
79
+ .map((lang) => lang.trim().toLowerCase())
80
+ .filter((lang) => lang.length > 0)
81
+ )
82
+ );
83
+
84
+ const isRepost = (post: Post) => {
85
+ const reason = post.feed?.reason;
86
+ if (!reason || typeof reason !== "object") {
87
+ return false;
88
+ }
89
+ const tag = (reason as { readonly _tag?: unknown })._tag;
90
+ return tag === "ReasonRepost";
91
+ };
92
+
93
+ const isQuote = (post: Post) => {
94
+ const tag = embedTag(post.embed);
95
+ return tag === "Record" || tag === "RecordWithMedia";
96
+ };
97
+
98
+ const toFlag = (value: boolean) => (value ? 1 : 0);
99
+
100
+ export const upsertPost = (
101
+ sql: SqlClient.SqlClient,
102
+ post: Post
103
+ ) =>
104
+ Effect.gen(function* () {
105
+ const createdAt = toIso(post.createdAt);
106
+ const createdDate = createdAt.slice(0, 10);
107
+ const postJson = yield* encodePostJson(post);
108
+ const normalizedLangs = normalizeLangs(post.langs);
109
+ const lang = normalizedLangs[0];
110
+ const isReply = Boolean(post.reply);
111
+ const quote = isQuote(post);
112
+ const repost = isRepost(post);
113
+ const original = !isReply && !quote && !repost;
114
+ const links = hasExternalLink(post);
115
+ const images = hasImages(post);
116
+ const video = hasVideo(post);
117
+ const media = hasMedia(post);
118
+ const embed = hasEmbed(post);
119
+ const metrics = post.metrics;
120
+ const likeCount = metrics?.likeCount ?? 0;
121
+ const repostCount = metrics?.repostCount ?? 0;
122
+ const replyCount = metrics?.replyCount ?? 0;
123
+
124
+ yield* sql`INSERT INTO posts (
125
+ uri,
126
+ created_at,
127
+ created_date,
128
+ author,
129
+ text,
130
+ lang,
131
+ is_reply,
132
+ is_quote,
133
+ is_repost,
134
+ is_original,
135
+ has_links,
136
+ has_media,
137
+ has_embed,
138
+ has_images,
139
+ has_video,
140
+ like_count,
141
+ repost_count,
142
+ reply_count,
143
+ post_json
144
+ )
145
+ VALUES (
146
+ ${post.uri},
147
+ ${createdAt},
148
+ ${createdDate},
149
+ ${post.author},
150
+ ${post.text},
151
+ ${lang},
152
+ ${toFlag(isReply)},
153
+ ${toFlag(quote)},
154
+ ${toFlag(repost)},
155
+ ${toFlag(original)},
156
+ ${toFlag(links)},
157
+ ${toFlag(media)},
158
+ ${toFlag(embed)},
159
+ ${toFlag(images)},
160
+ ${toFlag(video)},
161
+ ${likeCount},
162
+ ${repostCount},
163
+ ${replyCount},
164
+ ${postJson}
165
+ )
166
+ ON CONFLICT(uri) DO UPDATE SET
167
+ created_at = excluded.created_at,
168
+ created_date = excluded.created_date,
169
+ author = excluded.author,
170
+ text = excluded.text,
171
+ lang = excluded.lang,
172
+ is_reply = excluded.is_reply,
173
+ is_quote = excluded.is_quote,
174
+ is_repost = excluded.is_repost,
175
+ is_original = excluded.is_original,
176
+ has_links = excluded.has_links,
177
+ has_media = excluded.has_media,
178
+ has_embed = excluded.has_embed,
179
+ has_images = excluded.has_images,
180
+ has_video = excluded.has_video,
181
+ like_count = excluded.like_count,
182
+ repost_count = excluded.repost_count,
183
+ reply_count = excluded.reply_count,
184
+ post_json = excluded.post_json`;
185
+
186
+ yield* sql`DELETE FROM post_hashtag WHERE uri = ${post.uri}`;
187
+
188
+ const tags = Array.from(new Set(post.hashtags));
189
+ if (tags.length > 0) {
190
+ const rows = tags.map((tag) => ({ uri: post.uri, tag }));
191
+ yield* sql`INSERT INTO post_hashtag ${sql.insert(rows)}`;
192
+ }
193
+
194
+ yield* sql`DELETE FROM post_lang WHERE uri = ${post.uri}`;
195
+ if (normalizedLangs.length > 0) {
196
+ const rows = normalizedLangs.map((lang) => ({ uri: post.uri, lang }));
197
+ yield* sql`INSERT INTO post_lang ${sql.insert(rows)}`;
198
+ }
199
+ });
200
+
201
+ export const insertPostIfMissing = (
202
+ sql: SqlClient.SqlClient,
203
+ post: Post
204
+ ) =>
205
+ Effect.gen(function* () {
206
+ const createdAt = toIso(post.createdAt);
207
+ const createdDate = createdAt.slice(0, 10);
208
+ const postJson = yield* encodePostJson(post);
209
+ const normalizedLangs = normalizeLangs(post.langs);
210
+ const lang = normalizedLangs[0];
211
+ const isReply = Boolean(post.reply);
212
+ const quote = isQuote(post);
213
+ const repost = isRepost(post);
214
+ const original = !isReply && !quote && !repost;
215
+ const links = hasExternalLink(post);
216
+ const images = hasImages(post);
217
+ const video = hasVideo(post);
218
+ const media = hasMedia(post);
219
+ const embed = hasEmbed(post);
220
+ const metrics = post.metrics;
221
+ const likeCount = metrics?.likeCount ?? 0;
222
+ const repostCount = metrics?.repostCount ?? 0;
223
+ const replyCount = metrics?.replyCount ?? 0;
224
+
225
+ const rows = yield* sql`INSERT INTO posts (
226
+ uri,
227
+ created_at,
228
+ created_date,
229
+ author,
230
+ text,
231
+ lang,
232
+ is_reply,
233
+ is_quote,
234
+ is_repost,
235
+ is_original,
236
+ has_links,
237
+ has_media,
238
+ has_embed,
239
+ has_images,
240
+ has_video,
241
+ like_count,
242
+ repost_count,
243
+ reply_count,
244
+ post_json
245
+ )
246
+ VALUES (
247
+ ${post.uri},
248
+ ${createdAt},
249
+ ${createdDate},
250
+ ${post.author},
251
+ ${post.text},
252
+ ${lang},
253
+ ${toFlag(isReply)},
254
+ ${toFlag(quote)},
255
+ ${toFlag(repost)},
256
+ ${toFlag(original)},
257
+ ${toFlag(links)},
258
+ ${toFlag(media)},
259
+ ${toFlag(embed)},
260
+ ${toFlag(images)},
261
+ ${toFlag(video)},
262
+ ${likeCount},
263
+ ${repostCount},
264
+ ${replyCount},
265
+ ${postJson}
266
+ )
267
+ ON CONFLICT(uri) DO NOTHING
268
+ RETURNING uri`;
269
+
270
+ if (rows.length === 0) {
271
+ return false;
272
+ }
273
+
274
+ const tags = Array.from(new Set(post.hashtags));
275
+ if (tags.length > 0) {
276
+ const tagRows = tags.map((tag) => ({ uri: post.uri, tag }));
277
+ yield* sql`INSERT INTO post_hashtag ${sql.insert(tagRows)}`;
278
+ }
279
+
280
+ if (normalizedLangs.length > 0) {
281
+ const langRows = normalizedLangs.map((lang) => ({ uri: post.uri, lang }));
282
+ yield* sql`INSERT INTO post_lang ${sql.insert(langRows)}`;
283
+ }
284
+
285
+ return true;
286
+ });
287
+
288
+ export const deletePost = (sql: SqlClient.SqlClient, uri: PostUri) =>
289
+ sql`DELETE FROM posts WHERE uri = ${uri}`.pipe(Effect.asVoid);