@mepuka/skygent 0.3.0 → 0.3.1
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/package.json +1 -1
- package/src/cli/compact-output.ts +52 -0
- package/src/cli/config.ts +30 -5
- package/src/cli/feed.ts +52 -15
- package/src/cli/filter-errors.ts +5 -9
- package/src/cli/filter.ts +5 -7
- package/src/cli/graph.ts +128 -37
- package/src/cli/interval.ts +4 -33
- package/src/cli/jetstream.ts +2 -0
- package/src/cli/option-schemas.ts +22 -0
- package/src/cli/output-format.ts +11 -0
- package/src/cli/pagination.ts +10 -11
- package/src/cli/parse-errors.ts +12 -0
- package/src/cli/parse.ts +1 -47
- package/src/cli/pipe-input.ts +18 -0
- package/src/cli/pipe.ts +16 -19
- package/src/cli/post.ts +57 -12
- package/src/cli/query-fields.ts +13 -3
- package/src/cli/query.ts +18 -39
- package/src/cli/search.ts +8 -25
- package/src/cli/shared-options.ts +11 -63
- package/src/cli/shared.ts +1 -1
- package/src/cli/store-errors.ts +5 -9
- package/src/cli/store.ts +6 -0
- package/src/cli/sync-factory.ts +13 -21
- package/src/cli/sync.ts +32 -51
- package/src/cli/thread-options.ts +8 -16
- package/src/cli/view-thread.ts +18 -15
- package/src/cli/watch.ts +12 -25
- package/src/domain/primitives.ts +20 -3
- package/src/services/bsky-client.ts +11 -5
- package/src/services/shared.ts +48 -1
- package/src/services/store-cleaner.ts +5 -2
- package/src/services/store-renamer.ts +3 -1
- package/src/services/sync-engine.ts +13 -4
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { FeedGeneratorView, ListItemView, ListView, PostLike, ProfileView } from "../domain/bsky.js";
|
|
2
|
+
import type { Post } from "../domain/post.js";
|
|
3
|
+
|
|
4
|
+
type CompactProfile = {
|
|
5
|
+
readonly did: ProfileView["did"];
|
|
6
|
+
readonly handle: ProfileView["handle"];
|
|
7
|
+
readonly displayName?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const compactProfile = (profile: ProfileView): CompactProfile => ({
|
|
11
|
+
did: profile.did,
|
|
12
|
+
handle: profile.handle,
|
|
13
|
+
...(profile.displayName ? { displayName: profile.displayName } : {})
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const compactProfileView = (profile: ProfileView) =>
|
|
17
|
+
compactProfile(profile);
|
|
18
|
+
|
|
19
|
+
export const compactFeedGeneratorView = (feed: FeedGeneratorView) => ({
|
|
20
|
+
uri: feed.uri,
|
|
21
|
+
displayName: feed.displayName,
|
|
22
|
+
creator: compactProfile(feed.creator),
|
|
23
|
+
...(feed.likeCount !== undefined ? { likeCount: feed.likeCount } : {})
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const compactListView = (list: ListView) => ({
|
|
27
|
+
uri: list.uri,
|
|
28
|
+
name: list.name,
|
|
29
|
+
purpose: list.purpose,
|
|
30
|
+
creator: compactProfile(list.creator),
|
|
31
|
+
...(list.listItemCount !== undefined
|
|
32
|
+
? { listItemCount: list.listItemCount }
|
|
33
|
+
: {})
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const compactListItemView = (item: ListItemView) => ({
|
|
37
|
+
uri: item.uri,
|
|
38
|
+
subject: compactProfile(item.subject)
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const compactPostLike = (like: PostLike) => ({
|
|
42
|
+
actor: compactProfile(like.actor),
|
|
43
|
+
createdAt: like.createdAt,
|
|
44
|
+
indexedAt: like.indexedAt
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const compactPost = (post: Post) => ({
|
|
48
|
+
uri: post.uri,
|
|
49
|
+
author: post.author,
|
|
50
|
+
text: post.text,
|
|
51
|
+
createdAt: post.createdAt
|
|
52
|
+
});
|
package/src/cli/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Options } from "@effect/cli";
|
|
1
|
+
import { HelpDoc, Options } from "@effect/cli";
|
|
2
2
|
import { Option, Redacted } from "effect";
|
|
3
3
|
import { pickDefined } from "../services/shared.js";
|
|
4
4
|
import { OutputFormat } from "../domain/config.js";
|
|
@@ -6,13 +6,33 @@ import { AppConfig } from "../domain/config.js";
|
|
|
6
6
|
import type { LogFormat } from "./logging.js";
|
|
7
7
|
import type { SyncSettingsValue } from "../services/sync-settings.js";
|
|
8
8
|
import type { CredentialsOverridesValue } from "../services/credential-store.js";
|
|
9
|
+
import { NonNegativeInt, PositiveInt } from "./option-schemas.js";
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
const compactOption = Options.
|
|
12
|
-
|
|
12
|
+
const compactOption = Options.all({
|
|
13
|
+
full: Options.boolean("full").pipe(
|
|
14
|
+
Options.withDescription("Use full JSON output")
|
|
15
|
+
),
|
|
16
|
+
compact: Options.boolean("compact").pipe(
|
|
17
|
+
Options.withDescription("Use compact JSON output (default)")
|
|
18
|
+
)
|
|
13
19
|
}).pipe(
|
|
14
|
-
Options.
|
|
15
|
-
|
|
20
|
+
Options.mapTryCatch(
|
|
21
|
+
({ full, compact }) => {
|
|
22
|
+
if (full && compact) {
|
|
23
|
+
throw new Error("Use either --full or --compact, not both.");
|
|
24
|
+
}
|
|
25
|
+
if (full) return false;
|
|
26
|
+
if (compact) return true;
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
(error) =>
|
|
30
|
+
HelpDoc.p(
|
|
31
|
+
typeof error === "object" && error !== null && "message" in error
|
|
32
|
+
? String((error as { readonly message?: unknown }).message ?? error)
|
|
33
|
+
: String(error)
|
|
34
|
+
)
|
|
35
|
+
)
|
|
16
36
|
);
|
|
17
37
|
|
|
18
38
|
export const configOptions = {
|
|
@@ -44,22 +64,27 @@ export const configOptions = {
|
|
|
44
64
|
Options.withDescription("Override log format (json or human)")
|
|
45
65
|
),
|
|
46
66
|
syncConcurrency: Options.integer("sync-concurrency").pipe(
|
|
67
|
+
Options.withSchema(PositiveInt),
|
|
47
68
|
Options.optional,
|
|
48
69
|
Options.withDescription("Concurrent sync preparation workers (default: 5)")
|
|
49
70
|
),
|
|
50
71
|
syncBatchSize: Options.integer("sync-batch-size").pipe(
|
|
72
|
+
Options.withSchema(PositiveInt),
|
|
51
73
|
Options.optional,
|
|
52
74
|
Options.withDescription("Batch size for sync store writes (default: 100)")
|
|
53
75
|
),
|
|
54
76
|
syncPageLimit: Options.integer("sync-page-limit").pipe(
|
|
77
|
+
Options.withSchema(PositiveInt),
|
|
55
78
|
Options.optional,
|
|
56
79
|
Options.withDescription("Page size for sync fetches (default: 100)")
|
|
57
80
|
),
|
|
58
81
|
checkpointEvery: Options.integer("checkpoint-every").pipe(
|
|
82
|
+
Options.withSchema(PositiveInt),
|
|
59
83
|
Options.optional,
|
|
60
84
|
Options.withDescription("Checkpoint every N processed posts (default: 100)")
|
|
61
85
|
),
|
|
62
86
|
checkpointIntervalMs: Options.integer("checkpoint-interval-ms").pipe(
|
|
87
|
+
Options.withSchema(NonNegativeInt),
|
|
63
88
|
Options.optional,
|
|
64
89
|
Options.withDescription("Checkpoint interval in milliseconds (default: 5000)")
|
|
65
90
|
)
|
package/src/cli/feed.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
-
import { Effect, Stream } from "effect";
|
|
2
|
+
import { Effect, Option, Schema, Stream } from "effect";
|
|
3
3
|
import { renderTableLegacy } from "./doc/table.js";
|
|
4
4
|
import { renderFeedTable } from "./doc/table-renderers.js";
|
|
5
5
|
import { BskyClient } from "../services/bsky-client.js";
|
|
6
6
|
import { AppConfigService } from "../services/app-config.js";
|
|
7
7
|
import type { FeedGeneratorView } from "../domain/bsky.js";
|
|
8
|
-
import {
|
|
8
|
+
import { AtUri } from "../domain/primitives.js";
|
|
9
|
+
import { CliPreferences } from "./preferences.js";
|
|
10
|
+
import { compactFeedGeneratorView } from "./compact-output.js";
|
|
11
|
+
import { actorArg } from "./shared-options.js";
|
|
9
12
|
import { CliInputError } from "./errors.js";
|
|
10
13
|
import { withExamples } from "./help.js";
|
|
11
14
|
import { writeJson, writeJsonStream, writeText } from "./output.js";
|
|
@@ -14,18 +17,16 @@ import { emitWithFormat } from "./output-render.js";
|
|
|
14
17
|
import { cursorOption as baseCursorOption, limitOption as baseLimitOption, parsePagination } from "./pagination.js";
|
|
15
18
|
|
|
16
19
|
const feedUriArg = Args.text({ name: "uri" }).pipe(
|
|
20
|
+
Args.withSchema(AtUri),
|
|
17
21
|
Args.withDescription("Bluesky feed URI (at://...)")
|
|
18
22
|
);
|
|
19
23
|
|
|
20
24
|
const feedUrisArg = Args.text({ name: "uri" }).pipe(
|
|
21
25
|
Args.repeated,
|
|
26
|
+
Args.withSchema(Schema.mutable(Schema.Array(AtUri))),
|
|
22
27
|
Args.withDescription("Feed URIs to fetch")
|
|
23
28
|
);
|
|
24
29
|
|
|
25
|
-
const actorArg = Args.text({ name: "actor" }).pipe(
|
|
26
|
-
Args.withDescription("Bluesky handle or DID")
|
|
27
|
-
);
|
|
28
|
-
|
|
29
30
|
const limitOption = baseLimitOption.pipe(
|
|
30
31
|
Options.withDescription("Maximum number of results")
|
|
31
32
|
);
|
|
@@ -39,6 +40,17 @@ const formatOption = Options.choice("format", jsonNdjsonTableFormats).pipe(
|
|
|
39
40
|
Options.optional
|
|
40
41
|
);
|
|
41
42
|
|
|
43
|
+
const ensureSupportedFormat = (
|
|
44
|
+
format: Option.Option<typeof jsonNdjsonTableFormats[number]>,
|
|
45
|
+
configFormat: string
|
|
46
|
+
) =>
|
|
47
|
+
Option.isNone(format) && configFormat === "markdown"
|
|
48
|
+
? CliInputError.make({
|
|
49
|
+
message: 'Output format "markdown" is not supported for feed commands. Use --format json|ndjson|table.',
|
|
50
|
+
cause: { format: configFormat }
|
|
51
|
+
})
|
|
52
|
+
: Effect.void;
|
|
53
|
+
|
|
42
54
|
|
|
43
55
|
const renderFeedInfoTable = (
|
|
44
56
|
view: FeedGeneratorView,
|
|
@@ -56,16 +68,24 @@ const showCommand = Command.make(
|
|
|
56
68
|
({ uri, format }) =>
|
|
57
69
|
Effect.gen(function* () {
|
|
58
70
|
const appConfig = yield* AppConfigService;
|
|
71
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
72
|
+
const preferences = yield* CliPreferences;
|
|
59
73
|
const client = yield* BskyClient;
|
|
60
74
|
const result = yield* client.getFeedGenerator(uri);
|
|
75
|
+
const payload = preferences.compact
|
|
76
|
+
? {
|
|
77
|
+
...result,
|
|
78
|
+
view: compactFeedGeneratorView(result.view)
|
|
79
|
+
}
|
|
80
|
+
: result;
|
|
61
81
|
yield* emitWithFormat(
|
|
62
82
|
format,
|
|
63
83
|
appConfig.outputFormat,
|
|
64
84
|
jsonNdjsonTableFormats,
|
|
65
85
|
"json",
|
|
66
86
|
{
|
|
67
|
-
json: writeJson(
|
|
68
|
-
ndjson: writeJson(
|
|
87
|
+
json: writeJson(payload),
|
|
88
|
+
ndjson: writeJson(payload),
|
|
69
89
|
table: writeText(renderFeedInfoTable(result.view, result.isOnline, result.isValid))
|
|
70
90
|
}
|
|
71
91
|
);
|
|
@@ -84,6 +104,8 @@ const batchCommand = Command.make(
|
|
|
84
104
|
({ uris, format }) =>
|
|
85
105
|
Effect.gen(function* () {
|
|
86
106
|
const appConfig = yield* AppConfigService;
|
|
107
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
108
|
+
const preferences = yield* CliPreferences;
|
|
87
109
|
const client = yield* BskyClient;
|
|
88
110
|
if (uris.length === 0) {
|
|
89
111
|
return yield* CliInputError.make({
|
|
@@ -92,14 +114,21 @@ const batchCommand = Command.make(
|
|
|
92
114
|
});
|
|
93
115
|
}
|
|
94
116
|
const result = yield* client.getFeedGenerators(uris);
|
|
117
|
+
const feeds = preferences.compact
|
|
118
|
+
? result.feeds.map(compactFeedGeneratorView)
|
|
119
|
+
: result.feeds;
|
|
120
|
+
const payload = { ...result, feeds };
|
|
121
|
+
const feedsStream = Stream.fromIterable(
|
|
122
|
+
feeds as ReadonlyArray<FeedGeneratorView | ReturnType<typeof compactFeedGeneratorView>>
|
|
123
|
+
);
|
|
95
124
|
yield* emitWithFormat(
|
|
96
125
|
format,
|
|
97
126
|
appConfig.outputFormat,
|
|
98
127
|
jsonNdjsonTableFormats,
|
|
99
128
|
"json",
|
|
100
129
|
{
|
|
101
|
-
json: writeJson(
|
|
102
|
-
ndjson: writeJsonStream(
|
|
130
|
+
json: writeJson(payload),
|
|
131
|
+
ndjson: writeJsonStream(feedsStream),
|
|
103
132
|
table: writeText(renderFeedTable(result.feeds, undefined))
|
|
104
133
|
}
|
|
105
134
|
);
|
|
@@ -118,21 +147,29 @@ const byActorCommand = Command.make(
|
|
|
118
147
|
({ actor, limit, cursor, format }) =>
|
|
119
148
|
Effect.gen(function* () {
|
|
120
149
|
const appConfig = yield* AppConfigService;
|
|
150
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
151
|
+
const preferences = yield* CliPreferences;
|
|
121
152
|
const client = yield* BskyClient;
|
|
122
|
-
const { limit: limitValue, cursor: cursorValue } =
|
|
123
|
-
const
|
|
124
|
-
const result = yield* client.getActorFeeds(resolvedActor, {
|
|
153
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
154
|
+
const result = yield* client.getActorFeeds(actor, {
|
|
125
155
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
126
156
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
127
157
|
});
|
|
158
|
+
const feeds = preferences.compact
|
|
159
|
+
? result.feeds.map(compactFeedGeneratorView)
|
|
160
|
+
: result.feeds;
|
|
161
|
+
const payload = { ...result, feeds };
|
|
162
|
+
const feedsStream = Stream.fromIterable(
|
|
163
|
+
feeds as ReadonlyArray<FeedGeneratorView | ReturnType<typeof compactFeedGeneratorView>>
|
|
164
|
+
);
|
|
128
165
|
yield* emitWithFormat(
|
|
129
166
|
format,
|
|
130
167
|
appConfig.outputFormat,
|
|
131
168
|
jsonNdjsonTableFormats,
|
|
132
169
|
"json",
|
|
133
170
|
{
|
|
134
|
-
json: writeJson(
|
|
135
|
-
ndjson: writeJsonStream(
|
|
171
|
+
json: writeJson(payload),
|
|
172
|
+
ndjson: writeJsonStream(feedsStream),
|
|
136
173
|
table: writeText(renderFeedTable(result.feeds, result.cursor))
|
|
137
174
|
}
|
|
138
175
|
);
|
package/src/cli/filter-errors.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ParseResult } from "effect";
|
|
2
|
-
import { safeParseJson, issueDetails } from "./parse-errors.js";
|
|
2
|
+
import { safeParseJson, issueDetails, findJsonParseIssue, jsonParseTip } from "./parse-errors.js";
|
|
3
3
|
import { formatAgentError, type AgentErrorPayload } from "./errors.js";
|
|
4
4
|
|
|
5
5
|
const validFilterTags = [
|
|
@@ -76,19 +76,15 @@ export const formatFilterParseError = (error: ParseResult.ParseError, raw: strin
|
|
|
76
76
|
const received = safeParseJson(raw);
|
|
77
77
|
const receivedValue = received === undefined ? raw : received;
|
|
78
78
|
|
|
79
|
-
const jsonParseIssue = issues
|
|
80
|
-
(issue) =>
|
|
81
|
-
issue._tag === "Transformation" &&
|
|
82
|
-
typeof issue.message === "string" &&
|
|
83
|
-
issue.message.startsWith("JSON Parse error")
|
|
84
|
-
);
|
|
79
|
+
const jsonParseIssue = findJsonParseIssue(issues);
|
|
85
80
|
if (jsonParseIssue) {
|
|
81
|
+
const jsonMessage = jsonParseIssue.message ?? "Invalid JSON input.";
|
|
86
82
|
return jsonParseError({
|
|
87
83
|
message: "Invalid JSON in --filter-json.",
|
|
88
84
|
received: raw,
|
|
89
85
|
details: [
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
jsonMessage,
|
|
87
|
+
jsonParseTip
|
|
92
88
|
]
|
|
93
89
|
});
|
|
94
90
|
}
|
package/src/cli/filter.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { Chunk, Clock, Effect, Option, Stream } from "effect";
|
|
|
4
4
|
import { StoreQuery } from "../domain/events.js";
|
|
5
5
|
import { RawPost } from "../domain/raw.js";
|
|
6
6
|
import type { Post } from "../domain/post.js";
|
|
7
|
-
import { StoreName } from "../domain/primitives.js";
|
|
7
|
+
import { PostUri, StoreName } from "../domain/primitives.js";
|
|
8
8
|
import { BskyClient } from "../services/bsky-client.js";
|
|
9
9
|
import { FilterCompiler } from "../services/filter-compiler.js";
|
|
10
10
|
import { FilterLibrary } from "../services/filter-library.js";
|
|
@@ -25,6 +25,7 @@ import { withExamples } from "./help.js";
|
|
|
25
25
|
import { filterOption, filterJsonOption } from "./shared-options.js";
|
|
26
26
|
import { jsonTableFormats, resolveOutputFormat, textJsonFormats } from "./output-format.js";
|
|
27
27
|
import { filterDslDescription, filterJsonDescription } from "./filter-help.js";
|
|
28
|
+
import { PositiveInt } from "./option-schemas.js";
|
|
28
29
|
|
|
29
30
|
const filterNameArg = Args.text({ name: "name" }).pipe(
|
|
30
31
|
Args.withSchema(StoreName),
|
|
@@ -36,6 +37,7 @@ const postJsonOption = Options.text("post-json").pipe(
|
|
|
36
37
|
Options.optional
|
|
37
38
|
);
|
|
38
39
|
const postUriOption = Options.text("post-uri").pipe(
|
|
40
|
+
Options.withSchema(PostUri),
|
|
39
41
|
Options.withDescription("Bluesky post URI (at://...)."),
|
|
40
42
|
Options.optional
|
|
41
43
|
);
|
|
@@ -49,10 +51,12 @@ const storeTestOption = Options.text("store").pipe(
|
|
|
49
51
|
Options.optional
|
|
50
52
|
);
|
|
51
53
|
const testLimitOption = Options.integer("limit").pipe(
|
|
54
|
+
Options.withSchema(PositiveInt),
|
|
52
55
|
Options.withDescription("Number of posts to evaluate (default: 100)"),
|
|
53
56
|
Options.optional
|
|
54
57
|
);
|
|
55
58
|
const sampleSizeOption = Options.integer("sample-size").pipe(
|
|
59
|
+
Options.withSchema(PositiveInt),
|
|
56
60
|
Options.withDescription("Number of posts to evaluate (default: 1000)"),
|
|
57
61
|
Options.optional
|
|
58
62
|
);
|
|
@@ -294,12 +298,6 @@ export const filterTest = Command.make(
|
|
|
294
298
|
cause: { store: store.value, postJson, postUri }
|
|
295
299
|
});
|
|
296
300
|
}
|
|
297
|
-
if (Option.isSome(limit) && limit.value <= 0) {
|
|
298
|
-
return yield* CliInputError.make({
|
|
299
|
-
message: "--limit must be a positive integer.",
|
|
300
|
-
cause: { limit: limit.value }
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
301
|
const storeRef = yield* storeOptions.loadStoreRef(store.value);
|
|
304
302
|
const index = yield* StoreIndex;
|
|
305
303
|
const evaluateBatch = yield* runtime.evaluateBatch(expr);
|