@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,180 @@
|
|
|
1
|
+
import * as KeyValueStore from "@effect/platform/KeyValueStore";
|
|
2
|
+
import { Path } from "@effect/platform";
|
|
3
|
+
import * as FetchHttpClient from "@effect/platform/FetchHttpClient";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
5
|
+
import { BskyClient } from "../services/bsky-client.js";
|
|
6
|
+
import { FilterRuntime } from "../services/filter-runtime.js";
|
|
7
|
+
import { PostParser } from "../services/post-parser.js";
|
|
8
|
+
import { AppConfigService } from "../services/app-config.js";
|
|
9
|
+
import { CredentialStore } from "../services/credential-store.js";
|
|
10
|
+
import { StoreEventLog } from "../services/store-event-log.js";
|
|
11
|
+
import { StoreIndex } from "../services/store-index.js";
|
|
12
|
+
import { StoreDb } from "../services/store-db.js";
|
|
13
|
+
import { StoreManager } from "../services/store-manager.js";
|
|
14
|
+
import { StoreWriter } from "../services/store-writer.js";
|
|
15
|
+
import { StoreCommitter } from "../services/store-commit.js";
|
|
16
|
+
import { SyncEngine } from "../services/sync-engine.js";
|
|
17
|
+
import { SyncCheckpointStore } from "../services/sync-checkpoint-store.js";
|
|
18
|
+
import { SyncReporter } from "../services/sync-reporter.js";
|
|
19
|
+
import { SyncSettings } from "../services/sync-settings.js";
|
|
20
|
+
import { StoreCleaner } from "../services/store-cleaner.js";
|
|
21
|
+
import { LinkValidator } from "../services/link-validator.js";
|
|
22
|
+
import { TrendingTopics } from "../services/trending-topics.js";
|
|
23
|
+
import { ResourceMonitor } from "../services/resource-monitor.js";
|
|
24
|
+
import { CliOutput } from "./output.js";
|
|
25
|
+
import { DerivationEngine } from "../services/derivation-engine.js";
|
|
26
|
+
import { DerivationValidator } from "../services/derivation-validator.js";
|
|
27
|
+
import { DerivationSettings } from "../services/derivation-settings.js";
|
|
28
|
+
import { ViewCheckpointStore } from "../services/view-checkpoint-store.js";
|
|
29
|
+
import { LineageStore } from "../services/lineage-store.js";
|
|
30
|
+
import { FilterCompiler } from "../services/filter-compiler.js";
|
|
31
|
+
import { FilterSettings } from "../services/filter-settings.js";
|
|
32
|
+
import { OutputManager } from "../services/output-manager.js";
|
|
33
|
+
import { FilterLibrary } from "../services/filter-library.js";
|
|
34
|
+
import { StoreStats } from "../services/store-stats.js";
|
|
35
|
+
import { ProfileResolver } from "../services/profile-resolver.js";
|
|
36
|
+
import { IdentityResolver } from "../services/identity-resolver.js";
|
|
37
|
+
|
|
38
|
+
const appConfigLayer = AppConfigService.layer;
|
|
39
|
+
const credentialLayer = CredentialStore.layer.pipe(Layer.provideMerge(appConfigLayer));
|
|
40
|
+
const bskyLayer = BskyClient.layer.pipe(
|
|
41
|
+
Layer.provideMerge(appConfigLayer),
|
|
42
|
+
Layer.provideMerge(credentialLayer)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const storageLayer = Layer.unwrapEffect(
|
|
46
|
+
Effect.gen(function* () {
|
|
47
|
+
const config = yield* AppConfigService;
|
|
48
|
+
const path = yield* Path.Path;
|
|
49
|
+
const kvRoot = path.join(config.storeRoot, "kv");
|
|
50
|
+
return KeyValueStore.layerFileSystem(kvRoot);
|
|
51
|
+
})
|
|
52
|
+
).pipe(Layer.provide(appConfigLayer));
|
|
53
|
+
const storeDbLayer = StoreDb.layer.pipe(Layer.provideMerge(appConfigLayer));
|
|
54
|
+
const writerLayer = StoreWriter.layer.pipe(Layer.provideMerge(storeDbLayer));
|
|
55
|
+
const committerLayer = StoreCommitter.layer.pipe(
|
|
56
|
+
Layer.provideMerge(storeDbLayer),
|
|
57
|
+
Layer.provideMerge(writerLayer)
|
|
58
|
+
);
|
|
59
|
+
const eventLogLayer = StoreEventLog.layer.pipe(Layer.provideMerge(storeDbLayer));
|
|
60
|
+
const indexLayer = StoreIndex.layer.pipe(
|
|
61
|
+
Layer.provideMerge(storeDbLayer),
|
|
62
|
+
Layer.provideMerge(eventLogLayer)
|
|
63
|
+
);
|
|
64
|
+
const managerLayer = StoreManager.layer.pipe(Layer.provideMerge(appConfigLayer));
|
|
65
|
+
const cleanerLayer = StoreCleaner.layer.pipe(
|
|
66
|
+
Layer.provideMerge(managerLayer),
|
|
67
|
+
Layer.provideMerge(indexLayer),
|
|
68
|
+
Layer.provideMerge(eventLogLayer),
|
|
69
|
+
Layer.provideMerge(storeDbLayer)
|
|
70
|
+
);
|
|
71
|
+
const checkpointLayer = SyncCheckpointStore.layer.pipe(
|
|
72
|
+
Layer.provideMerge(storeDbLayer)
|
|
73
|
+
);
|
|
74
|
+
const linkValidatorLayer = LinkValidator.layer.pipe(
|
|
75
|
+
Layer.provideMerge(storageLayer),
|
|
76
|
+
Layer.provideMerge(FetchHttpClient.layer)
|
|
77
|
+
);
|
|
78
|
+
const trendingTopicsLayer = TrendingTopics.layer.pipe(
|
|
79
|
+
Layer.provideMerge(storageLayer),
|
|
80
|
+
Layer.provideMerge(bskyLayer)
|
|
81
|
+
);
|
|
82
|
+
const resourceMonitorLayer = ResourceMonitor.layer.pipe(
|
|
83
|
+
Layer.provideMerge(appConfigLayer)
|
|
84
|
+
);
|
|
85
|
+
const filterSettingsLayer = FilterSettings.layer;
|
|
86
|
+
const runtimeLayer = FilterRuntime.layer.pipe(
|
|
87
|
+
Layer.provideMerge(filterSettingsLayer),
|
|
88
|
+
Layer.provideMerge(linkValidatorLayer),
|
|
89
|
+
Layer.provideMerge(trendingTopicsLayer)
|
|
90
|
+
);
|
|
91
|
+
const syncSettingsLayer = SyncSettings.layer;
|
|
92
|
+
const syncLayer = SyncEngine.layer.pipe(
|
|
93
|
+
Layer.provideMerge(committerLayer),
|
|
94
|
+
Layer.provideMerge(indexLayer),
|
|
95
|
+
Layer.provideMerge(checkpointLayer),
|
|
96
|
+
Layer.provideMerge(runtimeLayer),
|
|
97
|
+
Layer.provideMerge(PostParser.layer),
|
|
98
|
+
Layer.provideMerge(bskyLayer),
|
|
99
|
+
Layer.provideMerge(SyncReporter.layer),
|
|
100
|
+
Layer.provideMerge(syncSettingsLayer)
|
|
101
|
+
);
|
|
102
|
+
const identityResolverLayer = IdentityResolver.layer.pipe(
|
|
103
|
+
Layer.provideMerge(storageLayer),
|
|
104
|
+
Layer.provideMerge(bskyLayer)
|
|
105
|
+
);
|
|
106
|
+
const profileResolverLayer = ProfileResolver.layer.pipe(
|
|
107
|
+
Layer.provideMerge(bskyLayer),
|
|
108
|
+
Layer.provideMerge(identityResolverLayer)
|
|
109
|
+
);
|
|
110
|
+
const viewCheckpointLayer = ViewCheckpointStore.layer.pipe(
|
|
111
|
+
Layer.provideMerge(storeDbLayer),
|
|
112
|
+
Layer.provideMerge(managerLayer)
|
|
113
|
+
);
|
|
114
|
+
const lineageLayer = LineageStore.layer.pipe(
|
|
115
|
+
Layer.provideMerge(storageLayer)
|
|
116
|
+
);
|
|
117
|
+
const compilerLayer = FilterCompiler.layer;
|
|
118
|
+
const postParserLayer = PostParser.layer;
|
|
119
|
+
const derivationEngineLayer = DerivationEngine.layer.pipe(
|
|
120
|
+
Layer.provideMerge(eventLogLayer),
|
|
121
|
+
Layer.provideMerge(committerLayer),
|
|
122
|
+
Layer.provideMerge(indexLayer),
|
|
123
|
+
Layer.provideMerge(compilerLayer),
|
|
124
|
+
Layer.provideMerge(runtimeLayer),
|
|
125
|
+
Layer.provideMerge(viewCheckpointLayer),
|
|
126
|
+
Layer.provideMerge(lineageLayer),
|
|
127
|
+
Layer.provideMerge(DerivationSettings.layer)
|
|
128
|
+
);
|
|
129
|
+
const derivationValidatorLayer = DerivationValidator.layer.pipe(
|
|
130
|
+
Layer.provideMerge(viewCheckpointLayer),
|
|
131
|
+
Layer.provideMerge(eventLogLayer),
|
|
132
|
+
Layer.provideMerge(managerLayer)
|
|
133
|
+
);
|
|
134
|
+
const outputManagerLayer = OutputManager.layer.pipe(
|
|
135
|
+
Layer.provideMerge(appConfigLayer),
|
|
136
|
+
Layer.provideMerge(managerLayer),
|
|
137
|
+
Layer.provideMerge(indexLayer),
|
|
138
|
+
Layer.provideMerge(runtimeLayer),
|
|
139
|
+
Layer.provideMerge(filterSettingsLayer),
|
|
140
|
+
Layer.provideMerge(compilerLayer)
|
|
141
|
+
);
|
|
142
|
+
const filterLibraryLayer = FilterLibrary.layer.pipe(
|
|
143
|
+
Layer.provideMerge(appConfigLayer)
|
|
144
|
+
);
|
|
145
|
+
const storeStatsLayer = StoreStats.layer.pipe(
|
|
146
|
+
Layer.provideMerge(appConfigLayer),
|
|
147
|
+
Layer.provideMerge(managerLayer),
|
|
148
|
+
Layer.provideMerge(indexLayer),
|
|
149
|
+
Layer.provideMerge(storeDbLayer),
|
|
150
|
+
Layer.provideMerge(lineageLayer),
|
|
151
|
+
Layer.provideMerge(derivationValidatorLayer),
|
|
152
|
+
Layer.provideMerge(eventLogLayer),
|
|
153
|
+
Layer.provideMerge(checkpointLayer)
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
export const CliLive = Layer.mergeAll(
|
|
157
|
+
appConfigLayer,
|
|
158
|
+
filterSettingsLayer,
|
|
159
|
+
credentialLayer,
|
|
160
|
+
CliOutput.layer,
|
|
161
|
+
resourceMonitorLayer,
|
|
162
|
+
managerLayer,
|
|
163
|
+
committerLayer,
|
|
164
|
+
indexLayer,
|
|
165
|
+
eventLogLayer,
|
|
166
|
+
cleanerLayer,
|
|
167
|
+
syncLayer,
|
|
168
|
+
checkpointLayer,
|
|
169
|
+
viewCheckpointLayer,
|
|
170
|
+
derivationEngineLayer,
|
|
171
|
+
derivationValidatorLayer,
|
|
172
|
+
lineageLayer,
|
|
173
|
+
outputManagerLayer,
|
|
174
|
+
storeStatsLayer,
|
|
175
|
+
compilerLayer,
|
|
176
|
+
postParserLayer,
|
|
177
|
+
filterLibraryLayer,
|
|
178
|
+
profileResolverLayer,
|
|
179
|
+
identityResolverLayer
|
|
180
|
+
);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Terminal } from "@effect/platform";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
import { SyncProgress } from "../domain/sync.js";
|
|
4
|
+
import { SyncReporter } from "../services/sync-reporter.js";
|
|
5
|
+
import type { ResourceMonitorService, ResourceWarning } from "../services/resource-monitor.js";
|
|
6
|
+
import type { CliOutputService } from "./output.js";
|
|
7
|
+
import { CliOutput } from "./output.js";
|
|
8
|
+
import { CliPreferences } from "./preferences.js";
|
|
9
|
+
|
|
10
|
+
type LogLevel = "INFO" | "WARN" | "ERROR" | "PROGRESS";
|
|
11
|
+
export type LogFormat = "json" | "human";
|
|
12
|
+
|
|
13
|
+
const nowIso = () => new Date().toISOString();
|
|
14
|
+
|
|
15
|
+
const encodeLog = (level: LogLevel, payload: Record<string, unknown>) =>
|
|
16
|
+
JSON.stringify({
|
|
17
|
+
timestamp: nowIso(),
|
|
18
|
+
level,
|
|
19
|
+
...payload
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const formatHuman = (level: LogLevel, payload: Record<string, unknown>) => {
|
|
23
|
+
if (level === "PROGRESS" && "progress" in payload) {
|
|
24
|
+
const progress = payload.progress as SyncProgress;
|
|
25
|
+
const rate = Number.isFinite(progress.rate)
|
|
26
|
+
? progress.rate.toFixed(2)
|
|
27
|
+
: String(progress.rate);
|
|
28
|
+
return `[PROGRESS] processed=${progress.processed} stored=${progress.stored} skipped=${progress.skipped} errors=${progress.errors} rate=${rate}/s elapsedMs=${progress.elapsedMs}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const message =
|
|
32
|
+
typeof payload.message === "string" && payload.message.length > 0
|
|
33
|
+
? payload.message
|
|
34
|
+
: "";
|
|
35
|
+
const rest = { ...payload };
|
|
36
|
+
delete rest.message;
|
|
37
|
+
const suffix = Object.keys(rest).length > 0 ? ` ${JSON.stringify(rest)}` : "";
|
|
38
|
+
const base = `[${level}]${message ? ` ${message}` : ""}`;
|
|
39
|
+
return `${base}${suffix}`.trim();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const encodeLogFormat = (
|
|
43
|
+
format: LogFormat,
|
|
44
|
+
level: LogLevel,
|
|
45
|
+
payload: Record<string, unknown>
|
|
46
|
+
) => (format === "human" ? formatHuman(level, payload) : encodeLog(level, payload));
|
|
47
|
+
|
|
48
|
+
const resolveLogFormat = Effect.gen(function* () {
|
|
49
|
+
const preferences = yield* Effect.serviceOption(CliPreferences);
|
|
50
|
+
const override = Option.flatMap(preferences, (value) =>
|
|
51
|
+
Option.fromNullable(value.logFormat)
|
|
52
|
+
);
|
|
53
|
+
if (Option.isSome(override)) {
|
|
54
|
+
return override.value;
|
|
55
|
+
}
|
|
56
|
+
const terminal = yield* Effect.serviceOption(Terminal.Terminal);
|
|
57
|
+
if (Option.isSome(terminal)) {
|
|
58
|
+
const isTTY = yield* terminal.value.isTTY.pipe(Effect.orElseSucceed(() => false));
|
|
59
|
+
return isTTY ? "human" : "json";
|
|
60
|
+
}
|
|
61
|
+
return "json" as const;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const logEventWith = (
|
|
65
|
+
output: CliOutputService,
|
|
66
|
+
format: LogFormat,
|
|
67
|
+
level: LogLevel,
|
|
68
|
+
payload: Record<string, unknown>
|
|
69
|
+
) => output.writeStderr(encodeLogFormat(format, level, payload));
|
|
70
|
+
|
|
71
|
+
const logEvent = (level: LogLevel, payload: Record<string, unknown>) =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const output = yield* CliOutput;
|
|
74
|
+
const format = yield* resolveLogFormat;
|
|
75
|
+
yield* logEventWith(output, format, level, payload);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const logInfo = (message: string, data?: Record<string, unknown>) =>
|
|
79
|
+
logEvent("INFO", { message, ...data });
|
|
80
|
+
|
|
81
|
+
export const logErrorEvent = (message: string, data?: Record<string, unknown>) =>
|
|
82
|
+
logEvent("ERROR", { message, ...data });
|
|
83
|
+
|
|
84
|
+
export const logWarn = (message: string, data?: Record<string, unknown>) =>
|
|
85
|
+
logEvent("WARN", { message, ...data });
|
|
86
|
+
|
|
87
|
+
export const logProgress = (progress: SyncProgress) =>
|
|
88
|
+
logEvent("PROGRESS", { operation: "sync", progress });
|
|
89
|
+
|
|
90
|
+
const warningDetails = (warning: ResourceWarning): Record<string, unknown> => {
|
|
91
|
+
switch (warning._tag) {
|
|
92
|
+
case "StoreSize":
|
|
93
|
+
return {
|
|
94
|
+
kind: warning._tag,
|
|
95
|
+
bytes: warning.bytes,
|
|
96
|
+
threshold: warning.threshold,
|
|
97
|
+
root: warning.root
|
|
98
|
+
};
|
|
99
|
+
case "MemoryRss":
|
|
100
|
+
return {
|
|
101
|
+
kind: warning._tag,
|
|
102
|
+
bytes: warning.bytes,
|
|
103
|
+
threshold: warning.threshold
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const makeSyncReporter = (
|
|
109
|
+
quiet: boolean,
|
|
110
|
+
monitor: ResourceMonitorService,
|
|
111
|
+
output: CliOutputService
|
|
112
|
+
) =>
|
|
113
|
+
SyncReporter.of({
|
|
114
|
+
report: (progress) =>
|
|
115
|
+
Effect.gen(function* () {
|
|
116
|
+
const format = yield* resolveLogFormat;
|
|
117
|
+
if (!quiet) {
|
|
118
|
+
yield* logEventWith(output, format, "PROGRESS", {
|
|
119
|
+
operation: "sync",
|
|
120
|
+
progress
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const warnings = yield* monitor.check();
|
|
124
|
+
if (warnings.length > 0) {
|
|
125
|
+
yield* Effect.forEach(
|
|
126
|
+
warnings,
|
|
127
|
+
(warning) =>
|
|
128
|
+
logEventWith(output, format, "WARN", {
|
|
129
|
+
message: "Resource warning",
|
|
130
|
+
...warningDetails(warning)
|
|
131
|
+
}),
|
|
132
|
+
{ discard: true }
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}).pipe(Effect.orElseSucceed(() => undefined))
|
|
136
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
2
|
+
import type { OutputFormat } from "../domain/config.js";
|
|
3
|
+
|
|
4
|
+
export const jsonNdjsonTableFormats = ["json", "ndjson", "table"] as const;
|
|
5
|
+
export type JsonNdjsonTableFormat = typeof jsonNdjsonTableFormats[number];
|
|
6
|
+
|
|
7
|
+
export const jsonTableFormats = ["json", "table"] as const;
|
|
8
|
+
export type JsonTableFormat = typeof jsonTableFormats[number];
|
|
9
|
+
|
|
10
|
+
export const textJsonFormats = ["text", "json"] as const;
|
|
11
|
+
export type TextJsonFormat = typeof textJsonFormats[number];
|
|
12
|
+
|
|
13
|
+
export const treeTableJsonFormats = ["tree", "table", "json"] as const;
|
|
14
|
+
export type TreeTableJsonFormat = typeof treeTableJsonFormats[number];
|
|
15
|
+
|
|
16
|
+
export const resolveOutputFormat = <T extends string>(
|
|
17
|
+
format: Option.Option<T>,
|
|
18
|
+
configFormat: OutputFormat,
|
|
19
|
+
supported: readonly T[],
|
|
20
|
+
fallback: T
|
|
21
|
+
) => {
|
|
22
|
+
if (Option.isSome(format)) {
|
|
23
|
+
return format.value;
|
|
24
|
+
}
|
|
25
|
+
return supported.includes(configFormat as T) ? (configFormat as T) : fallback;
|
|
26
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { BunSink } from "@effect/platform-bun";
|
|
2
|
+
import { SystemError, type PlatformError } from "@effect/platform/Error";
|
|
3
|
+
import { Context, Effect, Layer, Sink, Stream } from "effect";
|
|
4
|
+
|
|
5
|
+
const jsonLine = (value: unknown, pretty?: boolean) =>
|
|
6
|
+
JSON.stringify(value, null, pretty ? 2 : 0);
|
|
7
|
+
|
|
8
|
+
const ensureNewline = (value: string) => (value.endsWith("\n") ? value : `${value}\n`);
|
|
9
|
+
|
|
10
|
+
const writeToSink = (
|
|
11
|
+
sink: Sink.Sink<void, string | Uint8Array, never, PlatformError>,
|
|
12
|
+
value: string
|
|
13
|
+
) => Stream.fromIterable([value]).pipe(Stream.run(sink));
|
|
14
|
+
|
|
15
|
+
export interface CliOutputService {
|
|
16
|
+
readonly stdout: Sink.Sink<void, string | Uint8Array, never, PlatformError>;
|
|
17
|
+
readonly stderr: Sink.Sink<void, string | Uint8Array, never, PlatformError>;
|
|
18
|
+
readonly writeJson: (value: unknown, pretty?: boolean) => Effect.Effect<void, PlatformError>;
|
|
19
|
+
readonly writeText: (value: string) => Effect.Effect<void, PlatformError>;
|
|
20
|
+
readonly writeJsonStream: <A, E, R>(
|
|
21
|
+
stream: Stream.Stream<A, E, R>
|
|
22
|
+
) => Effect.Effect<void, E | PlatformError, R>;
|
|
23
|
+
readonly writeStderr: (value: string) => Effect.Effect<void, PlatformError>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class CliOutput extends Context.Tag("@skygent/CliOutput")<
|
|
27
|
+
CliOutput,
|
|
28
|
+
CliOutputService
|
|
29
|
+
>() {
|
|
30
|
+
static readonly layer = Layer.succeed(
|
|
31
|
+
CliOutput,
|
|
32
|
+
(() => {
|
|
33
|
+
const stdout = BunSink.fromWritable(
|
|
34
|
+
() => process.stdout,
|
|
35
|
+
(cause) =>
|
|
36
|
+
new SystemError({
|
|
37
|
+
module: "Stream",
|
|
38
|
+
method: "stdout",
|
|
39
|
+
reason: "Unknown",
|
|
40
|
+
cause
|
|
41
|
+
}),
|
|
42
|
+
{ endOnDone: false }
|
|
43
|
+
);
|
|
44
|
+
const stderr = BunSink.fromWritable(
|
|
45
|
+
() => process.stderr,
|
|
46
|
+
(cause) =>
|
|
47
|
+
new SystemError({
|
|
48
|
+
module: "Stream",
|
|
49
|
+
method: "stderr",
|
|
50
|
+
reason: "Unknown",
|
|
51
|
+
cause
|
|
52
|
+
}),
|
|
53
|
+
{ endOnDone: false }
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return CliOutput.of({
|
|
57
|
+
stdout,
|
|
58
|
+
stderr,
|
|
59
|
+
writeJson: (value, pretty) =>
|
|
60
|
+
writeToSink(stdout, ensureNewline(jsonLine(value, pretty))),
|
|
61
|
+
writeText: (value) => writeToSink(stdout, ensureNewline(value)),
|
|
62
|
+
writeJsonStream: (stream) =>
|
|
63
|
+
stream.pipe(
|
|
64
|
+
Stream.map((value) => `${jsonLine(value)}\n`),
|
|
65
|
+
Stream.run(stdout)
|
|
66
|
+
),
|
|
67
|
+
writeStderr: (value) => writeToSink(stderr, ensureNewline(value))
|
|
68
|
+
});
|
|
69
|
+
})()
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const writeJson = (value: unknown, pretty?: boolean) =>
|
|
74
|
+
Effect.flatMap(CliOutput, (output) => output.writeJson(value, pretty));
|
|
75
|
+
|
|
76
|
+
export const writeText = (value: string) =>
|
|
77
|
+
Effect.flatMap(CliOutput, (output) => output.writeText(value));
|
|
78
|
+
|
|
79
|
+
export const writeJsonStream = <A, E, R>(
|
|
80
|
+
stream: Stream.Stream<A, E, R>
|
|
81
|
+
): Effect.Effect<void, E | PlatformError, R | CliOutput> =>
|
|
82
|
+
Effect.flatMap(CliOutput, (output) => output.writeJsonStream(stream));
|
package/src/cli/parse.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Effect, ParseResult, Schema } from "effect";
|
|
2
|
+
import { CliJsonError } from "./errors.js";
|
|
3
|
+
|
|
4
|
+
type FormatParseErrorOptions = {
|
|
5
|
+
readonly label?: string;
|
|
6
|
+
readonly maxIssues?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const formatPath = (path: ReadonlyArray<unknown>) =>
|
|
10
|
+
path.length > 0 ? path.map((entry) => String(entry)).join(".") : "value";
|
|
11
|
+
|
|
12
|
+
const formatParseError = (
|
|
13
|
+
error: ParseResult.ParseError,
|
|
14
|
+
options?: FormatParseErrorOptions
|
|
15
|
+
) => {
|
|
16
|
+
const issues = ParseResult.ArrayFormatter.formatErrorSync(error);
|
|
17
|
+
if (issues.length === 0) {
|
|
18
|
+
return ParseResult.TreeFormatter.formatErrorSync(error);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const jsonParseIssue = issues.find(
|
|
22
|
+
(issue) =>
|
|
23
|
+
issue._tag === "Transformation" &&
|
|
24
|
+
typeof issue.message === "string" &&
|
|
25
|
+
issue.message.startsWith("JSON Parse error")
|
|
26
|
+
);
|
|
27
|
+
if (jsonParseIssue) {
|
|
28
|
+
const header = options?.label
|
|
29
|
+
? `Invalid JSON input for ${options.label}.`
|
|
30
|
+
: "Invalid JSON input.";
|
|
31
|
+
return [
|
|
32
|
+
header,
|
|
33
|
+
jsonParseIssue.message,
|
|
34
|
+
"Tip: wrap JSON in single quotes to avoid shell escaping issues."
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const maxIssues = options?.maxIssues ?? 6;
|
|
39
|
+
const lines = issues.slice(0, maxIssues).map((issue) => {
|
|
40
|
+
const path = formatPath(issue.path);
|
|
41
|
+
return `${path}: ${issue.message}`;
|
|
42
|
+
});
|
|
43
|
+
if (issues.length > maxIssues) {
|
|
44
|
+
lines.push(`Additional issues: ${issues.length - maxIssues}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const header = options?.label ? `Invalid ${options.label}.` : undefined;
|
|
48
|
+
return header ? [header, ...lines].join("\n") : lines.join("\n");
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type DecodeJsonOptions = {
|
|
52
|
+
readonly formatter?: (error: ParseResult.ParseError, raw: string) => string;
|
|
53
|
+
readonly label?: string;
|
|
54
|
+
readonly maxIssues?: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const decodeJson = <A, I, R>(
|
|
58
|
+
schema: Schema.Schema<A, I, R>,
|
|
59
|
+
input: string,
|
|
60
|
+
options?: DecodeJsonOptions
|
|
61
|
+
) =>
|
|
62
|
+
Schema.decodeUnknown(Schema.parseJson(schema))(input).pipe(
|
|
63
|
+
Effect.mapError((error) => {
|
|
64
|
+
const formatOptions =
|
|
65
|
+
options?.label !== undefined || options?.maxIssues !== undefined
|
|
66
|
+
? {
|
|
67
|
+
...(options?.label !== undefined ? { label: options.label } : {}),
|
|
68
|
+
...(options?.maxIssues !== undefined
|
|
69
|
+
? { maxIssues: options.maxIssues }
|
|
70
|
+
: {})
|
|
71
|
+
}
|
|
72
|
+
: undefined;
|
|
73
|
+
const message = ParseResult.isParseError(error)
|
|
74
|
+
? options?.formatter
|
|
75
|
+
? options.formatter(error, input)
|
|
76
|
+
: formatParseError(error, formatOptions)
|
|
77
|
+
: String(error);
|
|
78
|
+
return CliJsonError.make({ message, cause: error });
|
|
79
|
+
})
|
|
80
|
+
);
|
package/src/cli/post.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Context, Effect, Option, Stream } from "effect";
|
|
3
|
+
import { BskyClient } from "../services/bsky-client.js";
|
|
4
|
+
import { PostParser } from "../services/post-parser.js";
|
|
5
|
+
import type { PostLike, ProfileView } from "../domain/bsky.js";
|
|
6
|
+
import type { RawPost } from "../domain/raw.js";
|
|
7
|
+
import { renderPostsTable } from "../domain/format.js";
|
|
8
|
+
import { AppConfigService } from "../services/app-config.js";
|
|
9
|
+
import { withExamples } from "./help.js";
|
|
10
|
+
import { postUriArg, parseLimit } from "./shared-options.js";
|
|
11
|
+
import { writeJson, writeJsonStream, writeText } from "./output.js";
|
|
12
|
+
import { renderTableLegacy } from "./doc/table.js";
|
|
13
|
+
import { jsonNdjsonTableFormats, resolveOutputFormat } from "./output-format.js";
|
|
14
|
+
|
|
15
|
+
const limitOption = Options.integer("limit").pipe(
|
|
16
|
+
Options.withDescription("Maximum number of results"),
|
|
17
|
+
Options.optional
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const cursorOption = Options.text("cursor").pipe(
|
|
21
|
+
Options.withDescription("Pagination cursor"),
|
|
22
|
+
Options.optional
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const formatOption = Options.choice("format", jsonNdjsonTableFormats).pipe(
|
|
26
|
+
Options.withDescription("Output format (default: json)"),
|
|
27
|
+
Options.optional
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const cidOption = Options.text("cid").pipe(
|
|
31
|
+
Options.withDescription("Filter engagement by specific record CID"),
|
|
32
|
+
Options.optional
|
|
33
|
+
);
|
|
34
|
+
|
|
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
|
+
|
|
48
|
+
const renderLikesTable = (likes: ReadonlyArray<PostLike>, cursor: string | undefined) => {
|
|
49
|
+
const rows = likes.map((like) => [
|
|
50
|
+
like.actor.handle,
|
|
51
|
+
like.actor.displayName ?? "",
|
|
52
|
+
like.actor.did,
|
|
53
|
+
like.createdAt.toISOString(),
|
|
54
|
+
like.indexedAt.toISOString()
|
|
55
|
+
]);
|
|
56
|
+
const table = renderTableLegacy(
|
|
57
|
+
["HANDLE", "DISPLAY NAME", "DID", "CREATED AT", "INDEXED AT"],
|
|
58
|
+
rows
|
|
59
|
+
);
|
|
60
|
+
return cursor ? `${table}\n\nCursor: ${cursor}` : table;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type PostParserService = Context.Tag.Service<typeof PostParser>;
|
|
64
|
+
|
|
65
|
+
const parseRawPosts = (parser: PostParserService, posts: ReadonlyArray<RawPost>) =>
|
|
66
|
+
Effect.forEach(posts, (raw) => parser.parsePost(raw), { concurrency: "unbounded" });
|
|
67
|
+
|
|
68
|
+
const likesCommand = Command.make(
|
|
69
|
+
"likes",
|
|
70
|
+
{ uri: postUriArg, cid: cidOption, limit: limitOption, cursor: cursorOption, format: formatOption },
|
|
71
|
+
({ uri, cid, limit, cursor, format }) =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const appConfig = yield* AppConfigService;
|
|
74
|
+
const client = yield* BskyClient;
|
|
75
|
+
const parsedLimit = yield* parseLimit(limit);
|
|
76
|
+
const result = yield* client.getLikes(uri, {
|
|
77
|
+
...(Option.isSome(parsedLimit) ? { limit: parsedLimit.value } : {}),
|
|
78
|
+
...(Option.isSome(cursor) ? { cursor: cursor.value } : {}),
|
|
79
|
+
...(Option.isSome(cid) ? { cid: cid.value } : {})
|
|
80
|
+
});
|
|
81
|
+
const outputFormat = resolveOutputFormat(
|
|
82
|
+
format,
|
|
83
|
+
appConfig.outputFormat,
|
|
84
|
+
jsonNdjsonTableFormats,
|
|
85
|
+
"json"
|
|
86
|
+
);
|
|
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
|
+
})
|
|
97
|
+
).pipe(
|
|
98
|
+
Command.withDescription(
|
|
99
|
+
withExamples("List accounts that liked a post", [
|
|
100
|
+
"skygent post likes at://did:plc:example/app.bsky.feed.post/xyz",
|
|
101
|
+
"skygent post likes at://did:plc:example/app.bsky.feed.post/xyz --limit 50"
|
|
102
|
+
])
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const repostedByCommand = Command.make(
|
|
107
|
+
"reposted-by",
|
|
108
|
+
{ uri: postUriArg, cid: cidOption, limit: limitOption, cursor: cursorOption, format: formatOption },
|
|
109
|
+
({ uri, cid, limit, cursor, format }) =>
|
|
110
|
+
Effect.gen(function* () {
|
|
111
|
+
const appConfig = yield* AppConfigService;
|
|
112
|
+
const client = yield* BskyClient;
|
|
113
|
+
const parsedLimit = yield* parseLimit(limit);
|
|
114
|
+
const result = yield* client.getRepostedBy(uri, {
|
|
115
|
+
...(Option.isSome(parsedLimit) ? { limit: parsedLimit.value } : {}),
|
|
116
|
+
...(Option.isSome(cursor) ? { cursor: cursor.value } : {}),
|
|
117
|
+
...(Option.isSome(cid) ? { cid: cid.value } : {})
|
|
118
|
+
});
|
|
119
|
+
const outputFormat = resolveOutputFormat(
|
|
120
|
+
format,
|
|
121
|
+
appConfig.outputFormat,
|
|
122
|
+
jsonNdjsonTableFormats,
|
|
123
|
+
"json"
|
|
124
|
+
);
|
|
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
|
+
})
|
|
135
|
+
).pipe(
|
|
136
|
+
Command.withDescription(
|
|
137
|
+
withExamples("List accounts that reposted a post", [
|
|
138
|
+
"skygent post reposted-by at://did:plc:example/app.bsky.feed.post/xyz"
|
|
139
|
+
])
|
|
140
|
+
)
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const quotesCommand = Command.make(
|
|
144
|
+
"quotes",
|
|
145
|
+
{ uri: postUriArg, cid: cidOption, limit: limitOption, cursor: cursorOption, format: formatOption },
|
|
146
|
+
({ uri, cid, limit, cursor, format }) =>
|
|
147
|
+
Effect.gen(function* () {
|
|
148
|
+
const appConfig = yield* AppConfigService;
|
|
149
|
+
const client = yield* BskyClient;
|
|
150
|
+
const parser = yield* PostParser;
|
|
151
|
+
const parsedLimit = yield* parseLimit(limit);
|
|
152
|
+
const result = yield* client.getQuotes(uri, {
|
|
153
|
+
...(Option.isSome(parsedLimit) ? { limit: parsedLimit.value } : {}),
|
|
154
|
+
...(Option.isSome(cursor) ? { cursor: cursor.value } : {}),
|
|
155
|
+
...(Option.isSome(cid) ? { cid: cid.value } : {})
|
|
156
|
+
});
|
|
157
|
+
const posts = yield* parseRawPosts(parser, result.posts);
|
|
158
|
+
const outputFormat = resolveOutputFormat(
|
|
159
|
+
format,
|
|
160
|
+
appConfig.outputFormat,
|
|
161
|
+
jsonNdjsonTableFormats,
|
|
162
|
+
"json"
|
|
163
|
+
);
|
|
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
|
+
})
|
|
177
|
+
).pipe(
|
|
178
|
+
Command.withDescription(
|
|
179
|
+
withExamples("List quote-posts for a post", [
|
|
180
|
+
"skygent post quotes at://did:plc:example/app.bsky.feed.post/xyz"
|
|
181
|
+
])
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
export const postCommand = Command.make("post", {}).pipe(
|
|
186
|
+
Command.withSubcommands([likesCommand, repostedByCommand, quotesCommand]),
|
|
187
|
+
Command.withDescription(
|
|
188
|
+
withExamples("Inspect post engagement", [
|
|
189
|
+
"skygent post likes at://did:plc:example/app.bsky.feed.post/xyz",
|
|
190
|
+
"skygent post quotes at://did:plc:example/app.bsky.feed.post/xyz"
|
|
191
|
+
])
|
|
192
|
+
)
|
|
193
|
+
);
|