@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,126 @@
|
|
|
1
|
+
import * as Doc from "@effect/printer/Doc";
|
|
2
|
+
import type { Annotation } from "./annotation.js";
|
|
3
|
+
import { connector } from "./primitives.js";
|
|
4
|
+
|
|
5
|
+
type SDoc = Doc.Doc<Annotation>;
|
|
6
|
+
|
|
7
|
+
export interface RenderContext<E> {
|
|
8
|
+
readonly depth: number;
|
|
9
|
+
readonly isRoot: boolean;
|
|
10
|
+
readonly isLast: boolean;
|
|
11
|
+
readonly edge?: E;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TreeConfig<T, E = void> {
|
|
15
|
+
readonly children: (node: T) => ReadonlyArray<{ node: T; edge: E }>;
|
|
16
|
+
/** Return a single Doc (single-line) or an array of Docs (multi-line node).
|
|
17
|
+
* First element is the headline; subsequent elements are continuation lines
|
|
18
|
+
* that receive the content prefix (│ or spaces) without a connector. */
|
|
19
|
+
readonly renderNode: (node: T, ctx: RenderContext<E>) => SDoc | ReadonlyArray<SDoc>;
|
|
20
|
+
readonly details?: (node: T, ctx: RenderContext<E>) => ReadonlyArray<SDoc>;
|
|
21
|
+
readonly key: (node: T) => string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface WalkParams<T, E> {
|
|
25
|
+
readonly prefix: string;
|
|
26
|
+
readonly isLast: boolean;
|
|
27
|
+
readonly isRoot: boolean;
|
|
28
|
+
readonly path: ReadonlyArray<string>;
|
|
29
|
+
readonly edge: E | undefined;
|
|
30
|
+
readonly depth: number;
|
|
31
|
+
readonly docs: Array<SDoc>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const walkNode = <T, E>(
|
|
35
|
+
node: T,
|
|
36
|
+
config: TreeConfig<T, E>,
|
|
37
|
+
params: WalkParams<T, E>
|
|
38
|
+
): void => {
|
|
39
|
+
const { prefix, isLast, isRoot, path, edge, depth, docs } = params;
|
|
40
|
+
const key = config.key(node);
|
|
41
|
+
const ctx: RenderContext<E> = { depth, isRoot, isLast, edge: edge as E };
|
|
42
|
+
|
|
43
|
+
// 1. Headline with connector prefix (+ continuation lines for multi-line nodes)
|
|
44
|
+
const rendered = config.renderNode(node, ctx);
|
|
45
|
+
const nodeLines = Array.isArray(rendered) ? rendered : [rendered];
|
|
46
|
+
const connectorStr = isRoot ? "" : isLast ? "└── " : "├── ";
|
|
47
|
+
const contentPrefix = prefix + (isRoot ? "" : isLast ? " " : "│ ");
|
|
48
|
+
nodeLines.forEach((line, i) => {
|
|
49
|
+
if (i === 0) {
|
|
50
|
+
docs.push(Doc.cat(connector(prefix + connectorStr), line));
|
|
51
|
+
} else {
|
|
52
|
+
docs.push(Doc.cat(connector(contentPrefix), line));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 2. Cycle check: path-based
|
|
57
|
+
if (path.includes(key)) {
|
|
58
|
+
const nextPrefix = prefix + (isRoot ? "" : isLast ? " " : "│ ");
|
|
59
|
+
docs.push(Doc.cat(
|
|
60
|
+
connector(nextPrefix + "└── "),
|
|
61
|
+
Doc.annotate(Doc.text("(cycle detected)"), "cycle" as Annotation)
|
|
62
|
+
));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. Compute child prefix
|
|
67
|
+
const nextPrefix = prefix + (isRoot ? "" : isLast ? " " : "│ ");
|
|
68
|
+
const nextPath = [...path, key];
|
|
69
|
+
|
|
70
|
+
// 4. Collect detail lines and children
|
|
71
|
+
const detailDocs = config.details?.(node, ctx) ?? [];
|
|
72
|
+
const childEntries = config.children(node);
|
|
73
|
+
const items: Array<
|
|
74
|
+
| { readonly type: "detail"; readonly doc: SDoc }
|
|
75
|
+
| { readonly type: "child"; readonly node: T; readonly edge: E }
|
|
76
|
+
> = [
|
|
77
|
+
...detailDocs.map((d) => ({ type: "detail" as const, doc: d })),
|
|
78
|
+
...childEntries.map((c) => ({ type: "child" as const, node: c.node, edge: c.edge }))
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// 5. Render each item with proper connectors
|
|
82
|
+
items.forEach((item, i) => {
|
|
83
|
+
const itemIsLast = i === items.length - 1;
|
|
84
|
+
if (item.type === "detail") {
|
|
85
|
+
const itemConnector = itemIsLast ? "└── " : "├── ";
|
|
86
|
+
docs.push(Doc.cat(
|
|
87
|
+
connector(nextPrefix + itemConnector),
|
|
88
|
+
item.doc
|
|
89
|
+
));
|
|
90
|
+
} else {
|
|
91
|
+
walkNode(item.node, config, {
|
|
92
|
+
prefix: nextPrefix,
|
|
93
|
+
isLast: itemIsLast,
|
|
94
|
+
isRoot: false,
|
|
95
|
+
path: nextPath,
|
|
96
|
+
edge: item.edge,
|
|
97
|
+
depth: depth + 1,
|
|
98
|
+
docs
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const renderTree = <T, E = void>(
|
|
105
|
+
roots: ReadonlyArray<T>,
|
|
106
|
+
config: TreeConfig<T, E>
|
|
107
|
+
): SDoc => {
|
|
108
|
+
const docs: Array<SDoc> = [];
|
|
109
|
+
|
|
110
|
+
roots.forEach((root, i) => {
|
|
111
|
+
walkNode(root, config, {
|
|
112
|
+
prefix: "",
|
|
113
|
+
isLast: i === roots.length - 1,
|
|
114
|
+
isRoot: true,
|
|
115
|
+
path: [],
|
|
116
|
+
edge: undefined,
|
|
117
|
+
depth: 0,
|
|
118
|
+
docs
|
|
119
|
+
});
|
|
120
|
+
if (i < roots.length - 1) {
|
|
121
|
+
docs.push(Doc.empty);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return Doc.vsep(docs);
|
|
126
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export class CliJsonError extends Schema.TaggedError<CliJsonError>()(
|
|
4
|
+
"CliJsonError",
|
|
5
|
+
{
|
|
6
|
+
message: Schema.String,
|
|
7
|
+
cause: Schema.Defect
|
|
8
|
+
}
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
export class CliInputError extends Schema.TaggedError<CliInputError>()(
|
|
12
|
+
"CliInputError",
|
|
13
|
+
{
|
|
14
|
+
message: Schema.String,
|
|
15
|
+
cause: Schema.Defect
|
|
16
|
+
}
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
export type AgentErrorPayload = {
|
|
20
|
+
readonly error: string;
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly received?: unknown;
|
|
23
|
+
readonly expected?: unknown;
|
|
24
|
+
readonly fix?: string;
|
|
25
|
+
readonly details?: ReadonlyArray<string>;
|
|
26
|
+
readonly validTags?: ReadonlyArray<string>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const formatAgentError = (payload: AgentErrorPayload) =>
|
|
30
|
+
JSON.stringify(payload, null, 2);
|
|
31
|
+
|
|
32
|
+
const looksLikeJson = (value: string) => {
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
return trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const isAgentErrorPayload = (value: unknown): value is AgentErrorPayload => {
|
|
38
|
+
if (typeof value !== "object" || value === null) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (!("error" in value) || !("message" in value)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const error = (value as { readonly error?: unknown }).error;
|
|
45
|
+
const message = (value as { readonly message?: unknown }).message;
|
|
46
|
+
return typeof error === "string" && typeof message === "string";
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const parseAgentErrorPayload = (message: string): AgentErrorPayload | undefined => {
|
|
50
|
+
if (!looksLikeJson(message)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(message) as unknown;
|
|
55
|
+
return isAgentErrorPayload(parsed) ? parsed : undefined;
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ValidationError } from "@effect/cli";
|
|
2
|
+
import { Cause, Exit, Option } from "effect";
|
|
3
|
+
import { CliInputError, CliJsonError } from "./errors.js";
|
|
4
|
+
import {
|
|
5
|
+
BskyError,
|
|
6
|
+
ConfigError,
|
|
7
|
+
FilterCompileError,
|
|
8
|
+
FilterEvalError,
|
|
9
|
+
FilterLibraryError,
|
|
10
|
+
FilterNotFound,
|
|
11
|
+
StoreIoError,
|
|
12
|
+
StoreIndexError,
|
|
13
|
+
StoreNotFound
|
|
14
|
+
} from "../domain/errors.js";
|
|
15
|
+
import { SyncError } from "../domain/sync.js";
|
|
16
|
+
import { DerivationError } from "../domain/derivation.js";
|
|
17
|
+
|
|
18
|
+
export const exitCodeFor = (error: unknown): number => {
|
|
19
|
+
if (ValidationError.isValidationError(error)) return 2;
|
|
20
|
+
if (error instanceof CliJsonError) return 2;
|
|
21
|
+
if (error instanceof CliInputError) return 2;
|
|
22
|
+
if (error instanceof ConfigError) return 2;
|
|
23
|
+
if (error instanceof StoreNotFound) return 3;
|
|
24
|
+
if (error instanceof FilterNotFound) return 2;
|
|
25
|
+
if (error instanceof FilterLibraryError) return 2;
|
|
26
|
+
if (error instanceof StoreIoError || error instanceof StoreIndexError) return 7;
|
|
27
|
+
if (error instanceof FilterCompileError || error instanceof FilterEvalError) return 8;
|
|
28
|
+
if (error instanceof DerivationError) return 2;
|
|
29
|
+
if (error instanceof SyncError) {
|
|
30
|
+
switch (error.stage) {
|
|
31
|
+
case "source":
|
|
32
|
+
return 5;
|
|
33
|
+
case "filter":
|
|
34
|
+
return 8;
|
|
35
|
+
case "store":
|
|
36
|
+
return 7;
|
|
37
|
+
default:
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (error instanceof BskyError) return 5;
|
|
42
|
+
return 1;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const exitCodeFromExit = (exit: Exit.Exit<unknown, unknown>) => {
|
|
46
|
+
if (Exit.isSuccess(exit)) return 0;
|
|
47
|
+
const failure = Cause.failureOption(exit.cause);
|
|
48
|
+
return Option.match(failure, {
|
|
49
|
+
onNone: () => 1,
|
|
50
|
+
onSome: exitCodeFor
|
|
51
|
+
});
|
|
52
|
+
};
|
package/src/cli/feed.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { Effect, Option, Stream } from "effect";
|
|
3
|
+
import { renderTableLegacy } from "./doc/table.js";
|
|
4
|
+
import { BskyClient } from "../services/bsky-client.js";
|
|
5
|
+
import type { FeedGeneratorView } from "../domain/bsky.js";
|
|
6
|
+
import { AppConfigService } from "../services/app-config.js";
|
|
7
|
+
import { decodeActor, parseLimit } from "./shared-options.js";
|
|
8
|
+
import { CliInputError } from "./errors.js";
|
|
9
|
+
import { withExamples } from "./help.js";
|
|
10
|
+
import { writeJson, writeJsonStream, writeText } from "./output.js";
|
|
11
|
+
import { jsonNdjsonTableFormats, resolveOutputFormat } from "./output-format.js";
|
|
12
|
+
|
|
13
|
+
const feedUriArg = Args.text({ name: "uri" }).pipe(
|
|
14
|
+
Args.withDescription("Bluesky feed URI (at://...)")
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const feedUrisArg = Args.text({ name: "uri" }).pipe(
|
|
18
|
+
Args.repeated,
|
|
19
|
+
Args.withDescription("Feed URIs to fetch")
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const actorArg = Args.text({ name: "actor" }).pipe(
|
|
23
|
+
Args.withDescription("Bluesky handle or DID")
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const limitOption = Options.integer("limit").pipe(
|
|
27
|
+
Options.withDescription("Maximum number of results"),
|
|
28
|
+
Options.optional
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const cursorOption = Options.text("cursor").pipe(
|
|
32
|
+
Options.withDescription("Pagination cursor"),
|
|
33
|
+
Options.optional
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const formatOption = Options.choice("format", jsonNdjsonTableFormats).pipe(
|
|
37
|
+
Options.withDescription("Output format (default: json)"),
|
|
38
|
+
Options.optional
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const renderFeedTable = (
|
|
42
|
+
feeds: ReadonlyArray<FeedGeneratorView>,
|
|
43
|
+
cursor: string | undefined
|
|
44
|
+
) => {
|
|
45
|
+
const rows = feeds.map((feed) => [
|
|
46
|
+
feed.displayName,
|
|
47
|
+
feed.creator.handle,
|
|
48
|
+
feed.uri,
|
|
49
|
+
typeof feed.likeCount === "number" ? String(feed.likeCount) : ""
|
|
50
|
+
]);
|
|
51
|
+
const table = renderTableLegacy(["NAME", "CREATOR", "URI", "LIKES"], rows);
|
|
52
|
+
return cursor ? `${table}\n\nCursor: ${cursor}` : table;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const renderFeedInfoTable = (
|
|
56
|
+
view: FeedGeneratorView,
|
|
57
|
+
isOnline: boolean,
|
|
58
|
+
isValid: boolean
|
|
59
|
+
) =>
|
|
60
|
+
renderTableLegacy(
|
|
61
|
+
["NAME", "CREATOR", "URI", "ONLINE", "VALID"],
|
|
62
|
+
[[view.displayName, view.creator.handle, view.uri, String(isOnline), String(isValid)]]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const showCommand = Command.make(
|
|
66
|
+
"show",
|
|
67
|
+
{ uri: feedUriArg, format: formatOption },
|
|
68
|
+
({ uri, format }) =>
|
|
69
|
+
Effect.gen(function* () {
|
|
70
|
+
const appConfig = yield* AppConfigService;
|
|
71
|
+
const client = yield* BskyClient;
|
|
72
|
+
const result = yield* client.getFeedGenerator(uri);
|
|
73
|
+
const outputFormat = resolveOutputFormat(
|
|
74
|
+
format,
|
|
75
|
+
appConfig.outputFormat,
|
|
76
|
+
jsonNdjsonTableFormats,
|
|
77
|
+
"json"
|
|
78
|
+
);
|
|
79
|
+
if (outputFormat === "table") {
|
|
80
|
+
yield* writeText(renderFeedInfoTable(result.view, result.isOnline, result.isValid));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
yield* writeJson(result);
|
|
84
|
+
})
|
|
85
|
+
).pipe(
|
|
86
|
+
Command.withDescription(
|
|
87
|
+
withExamples("Show feed generator details", [
|
|
88
|
+
"skygent feed show at://did:plc:example/app.bsky.feed.generator/xyz"
|
|
89
|
+
])
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const batchCommand = Command.make(
|
|
94
|
+
"batch",
|
|
95
|
+
{ uris: feedUrisArg, format: formatOption },
|
|
96
|
+
({ uris, format }) =>
|
|
97
|
+
Effect.gen(function* () {
|
|
98
|
+
const appConfig = yield* AppConfigService;
|
|
99
|
+
const client = yield* BskyClient;
|
|
100
|
+
if (uris.length === 0) {
|
|
101
|
+
return yield* CliInputError.make({
|
|
102
|
+
message: "Provide at least one feed URI.",
|
|
103
|
+
cause: { uris }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const result = yield* client.getFeedGenerators(uris);
|
|
107
|
+
const outputFormat = resolveOutputFormat(
|
|
108
|
+
format,
|
|
109
|
+
appConfig.outputFormat,
|
|
110
|
+
jsonNdjsonTableFormats,
|
|
111
|
+
"json"
|
|
112
|
+
);
|
|
113
|
+
if (outputFormat === "ndjson") {
|
|
114
|
+
yield* writeJsonStream(Stream.fromIterable(result.feeds));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (outputFormat === "table") {
|
|
118
|
+
yield* writeText(renderFeedTable(result.feeds, undefined));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
yield* writeJson(result);
|
|
122
|
+
})
|
|
123
|
+
).pipe(
|
|
124
|
+
Command.withDescription(
|
|
125
|
+
withExamples("Fetch multiple feed generators", [
|
|
126
|
+
"skygent feed batch at://did:plc:example/app.bsky.feed.generator/one at://did:plc:example/app.bsky.feed.generator/two"
|
|
127
|
+
])
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const byActorCommand = Command.make(
|
|
132
|
+
"by",
|
|
133
|
+
{ actor: actorArg, limit: limitOption, cursor: cursorOption, format: formatOption },
|
|
134
|
+
({ actor, limit, cursor, format }) =>
|
|
135
|
+
Effect.gen(function* () {
|
|
136
|
+
const appConfig = yield* AppConfigService;
|
|
137
|
+
const client = yield* BskyClient;
|
|
138
|
+
const parsedLimit = yield* parseLimit(limit);
|
|
139
|
+
const resolvedActor = yield* decodeActor(actor);
|
|
140
|
+
const result = yield* client.getActorFeeds(resolvedActor, {
|
|
141
|
+
...(Option.isSome(parsedLimit) ? { limit: parsedLimit.value } : {}),
|
|
142
|
+
...(Option.isSome(cursor) ? { cursor: cursor.value } : {})
|
|
143
|
+
});
|
|
144
|
+
const outputFormat = resolveOutputFormat(
|
|
145
|
+
format,
|
|
146
|
+
appConfig.outputFormat,
|
|
147
|
+
jsonNdjsonTableFormats,
|
|
148
|
+
"json"
|
|
149
|
+
);
|
|
150
|
+
if (outputFormat === "ndjson") {
|
|
151
|
+
yield* writeJsonStream(Stream.fromIterable(result.feeds));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (outputFormat === "table") {
|
|
155
|
+
yield* writeText(renderFeedTable(result.feeds, result.cursor));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
yield* writeJson(result);
|
|
159
|
+
})
|
|
160
|
+
).pipe(
|
|
161
|
+
Command.withDescription(
|
|
162
|
+
withExamples("List feeds created by an actor", [
|
|
163
|
+
"skygent feed by alice.bsky.social",
|
|
164
|
+
"skygent feed by did:plc:example --limit 25"
|
|
165
|
+
])
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
export const feedCommand = Command.make("feed", {}).pipe(
|
|
170
|
+
Command.withSubcommands([showCommand, batchCommand, byActorCommand]),
|
|
171
|
+
Command.withDescription(
|
|
172
|
+
withExamples("Discover feed generators", [
|
|
173
|
+
"skygent feed show at://did:plc:example/app.bsky.feed.generator/xyz",
|
|
174
|
+
"skygent feed by alice.bsky.social"
|
|
175
|
+
])
|
|
176
|
+
)
|
|
177
|
+
);
|