@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.
- package/README.md +59 -0
- package/index.ts +146 -0
- package/package.json +56 -0
- package/src/cli/app.ts +75 -0
- package/src/cli/config-command.ts +140 -0
- package/src/cli/config.ts +91 -0
- package/src/cli/derive.ts +205 -0
- package/src/cli/doc/annotation.ts +36 -0
- package/src/cli/doc/filter.ts +69 -0
- package/src/cli/doc/index.ts +9 -0
- package/src/cli/doc/post.ts +155 -0
- package/src/cli/doc/primitives.ts +25 -0
- package/src/cli/doc/render.ts +18 -0
- package/src/cli/doc/table.ts +114 -0
- package/src/cli/doc/thread.ts +46 -0
- package/src/cli/doc/tree.ts +126 -0
- package/src/cli/errors.ts +59 -0
- package/src/cli/exit-codes.ts +52 -0
- package/src/cli/feed.ts +177 -0
- package/src/cli/filter-dsl.ts +1411 -0
- package/src/cli/filter-errors.ts +208 -0
- package/src/cli/filter-help.ts +70 -0
- package/src/cli/filter-input.ts +54 -0
- package/src/cli/filter.ts +435 -0
- package/src/cli/graph.ts +472 -0
- package/src/cli/help.ts +14 -0
- package/src/cli/interval.ts +35 -0
- package/src/cli/jetstream.ts +173 -0
- package/src/cli/layers.ts +180 -0
- package/src/cli/logging.ts +136 -0
- package/src/cli/output-format.ts +26 -0
- package/src/cli/output.ts +82 -0
- package/src/cli/parse.ts +80 -0
- package/src/cli/post.ts +193 -0
- package/src/cli/preferences.ts +11 -0
- package/src/cli/query-fields.ts +247 -0
- package/src/cli/query.ts +415 -0
- package/src/cli/range.ts +44 -0
- package/src/cli/search.ts +465 -0
- package/src/cli/shared-options.ts +169 -0
- package/src/cli/shared.ts +20 -0
- package/src/cli/store-errors.ts +80 -0
- package/src/cli/store-tree.ts +392 -0
- package/src/cli/store.ts +395 -0
- package/src/cli/sync-factory.ts +107 -0
- package/src/cli/sync.ts +366 -0
- package/src/cli/view-thread.ts +196 -0
- package/src/cli/view.ts +47 -0
- package/src/cli/watch.ts +344 -0
- package/src/db/migrations/store-catalog/001_init.ts +14 -0
- package/src/db/migrations/store-index/001_init.ts +34 -0
- package/src/db/migrations/store-index/002_event_log.ts +24 -0
- package/src/db/migrations/store-index/003_fts_and_derived.ts +52 -0
- package/src/db/migrations/store-index/004_query_indexes.ts +9 -0
- package/src/db/migrations/store-index/005_post_lang.ts +15 -0
- package/src/db/migrations/store-index/006_has_embed.ts +10 -0
- package/src/db/migrations/store-index/007_event_seq_and_checkpoints.ts +68 -0
- package/src/domain/bsky.ts +467 -0
- package/src/domain/config.ts +11 -0
- package/src/domain/credentials.ts +6 -0
- package/src/domain/defaults.ts +8 -0
- package/src/domain/derivation.ts +55 -0
- package/src/domain/errors.ts +71 -0
- package/src/domain/events.ts +55 -0
- package/src/domain/extract.ts +64 -0
- package/src/domain/filter-describe.ts +551 -0
- package/src/domain/filter-explain.ts +9 -0
- package/src/domain/filter.ts +797 -0
- package/src/domain/format.ts +91 -0
- package/src/domain/index.ts +13 -0
- package/src/domain/indexes.ts +17 -0
- package/src/domain/policies.ts +16 -0
- package/src/domain/post.ts +88 -0
- package/src/domain/primitives.ts +50 -0
- package/src/domain/raw.ts +140 -0
- package/src/domain/store.ts +103 -0
- package/src/domain/sync.ts +211 -0
- package/src/domain/text-width.ts +56 -0
- package/src/services/app-config.ts +278 -0
- package/src/services/bsky-client.ts +2113 -0
- package/src/services/credential-store.ts +408 -0
- package/src/services/derivation-engine.ts +502 -0
- package/src/services/derivation-settings.ts +61 -0
- package/src/services/derivation-validator.ts +68 -0
- package/src/services/filter-compiler.ts +269 -0
- package/src/services/filter-library.ts +371 -0
- package/src/services/filter-runtime.ts +821 -0
- package/src/services/filter-settings.ts +30 -0
- package/src/services/identity-resolver.ts +563 -0
- package/src/services/jetstream-sync.ts +636 -0
- package/src/services/lineage-store.ts +89 -0
- package/src/services/link-validator.ts +244 -0
- package/src/services/output-manager.ts +274 -0
- package/src/services/post-parser.ts +62 -0
- package/src/services/profile-resolver.ts +223 -0
- package/src/services/resource-monitor.ts +106 -0
- package/src/services/shared.ts +69 -0
- package/src/services/store-cleaner.ts +43 -0
- package/src/services/store-commit.ts +168 -0
- package/src/services/store-db.ts +248 -0
- package/src/services/store-event-log.ts +285 -0
- package/src/services/store-index-sql.ts +289 -0
- package/src/services/store-index.ts +1152 -0
- package/src/services/store-keys.ts +4 -0
- package/src/services/store-manager.ts +358 -0
- package/src/services/store-stats.ts +522 -0
- package/src/services/store-writer.ts +200 -0
- package/src/services/sync-checkpoint-store.ts +169 -0
- package/src/services/sync-engine.ts +547 -0
- package/src/services/sync-reporter.ts +16 -0
- package/src/services/sync-settings.ts +72 -0
- package/src/services/trending-topics.ts +226 -0
- package/src/services/view-checkpoint-store.ts +238 -0
- package/src/typeclass/chunk.ts +84 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Post } from "./post.js";
|
|
2
|
+
import { displayWidth, padEndDisplay } from "./text-width.js";
|
|
3
|
+
|
|
4
|
+
const headers = ["Created At", "Author", "Text", "URI"];
|
|
5
|
+
const textLimit = 80;
|
|
6
|
+
|
|
7
|
+
export const normalizeWhitespace = (text: string) =>
|
|
8
|
+
text
|
|
9
|
+
.replace(/\r\n/g, "\n")
|
|
10
|
+
.replace(/\r/g, "\n")
|
|
11
|
+
.split("\n")
|
|
12
|
+
.map((line) => line.replace(/[ \t]+/g, " ").trim())
|
|
13
|
+
.join("\n")
|
|
14
|
+
.trim();
|
|
15
|
+
|
|
16
|
+
export const collapseWhitespace = (text: string) =>
|
|
17
|
+
normalizeWhitespace(text).replace(/\n+/g, " ").trim();
|
|
18
|
+
|
|
19
|
+
export const truncate = (text: string, max: number) => {
|
|
20
|
+
if (text.length <= max) return text;
|
|
21
|
+
if (max <= 3) return text.slice(0, max);
|
|
22
|
+
return `${text.slice(0, max - 3)}...`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const sanitizeText = (text: string) => truncate(collapseWhitespace(text), textLimit);
|
|
26
|
+
|
|
27
|
+
const sanitizeMarkdown = (text: string) =>
|
|
28
|
+
sanitizeText(text).replace(/[\\|*_`\[\]]/g, "\\$&");
|
|
29
|
+
|
|
30
|
+
const postToRow = (post: Post) => [
|
|
31
|
+
post.createdAt.toISOString(),
|
|
32
|
+
post.author,
|
|
33
|
+
sanitizeText(post.text),
|
|
34
|
+
post.uri
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const postToMarkdownRow = (post: Post) => [
|
|
38
|
+
post.createdAt.toISOString(),
|
|
39
|
+
post.author,
|
|
40
|
+
sanitizeMarkdown(post.text),
|
|
41
|
+
post.uri.replace(/\|/g, "\\|")
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const renderTable = (
|
|
45
|
+
head: ReadonlyArray<string>,
|
|
46
|
+
rows: ReadonlyArray<ReadonlyArray<string>>
|
|
47
|
+
) => {
|
|
48
|
+
const widths = head.map((value, index) =>
|
|
49
|
+
Math.max(
|
|
50
|
+
displayWidth(value),
|
|
51
|
+
...rows.map((row) => displayWidth(row[index] ?? ""))
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const formatRow = (row: ReadonlyArray<string>) =>
|
|
56
|
+
row
|
|
57
|
+
.map((cell, index) => padEndDisplay(cell ?? "", widths[index] ?? 0))
|
|
58
|
+
.join(" ");
|
|
59
|
+
|
|
60
|
+
const header = formatRow(head);
|
|
61
|
+
const separator = widths.map((width) => "-".repeat(width)).join(" ");
|
|
62
|
+
const body = rows.map(formatRow);
|
|
63
|
+
|
|
64
|
+
return [header, separator, ...body].join("\n");
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const renderMarkdownHeader = (head: ReadonlyArray<string>) => {
|
|
68
|
+
const header = `| ${head.join(" | ")} |`;
|
|
69
|
+
const separator = `| ${head.map((label) => "-".repeat(Math.max(label.length, 3))).join(" | ")} |`;
|
|
70
|
+
return `${header}\n${separator}`;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const renderPostsMarkdownHeader = () => renderMarkdownHeader(headers);
|
|
74
|
+
|
|
75
|
+
export const renderPostMarkdownRow = (post: Post) =>
|
|
76
|
+
`| ${postToMarkdownRow(post).join(" | ")} |`;
|
|
77
|
+
|
|
78
|
+
const renderMarkdownTable = (
|
|
79
|
+
head: ReadonlyArray<string>,
|
|
80
|
+
rows: ReadonlyArray<ReadonlyArray<string>>
|
|
81
|
+
) => {
|
|
82
|
+
const header = renderMarkdownHeader(head);
|
|
83
|
+
const body = rows.map((row) => `| ${row.join(" | ")} |`);
|
|
84
|
+
return [header, ...body].join("\n");
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const renderPostsTable = (posts: ReadonlyArray<Post>) =>
|
|
88
|
+
renderTable(headers, posts.map(postToRow));
|
|
89
|
+
|
|
90
|
+
export const renderPostsMarkdown = (posts: ReadonlyArray<Post>) =>
|
|
91
|
+
renderMarkdownTable(headers, posts.map(postToMarkdownRow));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./primitives.js";
|
|
2
|
+
export * from "./post.js";
|
|
3
|
+
export * from "./policies.js";
|
|
4
|
+
export * from "./filter.js";
|
|
5
|
+
export * from "./errors.js";
|
|
6
|
+
export * from "./events.js";
|
|
7
|
+
export * from "./store.js";
|
|
8
|
+
export * from "./extract.js";
|
|
9
|
+
export * from "./raw.js";
|
|
10
|
+
export * from "./sync.js";
|
|
11
|
+
export * from "./defaults.js";
|
|
12
|
+
export * from "./bsky.js";
|
|
13
|
+
export * from "./credentials.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { EventSeq, Handle, Hashtag, PostUri, Timestamp } from "./primitives.js";
|
|
3
|
+
|
|
4
|
+
export class PostIndexEntry extends Schema.Class<PostIndexEntry>("PostIndexEntry")({
|
|
5
|
+
uri: PostUri,
|
|
6
|
+
createdDate: Schema.String,
|
|
7
|
+
hashtags: Schema.Array(Hashtag),
|
|
8
|
+
author: Schema.optional(Handle)
|
|
9
|
+
}) {}
|
|
10
|
+
|
|
11
|
+
export class IndexCheckpoint extends Schema.Class<IndexCheckpoint>("IndexCheckpoint")({
|
|
12
|
+
index: Schema.String,
|
|
13
|
+
version: Schema.NonNegativeInt,
|
|
14
|
+
lastEventSeq: EventSeq,
|
|
15
|
+
eventCount: Schema.NonNegativeInt,
|
|
16
|
+
updatedAt: Timestamp
|
|
17
|
+
}) {}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export class IncludeOnError extends Schema.TaggedClass<IncludeOnError>()("Include", {}) {}
|
|
4
|
+
export class ExcludeOnError extends Schema.TaggedClass<ExcludeOnError>()("Exclude", {}) {}
|
|
5
|
+
|
|
6
|
+
export class RetryOnError extends Schema.TaggedClass<RetryOnError>()("Retry", {
|
|
7
|
+
maxRetries: Schema.NonNegativeInt,
|
|
8
|
+
baseDelay: Schema.Duration
|
|
9
|
+
}) {}
|
|
10
|
+
|
|
11
|
+
export const FilterErrorPolicy = Schema.Union(
|
|
12
|
+
IncludeOnError,
|
|
13
|
+
ExcludeOnError,
|
|
14
|
+
RetryOnError
|
|
15
|
+
);
|
|
16
|
+
export type FilterErrorPolicy = typeof FilterErrorPolicy.Type;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { Did, Handle, Hashtag, PostCid, PostUri, Timestamp } from "./primitives.js";
|
|
3
|
+
import {
|
|
4
|
+
FeedContext,
|
|
5
|
+
Label,
|
|
6
|
+
PostEmbed,
|
|
7
|
+
PostMetrics,
|
|
8
|
+
PostViewerState,
|
|
9
|
+
ProfileBasic,
|
|
10
|
+
ReplyRef,
|
|
11
|
+
RichTextFacet,
|
|
12
|
+
SelfLabel,
|
|
13
|
+
ThreadgateView
|
|
14
|
+
} from "./bsky.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A normalized Bluesky post suitable for storage and filtering.
|
|
18
|
+
*
|
|
19
|
+
* This is the core data model for Skygent. Posts are parsed from Bluesky's
|
|
20
|
+
* AT Protocol format and normalized to a consistent structure for querying
|
|
21
|
+
* and filtering. Each post captures the content, metadata, relationships,
|
|
22
|
+
* and engagement metrics from the original Bluesky post.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const post = new Post({
|
|
27
|
+
* uri: "at://did:plc:abc/app.bsky.feed.post/123",
|
|
28
|
+
* author: "@alice.bsky.social",
|
|
29
|
+
* text: "Hello, Bluesky! #introduction",
|
|
30
|
+
* createdAt: "2024-01-15T10:30:00Z",
|
|
31
|
+
* hashtags: ["introduction"],
|
|
32
|
+
* mentions: [],
|
|
33
|
+
* links: []
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export class Post extends Schema.Class<Post>("Post")({
|
|
38
|
+
/** The AT Protocol URI uniquely identifying this post */
|
|
39
|
+
uri: PostUri,
|
|
40
|
+
/** The content identifier (CID) of the post record (optional for some contexts) */
|
|
41
|
+
cid: Schema.optional(PostCid),
|
|
42
|
+
/** The author's Bluesky handle (e.g., "@alice.bsky.social") */
|
|
43
|
+
author: Handle,
|
|
44
|
+
/** The author's decentralized identifier (DID) if resolved */
|
|
45
|
+
authorDid: Schema.optional(Did),
|
|
46
|
+
/** The author's profile information including display name and avatar */
|
|
47
|
+
authorProfile: Schema.optional(ProfileBasic),
|
|
48
|
+
/** The full text content of the post */
|
|
49
|
+
text: Schema.String,
|
|
50
|
+
/** ISO timestamp when the post was created */
|
|
51
|
+
createdAt: Timestamp,
|
|
52
|
+
/** Array of hashtags extracted from the post text (without # prefix) */
|
|
53
|
+
hashtags: Schema.Array(Hashtag),
|
|
54
|
+
/** Array of @mentions in the post (without @ prefix) */
|
|
55
|
+
mentions: Schema.Array(Handle),
|
|
56
|
+
/** Array of DIDs corresponding to the mentions if resolved */
|
|
57
|
+
mentionDids: Schema.optional(Schema.Array(Did)),
|
|
58
|
+
/** Array of external URLs extracted from the post */
|
|
59
|
+
links: Schema.Array(Schema.URL),
|
|
60
|
+
/** Rich text facets defining formatting and entity positions */
|
|
61
|
+
facets: Schema.optional(Schema.Array(RichTextFacet)),
|
|
62
|
+
/** Reference to the parent post if this is a reply */
|
|
63
|
+
reply: Schema.optional(ReplyRef),
|
|
64
|
+
/** Embedded media or records (images, videos, external links, quotes) */
|
|
65
|
+
embed: Schema.optional(PostEmbed),
|
|
66
|
+
/** The raw embedded record data for complex embed types */
|
|
67
|
+
recordEmbed: Schema.optional(Schema.Unknown),
|
|
68
|
+
/** Array of language codes for the post content */
|
|
69
|
+
langs: Schema.optional(Schema.Array(Schema.String)),
|
|
70
|
+
/** Array of user-defined tags on the post */
|
|
71
|
+
tags: Schema.optional(Schema.Array(Schema.String)),
|
|
72
|
+
/** Self-applied content labels for moderation */
|
|
73
|
+
selfLabels: Schema.optional(Schema.Array(SelfLabel)),
|
|
74
|
+
/** Moderation labels applied to this post */
|
|
75
|
+
labels: Schema.optional(Schema.Array(Label)),
|
|
76
|
+
/** Engagement metrics (likes, reposts, replies, quotes) */
|
|
77
|
+
metrics: Schema.optional(PostMetrics),
|
|
78
|
+
/** ISO timestamp when the post was indexed by Bluesky */
|
|
79
|
+
indexedAt: Schema.optional(Timestamp),
|
|
80
|
+
/** Viewer-specific state (like/repost status, thread muting) */
|
|
81
|
+
viewer: Schema.optional(PostViewerState),
|
|
82
|
+
/** Thread moderation settings if applied */
|
|
83
|
+
threadgate: Schema.optional(ThreadgateView),
|
|
84
|
+
/** Debug information for development purposes */
|
|
85
|
+
debug: Schema.optional(Schema.Unknown),
|
|
86
|
+
/** Context about how this post was retrieved (timeline, feed, etc.) */
|
|
87
|
+
feed: Schema.optional(FeedContext)
|
|
88
|
+
}) {}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export const Handle = Schema.Lowercase.pipe(
|
|
4
|
+
Schema.pattern(/^[a-z0-9][a-z0-9.-]{1,251}$/),
|
|
5
|
+
Schema.brand("Handle")
|
|
6
|
+
);
|
|
7
|
+
export type Handle = typeof Handle.Type;
|
|
8
|
+
|
|
9
|
+
// Bluesky's lexicon only requires: maxLength 640 bytes, maxGraphemes 64
|
|
10
|
+
// The extraction regex is stricter, but facets can contain anything
|
|
11
|
+
// We accept whatever Bluesky sends to avoid losing posts
|
|
12
|
+
// Require at least one non-whitespace character after #
|
|
13
|
+
export const Hashtag = Schema.String.pipe(
|
|
14
|
+
Schema.pattern(/^#\S.*$/u),
|
|
15
|
+
Schema.brand("Hashtag")
|
|
16
|
+
);
|
|
17
|
+
export type Hashtag = typeof Hashtag.Type;
|
|
18
|
+
|
|
19
|
+
export const AtUri = Schema.String.pipe(Schema.brand("AtUri"));
|
|
20
|
+
export type AtUri = typeof AtUri.Type;
|
|
21
|
+
|
|
22
|
+
export const PostUri = Schema.String.pipe(Schema.brand("PostUri"));
|
|
23
|
+
export type PostUri = typeof PostUri.Type;
|
|
24
|
+
|
|
25
|
+
export const PostCid = Schema.String.pipe(Schema.brand("PostCid"));
|
|
26
|
+
export type PostCid = typeof PostCid.Type;
|
|
27
|
+
|
|
28
|
+
export const Did = Schema.String.pipe(Schema.brand("Did"));
|
|
29
|
+
export type Did = typeof Did.Type;
|
|
30
|
+
|
|
31
|
+
export const Timestamp = Schema.Union(
|
|
32
|
+
Schema.DateFromString,
|
|
33
|
+
Schema.DateFromSelf
|
|
34
|
+
).pipe(Schema.brand("Timestamp"));
|
|
35
|
+
export type Timestamp = typeof Timestamp.Type;
|
|
36
|
+
|
|
37
|
+
export const EventId = Schema.ULID.pipe(Schema.brand("EventId"));
|
|
38
|
+
export type EventId = typeof EventId.Type;
|
|
39
|
+
|
|
40
|
+
export const EventSeq = Schema.NonNegativeInt.pipe(Schema.brand("EventSeq"));
|
|
41
|
+
export type EventSeq = typeof EventSeq.Type;
|
|
42
|
+
|
|
43
|
+
export const StoreName = Schema.String.pipe(
|
|
44
|
+
Schema.pattern(/^[a-z0-9][a-z0-9-_]{1,63}$/),
|
|
45
|
+
Schema.brand("StoreName")
|
|
46
|
+
);
|
|
47
|
+
export type StoreName = typeof StoreName.Type;
|
|
48
|
+
|
|
49
|
+
export const StorePath = Schema.String.pipe(Schema.brand("StorePath"));
|
|
50
|
+
export type StorePath = typeof StorePath.Type;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Effect, ParseResult, Schema } from "effect";
|
|
2
|
+
import { extractFromFacets, extractHashtags, extractLinks, extractMentions } from "./extract.js";
|
|
3
|
+
import { Post } from "./post.js";
|
|
4
|
+
import { Did, Handle, PostCid, PostUri } from "./primitives.js";
|
|
5
|
+
import {
|
|
6
|
+
EmbedUnknown,
|
|
7
|
+
FeedContext,
|
|
8
|
+
Label,
|
|
9
|
+
LegacyEntity,
|
|
10
|
+
PostEmbed,
|
|
11
|
+
PostMetrics,
|
|
12
|
+
PostViewerState,
|
|
13
|
+
ProfileBasic,
|
|
14
|
+
ReplyRef,
|
|
15
|
+
RichTextFacet,
|
|
16
|
+
SelfLabels,
|
|
17
|
+
ThreadgateView
|
|
18
|
+
} from "./bsky.js";
|
|
19
|
+
|
|
20
|
+
export class RawPostRecord extends Schema.Class<RawPostRecord>("RawPostRecord")({
|
|
21
|
+
$type: Schema.optional(Schema.Literal("app.bsky.feed.post")),
|
|
22
|
+
text: Schema.String,
|
|
23
|
+
createdAt: Schema.String,
|
|
24
|
+
facets: Schema.optional(Schema.Array(RichTextFacet)),
|
|
25
|
+
entities: Schema.optional(Schema.Array(LegacyEntity)),
|
|
26
|
+
reply: Schema.optional(ReplyRef),
|
|
27
|
+
embed: Schema.optional(Schema.Unknown),
|
|
28
|
+
langs: Schema.optional(Schema.Array(Schema.String)),
|
|
29
|
+
labels: Schema.optional(SelfLabels),
|
|
30
|
+
tags: Schema.optional(Schema.Array(Schema.String))
|
|
31
|
+
}) {}
|
|
32
|
+
|
|
33
|
+
export class RawPost extends Schema.Class<RawPost>("RawPost")({
|
|
34
|
+
uri: PostUri,
|
|
35
|
+
cid: Schema.optional(PostCid),
|
|
36
|
+
author: Handle,
|
|
37
|
+
authorDid: Schema.optional(Did),
|
|
38
|
+
authorProfile: Schema.optional(ProfileBasic),
|
|
39
|
+
record: RawPostRecord,
|
|
40
|
+
indexedAt: Schema.optional(Schema.String),
|
|
41
|
+
labels: Schema.optional(Schema.Array(Schema.encodedSchema(Label))),
|
|
42
|
+
metrics: Schema.optional(PostMetrics),
|
|
43
|
+
embed: Schema.optional(PostEmbed),
|
|
44
|
+
viewer: Schema.optional(PostViewerState),
|
|
45
|
+
threadgate: Schema.optional(ThreadgateView),
|
|
46
|
+
debug: Schema.optional(Schema.Unknown),
|
|
47
|
+
feed: Schema.optional(FeedContext),
|
|
48
|
+
_pageCursor: Schema.optional(Schema.String)
|
|
49
|
+
}) {}
|
|
50
|
+
|
|
51
|
+
export const PostFromRaw = Schema.transformOrFail(RawPost, Post, {
|
|
52
|
+
strict: true,
|
|
53
|
+
decode: (raw) => {
|
|
54
|
+
const unique = <T>(items: ReadonlyArray<T>) => Array.from(new Set(items));
|
|
55
|
+
const facetData = extractFromFacets(raw.record.facets);
|
|
56
|
+
const tagOverrides =
|
|
57
|
+
raw.record.tags?.map((tag) =>
|
|
58
|
+
tag.startsWith("#") ? tag : `#${tag}`
|
|
59
|
+
) ?? [];
|
|
60
|
+
const hashtags = unique([
|
|
61
|
+
...extractHashtags(raw.record.text),
|
|
62
|
+
...facetData.hashtags,
|
|
63
|
+
...tagOverrides
|
|
64
|
+
]);
|
|
65
|
+
const mentions = unique(extractMentions(raw.record.text));
|
|
66
|
+
const links = unique([
|
|
67
|
+
...extractLinks(raw.record.text),
|
|
68
|
+
...facetData.links
|
|
69
|
+
]);
|
|
70
|
+
const embed =
|
|
71
|
+
raw.embed ??
|
|
72
|
+
(raw.record.embed
|
|
73
|
+
? EmbedUnknown.make({
|
|
74
|
+
rawType:
|
|
75
|
+
typeof (raw.record.embed as { $type?: unknown })?.$type === "string"
|
|
76
|
+
? String((raw.record.embed as { $type?: unknown }).$type)
|
|
77
|
+
: "unknown",
|
|
78
|
+
data: raw.record.embed
|
|
79
|
+
})
|
|
80
|
+
: undefined);
|
|
81
|
+
return ParseResult.decodeUnknown(Schema.encodedSchema(Post))({
|
|
82
|
+
uri: raw.uri,
|
|
83
|
+
cid: raw.cid,
|
|
84
|
+
author: raw.author,
|
|
85
|
+
authorDid: raw.authorDid,
|
|
86
|
+
authorProfile: raw.authorProfile,
|
|
87
|
+
text: raw.record.text,
|
|
88
|
+
createdAt: raw.record.createdAt,
|
|
89
|
+
hashtags,
|
|
90
|
+
mentions,
|
|
91
|
+
mentionDids: facetData.mentionDids,
|
|
92
|
+
links,
|
|
93
|
+
facets: raw.record.facets,
|
|
94
|
+
reply: raw.record.reply,
|
|
95
|
+
embed,
|
|
96
|
+
langs: raw.record.langs,
|
|
97
|
+
tags: raw.record.tags,
|
|
98
|
+
selfLabels: raw.record.labels?.values,
|
|
99
|
+
labels: raw.labels,
|
|
100
|
+
metrics: raw.metrics,
|
|
101
|
+
indexedAt: raw.indexedAt,
|
|
102
|
+
viewer: raw.viewer,
|
|
103
|
+
threadgate: raw.threadgate,
|
|
104
|
+
debug: raw.debug,
|
|
105
|
+
feed: raw.feed,
|
|
106
|
+
recordEmbed: raw.record.embed
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
encode: (_encoded, _options, _ast, post) =>
|
|
110
|
+
Effect.gen(function* () {
|
|
111
|
+
const labels = post.labels
|
|
112
|
+
? yield* ParseResult.encodeUnknown(Schema.Array(Label))(post.labels)
|
|
113
|
+
: undefined;
|
|
114
|
+
return yield* ParseResult.decodeUnknown(RawPost)({
|
|
115
|
+
uri: post.uri,
|
|
116
|
+
cid: post.cid,
|
|
117
|
+
author: post.author,
|
|
118
|
+
authorDid: post.authorDid,
|
|
119
|
+
authorProfile: post.authorProfile,
|
|
120
|
+
indexedAt: post.indexedAt?.toISOString(),
|
|
121
|
+
labels,
|
|
122
|
+
metrics: post.metrics,
|
|
123
|
+
embed: post.embed,
|
|
124
|
+
viewer: post.viewer,
|
|
125
|
+
threadgate: post.threadgate,
|
|
126
|
+
debug: post.debug,
|
|
127
|
+
feed: post.feed,
|
|
128
|
+
record: {
|
|
129
|
+
text: post.text,
|
|
130
|
+
createdAt: post.createdAt.toISOString(),
|
|
131
|
+
facets: post.facets,
|
|
132
|
+
reply: post.reply,
|
|
133
|
+
embed: post.recordEmbed,
|
|
134
|
+
langs: post.langs,
|
|
135
|
+
labels: post.selfLabels ? { values: post.selfLabels } : undefined,
|
|
136
|
+
tags: post.tags
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
})
|
|
140
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { FilterExprSchema } from "./filter.js";
|
|
3
|
+
import { StoreName, StorePath, Timestamp } from "./primitives.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A reference to a Skygent store, uniquely identified by its name and root directory.
|
|
7
|
+
*
|
|
8
|
+
* Stores are the primary data containers in Skygent, holding filtered Bluesky posts.
|
|
9
|
+
* Each store maintains its own SQLite database and event log.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const storeRef = new StoreRef({
|
|
14
|
+
* name: "tech-posts",
|
|
15
|
+
* root: "/path/to/.skygent"
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class StoreRef extends Schema.Class<StoreRef>("StoreRef")({
|
|
20
|
+
/** The unique name of the store (2-64 characters, lowercase alphanumeric with hyphens and underscores) */
|
|
21
|
+
name: StoreName,
|
|
22
|
+
/** The absolute path to the store's root directory where database and files are stored */
|
|
23
|
+
root: StorePath
|
|
24
|
+
}) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Metadata about a store, tracking its creation and last update times.
|
|
28
|
+
*
|
|
29
|
+
* This is stored in the store's metadata file and used for tracking store state.
|
|
30
|
+
*/
|
|
31
|
+
export class StoreMetadata extends Schema.Class<StoreMetadata>("StoreMetadata")({
|
|
32
|
+
/** The unique name of the store */
|
|
33
|
+
name: StoreName,
|
|
34
|
+
/** The absolute path to the store's root directory */
|
|
35
|
+
root: StorePath,
|
|
36
|
+
/** ISO timestamp when the store was first created */
|
|
37
|
+
createdAt: Timestamp,
|
|
38
|
+
/** ISO timestamp when the store was last modified */
|
|
39
|
+
updatedAt: Timestamp
|
|
40
|
+
}) {}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Configuration for filter output format and destination.
|
|
44
|
+
*
|
|
45
|
+
* Defines how filtered posts should be exported from a store, including
|
|
46
|
+
* the output path and which formats (JSON, Markdown) to generate.
|
|
47
|
+
*/
|
|
48
|
+
export class FilterOutput extends Schema.Class<FilterOutput>("FilterOutput")({
|
|
49
|
+
/** The file system path where filtered output should be written */
|
|
50
|
+
path: Schema.String,
|
|
51
|
+
/** Whether to output posts as JSON */
|
|
52
|
+
json: Schema.Boolean,
|
|
53
|
+
/** Whether to output posts as Markdown */
|
|
54
|
+
markdown: Schema.Boolean
|
|
55
|
+
}) {}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A filter specification that defines which posts to include in a store.
|
|
59
|
+
*
|
|
60
|
+
* Each filter has a unique name within the store and an expression that
|
|
61
|
+
* determines which posts match. Filtered output can be exported to files.
|
|
62
|
+
*/
|
|
63
|
+
export class FilterSpec extends Schema.Class<FilterSpec>("FilterSpec")({
|
|
64
|
+
/** The unique name for this filter within the store */
|
|
65
|
+
name: Schema.String,
|
|
66
|
+
/** The filter expression that determines which posts to include */
|
|
67
|
+
expr: FilterExprSchema,
|
|
68
|
+
/** Configuration for how to output filtered posts */
|
|
69
|
+
output: FilterOutput
|
|
70
|
+
}) {}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Policy for handling duplicate posts during sync operations.
|
|
74
|
+
*
|
|
75
|
+
* - `dedupe`: Skip posts that already exist in the store (default)
|
|
76
|
+
* - `refresh`: Overwrite existing posts with fresh data from the API
|
|
77
|
+
*/
|
|
78
|
+
export const SyncUpsertPolicy = Schema.Literal("dedupe", "refresh");
|
|
79
|
+
export type SyncUpsertPolicy = typeof SyncUpsertPolicy.Type;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Complete configuration for a store.
|
|
83
|
+
*
|
|
84
|
+
* Defines the store's behavior including default output formats, automatic
|
|
85
|
+
* sync settings, and the set of filters that populate the store.
|
|
86
|
+
*/
|
|
87
|
+
export class StoreConfig extends Schema.Class<StoreConfig>("StoreConfig")({
|
|
88
|
+
/** Default output format settings for the store */
|
|
89
|
+
format: Schema.Struct({
|
|
90
|
+
/** Whether to enable JSON output by default */
|
|
91
|
+
json: Schema.Boolean,
|
|
92
|
+
/** Whether to enable Markdown output by default */
|
|
93
|
+
markdown: Schema.Boolean
|
|
94
|
+
}),
|
|
95
|
+
/** Whether to automatically sync when running watch mode */
|
|
96
|
+
autoSync: Schema.Boolean,
|
|
97
|
+
/** Optional ISO 8601 duration string for automatic sync intervals (e.g., "PT5M") */
|
|
98
|
+
syncInterval: Schema.optional(Schema.String),
|
|
99
|
+
/** Policy for handling duplicate posts during sync (defaults to "dedupe") */
|
|
100
|
+
syncPolicy: Schema.optional(SyncUpsertPolicy),
|
|
101
|
+
/** Array of filter specifications that determine which posts to store */
|
|
102
|
+
filters: Schema.Array(FilterSpec)
|
|
103
|
+
}) {}
|