@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,4 @@
1
+ import type { StoreRef } from "../domain/store.js";
2
+
3
+ export const storePrefix = (store: StoreRef): string =>
4
+ store.root.endsWith("/") ? store.root : `${store.root}/`;
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Store Manager Service
3
+ *
4
+ * Manages the lifecycle of content stores for Bluesky data.
5
+ * Stores are SQLite databases that persist filtered posts and metadata.
6
+ *
7
+ * **Responsibilities:**
8
+ * - Create new stores with configuration
9
+ * - List all existing stores
10
+ * - Retrieve store references and metadata
11
+ * - Delete stores and clean up resources
12
+ * - Manage store catalog database (SQLite)
13
+ *
14
+ * **Store Catalog:**
15
+ * Each store is tracked in a central catalog database (catalog.sqlite)
16
+ * with metadata: name, root path, creation date, config JSON.
17
+ *
18
+ * **Store Root:**
19
+ * Stores are organized under `{storeRoot}/stores/{storeName}/` with:
20
+ * - posts.sqlite - Main content database
21
+ * - Additional store-specific files
22
+ *
23
+ * **Database Schema:**
24
+ * The catalog database runs migrations from `../db/migrations/store-catalog`
25
+ * to maintain the stores table schema.
26
+ *
27
+ * @module services/store-manager
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * import { StoreManager } from "./services/store-manager.js";
32
+ * import { Effect } from "effect";
33
+ *
34
+ * const program = Effect.gen(function* () {
35
+ * const manager = yield* StoreManager;
36
+ *
37
+ * // Create a new store
38
+ * const storeRef = yield* manager.createStore("my-feed", {
39
+ * filter: { _tag: "Contains", text: "tech" },
40
+ * errorPolicy: { _tag: "Exclude" }
41
+ * });
42
+ *
43
+ * // List all stores
44
+ * const stores = yield* manager.listStores();
45
+ * for (const store of stores) {
46
+ * console.log(`${store.name}: ${store.root}`);
47
+ * }
48
+ *
49
+ * // Get store reference
50
+ * const ref = yield* manager.getStore("my-feed");
51
+ * if (Option.isSome(ref)) {
52
+ * console.log(`Store at: ${ref.value.root}`);
53
+ * }
54
+ * }).pipe(Effect.provide(StoreManager.layer));
55
+ * ```
56
+ */
57
+
58
+ import { FileSystem, Path } from "@effect/platform";
59
+ import { Chunk, Clock, Context, Effect, Exit, Layer, Option, Schema, Scope } from "effect";
60
+ import * as Reactivity from "@effect/experimental/Reactivity";
61
+ import * as Migrator from "@effect/sql/Migrator";
62
+ import * as MigratorFileSystem from "@effect/sql/Migrator/FileSystem";
63
+ import * as SqlClient from "@effect/sql/SqlClient";
64
+ import * as SqlSchema from "@effect/sql/SqlSchema";
65
+ import { SqliteClient } from "@effect/sql-sqlite-bun";
66
+ import { StoreIoError } from "../domain/errors.js";
67
+ import { StoreConfig, StoreMetadata, StoreRef } from "../domain/store.js";
68
+ import { StoreName, StorePath } from "../domain/primitives.js";
69
+ import { AppConfigService } from "./app-config.js";
70
+
71
+ const migrationsDir = decodeURIComponent(
72
+ new URL("../db/migrations/store-catalog", import.meta.url).pathname
73
+ );
74
+
75
+ const storeRootKey = (name: StoreName) => `stores/${name}`;
76
+ const manifestPath = Schema.decodeUnknownSync(StorePath)("stores");
77
+
78
+ const storeRow = Schema.Struct({
79
+ name: StoreName,
80
+ root: StorePath,
81
+ created_at: Schema.String,
82
+ updated_at: Schema.String,
83
+ config_json: Schema.String
84
+ });
85
+
86
+ const toStoreIoError = (path: StorePath) => (cause: unknown) =>
87
+ StoreIoError.make({ path, cause });
88
+
89
+ const decodeStorePath = (path: string) =>
90
+ Schema.decodeUnknown(StorePath)(path).pipe(
91
+ Effect.mapError(toStoreIoError(manifestPath))
92
+ );
93
+
94
+ const storeRefFromMetadata = (metadata: StoreMetadata) =>
95
+ StoreRef.make({ name: metadata.name, root: metadata.root });
96
+
97
+ /**
98
+ * Context tag and Layer implementation for the store manager service.
99
+ * Provides CRUD operations for content stores with SQLite persistence.
100
+ *
101
+ * **Methods:**
102
+ * - createStore: Creates a new store or returns existing if name exists
103
+ * - getStore: Retrieves store reference by name
104
+ * - listStores: Returns all stores sorted by name
105
+ * - getConfig: Gets configuration for a specific store
106
+ * - deleteStore: Removes a store from the catalog
107
+ *
108
+ * **Idempotency:**
109
+ * createStore is idempotent - if a store with the given name already exists,
110
+ * it returns the existing store reference instead of failing.
111
+ *
112
+ * **Error Handling:**
113
+ * All methods return StoreIoError for filesystem or database issues.
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * // Create and use a store
118
+ * const storeRef = yield* manager.createStore("tech-posts", {
119
+ * filter: { _tag: "Hashtag", tag: "tech" },
120
+ * errorPolicy: { _tag: "Retry", maxRetries: 3, baseDelay: Duration.seconds(1) }
121
+ * });
122
+ *
123
+ * // Check if store exists before creating
124
+ * const existing = yield* manager.getStore("tech-posts");
125
+ * if (Option.isNone(existing)) {
126
+ * yield* manager.createStore("tech-posts", config);
127
+ * }
128
+ *
129
+ * // Cleanup
130
+ * yield* manager.deleteStore("old-store");
131
+ * ```
132
+ */
133
+ export class StoreManager extends Context.Tag("@skygent/StoreManager")<
134
+ StoreManager,
135
+ {
136
+ /**
137
+ * Creates a new store or returns existing if name already exists.
138
+ * Stores creation timestamp and configuration in the catalog.
139
+ *
140
+ * @param name - Unique name for the store
141
+ * @param config - Store configuration including filter and error policy
142
+ * @returns Effect resolving to StoreRef (existing or newly created)
143
+ * @throws {StoreIoError} When database operations fail
144
+ */
145
+ readonly createStore: (
146
+ name: StoreName,
147
+ config: StoreConfig
148
+ ) => Effect.Effect<StoreRef, StoreIoError>;
149
+
150
+ /**
151
+ * Retrieves a store reference by name.
152
+ *
153
+ * @param name - Store name to look up
154
+ * @returns Effect resolving to Some(StoreRef) if found, None otherwise
155
+ * @throws {StoreIoError} When database operations fail
156
+ */
157
+ readonly getStore: (
158
+ name: StoreName
159
+ ) => Effect.Effect<Option.Option<StoreRef>, StoreIoError>;
160
+
161
+ /**
162
+ * Lists all stores sorted alphabetically by name.
163
+ *
164
+ * @returns Effect resolving to chunk of StoreMetadata
165
+ * @throws {StoreIoError} When database operations fail
166
+ */
167
+ readonly listStores: () => Effect.Effect<Chunk.Chunk<StoreMetadata>, StoreIoError>;
168
+
169
+ /**
170
+ * Retrieves configuration for a specific store.
171
+ *
172
+ * @param name - Store name to look up
173
+ * @returns Effect resolving to Some(StoreConfig) if found, None otherwise
174
+ * @throws {StoreIoError} When database operations fail or config parsing fails
175
+ */
176
+ readonly getConfig: (
177
+ name: StoreName
178
+ ) => Effect.Effect<Option.Option<StoreConfig>, StoreIoError>;
179
+
180
+ /**
181
+ * Deletes a store from the catalog.
182
+ * Note: This only removes the catalog entry, not the store files.
183
+ *
184
+ * @param name - Store name to delete
185
+ * @returns Effect resolving to void
186
+ * @throws {StoreIoError} When database operations fail
187
+ */
188
+ readonly deleteStore: (
189
+ name: StoreName
190
+ ) => Effect.Effect<void, StoreIoError>;
191
+ }
192
+ >() {
193
+ /**
194
+ * Scoped layer that creates the store manager service.
195
+ * Manages SQLite client lifecycle with automatic cleanup.
196
+ * Requires: AppConfigService, FileSystem, Path, Reactivity
197
+ */
198
+ static readonly layer = Layer.scoped(
199
+ StoreManager,
200
+ Effect.gen(function* () {
201
+ const appConfig = yield* AppConfigService;
202
+ const fs = yield* FileSystem.FileSystem;
203
+ const path = yield* Path.Path;
204
+ const reactivity = yield* Reactivity.Reactivity;
205
+
206
+ const scope = yield* Scope.make();
207
+ yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
208
+
209
+ const dbPath = path.join(appConfig.storeRoot, "catalog.sqlite");
210
+ const dbDir = path.dirname(dbPath);
211
+ yield* fs.makeDirectory(dbDir, { recursive: true });
212
+
213
+ const client = yield* SqliteClient.make({ filename: dbPath }).pipe(
214
+ Effect.provideService(Scope.Scope, scope),
215
+ Effect.provideService(Reactivity.Reactivity, reactivity)
216
+ );
217
+
218
+ const migrate = Migrator.make({})({
219
+ loader: MigratorFileSystem.fromFileSystem(migrationsDir)
220
+ });
221
+ yield* migrate.pipe(
222
+ Effect.provideService(SqlClient.SqlClient, client),
223
+ Effect.provideService(FileSystem.FileSystem, fs)
224
+ );
225
+
226
+ const decodeMetadataRow = (row: typeof storeRow.Type) =>
227
+ Schema.decodeUnknown(StoreMetadata)({
228
+ name: row.name,
229
+ root: row.root,
230
+ createdAt: row.created_at,
231
+ updatedAt: row.updated_at
232
+ }).pipe(Effect.mapError(toStoreIoError(manifestPath)));
233
+
234
+ const decodeConfigRow = (row: typeof storeRow.Type) =>
235
+ Schema.decodeUnknown(Schema.parseJson(StoreConfig))(row.config_json).pipe(
236
+ Effect.mapError(toStoreIoError(manifestPath))
237
+ );
238
+
239
+ const encodeConfigJson = (config: StoreConfig) =>
240
+ Schema.encode(Schema.parseJson(StoreConfig))(config).pipe(
241
+ Effect.mapError(toStoreIoError(manifestPath))
242
+ );
243
+
244
+ const findStore = SqlSchema.findAll({
245
+ Request: StoreName,
246
+ Result: storeRow,
247
+ execute: (name) =>
248
+ client`SELECT name, root, created_at, updated_at, config_json FROM stores WHERE name = ${name}`
249
+ });
250
+
251
+ const listStoresSql = SqlSchema.findAll({
252
+ Request: Schema.Void,
253
+ Result: storeRow,
254
+ execute: () =>
255
+ client`SELECT name, root, created_at, updated_at, config_json FROM stores ORDER BY name ASC`
256
+ });
257
+
258
+ const insertStore = SqlSchema.void({
259
+ Request: storeRow,
260
+ execute: (row) =>
261
+ client`INSERT INTO stores ${client.insert(row)}`
262
+ });
263
+
264
+ const deleteStoreSql = SqlSchema.void({
265
+ Request: StoreName,
266
+ execute: (name) =>
267
+ client`DELETE FROM stores WHERE name = ${name}`
268
+ });
269
+
270
+ const createStore = Effect.fn("StoreManager.createStore")(
271
+ (name: StoreName, config: StoreConfig) =>
272
+ decodeStorePath(storeRootKey(name)).pipe(
273
+ Effect.flatMap((root) =>
274
+ Effect.gen(function* () {
275
+ const existingRows = yield* findStore(name);
276
+ if (existingRows.length > 0) {
277
+ const existing = yield* decodeMetadataRow(existingRows[0]!);
278
+ return storeRefFromMetadata(existing);
279
+ }
280
+
281
+ const nowMillis = yield* Clock.currentTimeMillis;
282
+ const now = new Date(nowMillis).toISOString();
283
+ const configJson = yield* encodeConfigJson(config);
284
+ yield* insertStore({
285
+ name,
286
+ root,
287
+ created_at: now,
288
+ updated_at: now,
289
+ config_json: configJson
290
+ });
291
+
292
+ return StoreRef.make({ name, root });
293
+ }).pipe(Effect.mapError(toStoreIoError(root)))
294
+ )
295
+ )
296
+ );
297
+
298
+ const getStore = Effect.fn("StoreManager.getStore")((name: StoreName) => {
299
+ return decodeStorePath(storeRootKey(name)).pipe(
300
+ Effect.flatMap((root) =>
301
+ findStore(name).pipe(
302
+ Effect.flatMap((rows) =>
303
+ rows.length === 0
304
+ ? Effect.succeed(Option.none())
305
+ : decodeMetadataRow(rows[0]!).pipe(
306
+ Effect.map((metadata) =>
307
+ Option.some(storeRefFromMetadata(metadata))
308
+ )
309
+ )
310
+ ),
311
+ Effect.mapError(toStoreIoError(root))
312
+ )
313
+ )
314
+ );
315
+ });
316
+
317
+ const getConfig = Effect.fn("StoreManager.getConfig")((name: StoreName) => {
318
+ return decodeStorePath(storeRootKey(name)).pipe(
319
+ Effect.flatMap((root) =>
320
+ findStore(name).pipe(
321
+ Effect.flatMap((rows) =>
322
+ rows.length === 0
323
+ ? Effect.succeed(Option.none())
324
+ : decodeConfigRow(rows[0]!).pipe(Effect.map(Option.some))
325
+ ),
326
+ Effect.mapError(toStoreIoError(root))
327
+ )
328
+ )
329
+ );
330
+ });
331
+
332
+ const deleteStore = Effect.fn("StoreManager.deleteStore")((name: StoreName) => {
333
+ return decodeStorePath(storeRootKey(name)).pipe(
334
+ Effect.flatMap((root) =>
335
+ deleteStoreSql(name).pipe(Effect.mapError(toStoreIoError(root)))
336
+ )
337
+ );
338
+ });
339
+
340
+ const listStores = Effect.fn("StoreManager.listStores")(() =>
341
+ Effect.gen(function* () {
342
+ const rows = yield* listStoresSql(undefined);
343
+ if (rows.length === 0) {
344
+ return Chunk.empty<StoreMetadata>();
345
+ }
346
+ const decoded = yield* Effect.forEach(
347
+ rows,
348
+ (row) => decodeMetadataRow(row),
349
+ { discard: false }
350
+ );
351
+ return Chunk.fromIterable(decoded);
352
+ }).pipe(Effect.mapError(toStoreIoError(manifestPath)))
353
+ );
354
+
355
+ return StoreManager.of({ createStore, getStore, listStores, getConfig, deleteStore });
356
+ })
357
+ ).pipe(Layer.provide(Reactivity.layer));
358
+ }