@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,435 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { formatSchemaError } from "./shared.js";
|
|
3
|
+
import { Chunk, Clock, Effect, Option, Stream } from "effect";
|
|
4
|
+
import { StoreQuery } from "../domain/events.js";
|
|
5
|
+
import { RawPost } from "../domain/raw.js";
|
|
6
|
+
import type { Post } from "../domain/post.js";
|
|
7
|
+
import { StoreName } from "../domain/primitives.js";
|
|
8
|
+
import { BskyClient } from "../services/bsky-client.js";
|
|
9
|
+
import { FilterCompiler } from "../services/filter-compiler.js";
|
|
10
|
+
import { FilterLibrary } from "../services/filter-library.js";
|
|
11
|
+
import { FilterRuntime } from "../services/filter-runtime.js";
|
|
12
|
+
import { PostParser } from "../services/post-parser.js";
|
|
13
|
+
import { AppConfigService } from "../services/app-config.js";
|
|
14
|
+
import { StoreIndex } from "../services/store-index.js";
|
|
15
|
+
import { parseFilterExpr } from "./filter-input.js";
|
|
16
|
+
import { decodeJson } from "./parse.js";
|
|
17
|
+
import { writeJson, writeText } from "./output.js";
|
|
18
|
+
import { CliInputError, CliJsonError } from "./errors.js";
|
|
19
|
+
import { storeOptions } from "./store.js";
|
|
20
|
+
import { describeFilter, formatFilterExpr } from "../domain/filter-describe.js";
|
|
21
|
+
import { renderFilterDescriptionDoc } from "./doc/filter.js";
|
|
22
|
+
import { renderPlain, renderAnsi } from "./doc/render.js";
|
|
23
|
+
import { renderTableLegacy } from "./doc/table.js";
|
|
24
|
+
import { withExamples } from "./help.js";
|
|
25
|
+
import { filterOption, filterJsonOption } from "./shared-options.js";
|
|
26
|
+
import { jsonTableFormats, resolveOutputFormat, textJsonFormats } from "./output-format.js";
|
|
27
|
+
|
|
28
|
+
const filterNameArg = Args.text({ name: "name" }).pipe(
|
|
29
|
+
Args.withSchema(StoreName),
|
|
30
|
+
Args.withDescription("Filter name")
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const postJsonOption = Options.text("post-json").pipe(
|
|
34
|
+
Options.withDescription("Raw post JSON (app.bsky.feed.getPosts result)."),
|
|
35
|
+
Options.optional
|
|
36
|
+
);
|
|
37
|
+
const postUriOption = Options.text("post-uri").pipe(
|
|
38
|
+
Options.withDescription("Bluesky post URI (at://...)."),
|
|
39
|
+
Options.optional
|
|
40
|
+
);
|
|
41
|
+
const storeOption = Options.text("store").pipe(
|
|
42
|
+
Options.withSchema(StoreName),
|
|
43
|
+
Options.withDescription("Store name to sample for benchmarking")
|
|
44
|
+
);
|
|
45
|
+
const sampleSizeOption = Options.integer("sample-size").pipe(
|
|
46
|
+
Options.withDescription("Number of posts to evaluate (default: 1000)"),
|
|
47
|
+
Options.optional
|
|
48
|
+
);
|
|
49
|
+
const describeFormatOption = Options.choice("format", textJsonFormats).pipe(
|
|
50
|
+
Options.withDescription("Output format for filter descriptions (default: text)"),
|
|
51
|
+
Options.optional
|
|
52
|
+
);
|
|
53
|
+
const testFormatOption = Options.choice("format", textJsonFormats).pipe(
|
|
54
|
+
Options.withDescription("Output format for filter tests (default: text)"),
|
|
55
|
+
Options.optional
|
|
56
|
+
);
|
|
57
|
+
const listFormatOption = Options.choice("format", jsonTableFormats).pipe(
|
|
58
|
+
Options.withDescription("Output format (default: json)"),
|
|
59
|
+
Options.optional
|
|
60
|
+
);
|
|
61
|
+
const describeAnsiOption = Options.boolean("ansi").pipe(
|
|
62
|
+
Options.withDescription("Enable ANSI colors in output")
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const requireFilterExpr = (
|
|
66
|
+
filter: Option.Option<string>,
|
|
67
|
+
filterJson: Option.Option<string>
|
|
68
|
+
) =>
|
|
69
|
+
Option.match(filter, {
|
|
70
|
+
onNone: () =>
|
|
71
|
+
Option.match(filterJson, {
|
|
72
|
+
onNone: () =>
|
|
73
|
+
Effect.fail(
|
|
74
|
+
CliInputError.make({
|
|
75
|
+
message: "Provide --filter or --filter-json.",
|
|
76
|
+
cause: { filter: null, filterJson: null }
|
|
77
|
+
})
|
|
78
|
+
),
|
|
79
|
+
onSome: () => Effect.void
|
|
80
|
+
}),
|
|
81
|
+
onSome: () => Effect.void
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
const requireSinglePostInput = (
|
|
86
|
+
postJson: Option.Option<string>,
|
|
87
|
+
postUri: Option.Option<string>
|
|
88
|
+
) => {
|
|
89
|
+
if (Option.isSome(postJson) && Option.isSome(postUri)) {
|
|
90
|
+
return Effect.fail(
|
|
91
|
+
CliInputError.make({
|
|
92
|
+
message: "Use only one of --post-json or --post-uri.",
|
|
93
|
+
cause: { postJson: true, postUri: true }
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (Option.isNone(postJson) && Option.isNone(postUri)) {
|
|
98
|
+
return Effect.fail(
|
|
99
|
+
CliInputError.make({
|
|
100
|
+
message: "Provide --post-json or --post-uri.",
|
|
101
|
+
cause: { postJson: null, postUri: null }
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return Effect.void;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const parseRawPost = (raw: RawPost) =>
|
|
109
|
+
Effect.gen(function* () {
|
|
110
|
+
const parser = yield* PostParser;
|
|
111
|
+
return yield* parser.parsePost(raw).pipe(
|
|
112
|
+
Effect.mapError((error) =>
|
|
113
|
+
CliInputError.make({
|
|
114
|
+
message: `Invalid post payload: ${formatSchemaError(error)}`,
|
|
115
|
+
cause: error
|
|
116
|
+
})
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const loadPost = (
|
|
122
|
+
postJson: Option.Option<string>,
|
|
123
|
+
postUri: Option.Option<string>
|
|
124
|
+
): Effect.Effect<Post, CliInputError | CliJsonError, BskyClient | PostParser> =>
|
|
125
|
+
Effect.gen(function* () {
|
|
126
|
+
yield* requireSinglePostInput(postJson, postUri);
|
|
127
|
+
if (Option.isSome(postJson)) {
|
|
128
|
+
const raw = yield* decodeJson(RawPost, postJson.value);
|
|
129
|
+
return yield* parseRawPost(raw);
|
|
130
|
+
}
|
|
131
|
+
if (Option.isSome(postUri)) {
|
|
132
|
+
const client = yield* BskyClient;
|
|
133
|
+
const raw = yield* client.getPost(postUri.value).pipe(
|
|
134
|
+
Effect.mapError((error) =>
|
|
135
|
+
CliInputError.make({
|
|
136
|
+
message: `Failed to fetch post: ${error.message}`,
|
|
137
|
+
cause: error
|
|
138
|
+
})
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
return yield* parseRawPost(raw);
|
|
142
|
+
}
|
|
143
|
+
return yield* CliInputError.make({
|
|
144
|
+
message: "Provide --post-json or --post-uri.",
|
|
145
|
+
cause: { postJson: null, postUri: null }
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
export const filterList = Command.make("list", { format: listFormatOption }, ({ format }) =>
|
|
150
|
+
Effect.gen(function* () {
|
|
151
|
+
const appConfig = yield* AppConfigService;
|
|
152
|
+
const library = yield* FilterLibrary;
|
|
153
|
+
const names = yield* library.list();
|
|
154
|
+
const outputFormat = resolveOutputFormat(
|
|
155
|
+
format,
|
|
156
|
+
appConfig.outputFormat,
|
|
157
|
+
jsonTableFormats,
|
|
158
|
+
"json"
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (outputFormat === "table") {
|
|
162
|
+
const filters = yield* Effect.forEach(names, (name) =>
|
|
163
|
+
library.get(name as StoreName).pipe(Effect.map((expr) => ({ name, expr: formatFilterExpr(expr) })))
|
|
164
|
+
);
|
|
165
|
+
const rows = filters.map((f) => [f.name, f.expr]);
|
|
166
|
+
const table = renderTableLegacy(["NAME", "EXPRESSION"], rows);
|
|
167
|
+
yield* writeText(table);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
yield* writeJson(names);
|
|
172
|
+
})
|
|
173
|
+
).pipe(
|
|
174
|
+
Command.withDescription(
|
|
175
|
+
withExamples("List saved filters", ["skygent filter list"])
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
export const filterShow = Command.make(
|
|
180
|
+
"show",
|
|
181
|
+
{ name: filterNameArg },
|
|
182
|
+
({ name }) =>
|
|
183
|
+
Effect.gen(function* () {
|
|
184
|
+
const library = yield* FilterLibrary;
|
|
185
|
+
const expr = yield* library.get(name);
|
|
186
|
+
yield* writeJson(expr);
|
|
187
|
+
})
|
|
188
|
+
).pipe(
|
|
189
|
+
Command.withDescription(
|
|
190
|
+
withExamples("Show a saved filter", ["skygent filter show tech"])
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
export const filterCreate = Command.make(
|
|
195
|
+
"create",
|
|
196
|
+
{ name: filterNameArg, filter: filterOption, filterJson: filterJsonOption },
|
|
197
|
+
({ name, filter, filterJson }) =>
|
|
198
|
+
Effect.gen(function* () {
|
|
199
|
+
yield* requireFilterExpr(filter, filterJson);
|
|
200
|
+
const expr = yield* parseFilterExpr(filter, filterJson);
|
|
201
|
+
const library = yield* FilterLibrary;
|
|
202
|
+
yield* library.save(name, expr);
|
|
203
|
+
yield* writeJson({ name, saved: true });
|
|
204
|
+
})
|
|
205
|
+
).pipe(
|
|
206
|
+
Command.withDescription(
|
|
207
|
+
withExamples("Create or update a saved filter", [
|
|
208
|
+
"skygent filter create tech --filter 'hashtag:#tech'"
|
|
209
|
+
])
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
export const filterDelete = Command.make(
|
|
214
|
+
"delete",
|
|
215
|
+
{ name: filterNameArg },
|
|
216
|
+
({ name }) =>
|
|
217
|
+
Effect.gen(function* () {
|
|
218
|
+
const library = yield* FilterLibrary;
|
|
219
|
+
yield* library.remove(name);
|
|
220
|
+
yield* writeJson({ name, deleted: true });
|
|
221
|
+
})
|
|
222
|
+
).pipe(
|
|
223
|
+
Command.withDescription(
|
|
224
|
+
withExamples("Delete a saved filter", ["skygent filter delete tech"])
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
export const filterValidateAll = Command.make("validate-all", {}, () =>
|
|
229
|
+
Effect.gen(function* () {
|
|
230
|
+
const library = yield* FilterLibrary;
|
|
231
|
+
const results = yield* library.validateAll();
|
|
232
|
+
const summary = {
|
|
233
|
+
ok: results.filter((entry) => entry.ok).length,
|
|
234
|
+
failed: results.filter((entry) => !entry.ok).length
|
|
235
|
+
};
|
|
236
|
+
yield* writeJson({ summary, results });
|
|
237
|
+
})
|
|
238
|
+
).pipe(
|
|
239
|
+
Command.withDescription(
|
|
240
|
+
withExamples("Validate all saved filters", ["skygent filter validate-all"])
|
|
241
|
+
)
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
export const filterValidate = Command.make(
|
|
245
|
+
"validate",
|
|
246
|
+
{ filter: filterOption, filterJson: filterJsonOption },
|
|
247
|
+
({ filter, filterJson }) =>
|
|
248
|
+
Effect.gen(function* () {
|
|
249
|
+
yield* requireFilterExpr(filter, filterJson);
|
|
250
|
+
const expr = yield* parseFilterExpr(filter, filterJson);
|
|
251
|
+
const compiler = yield* FilterCompiler;
|
|
252
|
+
yield* compiler.validate(expr);
|
|
253
|
+
yield* writeJson({ ok: true });
|
|
254
|
+
})
|
|
255
|
+
).pipe(
|
|
256
|
+
Command.withDescription(
|
|
257
|
+
withExamples("Validate a filter expression", [
|
|
258
|
+
"skygent filter validate --filter 'author:alice.bsky.social'"
|
|
259
|
+
])
|
|
260
|
+
)
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
export const filterTest = Command.make(
|
|
264
|
+
"test",
|
|
265
|
+
{
|
|
266
|
+
filter: filterOption,
|
|
267
|
+
filterJson: filterJsonOption,
|
|
268
|
+
postJson: postJsonOption,
|
|
269
|
+
postUri: postUriOption,
|
|
270
|
+
format: testFormatOption
|
|
271
|
+
},
|
|
272
|
+
({ filter, filterJson, postJson, postUri, format }) =>
|
|
273
|
+
Effect.gen(function* () {
|
|
274
|
+
yield* requireFilterExpr(filter, filterJson);
|
|
275
|
+
const expr = yield* parseFilterExpr(filter, filterJson);
|
|
276
|
+
const runtime = yield* FilterRuntime;
|
|
277
|
+
const predicate = yield* runtime.evaluate(expr);
|
|
278
|
+
const post = yield* loadPost(postJson, postUri);
|
|
279
|
+
const ok = yield* predicate(post);
|
|
280
|
+
const outputFormat = Option.getOrElse(format, () => "text" as const);
|
|
281
|
+
if (outputFormat === "json") {
|
|
282
|
+
yield* writeJson({
|
|
283
|
+
ok,
|
|
284
|
+
post: { uri: post.uri, author: post.author },
|
|
285
|
+
filter: expr,
|
|
286
|
+
filterText: formatFilterExpr(expr)
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const author = post.author ? ` by ${post.author}` : "";
|
|
291
|
+
const lines = [
|
|
292
|
+
`Match: ${ok ? "yes" : "no"}`,
|
|
293
|
+
`Post: ${post.uri}${author}`,
|
|
294
|
+
`Filter: ${formatFilterExpr(expr)}`
|
|
295
|
+
];
|
|
296
|
+
yield* writeText(lines.join("\n"));
|
|
297
|
+
})
|
|
298
|
+
).pipe(
|
|
299
|
+
Command.withDescription(
|
|
300
|
+
withExamples("Test a filter against a single post", [
|
|
301
|
+
"skygent filter test --filter 'hashtag:#ai' --post-uri at://did:plc:example/app.bsky.feed.post/xyz"
|
|
302
|
+
])
|
|
303
|
+
)
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
export const filterExplain = Command.make(
|
|
307
|
+
"explain",
|
|
308
|
+
{
|
|
309
|
+
filter: filterOption,
|
|
310
|
+
filterJson: filterJsonOption,
|
|
311
|
+
postJson: postJsonOption,
|
|
312
|
+
postUri: postUriOption
|
|
313
|
+
},
|
|
314
|
+
({ filter, filterJson, postJson, postUri }) =>
|
|
315
|
+
Effect.gen(function* () {
|
|
316
|
+
yield* requireFilterExpr(filter, filterJson);
|
|
317
|
+
const expr = yield* parseFilterExpr(filter, filterJson);
|
|
318
|
+
const runtime = yield* FilterRuntime;
|
|
319
|
+
const explainer = yield* runtime.explain(expr);
|
|
320
|
+
const post = yield* loadPost(postJson, postUri);
|
|
321
|
+
const explanation = yield* explainer(post);
|
|
322
|
+
yield* writeJson({
|
|
323
|
+
ok: explanation.ok,
|
|
324
|
+
post: { uri: post.uri, author: post.author },
|
|
325
|
+
explanation
|
|
326
|
+
});
|
|
327
|
+
})
|
|
328
|
+
).pipe(
|
|
329
|
+
Command.withDescription(
|
|
330
|
+
withExamples("Explain why a post matches a filter", [
|
|
331
|
+
"skygent filter explain --filter 'hashtag:#ai' --post-uri at://did:plc:example/app.bsky.feed.post/xyz"
|
|
332
|
+
])
|
|
333
|
+
)
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
export const filterBenchmark = Command.make(
|
|
337
|
+
"benchmark",
|
|
338
|
+
{
|
|
339
|
+
store: storeOption,
|
|
340
|
+
filter: filterOption,
|
|
341
|
+
filterJson: filterJsonOption,
|
|
342
|
+
sampleSize: sampleSizeOption
|
|
343
|
+
},
|
|
344
|
+
({ store, filter, filterJson, sampleSize }) =>
|
|
345
|
+
Effect.gen(function* () {
|
|
346
|
+
yield* requireFilterExpr(filter, filterJson);
|
|
347
|
+
const expr = yield* parseFilterExpr(filter, filterJson);
|
|
348
|
+
const runtime = yield* FilterRuntime;
|
|
349
|
+
const index = yield* StoreIndex;
|
|
350
|
+
const storeRef = yield* storeOptions.loadStoreRef(store);
|
|
351
|
+
const limit = Option.getOrElse(sampleSize, () => 1000);
|
|
352
|
+
const evaluateBatch = yield* runtime.evaluateBatch(expr);
|
|
353
|
+
const query = StoreQuery.make({ scanLimit: limit, order: "desc" });
|
|
354
|
+
const stream = index.query(storeRef, query);
|
|
355
|
+
const start = yield* Clock.currentTimeMillis;
|
|
356
|
+
const result = yield* stream.pipe(
|
|
357
|
+
Stream.grouped(50),
|
|
358
|
+
Stream.runFoldEffect(
|
|
359
|
+
{ processed: 0, matched: 0 },
|
|
360
|
+
(state, batch) =>
|
|
361
|
+
evaluateBatch(batch).pipe(
|
|
362
|
+
Effect.map((results) => {
|
|
363
|
+
const processed = state.processed + Chunk.size(batch);
|
|
364
|
+
const matched =
|
|
365
|
+
state.matched +
|
|
366
|
+
Chunk.toReadonlyArray(results).filter(Boolean).length;
|
|
367
|
+
return { processed, matched };
|
|
368
|
+
})
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
);
|
|
372
|
+
const end = yield* Clock.currentTimeMillis;
|
|
373
|
+
const durationMs = end - start;
|
|
374
|
+
const avgMs = result.processed > 0 ? durationMs / result.processed : 0;
|
|
375
|
+
yield* writeJson({
|
|
376
|
+
store: storeRef.name,
|
|
377
|
+
processed: result.processed,
|
|
378
|
+
matched: result.matched,
|
|
379
|
+
durationMs,
|
|
380
|
+
avgMsPerPost: avgMs,
|
|
381
|
+
sampleSize: limit
|
|
382
|
+
});
|
|
383
|
+
})
|
|
384
|
+
).pipe(
|
|
385
|
+
Command.withDescription(
|
|
386
|
+
withExamples("Benchmark filter performance over stored posts", [
|
|
387
|
+
"skygent filter benchmark --store my-store --filter 'hashtag:#ai' --sample-size 500"
|
|
388
|
+
])
|
|
389
|
+
)
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
export const filterDescribe = Command.make(
|
|
393
|
+
"describe",
|
|
394
|
+
{ filter: filterOption, filterJson: filterJsonOption, format: describeFormatOption, ansi: describeAnsiOption },
|
|
395
|
+
({ filter, filterJson, format, ansi }) =>
|
|
396
|
+
Effect.gen(function* () {
|
|
397
|
+
yield* requireFilterExpr(filter, filterJson);
|
|
398
|
+
const expr = yield* parseFilterExpr(filter, filterJson);
|
|
399
|
+
const description = describeFilter(expr);
|
|
400
|
+
const outputFormat = Option.getOrElse(format, () => "text" as const);
|
|
401
|
+
if (outputFormat === "json") {
|
|
402
|
+
yield* writeJson(description);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const doc = renderFilterDescriptionDoc(description);
|
|
406
|
+
yield* writeText(ansi ? renderAnsi(doc) : renderPlain(doc));
|
|
407
|
+
})
|
|
408
|
+
).pipe(
|
|
409
|
+
Command.withDescription(
|
|
410
|
+
withExamples("Describe a filter in human-readable form", [
|
|
411
|
+
"skygent filter describe --filter 'hashtag:#ai'",
|
|
412
|
+
"skygent filter describe --filter 'hashtag:#ai' --format json"
|
|
413
|
+
])
|
|
414
|
+
)
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
export const filterCommand = Command.make("filter", {}).pipe(
|
|
418
|
+
Command.withSubcommands([
|
|
419
|
+
filterList,
|
|
420
|
+
filterShow,
|
|
421
|
+
filterCreate,
|
|
422
|
+
filterDelete,
|
|
423
|
+
filterValidateAll,
|
|
424
|
+
filterValidate,
|
|
425
|
+
filterTest,
|
|
426
|
+
filterExplain,
|
|
427
|
+
filterBenchmark,
|
|
428
|
+
filterDescribe
|
|
429
|
+
]),
|
|
430
|
+
Command.withDescription(
|
|
431
|
+
withExamples("Manage saved filters and filter tooling", [
|
|
432
|
+
"skygent filter list"
|
|
433
|
+
])
|
|
434
|
+
)
|
|
435
|
+
);
|