@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/src/cli/sync.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Options } from "@effect/cli";
|
|
2
|
-
import {
|
|
2
|
+
import { Effect, Layer, Option } from "effect";
|
|
3
3
|
import { Jetstream } from "effect-jetstream";
|
|
4
4
|
import { filterExprSignature } from "../domain/filter.js";
|
|
5
5
|
import { DataSource, SyncResult } from "../domain/sync.js";
|
|
@@ -28,25 +28,30 @@ import {
|
|
|
28
28
|
postFilterJsonOption,
|
|
29
29
|
authorFilterOption,
|
|
30
30
|
includePinsOption,
|
|
31
|
-
decodeActor,
|
|
32
31
|
quietOption,
|
|
33
32
|
refreshOption,
|
|
34
33
|
strictOption,
|
|
35
|
-
maxErrorsOption
|
|
36
|
-
parseMaxErrors,
|
|
37
|
-
parseLimit
|
|
34
|
+
maxErrorsOption
|
|
38
35
|
} from "./shared-options.js";
|
|
39
36
|
import {
|
|
40
37
|
depthOption as threadDepthOption,
|
|
41
38
|
parentHeightOption as threadParentHeightOption,
|
|
42
39
|
parseThreadDepth
|
|
43
40
|
} from "./thread-options.js";
|
|
41
|
+
import { DurationInput, PositiveInt } from "./option-schemas.js";
|
|
44
42
|
|
|
45
|
-
const
|
|
43
|
+
const syncLimitOption = Options.integer("limit").pipe(
|
|
44
|
+
Options.withSchema(PositiveInt),
|
|
45
|
+
Options.withDescription("Maximum number of posts to sync"),
|
|
46
|
+
Options.optional
|
|
47
|
+
);
|
|
48
|
+
const jetstreamLimitOption = Options.integer("limit").pipe(
|
|
49
|
+
Options.withSchema(PositiveInt),
|
|
46
50
|
Options.withDescription("Maximum number of Jetstream events to process"),
|
|
47
51
|
Options.optional
|
|
48
52
|
);
|
|
49
53
|
const durationOption = Options.text("duration").pipe(
|
|
54
|
+
Options.withSchema(DurationInput),
|
|
50
55
|
Options.withDescription("Stop after a duration (e.g. \"2 minutes\")"),
|
|
51
56
|
Options.optional
|
|
52
57
|
);
|
|
@@ -57,34 +62,10 @@ const parentHeightOption = threadParentHeightOption(
|
|
|
57
62
|
"Thread parent height to include (0-1000, default 80)"
|
|
58
63
|
);
|
|
59
64
|
|
|
60
|
-
const parseDuration = (value: Option.Option<string>) =>
|
|
61
|
-
Option.match(value, {
|
|
62
|
-
onNone: () => Effect.succeed(Option.none()),
|
|
63
|
-
onSome: (raw) =>
|
|
64
|
-
Effect.try({
|
|
65
|
-
try: () => Duration.decode(raw as Duration.DurationInput),
|
|
66
|
-
catch: (cause) =>
|
|
67
|
-
CliInputError.make({
|
|
68
|
-
message: `Invalid duration: ${raw}. Use formats like \"2 minutes\".`,
|
|
69
|
-
cause
|
|
70
|
-
})
|
|
71
|
-
}).pipe(
|
|
72
|
-
Effect.flatMap((duration) =>
|
|
73
|
-
Duration.toMillis(duration) < 0
|
|
74
|
-
? Effect.fail(
|
|
75
|
-
CliInputError.make({
|
|
76
|
-
message: "Duration must be non-negative.",
|
|
77
|
-
cause: duration
|
|
78
|
-
})
|
|
79
|
-
)
|
|
80
|
-
: Effect.succeed(Option.some(duration))
|
|
81
|
-
)
|
|
82
|
-
)
|
|
83
|
-
});
|
|
84
65
|
|
|
85
66
|
const timelineCommand = Command.make(
|
|
86
67
|
"timeline",
|
|
87
|
-
{ store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption },
|
|
68
|
+
{ store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption, limit: syncLimitOption },
|
|
88
69
|
makeSyncCommandBody("timeline", () => DataSource.timeline())
|
|
89
70
|
).pipe(
|
|
90
71
|
Command.withDescription(
|
|
@@ -101,7 +82,7 @@ const timelineCommand = Command.make(
|
|
|
101
82
|
|
|
102
83
|
const feedCommand = Command.make(
|
|
103
84
|
"feed",
|
|
104
|
-
{ uri: feedUriArg, store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption },
|
|
85
|
+
{ uri: feedUriArg, store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption, limit: syncLimitOption },
|
|
105
86
|
({ uri, ...rest }) => makeSyncCommandBody("feed", () => DataSource.feed(uri), { uri })(rest)
|
|
106
87
|
).pipe(
|
|
107
88
|
Command.withDescription(
|
|
@@ -117,7 +98,7 @@ const feedCommand = Command.make(
|
|
|
117
98
|
|
|
118
99
|
const listCommand = Command.make(
|
|
119
100
|
"list",
|
|
120
|
-
{ uri: listUriArg, store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption },
|
|
101
|
+
{ uri: listUriArg, store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption, limit: syncLimitOption },
|
|
121
102
|
({ uri, ...rest }) => makeSyncCommandBody("list", () => DataSource.list(uri), { uri })(rest)
|
|
122
103
|
).pipe(
|
|
123
104
|
Command.withDescription(
|
|
@@ -133,7 +114,7 @@ const listCommand = Command.make(
|
|
|
133
114
|
|
|
134
115
|
const notificationsCommand = Command.make(
|
|
135
116
|
"notifications",
|
|
136
|
-
{ store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption },
|
|
117
|
+
{ store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption, limit: syncLimitOption },
|
|
137
118
|
makeSyncCommandBody("notifications", () => DataSource.notifications())
|
|
138
119
|
).pipe(
|
|
139
120
|
Command.withDescription(
|
|
@@ -155,18 +136,18 @@ const authorCommand = Command.make(
|
|
|
155
136
|
postFilter: postFilterOption,
|
|
156
137
|
postFilterJson: postFilterJsonOption,
|
|
157
138
|
quiet: quietOption,
|
|
158
|
-
refresh: refreshOption
|
|
139
|
+
refresh: refreshOption,
|
|
140
|
+
limit: syncLimitOption
|
|
159
141
|
},
|
|
160
|
-
({ actor, filter, includePins, postFilter, postFilterJson, store, quiet, refresh }) =>
|
|
142
|
+
({ actor, filter, includePins, postFilter, postFilterJson, store, quiet, refresh, limit }) =>
|
|
161
143
|
Effect.gen(function* () {
|
|
162
|
-
const resolvedActor = yield* decodeActor(actor);
|
|
163
144
|
const apiFilter = Option.getOrUndefined(filter);
|
|
164
|
-
const source = DataSource.author(
|
|
145
|
+
const source = DataSource.author(actor, {
|
|
165
146
|
...(apiFilter !== undefined ? { filter: apiFilter } : {}),
|
|
166
147
|
...(includePins ? { includePins: true } : {})
|
|
167
148
|
});
|
|
168
149
|
const run = makeSyncCommandBody("author", () => source, {
|
|
169
|
-
actor
|
|
150
|
+
actor,
|
|
170
151
|
...(apiFilter !== undefined ? { filter: apiFilter } : {}),
|
|
171
152
|
...(includePins ? { includePins: true } : {})
|
|
172
153
|
});
|
|
@@ -175,7 +156,8 @@ const authorCommand = Command.make(
|
|
|
175
156
|
filter: postFilter,
|
|
176
157
|
filterJson: postFilterJson,
|
|
177
158
|
quiet,
|
|
178
|
-
refresh
|
|
159
|
+
refresh,
|
|
160
|
+
limit
|
|
179
161
|
});
|
|
180
162
|
})
|
|
181
163
|
).pipe(
|
|
@@ -201,12 +183,13 @@ const threadCommand = Command.make(
|
|
|
201
183
|
filter: filterOption,
|
|
202
184
|
filterJson: filterJsonOption,
|
|
203
185
|
quiet: quietOption,
|
|
204
|
-
refresh: refreshOption
|
|
186
|
+
refresh: refreshOption,
|
|
187
|
+
limit: syncLimitOption
|
|
205
188
|
},
|
|
206
|
-
({ uri, depth, parentHeight, filter, filterJson, store, quiet, refresh }) =>
|
|
189
|
+
({ uri, depth, parentHeight, filter, filterJson, store, quiet, refresh, limit }) =>
|
|
207
190
|
Effect.gen(function* () {
|
|
208
191
|
const { depth: depthValue, parentHeight: parentHeightValue } =
|
|
209
|
-
|
|
192
|
+
parseThreadDepth(depth, parentHeight);
|
|
210
193
|
const source = DataSource.thread(uri, {
|
|
211
194
|
...(depthValue !== undefined ? { depth: depthValue } : {}),
|
|
212
195
|
...(parentHeightValue !== undefined ? { parentHeight: parentHeightValue } : {})
|
|
@@ -216,7 +199,7 @@ const threadCommand = Command.make(
|
|
|
216
199
|
...(depthValue !== undefined ? { depth: depthValue } : {}),
|
|
217
200
|
...(parentHeightValue !== undefined ? { parentHeight: parentHeightValue } : {})
|
|
218
201
|
});
|
|
219
|
-
return yield* run({ store, filter, filterJson, quiet, refresh });
|
|
202
|
+
return yield* run({ store, filter, filterJson, quiet, refresh, limit });
|
|
220
203
|
})
|
|
221
204
|
).pipe(
|
|
222
205
|
Command.withDescription(
|
|
@@ -244,7 +227,7 @@ const jetstreamCommand = Command.make(
|
|
|
244
227
|
cursor: jetstreamOptions.cursor,
|
|
245
228
|
compress: jetstreamOptions.compress,
|
|
246
229
|
maxMessageSize: jetstreamOptions.maxMessageSize,
|
|
247
|
-
limit:
|
|
230
|
+
limit: jetstreamLimitOption,
|
|
248
231
|
duration: durationOption,
|
|
249
232
|
strict: strictOption,
|
|
250
233
|
maxErrors: maxErrorsOption
|
|
@@ -285,10 +268,8 @@ const jetstreamCommand = Command.make(
|
|
|
285
268
|
storeRef,
|
|
286
269
|
filterHash
|
|
287
270
|
);
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
const parsedMaxErrors = yield* parseMaxErrors(maxErrors);
|
|
291
|
-
if (Option.isNone(parsedLimit) && Option.isNone(parsedDuration)) {
|
|
271
|
+
const parsedDuration = duration;
|
|
272
|
+
if (Option.isNone(limit) && Option.isNone(parsedDuration)) {
|
|
292
273
|
return yield* CliInputError.make({
|
|
293
274
|
message:
|
|
294
275
|
"Jetstream sync requires --limit or --duration. Use watch jetstream for continuous streaming.",
|
|
@@ -304,9 +285,9 @@ const jetstreamCommand = Command.make(
|
|
|
304
285
|
});
|
|
305
286
|
const result = yield* Effect.gen(function* () {
|
|
306
287
|
const engine = yield* JetstreamSyncEngine;
|
|
307
|
-
const limitValue = Option.getOrUndefined(
|
|
288
|
+
const limitValue = Option.getOrUndefined(limit);
|
|
308
289
|
const durationValue = Option.getOrUndefined(parsedDuration);
|
|
309
|
-
const maxErrorsValue = Option.getOrUndefined(
|
|
290
|
+
const maxErrorsValue = Option.getOrUndefined(maxErrors);
|
|
310
291
|
return yield* engine.sync({
|
|
311
292
|
source: selection.source,
|
|
312
293
|
store: storeRef,
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { Options } from "@effect/cli";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Option } from "effect";
|
|
3
|
+
import { boundedInt } from "./option-schemas.js";
|
|
4
4
|
|
|
5
5
|
export const depthOption = (description: string) =>
|
|
6
6
|
Options.integer("depth").pipe(
|
|
7
|
+
Options.withSchema(boundedInt(0, 1000)),
|
|
7
8
|
Options.withDescription(description),
|
|
8
9
|
Options.optional
|
|
9
10
|
);
|
|
10
11
|
|
|
11
12
|
export const parentHeightOption = (description: string) =>
|
|
12
13
|
Options.integer("parent-height").pipe(
|
|
14
|
+
Options.withSchema(boundedInt(0, 1000)),
|
|
13
15
|
Options.withDescription(description),
|
|
14
16
|
Options.optional
|
|
15
17
|
);
|
|
@@ -17,17 +19,7 @@ export const parentHeightOption = (description: string) =>
|
|
|
17
19
|
export const parseThreadDepth = (
|
|
18
20
|
depth: Option.Option<number>,
|
|
19
21
|
parentHeight: Option.Option<number>
|
|
20
|
-
) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
parentHeight,
|
|
25
|
-
"parent-height",
|
|
26
|
-
0,
|
|
27
|
-
1000
|
|
28
|
-
);
|
|
29
|
-
return {
|
|
30
|
-
depth: Option.getOrUndefined(parsedDepth),
|
|
31
|
-
parentHeight: Option.getOrUndefined(parsedParentHeight)
|
|
32
|
-
};
|
|
33
|
-
});
|
|
22
|
+
) => ({
|
|
23
|
+
depth: Option.getOrUndefined(depth),
|
|
24
|
+
parentHeight: Option.getOrUndefined(parentHeight)
|
|
25
|
+
});
|
package/src/cli/view-thread.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
-
import { Chunk, Console, Effect, Option,
|
|
2
|
+
import { Chunk, Console, Effect, Option, Stream } from "effect";
|
|
3
3
|
import { PostUri, StoreName } from "../domain/primitives.js";
|
|
4
4
|
import type { Post } from "../domain/post.js";
|
|
5
5
|
import { all } from "../domain/filter.js";
|
|
@@ -11,19 +11,20 @@ import { StoreIndex } from "../services/store-index.js";
|
|
|
11
11
|
import { SyncEngine } from "../services/sync-engine.js";
|
|
12
12
|
import { renderThread } from "./doc/thread.js";
|
|
13
13
|
import { renderPlain, renderAnsi } from "./doc/render.js";
|
|
14
|
-
import { writeJson, writeText } from "./output.js";
|
|
14
|
+
import { CliOutput, writeJson, writeText } from "./output.js";
|
|
15
15
|
import { storeOptions } from "./store.js";
|
|
16
16
|
import { withExamples } from "./help.js";
|
|
17
17
|
import { CliInputError } from "./errors.js";
|
|
18
|
-
import { formatSchemaError } from "./shared.js";
|
|
19
18
|
import {
|
|
20
19
|
depthOption as threadDepthOption,
|
|
21
20
|
parentHeightOption as threadParentHeightOption,
|
|
22
21
|
parseThreadDepth
|
|
23
22
|
} from "./thread-options.js";
|
|
24
23
|
import { textJsonFormats } from "./output-format.js";
|
|
24
|
+
import { PositiveInt } from "./option-schemas.js";
|
|
25
25
|
|
|
26
26
|
const uriArg = Args.text({ name: "uri" }).pipe(
|
|
27
|
+
Args.withSchema(PostUri),
|
|
27
28
|
Args.withDescription("AT-URI of any post in the thread")
|
|
28
29
|
);
|
|
29
30
|
|
|
@@ -42,6 +43,7 @@ const ansiOption = Options.boolean("ansi").pipe(
|
|
|
42
43
|
);
|
|
43
44
|
|
|
44
45
|
const widthOption = Options.integer("width").pipe(
|
|
46
|
+
Options.withSchema(PositiveInt),
|
|
45
47
|
Options.withDescription("Line width for terminal output"),
|
|
46
48
|
Options.optional
|
|
47
49
|
);
|
|
@@ -70,10 +72,11 @@ export const threadCommand = Command.make(
|
|
|
70
72
|
},
|
|
71
73
|
({ uri, store, compact, ansi, width, format, depth, parentHeight }) =>
|
|
72
74
|
Effect.gen(function* () {
|
|
75
|
+
const output = yield* CliOutput;
|
|
73
76
|
const outputFormat = Option.getOrElse(format, () => "text" as const);
|
|
74
77
|
const w = Option.getOrUndefined(width);
|
|
75
78
|
const { depth: depthValue, parentHeight: parentHeightValue } =
|
|
76
|
-
|
|
79
|
+
parseThreadDepth(depth, parentHeight);
|
|
77
80
|
const d = depthValue ?? 6;
|
|
78
81
|
const ph = parentHeightValue ?? 80;
|
|
79
82
|
|
|
@@ -82,15 +85,15 @@ export const threadCommand = Command.make(
|
|
|
82
85
|
if (Option.isSome(store)) {
|
|
83
86
|
const index = yield* StoreIndex;
|
|
84
87
|
const storeRef = yield* storeOptions.loadStoreRef(store.value);
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const hasTarget = yield* index.hasUri(storeRef,
|
|
88
|
+
const totalPosts = yield* index.count(storeRef);
|
|
89
|
+
if (totalPosts > 20000) {
|
|
90
|
+
yield* output
|
|
91
|
+
.writeStderr(
|
|
92
|
+
`ℹ️ Store ${storeRef.name} has ${totalPosts} posts. Thread rendering will load all posts into memory.`
|
|
93
|
+
)
|
|
94
|
+
.pipe(Effect.catchAll(() => Effect.void));
|
|
95
|
+
}
|
|
96
|
+
const hasTarget = yield* index.hasUri(storeRef, uri);
|
|
94
97
|
if (!hasTarget) {
|
|
95
98
|
const engine = yield* SyncEngine;
|
|
96
99
|
const source = DataSource.thread(uri, { depth: d, parentHeight: ph });
|
|
@@ -100,7 +103,7 @@ export const threadCommand = Command.make(
|
|
|
100
103
|
const stream = index.query(storeRef, query);
|
|
101
104
|
const collected = yield* Stream.runCollect(stream);
|
|
102
105
|
const allPosts = Chunk.toReadonlyArray(collected);
|
|
103
|
-
const threadPosts = selectThreadPosts(allPosts, String(
|
|
106
|
+
const threadPosts = selectThreadPosts(allPosts, String(uri));
|
|
104
107
|
if (threadPosts.length === 0) {
|
|
105
108
|
return yield* CliInputError.make({
|
|
106
109
|
message: `Thread not found for ${uri}.`,
|
|
@@ -108,7 +111,7 @@ export const threadCommand = Command.make(
|
|
|
108
111
|
});
|
|
109
112
|
}
|
|
110
113
|
// B1: Hint when only root post exists in store
|
|
111
|
-
if (threadPosts.length === 1 && threadPosts[0]?.uri ===
|
|
114
|
+
if (threadPosts.length === 1 && threadPosts[0]?.uri === uri) {
|
|
112
115
|
yield* Console.log("\nℹ️ Only root post found in store. Use --no-store to fetch full thread from API.\n");
|
|
113
116
|
}
|
|
114
117
|
posts = threadPosts;
|
package/src/cli/watch.ts
CHANGED
|
@@ -14,7 +14,6 @@ import { withExamples } from "./help.js";
|
|
|
14
14
|
import { buildJetstreamSelection, jetstreamOptions } from "./jetstream.js";
|
|
15
15
|
import { makeWatchCommandBody } from "./sync-factory.js";
|
|
16
16
|
import { parseOptionalDuration } from "./interval.js";
|
|
17
|
-
import { CliInputError } from "./errors.js";
|
|
18
17
|
import {
|
|
19
18
|
feedUriArg,
|
|
20
19
|
listUriArg,
|
|
@@ -27,30 +26,32 @@ import {
|
|
|
27
26
|
postFilterJsonOption,
|
|
28
27
|
authorFilterOption,
|
|
29
28
|
includePinsOption,
|
|
30
|
-
decodeActor,
|
|
31
29
|
quietOption,
|
|
32
30
|
refreshOption,
|
|
33
31
|
strictOption,
|
|
34
|
-
maxErrorsOption
|
|
35
|
-
parseMaxErrors
|
|
32
|
+
maxErrorsOption
|
|
36
33
|
} from "./shared-options.js";
|
|
37
34
|
import {
|
|
38
35
|
depthOption as threadDepthOption,
|
|
39
36
|
parentHeightOption as threadParentHeightOption,
|
|
40
37
|
parseThreadDepth
|
|
41
38
|
} from "./thread-options.js";
|
|
39
|
+
import { DurationInput, PositiveInt } from "./option-schemas.js";
|
|
42
40
|
|
|
43
41
|
const intervalOption = Options.text("interval").pipe(
|
|
42
|
+
Options.withSchema(DurationInput),
|
|
44
43
|
Options.withDescription(
|
|
45
44
|
"Polling interval (e.g. \"30 seconds\", \"500 millis\") (default: 30 seconds)"
|
|
46
45
|
),
|
|
47
46
|
Options.optional
|
|
48
47
|
);
|
|
49
48
|
const maxCyclesOption = Options.integer("max-cycles").pipe(
|
|
49
|
+
Options.withSchema(PositiveInt),
|
|
50
50
|
Options.withDescription("Stop after N watch cycles"),
|
|
51
51
|
Options.optional
|
|
52
52
|
);
|
|
53
53
|
const untilOption = Options.text("until").pipe(
|
|
54
|
+
Options.withSchema(DurationInput),
|
|
54
55
|
Options.withDescription("Stop after a duration (e.g. \"10 minutes\")"),
|
|
55
56
|
Options.optional
|
|
56
57
|
);
|
|
@@ -179,14 +180,13 @@ const authorCommand = Command.make(
|
|
|
179
180
|
},
|
|
180
181
|
({ actor, filter, includePins, postFilter, postFilterJson, interval, maxCycles, until, store, quiet, refresh }) =>
|
|
181
182
|
Effect.gen(function* () {
|
|
182
|
-
const resolvedActor = yield* decodeActor(actor);
|
|
183
183
|
const apiFilter = Option.getOrUndefined(filter);
|
|
184
|
-
const source = DataSource.author(
|
|
184
|
+
const source = DataSource.author(actor, {
|
|
185
185
|
...(apiFilter !== undefined ? { filter: apiFilter } : {}),
|
|
186
186
|
...(includePins ? { includePins: true } : {})
|
|
187
187
|
});
|
|
188
188
|
const run = makeWatchCommandBody("author", () => source, {
|
|
189
|
-
actor
|
|
189
|
+
actor,
|
|
190
190
|
...(apiFilter !== undefined ? { filter: apiFilter } : {}),
|
|
191
191
|
...(includePins ? { includePins: true } : {})
|
|
192
192
|
});
|
|
@@ -232,7 +232,7 @@ const threadCommand = Command.make(
|
|
|
232
232
|
({ uri, depth, parentHeight, filter, filterJson, interval, maxCycles, until, store, quiet, refresh }) =>
|
|
233
233
|
Effect.gen(function* () {
|
|
234
234
|
const { depth: depthValue, parentHeight: parentHeightValue } =
|
|
235
|
-
|
|
235
|
+
parseThreadDepth(depth, parentHeight);
|
|
236
236
|
const source = DataSource.thread(uri, {
|
|
237
237
|
...(depthValue !== undefined ? { depth: depthValue } : {}),
|
|
238
238
|
...(parentHeightValue !== undefined ? { parentHeight: parentHeightValue } : {})
|
|
@@ -309,27 +309,14 @@ const jetstreamCommand = Command.make(
|
|
|
309
309
|
storeRef,
|
|
310
310
|
filterHash
|
|
311
311
|
);
|
|
312
|
-
const
|
|
313
|
-
const parsedUntil = yield* parseOptionalDuration(until);
|
|
314
|
-
const parsedMaxCycles = yield* Option.match(maxCycles, {
|
|
315
|
-
onNone: () => Effect.succeed(Option.none<number>()),
|
|
316
|
-
onSome: (value) =>
|
|
317
|
-
value <= 0
|
|
318
|
-
? Effect.fail(
|
|
319
|
-
CliInputError.make({
|
|
320
|
-
message: "--max-cycles must be a positive integer.",
|
|
321
|
-
cause: { maxCycles: value }
|
|
322
|
-
})
|
|
323
|
-
)
|
|
324
|
-
: Effect.succeed(Option.some(value))
|
|
325
|
-
});
|
|
312
|
+
const parsedUntil = parseOptionalDuration(until);
|
|
326
313
|
const engineLayer = JetstreamSyncEngine.layer.pipe(
|
|
327
314
|
Layer.provideMerge(Jetstream.live(selection.config))
|
|
328
315
|
);
|
|
329
316
|
yield* logInfo("Starting watch", { source: "jetstream", store: storeRef.name });
|
|
330
317
|
yield* Effect.gen(function* () {
|
|
331
318
|
const engine = yield* JetstreamSyncEngine;
|
|
332
|
-
const maxErrorsValue = Option.getOrUndefined(
|
|
319
|
+
const maxErrorsValue = Option.getOrUndefined(maxErrors);
|
|
333
320
|
const stream = engine.watch({
|
|
334
321
|
source: selection.source,
|
|
335
322
|
store: storeRef,
|
|
@@ -346,8 +333,8 @@ const jetstreamCommand = Command.make(
|
|
|
346
333
|
makeSyncReporter(quiet, monitor, output)
|
|
347
334
|
)
|
|
348
335
|
);
|
|
349
|
-
const limited = Option.isSome(
|
|
350
|
-
? outputStream.pipe(Stream.take(
|
|
336
|
+
const limited = Option.isSome(maxCycles)
|
|
337
|
+
? outputStream.pipe(Stream.take(maxCycles.value))
|
|
351
338
|
: outputStream;
|
|
352
339
|
const timed = Option.isSome(parsedUntil)
|
|
353
340
|
? limited.pipe(Stream.interruptWhen(Effect.sleep(parsedUntil.value)))
|
package/src/domain/primitives.ts
CHANGED
|
@@ -16,18 +16,35 @@ export const Hashtag = Schema.String.pipe(
|
|
|
16
16
|
);
|
|
17
17
|
export type Hashtag = typeof Hashtag.Type;
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
const atUriPattern = /^at:\/\/\S+$/;
|
|
20
|
+
|
|
21
|
+
export const AtUri = Schema.String.pipe(
|
|
22
|
+
Schema.pattern(atUriPattern),
|
|
23
|
+
Schema.brand("AtUri")
|
|
24
|
+
);
|
|
20
25
|
export type AtUri = typeof AtUri.Type;
|
|
21
26
|
|
|
22
|
-
export const PostUri = Schema.String.pipe(
|
|
27
|
+
export const PostUri = Schema.String.pipe(
|
|
28
|
+
Schema.pattern(atUriPattern),
|
|
29
|
+
Schema.brand("PostUri")
|
|
30
|
+
);
|
|
23
31
|
export type PostUri = typeof PostUri.Type;
|
|
24
32
|
|
|
25
33
|
export const PostCid = Schema.String.pipe(Schema.brand("PostCid"));
|
|
26
34
|
export type PostCid = typeof PostCid.Type;
|
|
27
35
|
|
|
28
|
-
export const Did = Schema.String.pipe(
|
|
36
|
+
export const Did = Schema.String.pipe(
|
|
37
|
+
Schema.pattern(/^did:\S+$/),
|
|
38
|
+
Schema.brand("Did")
|
|
39
|
+
);
|
|
29
40
|
export type Did = typeof Did.Type;
|
|
30
41
|
|
|
42
|
+
export const ActorId = Schema.String.pipe(
|
|
43
|
+
Schema.pattern(/^(did:\S+|[a-z0-9][a-z0-9.-]{1,251})$/),
|
|
44
|
+
Schema.brand("ActorId")
|
|
45
|
+
);
|
|
46
|
+
export type ActorId = typeof ActorId.Type;
|
|
47
|
+
|
|
31
48
|
export const Timestamp = Schema.Union(
|
|
32
49
|
Schema.DateFromString,
|
|
33
50
|
Schema.DateFromSelf
|
|
@@ -1256,6 +1256,7 @@ export class BskyClient extends Context.Tag("@skygent/BskyClient")<
|
|
|
1256
1256
|
const config = yield* AppConfigService;
|
|
1257
1257
|
const credentials = yield* CredentialStore;
|
|
1258
1258
|
const agent = new AtpAgent({ service: config.service });
|
|
1259
|
+
const publicAgent = new AtpAgent({ service: "https://public.api.bsky.app" });
|
|
1259
1260
|
|
|
1260
1261
|
const minInterval = yield* Config.duration("SKYGENT_BSKY_RATE_LIMIT").pipe(
|
|
1261
1262
|
Config.withDefault(Duration.millis(250))
|
|
@@ -1507,6 +1508,7 @@ export class BskyClient extends Context.Tag("@skygent/BskyClient")<
|
|
|
1507
1508
|
const getFollowers = (actor: string, opts?: GraphOptions) =>
|
|
1508
1509
|
Effect.gen(function* () {
|
|
1509
1510
|
yield* ensureAuth(false);
|
|
1511
|
+
const api = agent.hasSession ? agent : publicAgent;
|
|
1510
1512
|
const params = withCursor(
|
|
1511
1513
|
{ actor, limit: opts?.limit ?? 50 },
|
|
1512
1514
|
opts?.cursor
|
|
@@ -1514,7 +1516,7 @@ export class BskyClient extends Context.Tag("@skygent/BskyClient")<
|
|
|
1514
1516
|
const response = yield* withRetry(
|
|
1515
1517
|
withRateLimit(
|
|
1516
1518
|
Effect.tryPromise<AppBskyGraphGetFollowers.Response>(() =>
|
|
1517
|
-
|
|
1519
|
+
api.app.bsky.graph.getFollowers(params)
|
|
1518
1520
|
)
|
|
1519
1521
|
)
|
|
1520
1522
|
).pipe(Effect.mapError(toBskyError("Failed to fetch followers", "getFollowers")));
|
|
@@ -1531,6 +1533,7 @@ export class BskyClient extends Context.Tag("@skygent/BskyClient")<
|
|
|
1531
1533
|
const getFollows = (actor: string, opts?: GraphOptions) =>
|
|
1532
1534
|
Effect.gen(function* () {
|
|
1533
1535
|
yield* ensureAuth(false);
|
|
1536
|
+
const api = agent.hasSession ? agent : publicAgent;
|
|
1534
1537
|
const params = withCursor(
|
|
1535
1538
|
{ actor, limit: opts?.limit ?? 50 },
|
|
1536
1539
|
opts?.cursor
|
|
@@ -1538,7 +1541,7 @@ export class BskyClient extends Context.Tag("@skygent/BskyClient")<
|
|
|
1538
1541
|
const response = yield* withRetry(
|
|
1539
1542
|
withRateLimit(
|
|
1540
1543
|
Effect.tryPromise<AppBskyGraphGetFollows.Response>(() =>
|
|
1541
|
-
|
|
1544
|
+
api.app.bsky.graph.getFollows(params)
|
|
1542
1545
|
)
|
|
1543
1546
|
)
|
|
1544
1547
|
).pipe(Effect.mapError(toBskyError("Failed to fetch follows", "getFollows")));
|
|
@@ -1582,10 +1585,11 @@ export class BskyClient extends Context.Tag("@skygent/BskyClient")<
|
|
|
1582
1585
|
return { actor, relationships: [] };
|
|
1583
1586
|
}
|
|
1584
1587
|
yield* ensureAuth(false);
|
|
1588
|
+
const api = agent.hasSession ? agent : publicAgent;
|
|
1585
1589
|
const response = yield* withRetry(
|
|
1586
1590
|
withRateLimit(
|
|
1587
1591
|
Effect.tryPromise<AppBskyGraphGetRelationships.Response>(() =>
|
|
1588
|
-
|
|
1592
|
+
api.app.bsky.graph.getRelationships({ actor, others: [...others] })
|
|
1589
1593
|
)
|
|
1590
1594
|
)
|
|
1591
1595
|
).pipe(Effect.mapError(toBskyError("Failed to fetch relationships", "getRelationships")));
|
|
@@ -1602,6 +1606,7 @@ export class BskyClient extends Context.Tag("@skygent/BskyClient")<
|
|
|
1602
1606
|
const getList = (uri: string, opts?: GraphOptions) =>
|
|
1603
1607
|
Effect.gen(function* () {
|
|
1604
1608
|
yield* ensureAuth(false);
|
|
1609
|
+
const api = agent.hasSession ? agent : publicAgent;
|
|
1605
1610
|
const params = withCursor(
|
|
1606
1611
|
{ list: uri, limit: opts?.limit ?? 50 },
|
|
1607
1612
|
opts?.cursor
|
|
@@ -1609,7 +1614,7 @@ export class BskyClient extends Context.Tag("@skygent/BskyClient")<
|
|
|
1609
1614
|
const response = yield* withRetry(
|
|
1610
1615
|
withRateLimit(
|
|
1611
1616
|
Effect.tryPromise<AppBskyGraphGetList.Response>(() =>
|
|
1612
|
-
|
|
1617
|
+
api.app.bsky.graph.getList(params)
|
|
1613
1618
|
)
|
|
1614
1619
|
)
|
|
1615
1620
|
).pipe(Effect.mapError(toBskyError("Failed to fetch list", "getList")));
|
|
@@ -1626,6 +1631,7 @@ export class BskyClient extends Context.Tag("@skygent/BskyClient")<
|
|
|
1626
1631
|
const getLists = (actor: string, opts?: GraphListsOptions) =>
|
|
1627
1632
|
Effect.gen(function* () {
|
|
1628
1633
|
yield* ensureAuth(false);
|
|
1634
|
+
const api = agent.hasSession ? agent : publicAgent;
|
|
1629
1635
|
const params = withCursor(
|
|
1630
1636
|
{
|
|
1631
1637
|
actor,
|
|
@@ -1639,7 +1645,7 @@ export class BskyClient extends Context.Tag("@skygent/BskyClient")<
|
|
|
1639
1645
|
const response = yield* withRetry(
|
|
1640
1646
|
withRateLimit(
|
|
1641
1647
|
Effect.tryPromise<AppBskyGraphGetLists.Response>(() =>
|
|
1642
|
-
|
|
1648
|
+
api.app.bsky.graph.getLists(params)
|
|
1643
1649
|
)
|
|
1644
1650
|
)
|
|
1645
1651
|
).pipe(Effect.mapError(toBskyError("Failed to fetch lists", "getLists")));
|
package/src/services/shared.ts
CHANGED
|
@@ -19,10 +19,57 @@ export const pickDefined = <T extends Record<string, unknown>>(input: T): Partia
|
|
|
19
19
|
Object.entries(input).filter(([, value]) => value !== undefined)
|
|
20
20
|
) as Partial<T>;
|
|
21
21
|
|
|
22
|
+
type FormatParseErrorOptions = {
|
|
23
|
+
readonly label?: string;
|
|
24
|
+
readonly maxIssues?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const formatPath = (path: ReadonlyArray<unknown>) =>
|
|
28
|
+
path.length > 0 ? path.map((entry) => String(entry)).join(".") : "value";
|
|
29
|
+
|
|
30
|
+
export const formatParseError = (
|
|
31
|
+
error: ParseResult.ParseError,
|
|
32
|
+
options?: FormatParseErrorOptions
|
|
33
|
+
) => {
|
|
34
|
+
const issues = ParseResult.ArrayFormatter.formatErrorSync(error);
|
|
35
|
+
if (issues.length === 0) {
|
|
36
|
+
return ParseResult.TreeFormatter.formatErrorSync(error);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const jsonParseIssue = issues.find(
|
|
40
|
+
(issue) =>
|
|
41
|
+
issue._tag === "Transformation" &&
|
|
42
|
+
typeof issue.message === "string" &&
|
|
43
|
+
issue.message.startsWith("JSON Parse error")
|
|
44
|
+
);
|
|
45
|
+
if (jsonParseIssue) {
|
|
46
|
+
const header = options?.label
|
|
47
|
+
? `Invalid JSON input for ${options.label}.`
|
|
48
|
+
: "Invalid JSON input.";
|
|
49
|
+
return [
|
|
50
|
+
header,
|
|
51
|
+
jsonParseIssue.message,
|
|
52
|
+
"Tip: wrap JSON in single quotes to avoid shell escaping issues."
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const maxIssues = options?.maxIssues ?? 6;
|
|
57
|
+
const lines = issues.slice(0, maxIssues).map((issue) => {
|
|
58
|
+
const path = formatPath(issue.path);
|
|
59
|
+
return `${path}: ${issue.message}`;
|
|
60
|
+
});
|
|
61
|
+
if (issues.length > maxIssues) {
|
|
62
|
+
lines.push(`Additional issues: ${issues.length - maxIssues}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const header = options?.label ? `Invalid ${options.label}.` : undefined;
|
|
66
|
+
return header ? [header, ...lines].join("\n") : lines.join("\n");
|
|
67
|
+
};
|
|
68
|
+
|
|
22
69
|
/** Format a Schema parse error (or arbitrary unknown) as a readable string. */
|
|
23
70
|
export const formatSchemaError = (error: unknown) => {
|
|
24
71
|
if (ParseResult.isParseError(error)) {
|
|
25
|
-
return
|
|
72
|
+
return formatParseError(error);
|
|
26
73
|
}
|
|
27
74
|
return String(error);
|
|
28
75
|
};
|
|
@@ -11,7 +11,10 @@ export class StoreCleaner extends Context.Tag("@skygent/StoreCleaner")<
|
|
|
11
11
|
{
|
|
12
12
|
readonly deleteStore: (
|
|
13
13
|
name: StoreName
|
|
14
|
-
) => Effect.Effect<
|
|
14
|
+
) => Effect.Effect<
|
|
15
|
+
{ readonly deleted: boolean; readonly reason?: "missing" },
|
|
16
|
+
StoreError
|
|
17
|
+
>;
|
|
15
18
|
}
|
|
16
19
|
>() {
|
|
17
20
|
static readonly layer = Layer.effect(
|
|
@@ -26,7 +29,7 @@ export class StoreCleaner extends Context.Tag("@skygent/StoreCleaner")<
|
|
|
26
29
|
Effect.gen(function* () {
|
|
27
30
|
const storeOption = yield* manager.getStore(name);
|
|
28
31
|
if (Option.isNone(storeOption)) {
|
|
29
|
-
return { deleted: false } as const;
|
|
32
|
+
return { deleted: false, reason: "missing" } as const;
|
|
30
33
|
}
|
|
31
34
|
const store = storeOption.value;
|
|
32
35
|
yield* eventLog.clear(store);
|
|
@@ -13,6 +13,7 @@ type StoreRenameResult = {
|
|
|
13
13
|
readonly from: StoreName;
|
|
14
14
|
readonly to: StoreName;
|
|
15
15
|
readonly moved: boolean;
|
|
16
|
+
readonly movedOnDisk: boolean;
|
|
16
17
|
readonly lineagesUpdated: number;
|
|
17
18
|
readonly checkpointsUpdated: number;
|
|
18
19
|
};
|
|
@@ -262,7 +263,8 @@ export class StoreRenamer extends Context.Tag("@skygent/StoreRenamer")<
|
|
|
262
263
|
return {
|
|
263
264
|
from,
|
|
264
265
|
to,
|
|
265
|
-
moved:
|
|
266
|
+
moved: true,
|
|
267
|
+
movedOnDisk: fromExists,
|
|
266
268
|
lineagesUpdated,
|
|
267
269
|
checkpointsUpdated
|
|
268
270
|
} satisfies StoreRenameResult;
|