@mepuka/skygent 0.2.0 → 0.3.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 +269 -31
- package/index.ts +18 -3
- package/package.json +1 -1
- package/src/cli/app.ts +4 -2
- package/src/cli/config.ts +20 -3
- package/src/cli/doc/table-renderers.ts +29 -0
- package/src/cli/doc/thread.ts +2 -4
- package/src/cli/exit-codes.ts +2 -0
- package/src/cli/feed.ts +35 -55
- package/src/cli/filter-dsl.ts +146 -11
- package/src/cli/filter-errors.ts +9 -3
- package/src/cli/filter-help.ts +7 -0
- package/src/cli/filter-input.ts +3 -2
- package/src/cli/filter.ts +84 -4
- package/src/cli/graph.ts +193 -156
- package/src/cli/input.ts +45 -0
- package/src/cli/layers.ts +10 -0
- package/src/cli/logging.ts +8 -0
- package/src/cli/output-render.ts +14 -0
- package/src/cli/pagination.ts +18 -0
- package/src/cli/parse-errors.ts +18 -0
- package/src/cli/pipe.ts +157 -0
- package/src/cli/post.ts +43 -66
- package/src/cli/query.ts +349 -74
- package/src/cli/search.ts +92 -118
- package/src/cli/shared.ts +0 -19
- package/src/cli/store-errors.ts +24 -13
- package/src/cli/store-tree.ts +6 -4
- package/src/cli/store.ts +35 -2
- package/src/cli/stream-merge.ts +105 -0
- package/src/cli/sync-factory.ts +28 -3
- package/src/cli/sync.ts +16 -18
- package/src/cli/thread-options.ts +33 -0
- package/src/cli/time.ts +171 -0
- package/src/cli/view-thread.ts +12 -18
- package/src/cli/watch.ts +61 -19
- package/src/domain/errors.ts +6 -1
- package/src/domain/format.ts +21 -0
- package/src/domain/order.ts +24 -0
- package/src/graph/relationships.ts +129 -0
- package/src/services/jetstream-sync.ts +4 -4
- package/src/services/lineage-store.ts +15 -1
- package/src/services/store-commit.ts +60 -0
- package/src/services/store-manager.ts +69 -2
- package/src/services/store-renamer.ts +286 -0
- package/src/services/store-stats.ts +7 -5
- package/src/services/sync-engine.ts +136 -85
- package/src/services/sync-reporter.ts +3 -1
- package/src/services/sync-settings.ts +24 -0
package/src/cli/pipe.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Chunk, Effect, Option, Ref, Stream } from "effect";
|
|
3
|
+
import { ParseResult } from "effect";
|
|
4
|
+
import { RawPost } from "../domain/raw.js";
|
|
5
|
+
import type { Post } from "../domain/post.js";
|
|
6
|
+
import { FilterRuntime } from "../services/filter-runtime.js";
|
|
7
|
+
import { PostParser } from "../services/post-parser.js";
|
|
8
|
+
import { CliInput } from "./input.js";
|
|
9
|
+
import { CliInputError, CliJsonError } from "./errors.js";
|
|
10
|
+
import { parseFilterExpr } from "./filter-input.js";
|
|
11
|
+
import { decodeJson } from "./parse.js";
|
|
12
|
+
import { withExamples } from "./help.js";
|
|
13
|
+
import { filterOption, filterJsonOption } from "./shared-options.js";
|
|
14
|
+
import { formatSchemaError } from "./shared.js";
|
|
15
|
+
import { writeJsonStream, writeText } from "./output.js";
|
|
16
|
+
import { filterByFlags } from "../typeclass/chunk.js";
|
|
17
|
+
import { logErrorEvent, logWarn } from "./logging.js";
|
|
18
|
+
|
|
19
|
+
const onErrorOption = Options.choice("on-error", ["fail", "skip", "report"]).pipe(
|
|
20
|
+
Options.withDescription("Behavior on invalid input lines"),
|
|
21
|
+
Options.withDefault("fail" as const)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const batchSizeOption = Options.integer("batch-size").pipe(
|
|
25
|
+
Options.withDescription("Posts per filter batch (default: 50)"),
|
|
26
|
+
Options.optional
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const requireFilterExpr = (
|
|
30
|
+
filter: Option.Option<string>,
|
|
31
|
+
filterJson: Option.Option<string>
|
|
32
|
+
) =>
|
|
33
|
+
Option.isNone(filter) && Option.isNone(filterJson)
|
|
34
|
+
? Effect.fail(
|
|
35
|
+
CliInputError.make({
|
|
36
|
+
message: "Provide --filter or --filter-json.",
|
|
37
|
+
cause: { filter: null, filterJson: null }
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
: Effect.void;
|
|
41
|
+
|
|
42
|
+
const truncate = (value: string, max = 500) =>
|
|
43
|
+
value.length > max ? `${value.slice(0, max)}...` : value;
|
|
44
|
+
|
|
45
|
+
const formatPipeError = (error: unknown) => {
|
|
46
|
+
if (error instanceof CliJsonError || error instanceof CliInputError) {
|
|
47
|
+
return error.message;
|
|
48
|
+
}
|
|
49
|
+
if (ParseResult.isParseError(error)) {
|
|
50
|
+
return formatSchemaError(error);
|
|
51
|
+
}
|
|
52
|
+
if (typeof error === "object" && error !== null && "message" in error) {
|
|
53
|
+
const message = (error as { readonly message?: unknown }).message;
|
|
54
|
+
if (typeof message === "string" && message.length > 0) {
|
|
55
|
+
return message;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return String(error);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const pipeCommand = Command.make(
|
|
62
|
+
"pipe",
|
|
63
|
+
{ filter: filterOption, filterJson: filterJsonOption, onError: onErrorOption, batchSize: batchSizeOption },
|
|
64
|
+
({ filter, filterJson, onError, batchSize }) =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
if (process.stdin.isTTY) {
|
|
67
|
+
return yield* CliInputError.make({
|
|
68
|
+
message: "stdin is a TTY. Pipe NDJSON input into skygent pipe.",
|
|
69
|
+
cause: { isTTY: true }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
yield* requireFilterExpr(filter, filterJson);
|
|
73
|
+
|
|
74
|
+
const input = yield* CliInput;
|
|
75
|
+
const parser = yield* PostParser;
|
|
76
|
+
const runtime = yield* FilterRuntime;
|
|
77
|
+
const expr = yield* parseFilterExpr(filter, filterJson);
|
|
78
|
+
const evaluateBatch = yield* runtime.evaluateBatch(expr);
|
|
79
|
+
|
|
80
|
+
const size = Option.getOrElse(batchSize, () => 50);
|
|
81
|
+
if (size <= 0) {
|
|
82
|
+
return yield* CliInputError.make({
|
|
83
|
+
message: "--batch-size must be a positive integer.",
|
|
84
|
+
cause: { batchSize: size }
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const lineRef = yield* Ref.make(0);
|
|
89
|
+
const countRef = yield* Ref.make(0);
|
|
90
|
+
|
|
91
|
+
const parsed = input.lines.pipe(
|
|
92
|
+
Stream.map((line) => line.trim()),
|
|
93
|
+
Stream.filter((line) => line.length > 0),
|
|
94
|
+
Stream.mapEffect((line) =>
|
|
95
|
+
Ref.updateAndGet(lineRef, (value) => value + 1).pipe(
|
|
96
|
+
Effect.map((lineNumber) => ({ line, lineNumber }))
|
|
97
|
+
)
|
|
98
|
+
),
|
|
99
|
+
Stream.mapEffect(({ line, lineNumber }) =>
|
|
100
|
+
decodeJson(RawPost, line).pipe(
|
|
101
|
+
Effect.flatMap((raw) => parser.parsePost(raw)),
|
|
102
|
+
Effect.map(Option.some),
|
|
103
|
+
Effect.catchAll((error) => {
|
|
104
|
+
if (onError === "fail") {
|
|
105
|
+
return Effect.fail(error);
|
|
106
|
+
}
|
|
107
|
+
const message = formatPipeError(error);
|
|
108
|
+
const payload = {
|
|
109
|
+
line: lineNumber,
|
|
110
|
+
message,
|
|
111
|
+
input: truncate(line)
|
|
112
|
+
};
|
|
113
|
+
const log =
|
|
114
|
+
onError === "report"
|
|
115
|
+
? logErrorEvent("Invalid input line", payload)
|
|
116
|
+
: logWarn("Skipping invalid input line", payload);
|
|
117
|
+
return log.pipe(
|
|
118
|
+
Effect.ignore,
|
|
119
|
+
Effect.as(Option.none<Post>())
|
|
120
|
+
);
|
|
121
|
+
})
|
|
122
|
+
)
|
|
123
|
+
),
|
|
124
|
+
Stream.filterMap((value) => value)
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const filtered = parsed.pipe(
|
|
128
|
+
Stream.grouped(size),
|
|
129
|
+
Stream.mapEffect((batch) =>
|
|
130
|
+
evaluateBatch(batch).pipe(
|
|
131
|
+
Effect.map((flags) => filterByFlags(batch, flags))
|
|
132
|
+
)
|
|
133
|
+
),
|
|
134
|
+
Stream.mapConcat((chunk) => Chunk.toReadonlyArray(chunk)),
|
|
135
|
+
Stream.tap(() => Ref.update(countRef, (count) => count + 1))
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
yield* writeJsonStream(filtered);
|
|
139
|
+
const count = yield* Ref.get(countRef);
|
|
140
|
+
if (count === 0) {
|
|
141
|
+
yield* writeText("[]");
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
).pipe(
|
|
145
|
+
Command.withDescription(
|
|
146
|
+
withExamples(
|
|
147
|
+
"Filter raw post NDJSON from stdin",
|
|
148
|
+
[
|
|
149
|
+
"skygent pipe --filter 'hashtag:#ai' < posts.ndjson",
|
|
150
|
+
"cat posts.ndjson | skygent pipe --filter-json '{\"_tag\":\"All\"}'"
|
|
151
|
+
],
|
|
152
|
+
[
|
|
153
|
+
"Note: stdin must be raw post NDJSON (app.bsky.feed.getPosts result)."
|
|
154
|
+
]
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
);
|
package/src/cli/post.ts
CHANGED
|
@@ -2,24 +2,25 @@ import { Command, Options } from "@effect/cli";
|
|
|
2
2
|
import { Context, Effect, Option, Stream } from "effect";
|
|
3
3
|
import { BskyClient } from "../services/bsky-client.js";
|
|
4
4
|
import { PostParser } from "../services/post-parser.js";
|
|
5
|
-
import type { PostLike
|
|
5
|
+
import type { PostLike } from "../domain/bsky.js";
|
|
6
6
|
import type { RawPost } from "../domain/raw.js";
|
|
7
7
|
import { renderPostsTable } from "../domain/format.js";
|
|
8
8
|
import { AppConfigService } from "../services/app-config.js";
|
|
9
9
|
import { withExamples } from "./help.js";
|
|
10
|
-
import { postUriArg
|
|
10
|
+
import { postUriArg } from "./shared-options.js";
|
|
11
11
|
import { writeJson, writeJsonStream, writeText } from "./output.js";
|
|
12
12
|
import { renderTableLegacy } from "./doc/table.js";
|
|
13
|
-
import {
|
|
13
|
+
import { renderProfileTable } from "./doc/table-renderers.js";
|
|
14
|
+
import { jsonNdjsonTableFormats } from "./output-format.js";
|
|
15
|
+
import { emitWithFormat } from "./output-render.js";
|
|
16
|
+
import { cursorOption as baseCursorOption, limitOption as baseLimitOption, parsePagination } from "./pagination.js";
|
|
14
17
|
|
|
15
|
-
const limitOption =
|
|
16
|
-
Options.withDescription("Maximum number of results")
|
|
17
|
-
Options.optional
|
|
18
|
+
const limitOption = baseLimitOption.pipe(
|
|
19
|
+
Options.withDescription("Maximum number of results")
|
|
18
20
|
);
|
|
19
21
|
|
|
20
|
-
const cursorOption =
|
|
21
|
-
Options.withDescription("Pagination cursor")
|
|
22
|
-
Options.optional
|
|
22
|
+
const cursorOption = baseCursorOption.pipe(
|
|
23
|
+
Options.withDescription("Pagination cursor")
|
|
23
24
|
);
|
|
24
25
|
|
|
25
26
|
const formatOption = Options.choice("format", jsonNdjsonTableFormats).pipe(
|
|
@@ -32,18 +33,6 @@ const cidOption = Options.text("cid").pipe(
|
|
|
32
33
|
Options.optional
|
|
33
34
|
);
|
|
34
35
|
|
|
35
|
-
const renderProfileTable = (
|
|
36
|
-
actors: ReadonlyArray<ProfileView>,
|
|
37
|
-
cursor: string | undefined
|
|
38
|
-
) => {
|
|
39
|
-
const rows = actors.map((actor) => [
|
|
40
|
-
actor.handle,
|
|
41
|
-
actor.displayName ?? "",
|
|
42
|
-
actor.did
|
|
43
|
-
]);
|
|
44
|
-
const table = renderTableLegacy(["HANDLE", "DISPLAY NAME", "DID"], rows);
|
|
45
|
-
return cursor ? `${table}\n\nCursor: ${cursor}` : table;
|
|
46
|
-
};
|
|
47
36
|
|
|
48
37
|
const renderLikesTable = (likes: ReadonlyArray<PostLike>, cursor: string | undefined) => {
|
|
49
38
|
const rows = likes.map((like) => [
|
|
@@ -72,27 +61,23 @@ const likesCommand = Command.make(
|
|
|
72
61
|
Effect.gen(function* () {
|
|
73
62
|
const appConfig = yield* AppConfigService;
|
|
74
63
|
const client = yield* BskyClient;
|
|
75
|
-
const
|
|
64
|
+
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
76
65
|
const result = yield* client.getLikes(uri, {
|
|
77
|
-
...(
|
|
78
|
-
...(
|
|
66
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
67
|
+
...(cursorValue !== undefined ? { cursor: cursorValue } : {}),
|
|
79
68
|
...(Option.isSome(cid) ? { cid: cid.value } : {})
|
|
80
69
|
});
|
|
81
|
-
|
|
70
|
+
yield* emitWithFormat(
|
|
82
71
|
format,
|
|
83
72
|
appConfig.outputFormat,
|
|
84
73
|
jsonNdjsonTableFormats,
|
|
85
|
-
"json"
|
|
74
|
+
"json",
|
|
75
|
+
{
|
|
76
|
+
json: writeJson(result),
|
|
77
|
+
ndjson: writeJsonStream(Stream.fromIterable(result.likes)),
|
|
78
|
+
table: writeText(renderLikesTable(result.likes, result.cursor))
|
|
79
|
+
}
|
|
86
80
|
);
|
|
87
|
-
if (outputFormat === "ndjson") {
|
|
88
|
-
yield* writeJsonStream(Stream.fromIterable(result.likes));
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (outputFormat === "table") {
|
|
92
|
-
yield* writeText(renderLikesTable(result.likes, result.cursor));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
yield* writeJson(result);
|
|
96
81
|
})
|
|
97
82
|
).pipe(
|
|
98
83
|
Command.withDescription(
|
|
@@ -110,27 +95,23 @@ const repostedByCommand = Command.make(
|
|
|
110
95
|
Effect.gen(function* () {
|
|
111
96
|
const appConfig = yield* AppConfigService;
|
|
112
97
|
const client = yield* BskyClient;
|
|
113
|
-
const
|
|
98
|
+
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
114
99
|
const result = yield* client.getRepostedBy(uri, {
|
|
115
|
-
...(
|
|
116
|
-
...(
|
|
100
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
101
|
+
...(cursorValue !== undefined ? { cursor: cursorValue } : {}),
|
|
117
102
|
...(Option.isSome(cid) ? { cid: cid.value } : {})
|
|
118
103
|
});
|
|
119
|
-
|
|
104
|
+
yield* emitWithFormat(
|
|
120
105
|
format,
|
|
121
106
|
appConfig.outputFormat,
|
|
122
107
|
jsonNdjsonTableFormats,
|
|
123
|
-
"json"
|
|
108
|
+
"json",
|
|
109
|
+
{
|
|
110
|
+
json: writeJson(result),
|
|
111
|
+
ndjson: writeJsonStream(Stream.fromIterable(result.repostedBy)),
|
|
112
|
+
table: writeText(renderProfileTable(result.repostedBy, result.cursor))
|
|
113
|
+
}
|
|
124
114
|
);
|
|
125
|
-
if (outputFormat === "ndjson") {
|
|
126
|
-
yield* writeJsonStream(Stream.fromIterable(result.repostedBy));
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (outputFormat === "table") {
|
|
130
|
-
yield* writeText(renderProfileTable(result.repostedBy, result.cursor));
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
yield* writeJson(result);
|
|
134
115
|
})
|
|
135
116
|
).pipe(
|
|
136
117
|
Command.withDescription(
|
|
@@ -148,31 +129,27 @@ const quotesCommand = Command.make(
|
|
|
148
129
|
const appConfig = yield* AppConfigService;
|
|
149
130
|
const client = yield* BskyClient;
|
|
150
131
|
const parser = yield* PostParser;
|
|
151
|
-
const
|
|
132
|
+
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
152
133
|
const result = yield* client.getQuotes(uri, {
|
|
153
|
-
...(
|
|
154
|
-
...(
|
|
134
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
135
|
+
...(cursorValue !== undefined ? { cursor: cursorValue } : {}),
|
|
155
136
|
...(Option.isSome(cid) ? { cid: cid.value } : {})
|
|
156
137
|
});
|
|
157
138
|
const posts = yield* parseRawPosts(parser, result.posts);
|
|
158
|
-
|
|
139
|
+
yield* emitWithFormat(
|
|
159
140
|
format,
|
|
160
141
|
appConfig.outputFormat,
|
|
161
142
|
jsonNdjsonTableFormats,
|
|
162
|
-
"json"
|
|
143
|
+
"json",
|
|
144
|
+
{
|
|
145
|
+
json: writeJson({
|
|
146
|
+
...result,
|
|
147
|
+
posts
|
|
148
|
+
}),
|
|
149
|
+
ndjson: writeJsonStream(Stream.fromIterable(posts)),
|
|
150
|
+
table: writeText(renderPostsTable(posts))
|
|
151
|
+
}
|
|
163
152
|
);
|
|
164
|
-
if (outputFormat === "ndjson") {
|
|
165
|
-
yield* writeJsonStream(Stream.fromIterable(posts));
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (outputFormat === "table") {
|
|
169
|
-
yield* writeText(renderPostsTable(posts));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
yield* writeJson({
|
|
173
|
-
...result,
|
|
174
|
-
posts
|
|
175
|
-
});
|
|
176
153
|
})
|
|
177
154
|
).pipe(
|
|
178
155
|
Command.withDescription(
|